perf: stop reading whole session files, and index them incrementally - #82
Conversation
On a machine with ~2.3 GB under ~/.claude/projects, the main process sat at a 139 MB median but spiked past 250 MB in 24% of samples, peaking at 498 MB (921 MB across all processes) — with a single terminal open, so neither the terminals nor the grid view were involved. Two causes, both "read the whole file to use a little of it". A session .jsonl held as a JS string costs ~2x its size in RAM, since V8 stores non-latin1 text as UTF-16. 1. Three sites read an entire file just to get its head: schedule-runner.js kept 4000 chars — every 60s, for every project folder main.js kept 8000 chars derive-project-path kept the first line carrying `cwd` The scheduler one dominated: a 61 MB session file allocated ~122 MB once a minute, which is the sawtooth in the main process. 2. readSessionFile re-read the file in full on every append, to produce ~9 KB of metadata. The projects watcher fires that on each write, so an active session re-read its whole history every few seconds. Session files are append-only (folder-index-state.js already relies on it; measured here: 0 rewrites and 0 truncations across 1214 files), and every field readSessionFile extracts is either a first occurrence or a running total. So it now resumes from the byte offset the previous pass reached, persisted alongside a 4 KB head hash and a size check that fall back to a full read if the file was rewritten or truncated. Adds jsonl-scan.js with the two supported ways to walk these files — scanLines (chunked, resumable, early-exit) and readHead. Measured on the same workload, main process over 4 minutes: before rss 136 -> 520 MB, heap peak 360 MB, 3 jumps of +354 MB after rss 145 -> 170 MB, heap peak 12 MB, 0 jumps A/B against the released build over 11 minutes, one terminal open: peak across all processes 921 -> 526 MB main process peak 498 -> 158 MB samples above 250 MB 24% -> 0% Re-indexing after an append: ~0 MB and 22 ms, from 152 MB and 447 ms. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e test Branch was 14 behind. Two things needed fixing, both merge-order artifacts — doctly#60 and doctly#65 landed after this branch was written. schedule-runner.js conflicted, but the two changes compose rather than compete. doctly#65 made scanSchedules prefer a cache_meta folder→projectPath lookup, falling back to reading a JSONL head only for folders missing from the cache; this branch made that head read cheap. Resolved by keeping doctly#65's structure and putting readHead(…, 4096) inside its readProjectPathFromJsonl fallback, so the common path does no file read at all and the fallback no longer loads a possibly-hundreds-of-MB file to look at its first line. test/reconcile-cache.test.js failed because refreshFolder now fetches the cached row to use as resume state, and that test's fake db predates the method: ✖ reconcileCacheFromFilesystem indexes new and stale folders … getCachedSession → undefined Added getCachedSession() { return null; } to the fake, modelling a session with nothing indexed yet. Fixed in the fake rather than guarding the call site: the real db provides the method, and a guard would mask genuine wiring errors. 27/27 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Reviewed this properly — it's careful work and the diagnosis holds up on my machine too. For calibration: 3,290 session files, 3.1 GB, largest 136.6 MB. Under the current code an append to that file re-reads all of it into a JS string — roughly 273 MB of transient allocation — every few seconds while the session is live. Your sawtooth reproduces. Heads up: I pushed a merge commit to this branch (5016379) fixing the two things that were blocking it. Both were merge-order artifacts, not defects in your work — #60 and #65 landed after you wrote this. Details below so nothing is a surprise. What's done right
The resume-safety triad is the part I'd have most expected to find wrong, and it's complete: The migration is additive with no cache wipe — NULL I also specifically audited the riskiest refactor here, narrowing What I changed in 5016379
I added Remaining — your callLong lines are quadratic. Two test gaps. The truncation path ( Smaller notes. Nothing above blocks merging. Happy to take it as-is and file the long-line concat separately if you'd rather land the win now. |
Summary
Claude session transcripts can grow to hundreds of megabytes. Reading the entire file again on each append creates large transient allocations in Switchboard's main process, even when only a little session metadata has changed.
This PR keeps @Davidb-2107's original chunked-reader and incremental-indexing contribution, adapted to the current harness and database architecture. Current
mainwas merged into the contributor branch without rewriting the original commits.Changes
harnesses/claude.js, preserving the currentruntime,sessionFile,fileMtime, title handling and transcript timestamp bounds. Cached parser state survives database reopen, so subsequent appends resume at the previous byte offset.Resume safety
Reset state when the head changes, the file is replaced or truncated, or a changed file has not grown. Version the saved state so older parser rows without timestamp accumulators receive a full read. A final line without a newline is displayed where parseable but disables resume until a later full pass can account for it safely.
This remains an optimization for append-only transcripts: an in-place edit beyond the 4 KiB guard that also grows the same file can evade validation and requires a full re-index. It does not claim to detect arbitrary edits throughout a transcript.
Validation
npm test: 192 passed, 0 failed on the updated branch (172 current-main tests plus 20 indexing/scanner/regression tests).Coverage includes:
git diff --cached --checkpass.Tests used existing local dependencies via
NODE_PATH, with SQLite checks under Electron's Node mode in disposable data directories. No live-user transcript, running application, or original checkout was modified. The contributor's original whole-application RSS benchmark has not been rerun on this port; the current performance checks measure bounded I/O and copying. GitHub platform builds run on the pushed commit.