Skip to content

perf(worktree-activity): stream JSONL transcripts instead of readFile#87

Merged
Juliusolsson05 merged 2 commits into
mainfrom
feat/perf-jsonl-streaming-transcript-parser
May 13, 2026
Merged

perf(worktree-activity): stream JSONL transcripts instead of readFile#87
Juliusolsson05 merged 2 commits into
mainfrom
feat/perf-jsonl-streaming-transcript-parser

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Summary

The 60 s background refresh in WorktreeActivityIndex was calling parseTranscriptForActivity per discovered transcript, and that function used readFile(path, 'utf-8').then(t => t.split('\n').map(JSON.parse)). For a 50 MB JSONL transcript that pattern transiently kept four representations live: the 50 MB Buffer, the 50 MB JS string, the 50 MB-ish array of substrings, and the parsed objects. Sequentially across all candidates per refresh, this matched the 100–200 MB spike-and-drop pattern visible in the system-perf popover every ~60 seconds and is the most likely primary cause of the recent main-process OOM crashes the user has been seeing during normal work.

This PR:

  1. Adds a small shared streamJsonl helper in src/shared/runtime/ that yields parsed JSONL objects one line at a time using createReadStream + readline.createInterface (same pattern ghostJournal.ts already uses).
  2. Replaces the readFile + split + map body of parseTranscriptForActivity with a for await loop over streamJsonl.

Peak transient memory per parsed transcript drops from O(file_size) to O(longest_line) — typically tens of KB per line even for large tool_use entries.

Out of scope (separate PRs)

  • The same pattern exists in src/main/sessionIndex.ts:287 (palette search) and src/main/sessions/historyLoader.ts:121 (session resume + pagination). Both will reuse streamJsonl in follow-up PRs.
  • The 30 MB worktree-activity-index.json file in indexStore.ts is single-JSON, not JSONL — streaming it requires a format migration shim or a JSON-stream parser dependency. Worth doing later, deliberately deferred from this PR.

Test plan

  • npm run test:worktree-activity passes (unchanged from baseline)
  • npm run test:review-fixes passes
  • npm run build:app succeeds
  • git diff main | grep -ic cc-shell returns 0
  • Manual: AGENT_CODE_PERF=1 npm run dev, open badge popover, observe large_object_space over ~5 minutes. The 60 s spike pattern that was hitting 100–200 MB should drop substantially (target: <30 MB per refresh).

Stats

2 files changed (+78 / -11).

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits May 13, 2026 14:25
The dominant source of transient memory spikes in the main process is
the `readFile(path, 'utf8') + split('\n') + map(JSON.parse)` pattern
used in several JSONL-reading sites. For a 50 MB transcript the
pattern transiently keeps a 50 MB Buffer, a 50 MB string, a 50 MB
array of substrings, AND the parsed JS objects all live at once.

This helper wraps the existing createReadStream + readline pattern
already used in ghostJournal.ts so callers can replace those whole-file
reads with a one-line-at-a-time async iterable. Peak transient memory
drops from O(file_size) to O(longest_line).

Used by the next commit in this branch. Future commits should be able
to reuse this in sessionIndex and historyLoader the same way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
parseTranscriptForActivity used to do
`readFile(path) → text.split('\n') → map(JSON.parse)` per transcript.
For a 50 MB Claude transcript that transiently kept the 50 MB Buffer,
50 MB JS string, the split-array of substrings (~50 MB), and the
parsed JS objects all live at once. With WorktreeActivityIndex's
60 s background refresh calling this for each candidate sequentially,
the per-tick peak was ~100-200 MB — visible in the system-perf
popover as a recurring spike-and-drop pattern every minute,
suspected primary cause of recent main-process OOMs.

Switching to streamJsonl drops the transient peak to one line at a
time. Same output shape (IndexedTranscript), same malformed-line
behaviour (silently skip), same call site contract. Verified
test:worktree-activity passes unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant