perf(config): queries stop raising an error per result path when codegraph.json is absent

Merged
BO
14d ago
+28-2

Warm codegraph_explore calls are 18-19% faster on gin and nest and 5.6% faster on n8n when the project has no codegraph.json (gin 25.6 to 20.9 ms, nest 50.7 to 41.1 ms, n8n 238 to 225 ms, median per call).

Warm explore call: gin 25.6 to 20.9 ms, nest 50.7 to 41.1 ms, n8n 238 to 225 ms; gin with a codegraph.json is unchanged at about 21 ms

Metric (median of 5 runs per build)BeforeAfterBenefitBandResult
gin explore call (ms)25.620.918% lower (1.23x)18.3%lower
nest explore call (ms)50.741.119% lower (1.23x)6.1%lower
n8n explore call (ms)238.4225.15.6% lower (1.06x)4.3%lower
gin + codegraph.json explore call (ms)21.021.10.5% higher12.0%no clear change
nest, server CPU over the timed calls (ms)4,8504,02017% lower (1.21x)3.0%lower
n8n, server CPU over the timed calls (ms)31,44029,6305.8% lower (1.06x)3.5%lower
gin, server CPU over the timed calls (ms)3,6903,17014% lower (1.16x)21.5%no clear change
peak memory, cold start to first answer, all four projectsno clear change

The ranking step that pushes deprioritized paths down asks for the project's codegraph.json once per result path. The file is mtime-cached, but the check for its existence used fs.statSync, which raises ENOENT for a missing file, so a project with no config built and caught an error for every result path. statSync now takes { throwIfNoEntry: false }; an unreadable path other than a missing one is still treated as no config. Edits to the file are still picked up without a restart. This was the one concrete lead from a read-only profile of where the engine spends its time (statSync was 13.8% of busy CPU on a gin query run; 0% with this change in a separate profile run).

Evidence

  • Workload: one serve --mcp process per run on an index built once, answering the same call sequence (explore queries, then search, callers, callees, impact and node for 5 symbols; 3 discarded passes, 10 timed). Baseline is fork/consolidated at 6c8e8606; candidate is the same plus this diff, so dist/project-config.js is the only built file that differs. Same Node 24.21.0, same native kernel build, same indexes and query files (hashes recorded).
  • Design: baseline, candidate and a second copy of the baseline (the run-to-run noise floor), rotated over 5 accepted rounds on a 16-vCPU WSL2 host under the host benchmark lock, normal priority. Band is the larger of 3% and the second baseline's range; a change counts only if it is outside the band and the runs do not overlap.
  • Same answers: every response is byte-identical across the three builds (hash of all timed responses) on all four projects.
  • Gin's band is wide (18.3%) because one noise-floor run took 29.2 ms; all five candidate runs (19.0 to 21.2 ms) are below all ten baseline runs (24.4 to 29.2 ms). The 5.6% on n8n is outside its 4.3% band and the runs do not overlap, but it is a small margin.
  • Rejected: one round was discarded because a swap counter moved by one page, and one start of round was refused at load 2.13; the round was rerun. Raw runs and the receipt are kept.
  • Checks: __tests__/deprioritize-config.test.ts (18 tests, two new: deleting and re-adding the config file, and a root whose parent is a file), plus the include, exclude, extension, sessions, workspace and Pascal config tests: 162 passed. readme-sync and prepare-release tests pass. The two new tests pass on the old code as well, since they guard behavior kept, not the speed.
  • README rows checked: lines 193, 195 and 264 quote the fork's warm-question time against upstream (gin 26 ms vs 24 ms, 3.2x on n8n). They come from the 2026-10-12 fork-vs-upstream run and are not edited here; they likely understate the fork now, and a fresh comparison against upstream is needed to update them. No feature, command, tool, language, agent target or config key changed.

Not covered

Bun, Windows, indexing and sync times, and any comparison against upstream. Peak memory and cold start did not change within the band.


Summary by cubic

Speeds up codegraph_explore and other queries in projects without a codegraph.json by stopping the per-result-path error that was raised and caught when the config file was missing.

  • fs.statSync now uses throwIfNoEntry: false, so a missing file returns undefined instead of throwing ENOENT.
  • Missing-file checks now return the default config directly, matching prior behavior while avoiding error construction per result path.
  • Other unreadable paths (permissions, ENOTDIR) are still treated as no config.
  • Deletions, additions, and edits to codegraph.json continue to be picked up without a restart.
  • Warm explore calls measure 18–19% faster on gin and nest and 5.6% faster on n8n (median, on the host benchmark protocol); results are byte-identical and peak memory and cold start are unchanged.

Tests

  • Adds coverage for deleting and re-adding the config file, and for a root whose parent is a file (ENOTDIR).
  • Full suite passes with 162 tests, including the new cases.

Summary by CodeRabbit

  • Bug Fixes
    • Projects without a codegraph.json file can now be checked without generating an error for each query result.
    • Changes to the configuration file—including editing, adding, or deleting it—are detected without restarting.
    • Configuration loading now returns an empty configuration when the project’s parent path is a file.