From c63e3d7cae508bfc7eb18d11bbfbc9299ca3052b Mon Sep 17 00:00:00 2001 From: bompus Date: Sat, 31 Oct 2026 10:54:02 -0600 Subject: [PATCH 1/2] perf(resolve): pool the prerequisite phase again #591 moved the prerequisite (imports/extends/implements) batches to the main-thread kernel so the workers could open one post-prerequisite snapshot. That serialized the phase on one core: at matched graph sizes n8n's full index went 54.5-57.2 s -> 70.3-70.7 s and supabase 17.3-17.5 s -> 21.8-22.5 s, with index CPU falling from 630% to 460%. Restore the two-phase pool: open on the pre-prerequisite snapshot so the prerequisite refs resolve across the workers, then take a second snapshot after their supertype edges persist and recycle the workers onto it. Keep #591's early spawn, single worker lookup, "loaded" message and synthesis-boundary open; a sweep with no calls batch refreshes before synthesis instead of dropping the pool. A/B on the 24-core host: supabase 21.60/21.88 -> 17.11/17.27 s, n8n 70.72/73.17 -> 53.58/53.91 s. Peak index RSS on n8n rises ~0.6 GB from the second snapshot generation. --- src/resolution/index.ts | 113 ++++++++++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 21 deletions(-) diff --git a/src/resolution/index.ts b/src/resolution/index.ts index aa49aa2af..52caca2d9 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -1454,6 +1454,31 @@ export class ReferenceResolver { return this.kernelReaderSnapshot; } + private kernelSnapshotSeq = 0; + + /** + * A second snapshot for the same run, taken after the prerequisite phase so + * it holds every implements/extends edge the calls phase reads. The workers + * still have the first copy open, so this is a new inode (its own -shm) + * rather than an overwrite; the caller recycles the workers onto it. Returns + * null on any failure (the caller then destroys the pool and resolves on the + * main thread). + */ + private async refreshKernelReaderSnapshot(fold: () => Promise): Promise { + const dbPath = this.queries.getDatabasePath(); + if (!dbPath || !(await fold())) return null; + const snap = `${dbPath}.kr-snapshot-${++this.kernelSnapshotSeq}`; + try { + fs.rmSync(`${snap}-wal`, { force: true }); + fs.rmSync(`${snap}-shm`, { force: true }); + await fs.promises.copyFile(dbPath, snap); + } catch { + return null; + } + this.kernelReaderSnapshot = snap; + return snap; + } + private openKernelResolver( parallelDbPath: string | undefined, generation?: string, @@ -2360,15 +2385,19 @@ export class ReferenceResolver { let pool: ResolverPool | null = null; let poolReady = false; // Supertype walks read implements/extends edges, which only the - // prerequisite phase writes. The workers are spawned before that phase - // but open their snapshot after it (openPool), so it carries them. + // prerequisite phase writes. Workers open a snapshot BEFORE that phase so + // its refs resolve in parallel with every other batch; once the phase is + // persisted the pool is recycled onto a second snapshot that carries the + // supertype edges (refreshSnapshotIfDue). let inPrereqPhase = true; // Prerequisite batches read whose supertype edges are not inserted yet. // The prefetch reads the first calls page (ending the phase) while the // last prerequisite batch is still in flight, so the phase flag alone // says nothing about what the db holds. let prereqUnpersisted = 0; + let prereqBatchesPersisted = 0; const supertypesPersisted = (): boolean => !inPrereqPhase && prereqUnpersisted === 0; + let snapshotComplete = false; const destroyPool = async (): Promise => { const p = pool; pool = null; @@ -2376,10 +2405,8 @@ export class ReferenceResolver { if (p) await p.destroy().catch(() => undefined); }; // Spawn the workers as soon as the pool is gated in. Their ~60ms module - // load then overlaps the prerequisite batches, which resolve on the - // main-thread kernel; the snapshot and open happen once those edges are - // persisted (openPool), so the old pre-prerequisite snapshot and its - // refresh collapse into one copy after the phase. + // load overlaps the kernel-reader snapshot copy and the prerequisite + // batches, which resolve in the pool like every other batch. const preparePool = async (): Promise => { if (!parallel) return null; const poolSize = ResolverPool.preflight(parallel.dbPath, total); @@ -2390,13 +2417,13 @@ export class ReferenceResolver { } return p; }; - // The prerequisite→calls boundary: close the main kernel, take the one - // snapshot the workers read, and open them. Held until every prerequisite - // edge is persisted so the snapshot carries the supertypes the walks use. - const openPool = async (): Promise => { + // Open the workers over the snapshot as it stands. Called before the + // first batch (the prerequisite phase included — those refs resolve in + // the pool), with `supertypesComplete` telling the kernel whether the + // implements/extends edges are all in yet. + const openPool = async (supertypesComplete: boolean): Promise => { if (!pool) return false; if (poolReady) return true; - if (!supertypesPersisted()) return false; const t0 = Date.now(); // Once workers attach, NO rusqlite conn may share the live -shm — not // even the main-thread kernel conn (only same-thread conns are @@ -2421,7 +2448,10 @@ export class ReferenceResolver { await destroyPool(); return false; } - pool.open(parallel!.dbPath, this.projectRoot, kernelDbPath, true); + pool.open(parallel!.dbPath, this.projectRoot, kernelDbPath, supertypesComplete); + // An open over a snapshot that already carries every supertype edge + // needs no post-prerequisite refresh. + if (supertypesComplete) snapshotComplete = true; try { await pool.ready(); poolReady = true; @@ -2434,6 +2464,42 @@ export class ReferenceResolver { } return poolReady; }; + // The prerequisite→calls boundary: every implements/extends edge the walks + // read is persisted, so hand the workers a snapshot that has them (a fresh + // copy only if a prerequisite batch wrote any) before the first calls + // batch fans out. A failed refresh destroys the pool; the calls batches + // then resolve on the main thread. + const refreshSnapshotIfDue = async (): Promise => { + if (!pool || snapshotComplete || !supertypesPersisted() || !this.kernelReaderSnapshot || !parallel?.foldWalForSnapshot) return; + snapshotComplete = true; + const tLp = Date.now(); + try { + const stale = this.kernelReaderSnapshot; + const fresh = prereqBatchesPersisted > 0 + ? await this.refreshKernelReaderSnapshot(parallel.foldWalForSnapshot) + : stale; + if (!fresh) throw new Error('snapshot copy failed'); + await pool.recycleWorkers(fresh); + refsSinceRecycle = 0; + if (process.env.CODEGRAPH_SYNTH_TIMINGS) { + console.error(`[pool-timing] kernel snapshot refreshed after the prerequisite phase: ${fresh} (${Date.now() - tLp}ms)`); + } + if (fresh !== stale) { + for (const suf of ['', '-wal', '-shm']) { + try { fs.rmSync(stale + suf, { force: true }); } catch { /* best-effort scratch cleanup */ } + } + } + } catch (err) { + if (process.env.CODEGRAPH_SYNTH_TIMINGS) { + console.error(`[pool-timing] kernel snapshot refresh failed, resolving on the main thread: ${err instanceof Error ? err.message : String(err)}`); + } + logDebug('Kernel snapshot refresh failed; resolving on the main thread', { + error: err instanceof Error ? err.message : String(err), + }); + await destroyPool(); + } + lp('recycle', tLp); + }; if (parallel && total >= minRefsForPool()) { pool = await preparePool(); } @@ -2506,9 +2572,11 @@ export class ReferenceResolver { // the supertype edges already persisted, so begin-time sees the same // state as settle-time. const beginBatch = async (batch: BatchPage): Promise => { - // Prerequisite batches resolve on the main kernel while the workers - // load; the first calls batch opens the pool over the one snapshot. - if (!batch.prereq) await openPool(); + // Open the pool on the pre-prerequisite snapshot so the prerequisite + // refs resolve across the workers too; the calls phase refreshes it + // once their supertype edges are persisted. + if (pool && !poolReady) await openPool(supertypesPersisted()); + if (!batch.prereq) await refreshSnapshotIfDue(); if (pool && poolReady) { return { mode: 'pool', @@ -2794,6 +2862,7 @@ export class ReferenceResolver { } if (batch.prereq) { prereqUnpersisted--; + prereqBatchesPersisted++; } lp('insertEdges', tLp); @@ -2946,12 +3015,14 @@ export class ReferenceResolver { } // A sweep whose refs are all prerequisites (imports/extends/implements) - // never enters a calls batch, so openPool never ran. Synthesis would then - // post passes to workers that never opened; above MAIN_RETRY_MAX_NODES it - // skips those passes and silently drops their edges. Open the pool now, or - // discard it so synthesis takes the sequential path. - if (pool && !poolReady) { - if (!(await openPool())) await destroyPool(); + // never enters a calls batch, so the post-prerequisite refresh never ran. + // Refresh now so synthesis reads the supertype edges the loop just + // persisted, then keep the pool for its passes (or discard it and let + // synthesis take the sequential path). + if (pool && poolReady) { + await refreshSnapshotIfDue(); + } else if (pool && !poolReady) { + if (!(await openPool(supertypesPersisted()))) await destroyPool(); } // Dynamic-edge synthesis: now that all base `calls` edges are persisted, From d32cef59a5770ae161fb12eb1ced849917f3ad9f Mon Sep 17 00:00:00 2001 From: bompus Date: Sat, 31 Oct 2026 11:18:10 -0600 Subject: [PATCH 2/2] docs: re-measure after the prerequisite-pool fix Second 2026-10-31 Node campaign at the fix head (c63e3d7c) against upstream cd04b21a. Full index: CPython 27.06 s against 46.34 (1.71x), n8n 53.95 against 71.36 (1.32x), pretix 11%, discourse 5% and Alamofire 4% faster; gin 8% and supabase 11% slower (supabase on 33% more nodes). One-file sync is faster on all seven, 1.08x to 2.69x; MCP warm explore 1.75x, idle memory 2.19 against 4.46 GiB. The new doc records the bisect that names #591, the matched-host A/B, the spreads and the admission receipt. The first 2026-10-31 run and the 2026-10-30 doc keep their dates and now point at the attribution; the unit conversion for the MCP memory rows is MiB/1024, correcting the /1000 values in the first run's table and list. --- README.md | 68 ++++++------ assets/fork-vs-upstream-performance.svg | 56 +++++----- .../benchmarks/fork-vs-upstream-2026-10-31.md | 14 +-- .../fork-vs-upstream-2026-10-31b.md | 100 ++++++++++++++++++ .../fork-vs-upstream-node-2026-10-30.md | 2 + .../benchmarks/runtime/render-node-cards.py | 2 +- 6 files changed, 173 insertions(+), 69 deletions(-) create mode 100644 docs/benchmarks/fork-vs-upstream-2026-10-31b.md diff --git a/README.md b/README.md index dedbe3ce3..ae9cce436 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates. ## About this fork -This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`60b0d7a9`](https://github.com/colbymchenry/codegraph/commit/60b0d7a92dc001de1be37943ef3cd1cc955d693d) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests. +This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`cd04b21a`](https://github.com/colbymchenry/codegraph/commit/cd04b21a314757b867de36ba5992ffc32eb3cb1c) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests. The fork publishes no releases. The install scripts, npm package, badges and `codegraph upgrade` further down this page install **upstream's** releases. To run the fork, build it from source (below). @@ -198,47 +198,47 @@ Call resolution the fork adds: ### Measured results -Upstream `main` at `cd04b21a` against the fork at `cf2a37a5`, Node.js 24.22.2 on the same 24-core bare-metal Linux host, measured 2026-10-31 under the host benchmark lock. Seven corpora, arms in mirrored order; index figures are the median of two runs and sync figures the median of four. A comparison counts as a change only when the gap exceeds that comparison's own run-to-run spread — the larger of the two arms' (max − min) / median — and cells that cannot show that are marked no clear change rather than given a fixed band. Bun was not re-measured in this run; the Bun figures below are from the 2026-10-28 campaign, and the CLI table is from the 2026-10-27 run at `bc27fdb4`. The numbers, method, spreads, graph sizes and admission receipts are in [`docs/benchmarks/fork-vs-upstream-2026-10-31.md`](docs/benchmarks/fork-vs-upstream-2026-10-31.md). The scripts are in [`scripts/benchmarks/runtime/`](scripts/benchmarks/runtime/). +Upstream `main` at `cd04b21a` against the fork at `c63e3d7c`, Node.js 24.22.2 on the same 24-core bare-metal Linux host, measured 2026-10-31 under the host benchmark lock. This is the second 2026-10-31 run, after the prerequisite-pool fix below; the first one exposed that regression and is kept as [its own record](docs/benchmarks/fork-vs-upstream-2026-10-31.md). Seven corpora, arms in mirrored order; index figures are the median of two runs and sync figures the median of four. A comparison counts as a change only when the gap exceeds that comparison's own run-to-run spread — the larger of the two arms' (max − min) / median — and cells that cannot show that are marked no clear change rather than given a fixed band. Bun was not re-measured in this run; the Bun figures below are from the 2026-10-28 campaign, and the CLI table is from the 2026-10-27 run at `bc27fdb4`. The numbers, method, spreads, graph sizes and admission receipts are in [`docs/benchmarks/fork-vs-upstream-2026-10-31b.md`](docs/benchmarks/fork-vs-upstream-2026-10-31b.md). The scripts are in [`scripts/benchmarks/runtime/`](scripts/benchmarks/runtime/). -![Fork against upstream CodeGraph on seven corpora, Node, 2026-10-31: the fork syncs a one-file edit 1.04 to 2.69 times faster, indexes CPython 1.47 times faster, and trails gin by 6% and discourse by 3%, and is 36% slower on supabase's 33% larger graph.](assets/fork-vs-upstream-performance.svg?v=12) +![Fork against upstream CodeGraph on seven corpora, Node, 2026-10-31: the fork syncs a one-file edit 1.08 to 2.69 times faster, indexes CPython 1.71 times faster and n8n 1.32 times faster, and trails gin by 8% and supabase by 11% on a graph with 33% more nodes.](assets/fork-vs-upstream-performance.svg?v=13) -*2026-10-31 Node run, fork `cf2a37a5` against upstream `cd04b21a`. Ratios are exact; the tables mark which gaps clear the run-to-run spread.* +*2026-10-31 Node run, fork `c63e3d7c` against upstream `cd04b21a`. Ratios are exact; the tables mark which gaps clear the run-to-run spread.* -The fork's one-file sync is faster on all seven corpora: 4% (discourse) to 63% (n8n), or 1.04× to 2.69×. Full indexing: CPython is 1.47× faster (31.38 s against 46.30 s), Alamofire 7% faster and n8n about level (1%); pretix is a tie inside its 6.6% fork spread; gin is 6% slower, discourse 3% slower and supabase 36% slower; the fork's own index also regressed at identical graph sizes on supabase and n8n (below). The speed costs memory on the smaller corpora — index peaks +64% on gin, +40% on Alamofire, +28% on pretix and +11% on supabase — while it is lower on CPython (−13%), discourse (−13%) and n8n (−30%). MCP on n8n: warm explore 1.79× faster, the eight-explore wave 1.28× faster and `initialize` 2× faster, with busy memory 21% lower and idle memory 50% lower; the watcher's one-file re-index is the one loss, 13% slower (779 ms against 689 ms). +The fork's one-file sync is faster on all seven corpora: 8% (discourse) to 63% (n8n), or 1.08× to 2.69×. Full indexing: CPython is 1.71× faster (27.06 s against 46.34 s), n8n 1.32× faster (53.95 s against 71.36 s), pretix 11% faster and Alamofire and discourse 4–5% faster; gin is 8% slower (0.07 s absolute on a 119-file repo) and supabase 11% slower on a graph with 33% more nodes and 12% more edges. The speed costs memory on the smaller corpora — index peaks +50% on gin, +37% on Alamofire, +23% on pretix and +1% on supabase — while it is lower on CPython (−17%), discourse (−12%) and n8n (−27%). MCP on n8n: warm explore 1.75× faster, the eight-explore wave 1.26× faster and `initialize` 1.79× faster, with busy memory 21% lower and idle memory 51% lower; the watcher's one-file re-index is the one loss, 9% slower (766 ms against 702 ms). -The [2026-10-28 Node+Bun campaign](docs/benchmarks/fork-vs-upstream-node-bun-2026-10-27.md) and the [2026-10-30 Node re-measurement](docs/benchmarks/fork-vs-upstream-node-2026-10-30.md) are superseded by this run and kept with their original dates and commits. +The [2026-10-28 Node+Bun campaign](docs/benchmarks/fork-vs-upstream-node-bun-2026-10-27.md), the [2026-10-30 Node re-measurement](docs/benchmarks/fork-vs-upstream-node-2026-10-30.md) and the [first 2026-10-31 run](docs/benchmarks/fork-vs-upstream-2026-10-31.md) are superseded by this run and kept with their original dates and commits. -The fork starts its resolver pool at 12,000 pending references on a full index (150,000 before #590). Below that, it can start the pool part-way through if at least 20,000 unresolved references remain and the estimated main-thread work is at least 0.4 s. An explicit `CODEGRAPH_PARALLEL_RESOLVE_MIN` replaces both gates. The pool sizes itself at about one worker per 2,000 refs, capped at 25,000 refs per worker and by CPU and memory headroom (8 workers on gin, 12 on the larger corpora here); the bulk edge/ref index windows keep their own 150,000-ref floor. Workers spawn while the prerequisite batches resolve on the main thread, then open one snapshot taken after those edges persist (#591). Resolution proceeds in pages of 40,000 references (`CODEGRAPH_RESOLVE_BATCH` overrides), across up to 12 workers (`CODEGRAPH_RESOLVE_WORKERS` sets the count, `0` disables the pool; `CODEGRAPH_NO_PARALLEL_RESOLVE=1` does the same). +The fork starts its resolver pool at 12,000 pending references on a full index (150,000 before #590). Below that, it can start the pool part-way through if at least 20,000 unresolved references remain and the estimated main-thread work is at least 0.4 s. An explicit `CODEGRAPH_PARALLEL_RESOLVE_MIN` replaces both gates. The pool sizes itself at about one worker per 2,000 refs, capped at 25,000 refs per worker and by CPU and memory headroom (8 workers on gin, 12 on the larger corpora here); the bulk edge/ref index windows keep their own 150,000-ref floor. Workers spawn as the pool is gated in and open a pre-prerequisite snapshot, so the prerequisite batches resolve across the pool; a second snapshot is taken once their supertype edges persist and the workers recycle onto it. Resolution proceeds in pages of 40,000 references (`CODEGRAPH_RESOLVE_BATCH` overrides), across up to 12 workers (`CODEGRAPH_RESOLVE_WORKERS` sets the count, `0` disables the pool; `CODEGRAPH_NO_PARALLEL_RESOLVE=1` does the same). **Full index** (`codegraph init`), Node, 2026-10-31; time, peak memory and graph: | Corpus | Upstream | Fork | Fork / upstream | Upstream nodes / edges | Fork nodes / edges | |---|---:|---:|---:|---:|---:| -| gin (Go, 119 files) | 0.84 s, 450 MiB | 0.89 s, 738 MiB | **6% slower** | 2,660 / 8,050 | 3,089 / 8,546 | -| Alamofire (Swift, 129 files) | 1.24 s, 830 MiB | 1.16 s, 1,159 MiB | **7% faster** | 4,600 / 12,512 | 5,614 / 13,682 | -| pretix (Python + JS, 1,472 files) | 9.41 s, 3,480 MiB | 9.13 s, 4,441 MiB | no clear change | 32,783 / 79,319 | 32,799 / 73,769 | -| CPython (C + Python, 3,708 files) | 46.30 s, 7,420 MiB | 31.38 s, 6,431 MiB | **1.47× faster** | 169,618 / 615,814 | 170,311 / 605,273 | -| discourse (Ruby + JS, 20,277 files) | 15.20 s, 4,418 MiB | 15.71 s, 3,850 MiB | **3% slower** | 163,411 / 314,691 | 168,110 / 310,348 | -| supabase (React + Next.js + TS, 10,716 files) | 15.80 s, 5,631 MiB | 21.55 s, 6,257 MiB | **36% slower** | 114,440 / 292,021 | 152,274 / 328,137 | -| n8n (Vue + TS, 24,434 files) | 71.12 s, 12,352 MiB | 70.44 s, 8,659 MiB | **1% faster** | 315,422 / 1,239,521 | 328,519 / 1,236,342 | +| gin (Go, 119 files) | 0.81 s, 496 MiB | 0.88 s, 744 MiB | **8% slower** | 2,660 / 8,050 | 3,089 / 8,546 | +| Alamofire (Swift, 129 files) | 1.23 s, 850 MiB | 1.17 s, 1,168 MiB | **4% faster** | 4,600 / 12,512 | 5,614 / 13,682 | +| pretix (Python + JS, 1,472 files) | 9.13 s, 3,547 MiB | 8.08 s, 4,367 MiB | **11% faster** | 32,783 / 79,319 | 32,799 / 73,769 | +| CPython (C + Python, 3,708 files) | 46.34 s, 7,388 MiB | 27.06 s, 6,168 MiB | **1.71× faster** | 169,618 / 615,814 | 170,311 / 605,273 | +| discourse (Ruby + JS, 20,277 files) | 15.13 s, 4,440 MiB | 14.38 s, 3,906 MiB | **5% faster** | 163,411 / 314,691 | 168,110 / 310,348 | +| supabase (React + Next.js + TS, 10,716 files) | 15.70 s, 5,830 MiB | 17.37 s, 5,877 MiB | **11% slower** | 114,440 / 292,021 | 152,274 / 328,137 | +| n8n (Vue + TS, 24,434 files) | 71.36 s, 12,219 MiB | 53.95 s, 8,867 MiB | **1.32× faster** | 315,422 / 1,239,521 | 328,519 / 1,236,342 | -The fork's larger graphs are part of the gin, Alamofire and supabase difference; pretix and CPython are within 1% on nodes. Peak-memory change, fork against upstream: gin +64%, Alamofire +40%, pretix +28%, CPython −13%, discourse −13%, supabase +11%, n8n −30%. +The fork indexes faster on five of seven corpora, by 4% (Alamofire) to 1.71× (CPython). pretix's 11% is close to its 9% fork spread but clears it. The two losses are gin (8%, 0.07 s absolute on a 119-file repo) and supabase (11% on a graph with 33% more nodes and 12% more edges; per node the fork is faster). The fork's larger graphs are part of the gin, Alamofire and supabase difference; pretix and CPython are within 1% on nodes. Peak-memory change, fork against upstream: gin +50%, Alamofire +37%, pretix +23%, CPython −17%, discourse −12%, supabase +1%, n8n −27%. -**Fork-internal index regression (unattributed).** The fork's index is slower than its own 2026-10-28 build at identical graph sizes for supabase (15.6 s → 21.55 s, +38%, 152,274 nodes both runs) and n8n (49.4 s → 70.44 s, +43%, 328,519 nodes both runs). The other fork index times also rose against that table on the same corpora: CPython 25.5 → 31.38 s, pretix 7.48 → 9.13 s, discourse 13.2 → 15.71 s; Alamofire (1.19 → 1.16 s) and gin (1.02 → 0.89 s) improved. The cause is not attributed; the [2026-10-30 re-measurement](docs/benchmarks/fork-vs-upstream-node-2026-10-30.md) required a bisect before the index results were used to decide fork maintenance, and it has not been recorded. The supabase graph's 33% more nodes is a graph-size caveat, not an explanation of the fork-internal slowdown. +**The 2026-10-31 regression is closed.** The first run that day showed supabase 36% and n8n level, but the fork's own index had slowed at matched graph sizes. A bisect named #591 (`05077514`): it moved the prerequisite (imports/extends/implements) batches to the main-thread kernel so the workers could open one post-prerequisite snapshot, serializing that phase on one core — supabase 17.27/17.46 s to 21.84/22.51 s, n8n 54.46/57.22 s to 70.34/70.70 s, index CPU ~630% to ~460%. #590 alone was inside the pre-#590 spread. `c63e3d7c` opens the pool on the pre-prerequisite snapshot again and recycles it onto a second snapshot after the supertypes persist; the interleaved A/B at fixed graph sizes was supabase 21.60/21.88 s → 17.11/17.27 s and n8n 70.72/73.17 s → 53.58/53.91 s. The cost is peak memory on n8n: 8.8–8.9 GiB against 8.2–8.5 GiB, still below upstream's 11.9 GiB. Evidence: [the fix's measurement doc](docs/benchmarks/fork-vs-upstream-2026-10-31b.md) and the [pre-fix run](docs/benchmarks/fork-vs-upstream-2026-10-31.md). **One-file sync** (edit one file, `codegraph sync`), Node, 2026-10-31: | Corpus | Upstream | Fork | Upstream / fork | |---|---:|---:|---:| | gin | 0.37 s | 0.30 s | **1.23×** | -| Alamofire | 0.41 s | 0.29 s | **1.41×** | -| pretix | 1.52 s | 0.92 s | **1.65×** | -| CPython | 5.21 s | 2.56 s | **2.03×** | -| discourse | 1.04 s | 0.99 s | **1.04×** | -| supabase | 7.41 s | 3.44 s | **2.15×** | -| n8n | 11.34 s | 4.22 s | **2.69×** | +| Alamofire | 0.39 s | 0.28 s | **1.39×** | +| pretix | 1.50 s | 0.93 s | **1.62×** | +| CPython | 5.21 s | 2.54 s | **2.05×** | +| discourse | 1.06 s | 0.98 s | **1.08×** | +| supabase | 7.49 s | 3.45 s | **2.17×** | +| n8n | 11.37 s | 4.23 s | **2.69×** | -Every cell clears its run-to-run spread; discourse's 4% is the smallest. +Every cell clears its run-to-run spread; discourse's 8% is the smallest. Both builds rebuild the links inferred from events, callbacks and function pointers after a sync, so a sync ends with the graph a full index would build ([colbymchenry/codegraph#1988](https://github.com/colbymchenry/codegraph/issues/1988)). Upstream reruns every inference pass inside each sync that touches an inferred link; the fork reruns the same passes a full index runs once edits pause. `CODEGRAPH_SYNC_RESYNTHESIS=0` turns the fork's rebuild off, trading it for stale inferred links. @@ -246,17 +246,17 @@ Both builds rebuild the links inferred from events, callbacks and function point | | Upstream | Fork | |---|---:|---:| -| `initialize` answered | 117 ms | 58 ms | -| Start to first explore answer | 2.96 s | 2.64 s | -| Explore, warm median | 243 ms | 136 ms | -| Explore, p90 | 286 ms | 177 ms | -| 8 explores at once | 2.41 s | 1.87 s | -| Memory while busy | 3.81 GiB | 3.00 GiB | -| Edited file re-indexed by the watcher | 689 ms | 779 ms | -| Memory at idle | 4.38 GiB | 2.18 GiB | +| `initialize` answered | 115 ms | 64 ms | +| Start to first explore answer | 2.91 s | 2.70 s | +| Explore, warm median | 240 ms | 138 ms | +| Explore, p90 | 282 ms | 178 ms | +| 8 explores at once | 2.38 s | 1.88 s | +| Memory while busy | 3.81 GiB | 3.02 GiB | +| Edited file re-indexed by the watcher | 702 ms | 766 ms | +| Memory at idle | 4.46 GiB | 2.19 GiB | | CPU at idle, share of one core | 0.40% | 0.20% | -The idle row is the process tree's RSS at the harness's idle sample, 70 s after the last query. The same measure across runs: **3.58 GiB** on 2026-10-27 (`bc27fdb4`, the old README row), **4.21 GiB** on 2026-10-28 (`5784e289`, the clean re-measure), **19.7 GiB** on 2026-10-30 (`a9dc5a61`, a real regression), and **2.18 GiB** now (`cf2a37a5`, #593). The “20 GB → 2 GB” statement is the 2026-10-30 to 2026-10-31 change on this harness, not a different definition from the earlier rows. +The idle row is the process tree's RSS at the harness's idle sample, 70 s after the last query. The same measure across runs: **3.58 GiB** on 2026-10-27 (`bc27fdb4`, the old README row), **4.21 GiB** on 2026-10-28 (`5784e289`, the clean re-measure), **19.7 GiB** on 2026-10-30 (`a9dc5a61`, a real regression), **2.13 GiB** in the first 2026-10-31 run (`cf2a37a5`, #593) and **2.19 GiB** now (`c63e3d7c`). The “20 GB → 2 GB” statement is the 2026-10-30 to 2026-10-31 change on this harness, not a different definition from the earlier rows. **CLI startup** on gin (`hyperfine`, mean of 20 runs), 2026-10-27, fork `bc27fdb4` against upstream `69af19ac`; not re-measured on 2026-10-31: @@ -283,8 +283,8 @@ The [benchmark](#benchmark-results) and [speed](#built-for-speed--the-rust-kerne - **Larger database:** 12–20% bigger on six of the seven corpora above, and 38% on supabase, which has 1,978 Markdown files. It holds Markdown, binding rows and more nodes. - **Fewer edges on some projects:** 7% fewer on pretix and 1.7% fewer on CPython, because the fork declines links it cannot confirm. Some of those were correct links. -- **More memory while indexing on four projects:** in the 2026-10-31 comparison the fork's full index peaks higher on gin (+64%), Alamofire (+40%), pretix (+28%) and supabase (+11%) — the in-process resolver-pool workers used on small and mid-size projects — while it is lower on CPython (−13%), discourse (−13%) and n8n (−30%). -- **MCP watcher sync:** the daemon's one-file re-index is 13% slower (779 ms against 689 ms) because it also runs the deferred synthesized-edge refresh; the CLI one-file sync is faster on all seven corpora. +- **More memory while indexing on four projects:** in the 2026-10-31 comparison the fork's full index peaks higher on gin (+50%), Alamofire (+37%), pretix (+23%) and supabase (+1%) — the in-process resolver-pool workers used on small and mid-size projects, plus the second pool snapshot on large ones — while it is lower on CPython (−17%), discourse (−12%) and n8n (−27%). +- **MCP watcher sync:** the daemon's one-file re-index is 9% slower (766 ms against 702 ms) because it also runs the deferred synthesized-edge refresh; the CLI one-file sync is faster on all seven corpora. --- diff --git a/assets/fork-vs-upstream-performance.svg b/assets/fork-vs-upstream-performance.svg index a38cba68d..ec1d74ec3 100644 --- a/assets/fork-vs-upstream-performance.svg +++ b/assets/fork-vs-upstream-performance.svg @@ -4,7 +4,7 @@ Fork vs upstream on Node — seven corpora, 2026-10-31 -Upstream cd04b21a · fork cf2a37a5 · Node 24.22.2 · Node-only run +Upstream cd04b21a · fork c63e3d7c · Node 24.22.2 · Node-only run Index: median of 2 runs, fork / upstream (lower is better). Sync: median of 4 runs, upstream / fork (higher is better). Full index @@ -18,26 +18,26 @@ 1.5× gin (Go) - -1.06× + +1.08× Alamofire (Swift) - -0.94× + +0.96× pretix (Python) - -0.97× + +0.89× CPython (C/Py) - -0.68× + +0.58× discourse (Ruby) - -1.03× + +0.95× supabase (TS) - -1.36× + +1.11× n8n (Vue/TS) - -0.99× + +0.76× One-file sync 0.5× @@ -55,24 +55,24 @@ 1.23× Alamofire (Swift) - -1.41× + +1.39× pretix (Python) - -1.65× + +1.62× CPython (C/Py) - -2.03× + +2.05× discourse (Ruby) - -1.04× + +1.08× supabase (TS) - -2.15× + +2.17× n8n (Vue/TS) - -2.69× + +2.69× Exact ratios; the tables mark which gaps clear the larger arm's run-to-run spread. -Method, spreads, graph sizes, admission receipts: docs/benchmarks/fork-vs-upstream-2026-10-31.md +Method, spreads, graph sizes, admission receipts: docs/benchmarks/fork-vs-upstream-2026-10-31b.md CodeGraph fork · bompus/codegraph · measured under the host benchmark lock \ No newline at end of file diff --git a/docs/benchmarks/fork-vs-upstream-2026-10-31.md b/docs/benchmarks/fork-vs-upstream-2026-10-31.md index c57b20d74..27f69d933 100644 --- a/docs/benchmarks/fork-vs-upstream-2026-10-31.md +++ b/docs/benchmarks/fork-vs-upstream-2026-10-31.md @@ -1,5 +1,7 @@ # Fork against upstream on Node, 2026-10-31 +> **Superseded for index, sync and MCP by [fork-vs-upstream-2026-10-31b.md](fork-vs-upstream-2026-10-31b.md)**, measured after the #591 prerequisite-pool fix. This page records the pre-fix run and the regression it exposed. + A Node-only comparison at the reconciled fork head against upstream `main` at the fork's catch-up target. This supersedes the 2026-10-27/28 Node+Bun campaign and the 2026-10-30 Node re-measurement for the figures it covers; those documents keep their original dates and commits. - Upstream: `cd04b21a` (`fix(python): a base's type argument is no supertype…`). @@ -26,9 +28,9 @@ Two earlier attempts in this window were discarded: the admission script compare Peak-memory change, fork against upstream: gin +64%, Alamofire +40%, pretix +28%, CPython −13%, discourse −13%, supabase +11%, n8n −30%. The fork's larger graphs on gin, Alamofire and supabase are part of the memory and time difference; pretix and CPython are within 1% on nodes. -### Fork-internal index regression (unattributed) +### Fork-internal index regression (bisected to #591; fixed) -The fork's own index is slower than its 2026-10-28 build at identical graph sizes for supabase (15.6 s → 21.55 s, +38%, 152,274 nodes both runs) and n8n (49.4 s → 70.44 s, +43%, 328,519 nodes both runs). Other fork index times also rose against that table on the same corpora: CPython 25.5 → 31.38 s, pretix 7.48 → 9.13 s, discourse 13.2 → 15.71 s; Alamofire (1.19 → 1.16 s) and gin (1.02 → 0.89 s) improved. The cause is not attributed. The [2026-10-30 re-measurement](fork-vs-upstream-node-2026-10-30.md) required a bisect before the index results are used to decide fork maintenance, and none is recorded. The supabase graph's 33% more nodes is a graph-size caveat, not an explanation of the fork-internal slowdown. +The fork's own index was slower at identical graph sizes. The regression is #591 (`05077514`): a matched-host bisect (same corpus state, fresh index per run) measured supabase at 17.27/17.46 s on `216093df` (pre-#590) against 21.84/22.51 s on `05077514`, and n8n at 54.46/57.22 s against 70.34/70.70 s, with n8n's index CPU falling from ~630% to ~460%. #590 alone (`abc462ea`) was inside the pre-#590 spread. The 2026-10-30 re-measurement's line, "bisect before the index results are used to decide fork maintenance", is answered: #591 moved the prerequisite batches to the main-thread kernel, serializing them; `c63e3d7c` restores the two-phase pool. The comparison against the 2026-10-28 build looks larger (supabase 15.6 → 21.55 s, n8n 49.4 → 70.44 s) because those campaigns ran on different days; the matched-host bisect is the attribution. The [re-measurement after the fix](fork-vs-upstream-2026-10-31b.md) puts supabase at 17.37 s and n8n at 53.95 s. ## One-file sync @@ -55,9 +57,9 @@ The fork syncs faster on all seven corpora; the smallest gap, discourse, is smal | Explore, warm median | 243 ms | 136 ms | 1.79x faster | | Explore, p90 | 286 ms | 177 ms | 1.62x faster | | 8 explores at once | 2.41 s | 1.87 s | 1.28x faster | -| Memory while busy (process tree) | 3.81 GiB | 3.00 GiB | 21% lower | +| Memory while busy (process tree) | 3.72 GiB | 2.93 GiB | 21% lower | | Edited file re-indexed by the watcher | 689 ms | 779 ms | **13% slower** | -| Memory at idle (process tree, 70 s after last query) | 4.38 GiB | 2.18 GiB | 50% lower | +| Memory at idle (process tree, 70 s after last query) | 4.27 GiB | 2.13 GiB | 50% lower | | CPU at idle, share of one core | 0.40% | 0.20% | lower | The watcher sync is the one MCP loss. The CLI's one-file sync is faster (above) because it resolves in the foreground; the daemon's watcher also runs the deferred synthesized-edge refresh, and that pass is heavier in the fork. @@ -69,9 +71,9 @@ Four Node figures exist for n8n and they are the same measure — the process tr - 2026-10-27, fork `bc27fdb4`: 3.58 GiB (the old README row). - 2026-10-28, fork `5784e289`: 4.21 GiB (the clean re-measure). - 2026-10-30, fork `a9dc5a61`: 19.7 GiB. This was a real regression on that build, not a different metric. -- 2026-10-31, fork `cf2a37a5`: 2.18 GiB, after #593 shares one native resolver across the importer name-match rescan. +- 2026-10-31, fork `cf2a37a5`: 2.13 GiB, after #593 shares one native resolver across the importer name-match rescan. -The earlier "20 GB → 2 GB" statement is the 2026-10-30 to 2026-10-31 change on this harness. It is not comparable to a JS-heap-only figure, and the 2026-10-28 3.58 GiB row should not be read as contradicting it. +The earlier "20 GB → 2 GB" statement is the 2026-10-30 to 2026-10-31 change on this harness. It is not comparable to a JS-heap-only figure, and the 2026-10-27 3.58 GiB row should not be read as contradicting it. ## What was not measured diff --git a/docs/benchmarks/fork-vs-upstream-2026-10-31b.md b/docs/benchmarks/fork-vs-upstream-2026-10-31b.md new file mode 100644 index 000000000..87195b8a9 --- /dev/null +++ b/docs/benchmarks/fork-vs-upstream-2026-10-31b.md @@ -0,0 +1,100 @@ +# Fork against upstream on Node, 2026-10-31, after the prerequisite-pool fix + +A Node-only comparison at the fork head that fixes the index regression the first 2026-10-31 run exposed. It supersedes that run for every figure it covers; [fork-vs-upstream-2026-10-31.md](fork-vs-upstream-2026-10-31.md) keeps the pre-fix data and the regression record. + +- Upstream: `cd04b21a` (`fix(python): a base's type argument is no supertype…`). +- Fork: `c63e3d7c` (`perf(resolve): pool the prerequisite phase again`), on top of `0d4c48dc`. +- Host: 24-core bare-metal Ubuntu, 122 GiB, Node.js 24.22.2, release kernel. +- Corpora: gin, Alamofire, pretix, CPython, discourse, supabase, n8n. +- Arms in mirrored order; full index is the median of two runs, one-file sync the median of four (two edits, two mirrored passes). Peak memory is `/usr/bin/time -v` maximum resident set. +- A difference counts only when the fork/upstream gap exceeds the larger of the two arms' `(max − min) / median` for that metric. Spreads are listed per cell; no fixed band is used. +- The host benchmark lock was held for the campaign and the admission check passed before it: CPU 0.8 busy cores of 4 allowed, 98.9 GiB available of 8 required, memory-full PSI 0 ms of 10, I/O-full PSI 9.4 ms of 100. Receipt: `~/cg-scratch/bench-2026-10-31d/admission.json`; raw runs `index-sync.tsv`, `mcp.jsonl` beside it. + +## The regression this run fixes + +The first 2026-10-31 run at `cf2a37a5` was 36% slower than upstream on supabase and level on n8n, but the fork's own index had slowed at matched graph sizes against the 2026-10-28 build. A bisect on the same host and corpus state (fresh index per run) names the commit: + +| Commit | What it is | supabase | n8n | n8n index CPU | +|---|---|---:|---:|---:| +| `216093df` | parent of #590 | 17.27 / 17.46 s | 54.46 / 57.22 s | 630–636% | +| `abc462ea` | #590 (pool sizing) | 17.98 / 18.29 s | 55.89 / 58.48 s | 626–627% | +| `05077514` | #591 (early spawn, one snapshot) | 21.84 / 22.51 s | 70.34 / 70.70 s | 458–462% | + +`#590` alone is inside the pre-#590 spread; its two rows above ran at 1-minute load 9–11, and both corpora sit above the 25k-refs-per-worker cap, so the sizing change does not move their worker count. `#591` is the regression: it moved the prerequisite (`imports`/`extends`/`implements`) batches onto the main-thread kernel so the workers could open one post-prerequisite snapshot. That saved a snapshot copy and serialized the whole prerequisite phase on one core — the index CPU drop from ~630% to ~460% is that phase losing its workers. + +The fix (`c63e3d7c`) opens the workers on the pre-prerequisite snapshot so the prerequisite refs resolve across the pool, then takes a second snapshot once their supertype edges persist and recycles the workers onto it. It keeps #591's early spawn, single worker-script lookup, `loaded` first message and synthesis-boundary open, and refreshes before synthesis when a sweep had no calls batch. An interleaved A/B on the same host and corpora: + +| Corpus | `0d4c48dc` (before) | `c63e3d7c` (after) | +|---|---:|---:| +| supabase | 21.60 / 21.88 s | 17.11 / 17.27 s | +| n8n | 70.72 / 73.17 s | 53.58 / 53.91 s | + +The cost of the second snapshot generation is peak memory: n8n's index peak is 8.8–8.9 GiB here against 8.2–8.5 GiB before, still 27% below upstream's 11.9 GiB. + +## Full index + +| Corpus | Upstream | Fork | Fork / upstream | Verdict | Upstream RSS | Fork RSS | Nodes up/fork | Edges up/fork | +|---|---:|---:|---:|---|---:|---:|---:|---:| +| gin | 0.81 s | 0.88 s | 1.08 | **8% slower** (spreads 1.2% / 0.0%) | 496 MiB | 744 MiB | 2,660 / 3,089 | 8,050 / 8,546 | +| Alamofire | 1.23 s | 1.17 s | 0.96 | **4% faster** (1.6% / 0.9%) | 850 MiB | 1,168 MiB | 4,600 / 5,614 | 12,512 / 13,682 | +| pretix | 9.13 s | 8.08 s | 0.89 | **11% faster** (0.2% / 9.0%) | 3,547 MiB | 4,367 MiB | 32,783 / 32,799 | 79,319 / 73,769 | +| CPython | 46.34 s | 27.06 s | 0.58 | **1.71x faster** (0.1% / 0.2%) | 7,388 MiB | 6,168 MiB | 169,618 / 170,311 | 615,814 / 605,273 | +| discourse | 15.13 s | 14.38 s | 0.95 | **5% faster** (0.5% / 0.1%) | 4,440 MiB | 3,906 MiB | 163,411 / 168,110 | 314,691 / 310,348 | +| supabase | 15.70 s | 17.37 s | 1.11 | **11% slower** (2.7% / 2.4%), 33% more nodes | 5,830 MiB | 5,877 MiB | 114,440 / 152,274 | 292,021 / 328,137 | +| n8n | 71.36 s | 53.95 s | 0.76 | **1.32x faster** (0.5% / 0.2%) | 12,219 MiB | 8,867 MiB | 315,422 / 328,519 | 1,239,521 / 1,236,342 | + +The fork indexes faster on five of seven corpora, by 4% (Alamofire) to 1.71× (CPython). pretix's 11% is close to its 9.0% fork spread but clears it. The two losses are gin (8%, 0.07 s absolute on a 119-file repo) and supabase (11% on a graph with 33% more nodes and 12% more edges; per node the fork is faster). Peak-memory change, fork against upstream: gin +50%, Alamofire +37%, pretix +23%, CPython −17%, discourse −12%, supabase +1%, n8n −27%. + +## One-file sync + +| Corpus | Upstream | Fork | Upstream / fork | Verdict | +|---|---:|---:|---:|---| +| gin | 0.37 s | 0.30 s | 1.23 | **19% faster** (spreads 0.0% / 3.3%) | +| Alamofire | 0.39 s | 0.28 s | 1.39 | **28% faster** (0.0% / 3.6%) | +| pretix | 1.50 s | 0.93 s | 1.62 | **38% faster** (0.7% / 1.1%) | +| CPython | 5.21 s | 2.54 s | 2.05 | **51% faster** (1.2% / 0.8%) | +| discourse | 1.06 s | 0.98 s | 1.08 | **8% faster** (3.8% / 3.1%) | +| supabase | 7.49 s | 3.45 s | 2.17 | **54% faster** (5.3% / 0.9%) | +| n8n | 11.37 s | 4.23 s | 2.69 | **63% faster** (0.7% / 2.1%) | + +The fork syncs faster on all seven corpora, unchanged by the pool fix; the smallest gap, discourse, clears its widest spread. + +## MCP server on n8n + +`codegraph serve --mcp`; memory and CPU summed over the server and the daemon. Two runs per arm; medians. + +| | Upstream | Fork | Change | +|---|---:|---:|---:| +| `initialize` answered | 115 ms | 64 ms | 1.79x faster | +| Start to first explore answer | 2.91 s | 2.70 s | 7% faster | +| Explore, warm median | 240 ms | 138 ms | 1.75x faster | +| Explore, p90 | 282 ms | 178 ms | 1.58x faster | +| 8 explores at once | 2.38 s | 1.88 s | 1.26x faster | +| Memory while busy (process tree) | 3.81 GiB | 3.02 GiB | 21% lower | +| Edited file re-indexed by the watcher | 702 ms | 766 ms | **9% slower** | +| Memory at idle (process tree, 70 s after last query) | 4.46 GiB | 2.19 GiB | 51% lower | +| CPU at idle, share of one core | 0.40% | 0.20% | lower | + +The watcher sync is the one MCP loss. The CLI's one-file sync is faster (above) because it resolves in the foreground; the daemon's watcher also runs the deferred synthesized-edge refresh, and that pass is heavier in the fork. + +### Idle memory: which number is which + +The Node figures for n8n are the same measure — the process tree's RSS at the harness's idle sample, 70 s after the last query — at different heads: + +- 2026-10-27, fork `bc27fdb4`: 3.58 GiB (the old README row). +- 2026-10-28, fork `5784e289`: 4.21 GiB (the clean re-measure). +- 2026-10-30, fork `a9dc5a61`: 19.7 GiB. This was a real regression on that build, not a different metric. +- 2026-10-31, fork `cf2a37a5`: 2.18 GiB, after #593 shares one native resolver across the importer name-match rescan. +- 2026-10-31, fork `c63e3d7c`: 2.19 GiB, this run. + +The earlier "20 GB → 2 GB" statement is the 2026-10-30 to 2026-10-31 change on this harness. It is not comparable to a JS-heap-only figure, and the 3.58 GiB row should not be read as contradicting it. + +## What was not measured + +- Bun. The 2026-10-28 Node+Bun campaign remains the source for Bun; this run is Node-only. +- CLI startup. The 2026-10-27 hyperfine table at fork `bc27fdb4` against upstream `69af19ac` remains the source and is dated in the README. +- The 40+ upstream commits' individual effects. This is a whole-tree comparison, not a per-commit attribution. + +## Reproducing + +The raw run is `~/cg-scratch/bench-2026-10-31d/` (`index-sync.tsv`, `mcp.jsonl`, `admission.json`, logs), and the bisect/A-B receipts are in `~/cg-scratch/bisect-index-20261031/`. The scripts are `scripts/benchmarks/runtime/run-index-sync.sh`, `run-mcp.sh`, `index-sync.sh`, `mcp-bench.mjs` and the card generator `render-node-cards.py`; the corpus list is `~/cg-scratch/index-sync-2026-10-28.list`. The upstream arm needs its own production `node_modules` and `npm run copy-assets` (WASM grammars); the fork uses the native kernel. diff --git a/docs/benchmarks/fork-vs-upstream-node-2026-10-30.md b/docs/benchmarks/fork-vs-upstream-node-2026-10-30.md index 05f621edf..4377c4039 100644 --- a/docs/benchmarks/fork-vs-upstream-node-2026-10-30.md +++ b/docs/benchmarks/fork-vs-upstream-node-2026-10-30.md @@ -40,6 +40,8 @@ The morning campaign at `bench-2026-10-30` measured the same corpus state and th This is not attributed: the candidate deltas are #590's smooth worker sizing, #591's main-thread prerequisite phase and single snapshot, or an interaction with the host load. Before the fork's index results are used to decide whether to keep maintaining it, bisect n8n between `afcdb6b6`, #590's merge `abc462ea`, and #591's head. +**Attributed 2026-10-31.** The bisect found #591 (`05077514`): on the same host and corpus state, supabase moved from 17.27/17.46 s at `216093df` (pre-#590) to 21.84/22.51 s, and n8n from 54.46/57.22 s to 70.34/70.70 s, while n8n's index CPU fell from ~630% to ~460%. #590 alone was inside the pre-#590 spread. The prerequisite batches ran on one main-thread kernel instead of the pool; the fix restores the two-phase pool. The post-fix re-measurement is [fork-vs-upstream-2026-10-31b.md](fork-vs-upstream-2026-10-31b.md). + ## MCP The MCP arm (`codegraph serve --mcp`, eight concurrent explores, watcher sync, idle RSS/CPU after the 60 s retire window) was run after this document's index/sync section; its numbers are recorded in `mcp.jsonl` next to the raw TSV. diff --git a/scripts/benchmarks/runtime/render-node-cards.py b/scripts/benchmarks/runtime/render-node-cards.py index 94aa0803c..6f449e1a8 100644 --- a/scripts/benchmarks/runtime/render-node-cards.py +++ b/scripts/benchmarks/runtime/render-node-cards.py @@ -107,7 +107,7 @@ def px(v): panel(pad, 'Full index', 'index') panel(pad * 2 + panel_w, 'One-file sync', 'sync') svg.append(f'Exact ratios; the tables mark which gaps clear the larger arm\'s run-to-run spread.') -svg.append(f'Method, spreads, graph sizes, admission receipts: docs/benchmarks/fork-vs-upstream-2026-10-31.md') +svg.append(f'Method, spreads, graph sizes, admission receipts: docs/benchmarks/fork-vs-upstream-2026-10-31b.md') svg.append(f'CodeGraph fork · bompus/codegraph · measured under the host benchmark lock') svg.append('') open(args.out, 'w').write('\n'.join(svg))