From 9dac04e32e272e6d0bd446fd4ea4f9ea8b7856ee Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 06:37:43 +0000 Subject: [PATCH] Cache per-entry augmentations in B-tree nodes to reduce heap object churn When path-copying a B-tree node, unchanged entries (same tuple identity) now reuse their previously computed augmentation instead of re-calling extractAugmentation. For transduced tables backed by ConcatTreeList, this avoids creating duplicate rope-leaf heap objects that the serializer would otherwise treat as cache misses. Measured on a 1024-row / 4-team schema: transducedByTeam heap object cache misses per mutation: 37 -> 7 Total system-wide: ~49 -> 19 Co-Authored-By: Konstantin Wohlwend --- .../data-structures/augmented-tree-map.ts | 116 +++++++++++++----- 1 file changed, 82 insertions(+), 34 deletions(-) diff --git a/apps/bulldozer-js/src/databases/piledriver/data-structures/augmented-tree-map.ts b/apps/bulldozer-js/src/databases/piledriver/data-structures/augmented-tree-map.ts index 6177fb0da..ab2c890b3 100644 --- a/apps/bulldozer-js/src/databases/piledriver/data-structures/augmented-tree-map.ts +++ b/apps/bulldozer-js/src/databases/piledriver/data-structures/augmented-tree-map.ts @@ -31,6 +31,11 @@ type Split = { entry: Entry> // Persisted B-tree node. Children are heap objects, so unchanged subtrees are shared across versions. type Node = { entries: Entry>[], + // Cached per-entry augmentations, parallel to `entries`. When a node is path-copied during a + // mutation, unchanged entries (same tuple identity) can reuse their cached augmentation instead + // of re-calling extractAugmentation. This avoids creating duplicate heap objects for the same + // data, letting the downstream serializer recognise them as already-written (heap cache hits). + entryAugmentations?: A[], children: Child[], augmentation: A, size: number, @@ -291,7 +296,11 @@ export class AugmentedTreeMultiMap, StoredValue>[], children: Child, Augmentation>[] = []) { + // cachedEntryAugs: when provided, maps entry tuple references to their previously computed + // augmentations. Unchanged entries (same tuple identity) get a cache hit, avoiding a redundant + // extractAugmentation call and — critically — reusing the same PiledriverHeapObjects so that + // downstream serialization sees them as already-written (heap object cache hits). + private async make(entries: Entry, StoredValue>[], children: Child, Augmentation>[] = [], cachedEntryAugs?: ReadonlyMap) { if (!entries.length && children.length !== 1) throw new Error("Invalid empty B-tree node"); if (!entries.length) { const [onlyChild] = children; @@ -307,6 +316,7 @@ export class AugmentedTreeMultiMap, Value, Augmentation>): ReadonlyMap | undefined { + if (!node.entryAugmentations || node.entryAugmentations.length !== node.entries.length) return undefined; + const cache = new Map(); + for (let i = 0; i < node.entries.length; i++) { + cache.set(node.entries[i], node.entryAugmentations[i]); + } + return cache; + } + private arity() { return Math.max(3, this.options.arity ?? defaultArity); } @@ -377,15 +404,15 @@ export class AugmentedTreeMultiMap, StoredValue>[], children: Child, Augmentation>[]) { + private async split(entries: Entry, StoredValue>[], children: Child, Augmentation>[], cachedEntryAugs?: ReadonlyMap) { const middle = entries.length >> 1; - const left = await this.make(entries.slice(0, middle), children.length ? children.slice(0, middle + 1) : []); - const right = await this.make(entries.slice(middle + 1), children.length ? children.slice(middle + 1) : []); + const left = await this.make(entries.slice(0, middle), children.length ? children.slice(0, middle + 1) : [], cachedEntryAugs); + const right = await this.make(entries.slice(middle + 1), children.length ? children.slice(middle + 1) : [], cachedEntryAugs); return { root: left, split: { entry: entries[middle], right } }; } - private async done(entries: Entry, StoredValue>[], children: Child, Augmentation>[], added: boolean) { - const result = entries.length > this.maxEntries() ? await this.split(entries, children) : { root: await this.make(entries, children) }; + private async done(entries: Entry, StoredValue>[], children: Child, Augmentation>[], added: boolean, cachedEntryAugs?: ReadonlyMap) { + const result = entries.length > this.maxEntries() ? await this.split(entries, children, cachedEntryAugs) : { root: await this.make(entries, children, cachedEntryAugs) }; return { ...result, added }; } @@ -396,16 +423,17 @@ export class AugmentedTreeMultiMap, Augmentation> | null, key: MultiKey, isRoot = false): Promise<{ root: Child, Augmentation> | null, deleted: boolean }> { @@ -424,11 +452,12 @@ export class AugmentedTreeMultiMap, Augmentation> | null)[]; + const cachedEntryAugs = this.buildEntryAugCache(node); if (found) { if (!children.length) { entries.splice(index, 1); - return await this.afterDelete(entries, children, isRoot, true); + return await this.afterDelete(entries, children, isRoot, true, cachedEntryAugs); } // Replace the separator with its predecessor or successor (from whichever side has more @@ -441,12 +470,12 @@ export class AugmentedTreeMultiMap, Augmentation>): Promise<{ root: Child, Augmentation> | null, entry: Entry, StoredValue> }> { const node = (await this.node(child.ref))!; const entries = [...node.entries]; const children = [...node.children] as (Child, Augmentation> | null)[]; + const cachedEntryAugs = this.buildEntryAugCache(node); if (!children.length) { const [entry] = entries.splice(0, 1); - return { root: entries.length ? await this.make(entries) : null, entry }; + return { root: entries.length ? await this.make(entries, [], cachedEntryAugs) : null, entry }; } const result = await this.deleteMin(children[0]!); children[0] = result.root; - return { root: (await this.fixChild(entries, children, 0, false)).root, entry: result.entry }; + return { root: (await this.fixChild(entries, children, 0, false, cachedEntryAugs)).root, entry: result.entry }; } private async deleteMax(child: Child, Augmentation>): Promise<{ root: Child, Augmentation> | null, entry: Entry, StoredValue> }> { const node = (await this.node(child.ref))!; const entries = [...node.entries]; const children = [...node.children] as (Child, Augmentation> | null)[]; + const cachedEntryAugs = this.buildEntryAugCache(node); if (!children.length) { const entry = entries.pop()!; - return { root: entries.length ? await this.make(entries) : null, entry }; + return { root: entries.length ? await this.make(entries, [], cachedEntryAugs) : null, entry }; } const index = children.length - 1; const result = await this.deleteMax(children[index]!); children[index] = result.root; - return { root: (await this.fixChild(entries, children, index, false)).root, entry: result.entry }; + return { root: (await this.fixChild(entries, children, index, false, cachedEntryAugs)).root, entry: result.entry }; } - private async afterDelete(entries: Entry, StoredValue>[], children: (Child, Augmentation> | null)[], isRoot: boolean, deleted: boolean) { + private async afterDelete(entries: Entry, StoredValue>[], children: (Child, Augmentation> | null)[], isRoot: boolean, deleted: boolean, cachedEntryAugs?: ReadonlyMap) { const liveChildren = children.filter(child => child !== null); if (!entries.length) return { root: isRoot ? liveChildren[0] ?? null : liveChildren[0] ? await this.make([], [liveChildren[0]]) : null, deleted }; - return { root: await this.make(entries, liveChildren), deleted }; + return { root: await this.make(entries, liveChildren, cachedEntryAugs), deleted }; } - private async fixChild(entries: Entry, StoredValue>[], children: (Child, Augmentation> | null)[], index: number, isRoot: boolean): Promise<{ root: Child, Augmentation> | null, deleted: boolean }> { + private async fixChild(entries: Entry, StoredValue>[], children: (Child, Augmentation> | null)[], index: number, isRoot: boolean, cachedEntryAugs?: ReadonlyMap): Promise<{ root: Child, Augmentation> | null, deleted: boolean }> { const child = children[index]; - if (child && child.entryCount >= this.minEntries()) return await this.afterDelete(entries, children, isRoot, true); + if (child && child.entryCount >= this.minEntries()) return await this.afterDelete(entries, children, isRoot, true, cachedEntryAugs); const left = children[index - 1]; if (left && left.entryCount > this.minEntries()) { @@ -506,9 +537,9 @@ export class AugmentedTreeMultiMap, Augmentation> | null, entry: Entry, StoredValue>, right: Child, Augmentation> | null) { - const leftNode = left ? (await this.node(left.ref))! : { entries: [], children: [] }; - const rightNode = right ? (await this.node(right.ref))! : { entries: [], children: [] }; + private async mergeChildren(left: Child, Augmentation> | null, entry: Entry, StoredValue>, right: Child, Augmentation> | null, parentEntryAugs?: ReadonlyMap) { + const leftNode = left ? (await this.node(left.ref))! : null; + const rightNode = right ? (await this.node(right.ref))! : null; + + // Build a combined entry augmentation cache from both child nodes and the parent separator + const cache = new Map(); + if (leftNode) { + const lc = this.buildEntryAugCache(leftNode); + if (lc) for (const [e, a] of lc) cache.set(e, a); + } + if (rightNode) { + const rc = this.buildEntryAugCache(rightNode); + if (rc) for (const [e, a] of rc) cache.set(e, a); + } + const sepAug = parentEntryAugs?.get(entry); + if (sepAug !== undefined) cache.set(entry, sepAug); + return await this.make( - [...leftNode.entries, entry, ...rightNode.entries], - [...leftNode.children, ...rightNode.children], + [...(leftNode?.entries ?? []), entry, ...(rightNode?.entries ?? [])], + [...(leftNode?.children ?? []), ...(rightNode?.children ?? [])], + cache.size ? cache : undefined, ); } @@ -558,7 +604,9 @@ export class AugmentedTreeMultiMap