Skip to content

perf(jsonl): read each transcript once per render instead of five times - #568

Merged
sirmalloc merged 2 commits into
sirmalloc:mainfrom
zachthedev:fix-transcript-read-amplification
Sep 2, 2026
Merged

perf(jsonl): read each transcript once per render instead of five times#568
sirmalloc merged 2 commits into
sirmalloc:mainfrom
zachthedev:fix-transcript-read-amplification

Conversation

@zachthedev

@zachthedev zachthedev commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

A render reads the session transcript five times. Neither reader caches, so each caller re-reads and
re-splits the whole file: getTokenMetrics, getSessionDuration, getSpeedMetricsCollection
(jsonl-metrics.ts:94,162,548), getCompactionStats (compaction.ts:94) and
getTranscriptThinkingEffort (jsonl-metadata.ts:47).

507.6 MB transcript, three interleaved rounds, disk reads from GetProcessIoCounters:

before after
time 10,814 ms 5,774 ms
peak RSS 3,106 MB 1,795 MB
read 2,551 MB 521 MB

Output byte-identical. Amplification 5.0x → 1.03x.

Keyed on dev+ino, not the path string. transcript_path arrives with backslashes while the
glob at jsonl-blocks.ts:43 yields forward slashes for the same file, so a path key stores it twice.
Stat is bigint because inodes here exceed Number.MAX_SAFE_INTEGER. Size and mtime form the version,
so an appended transcript is re-read.

The block sweep opts out ({ cache: false }). It visits each transcript once, so caching cannot
hit; measured 0 hits over four runs, and retaining its reads took peak RSS from 140 MB to 410 MB on a
ten-file probe.

Readers return readonly string[], since five callers share one array.

#551 conflicts textually — both rewrite these two functions. Intents compose: #551 streams (fixing
#550's ceiling and peak), this removes the repeat walks.

18 tests. Verified by mutation, each mutation asserted applied: cap to 1, cap to 1e6, version dropping
size, version dropping mtime, eviction removed, identity by path, no move-to-end, cache: false
ignored on each reader, version ignored on read. All ten fail the suite.

A render reads the session transcript from five call sites: token
metrics, session duration, speed metrics, compaction stats and thinking
effort. Neither reader held a cache, so each one re-read and re-split
the whole file. On a 507.6 MB transcript that is 2,551 MB read from
disk for one repaint, a 5.0x amplification.

Memoize both readers per process, keyed on file identity plus size and
modification time. Measured on that transcript over three interleaved
rounds: 10,814 ms to 5,774 ms, peak RSS 3,106 MB to 1,795 MB, disk
reads 2,551 MB to 521 MB, output byte-identical.

Key on device and inode rather than the path string. Windows accepts
many spellings of one path and this codebase mixes them: transcript_path
arrives with backslashes while the glob in jsonl-blocks yields forward
slashes for the same file, so a path-keyed cache stores the largest file
in the working set twice. Take the stat as bigint because inodes here
exceed Number.MAX_SAFE_INTEGER and a plain stat loses low bits.

Let getAllTimestampsFromFile opt out. That sweep visits each transcript
once across as many as the lookback selects, so caching cannot hit, and
retaining those lines would hold arbitrarily many whole transcripts for
the life of the process. Measured at zero cache hits over four runs
against a corpus of 18,837 transcripts, and retaining them raised peak
RSS from 140 MB to 410 MB on a ten-file probe.

Return readonly string[], since five callers now share one array.
The case asserted that a backslash and a forward-slash spelling of one
transcript share a cache entry. A backslash separates paths on Windows
but is an ordinary filename character elsewhere, so on the Linux runner
that spelling named a file which does not exist and the read threw.

Build the spellings by hand for the platform in hand. Every platform
gets the dot-segment and doubled-separator forms, which path.join would
have normalized back into the string they must differ from, and Windows
adds the slash-direction form. Verified against a path-keyed identity
with the Windows-only spelling suppressed, so the case still fails on
the Linux subset alone.
@sirmalloc
sirmalloc merged commit 459a48c into sirmalloc:main Sep 2, 2026
3 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