Skip to content

perf: keep multi-file appends on the incremental path (#99) - #111

Merged
Carl723000 merged 3 commits into
ClaudeCodeUsage:mainfrom
rsyuzyov:perf/multi-file-append-fast-path
Sep 22, 2026
Merged

Carl723000 merged 3 commits into
ClaudeCodeUsage:mainfrom
rsyuzyov:perf/multi-file-append-fast-path

Conversation

@rsyuzyov

Copy link
Copy Markdown
Contributor

Second half of the CPU path in #99, on top of #109 (independent — this one stands alone, but the profile in #109 is what pointed here).

Problem

With #109 merged, the extension host on this machine still held a core. The profile moved rather than cleared: 74% addToBucket, 25% analyzeLine. Not a parsing cost — a volume one. The diagnostics say it plainly:

files(discovered=644 changed=3 reused=641)
io(bytes=1 398 890 034  lines=449 046)
incremental(bodies=644)
ms(manifest=0.87  read-parse=55 824  total=56 729)

Three files changed; 644 bodies re-read, 1.4 GB, 56–86 s per refresh. With startup, focus, watch and poll all triggering, refreshes never finish before the next one is due.

The cause is the content-analysis gate:

const soleSourcePlan = sourcePlans.length === 1 && plans.length === 1 ? sourcePlans[0] : undefined;
const safeTailAppend = Boolean(soleSourcePlan?.kind === 'append' && ...);
const unsafeSourceMutation = deletions.length > 0 || moves.length > 0 ||
  (sourcePlans.length > 0 && !safeTailAppend);

Exactly one changed file. A machine running several agents appends to several transcripts between two refreshes, so the gate never holds and replaceWithFullAnalysisPlans() turns all 644 files into rebuild plans. One user with one session gets the fast path; anyone running concurrent sessions gets a full rebuild forever.

Fix

The application side already loops over parsedPlans, so only the gate limited this. Allowing N pure tail appends needs two ordering guarantees the single-file case never exercised:

  1. Parse appends in full-scan order. The first file to carry a UUID owns it, so parse order must match the full loader's. The comparator returned 0 for two appends (if (left.kind === 'append') return 0), leaving discovery order to decide — which is exactly what the existing test concurrent appends resolve new content UUID owners in full-scan file order forbids. That test failed with the gate opened and passes once the comparator orders appends by timestamp.
  2. Attribute each new UUID to the file that carries it, earliest first, instead of parsedPlans[0].fileId — correct while only one plan could ever reach that code, wrong as soon as two can.

The per-file UUID ownership check that forces a rebuild now runs for every appended file instead of only a lone one.

Measurement

Real transcripts, 15 files / 34 MB, three of them appended (frozen clock so the analysis cutoff does not move between passes):

body reads bytes read warm refresh
before 15 34 MB 8680 ms
after 3 0.02 MB 586 ms

Tests

New case concurrent appends to several files stay incremental instead of forcing a full rebuild: 64 files, three appended, asserts bodyReads === 3, exact appended byte count, and deepEqual against ClaudeDataLoader.loadUsageRecords. It fails on main with actual: 64, expected: 3.

claudeIncrementalIndex suite: 39/40, the one failure (content analysis is materialized from per-file contributions without a second body scan) fails identically on a clean main here.

Full npm test: 1025 pass on this branch and 1025 on main. The 8–9 failures are Windows filesystem cases (EPERM: symlink, temp rename/rmdir races) in the Codex lease/manifest suites, and their exact set shifts between runs on the same commit — two runs of this branch failed different members of that group.

…e#99)

The content analysis fast path required exactly one changed file
(sourcePlans.length === 1 && plans.length === 1). A machine running several
agents appends to several transcripts between two refreshes, so that gate never
held: every refresh fell back to replaceWithFullAnalysisPlans() and re-read
every file. The diagnostics show the shape of it - changed=3 reused=641
alongside bodies=644 and bytes=1.4 GB.

The application side already loops over plans, so the gate is what limited it.
Allow any number of pure tail appends, and make two ordering guarantees the
single-file case never needed:

- parse appends in full-scan order, so the first file to carry a UUID owns it
  exactly as in the full loader (the comparator returned 0 for two appends,
  leaving discovery order to decide);
- attribute each new UUID to the appended file that actually carries it,
  earliest first, instead of to parsedPlans[0].

The per-file UUID ownership check that forces a rebuild now runs for every
appended file rather than only a lone one.

Measured on 15 real transcripts (34 MB), three files appended: 15 body reads
and 8680 ms became 3 body reads and 586 ms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🤖 Automated first-pass review

The configured analysis model did not return a usable analysis for this pull request. The item has still been received and remains available for maintainer review.

To help with review, please ensure the change summary, tests, and any user-facing documentation or localization updates are included.


🤖 Posted automatically by CCU Bot — not a maintainer decision.

Carl723000 and others added 2 commits September 22, 2026 11:10
Preserve both contributor changelog entries while bringing ClaudeCodeUsage#111 onto the latest tested main.

Co-authored-by: OpenAI Codex <215057067+openai-codex[bot]@users.noreply.github.com>
@Carl723000
Carl723000 merged commit 40271d2 into ClaudeCodeUsage:main Sep 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants