perf(extraction): read the next file batch while the current one is dispatched

Merged
BO
13d ago
+42-34

Indexing a large project spends a median 3–4% less time in its parse loop (supabase 5.85 s to 5.67 s, n8n 15.48 s to 14.85 s) because the next batch of files is read while the current one is being parsed. The gain does not reach the whole index time on any project I measured, so this is a small change in one phase, not a faster init.

codegraph init, median of 4 runs per buildParse loop beforeParse loop afterParse-loop benefitWhole index beforeWhole index afterWhole-index change
Alamofire (small)0.37 s0.36 s2.1% (ranges overlap)1.69 s1.73 sno clear change (2.1% slower)
gin (Go, small)0.25 s0.23 s7.1% of a 0.25 s phase (ranges overlap)1.06 s1.04 sno clear change (1.9% faster)
pretix (mid)2.57 s2.48 s3.4% (ranges overlap)10.26 s9.93 sno clear change (3.1% faster, ranges overlap)
supabase (large)5.85 s5.67 s3.1% (ranges do not overlap)19.27 s19.25 sno clear change
n8n (largest)15.48 s14.85 s4.0% (ranges do not overlap)69.89 s68.95 sno clear change (1.3% faster)

Peak memory differs by +0.2% to +0.9% (no clear change), and the node and edge counts plus an md5 of the ordered node and edge rows are identical in every run on every project. Builds f045e5f7 and the same with this change; 40 runs, 0 rejected, order B C C B B C C B per project, declared before the runs. Under the rule I declared there ("land only when whole-index time is clear") this was rejected; I am landing it because the parse-loop gain is real on the two largest projects, nothing got worse beyond noise, and the change is small.

What changes

The parse loop read 10 files, waited, then handed them to the parser pool, so the pool sat idle during every read. readBatch is now a function; the loop starts reading batch n+1 before it dispatches batch n. At most one extra batch of file contents is held in memory (peak memory stayed within 1% in the runs above). The read function is unchanged and never rejects, so an abort in the middle leaves no unhandled error.

Checks

  • Full vitest suite on this head: 702 files, 8,952 passed, 39 skipped, 0 failed; no Native stack trace or Worker exited unexpectedly in the raw output.
  • README: checked "About this fork"; no row mentions the parse loop's read step. CHANGELOG entry added under Fixes.

Not covered: why overlapping the read recovers only part of the 6–9% the stage timers allowed (the UTF-8 decode and path validation still run on the main thread); Bun; Windows and macOS.


Summary by cubic

Speeds up indexing by reading the next batch of files while the current batch is being parsed, so the parser pool no longer sits idle during reads. Parse-loop time falls a median 3–4% on large projects (supabase 5.85 s to 5.67 s, n8n 15.48 s to 14.85 s); whole-index time stays within run-to-run variation, graph output is identical, and peak memory stays within 1%. At most one extra batch of file contents is held in memory, and the read function still never rejects.

  • Reading the next batch is cancelled once indexing is aborted, so no new reads start after an abort.

Summary by CodeRabbit

  • Performance
    • File reading now overlaps with parsing, reducing median parsing time by 3–4% on two large projects. Overall indexing time remained within normal run-to-run variation across five projects.
    • Index results are unchanged, and peak memory use varied by less than 1% from previous behavior. At most one additional batch of file contents is held in memory during indexing.