Skip to content

[medium] perf(render): skip the transcript scan when no widget reads it - #634

Open
elhoim wants to merge 2 commits into
sirmalloc:mainfrom
elhoim:perf/transcript-scan
Open

elhoim wants to merge 2 commits into
sirmalloc:mainfrom
elhoim:perf/transcript-scan

Conversation

@elhoim

@elhoim elhoim commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

BLUF

  • Priority: medium.
  • Every render parsed the whole session transcript (it grows all session), even when no configured widget reads anything from it.
  • The render now scans only when a widget will use the result. Context widgets and the full-until-compact flex mode fall back to transcript token metrics only when the status JSON lacks the matching context_window field. Cache widgets read them only in session scope.
  • Measured CPU per render with the default config: -19% on a 5 MB transcript, -57% on a 50 MB one. Configs with token widgets are unchanged. Output is byte-identical.

Details

Which widgets and modes read which part of the transcript analysis (at 35440e4):

Consumer Field When it reads it
tokens-input, tokens-output tokenMetrics always (preferred over context_window totals)
tokens-cached, tokens-total tokenMetrics always (no other source)
cache-hit-rate, cache-read, cache-write tokenMetrics session scope only
context-length, context-percentage-usable tokenMetrics.contextLength context_window gives no context length
context-percentage, flex full-until-compact tokenMetrics.contextLength context_window gives no used percentage
context-bar tokenMetrics no context length, or no context_window_size (it uses the mere presence of metrics to choose the model's default window)
session-clock sessionDuration already gated (hasSessionClock && !hasSessionDurationInStatusJson)
speed widgets speedMetricsCollection (+ subagent files) already gated (hasSpeedItems)
compaction-counter, thinking-effort, session-name their own fields already gated
  • New needsTranscriptTokenMetrics(lines, settings, data) (src/utils/transcript-requirements.ts) implements the token-metrics rows. It reads the same getContextWindowMetrics fields the widgets test, and it resolves legacy widget types.
  • renderMultipleLines passes includeTokenMetrics through. It skips getTranscriptAnalysis when no flag is set, and tokenMetrics is then null.
  • getTranscriptAnalysis gains includeTokenMetrics (default true, so other callers are unchanged). TranscriptAnalysis.tokenMetrics is null only when a caller turns it off.
  • includeSubagents: true is hard-coded, but it has no effect without speed widgets: the agent-id walk and the subagent re-reads only run when includeSpeedMetrics is set. When speed widgets are configured they need that data, so this PR leaves it alone.

Tests:

  • A property test renders every data-only widget (the ones that do not shell out, read the clock, or read caches) twice: with transcript metrics and with null. It covers five payload shapes and both cache scopes. It fails if the outputs differ while the gate says the scan can be skipped. It also checks the flex-mode percentage. It fails if the context-bar window-size case or any token widget is dropped from the gate (verified by mutation).
  • A table test covers the gate itself. A jsonl-metrics test checks that includeTokenMetrics: false gives tokenMetrics === null and still collects the other fields.

Overlap: #591 (ours) and #622 touch token counting in src/utils/jsonl-metrics.ts. This PR only adds the option and the nullable return there, so any conflict is trivial. #640 (ours, transcript pre-filter) edits the scan loop in the same file. It is independent of this PR: this one skips the scan when nothing reads it, and #640 makes the scans that remain cheaper.

Measurements

node dist/ccstatusline.js. Base and patched arms were interleaved round-robin in one run with 20 passes plus a node -e 0 control. CPU is user+sys including children, in ms. Settings use gitCacheTtlSeconds: 0. Payloads carry a full context_window. Transcripts are synthetic (5 MB and 50 MB, Claude Code record mix).

Arm CPU median CPU p90 Wall median
control node -e 0 109 137 942
base, default config, 5 MB 1575 1750 10797
patched, default config, 5 MB 1270 1430 8625
base, default config, 50 MB 2837 3208 20699
patched, default config, 50 MB 1222 1359 9261
base, all-widgets config, 5 MB 2073 2256 14664
patched, all-widgets config, 5 MB 1991 2307 13126

Load1 during the run was min 48.6, median 56.6, max 64.2 on 6 cores. The host was heavily loaded, so compare ratios, not absolute times. With the patch, the default-config cost no longer depends on transcript size. The all-widgets config still scans (it has token and speed widgets), and its p90s overlap, so it is unchanged within noise.

Byte-identity: I compared stdout of base and patched on 90 config 脳 payload pairs. That covers every benchmark config, a config with every context/cache widget plus full-until-compact, and a config with token widgets. Payloads include ones without context_window, without context_window_size, with only used_percentage, and with a missing transcript. All 90 pairs are identical.

Checks

  • bun run lint: clean.
  • bun run build: OK.
  • bun test: the new and changed suites pass. On this loaded host the full run also failed in fetchUsageData error handling, custom command capture, and some TUI menu suites (timeouts). Running those 9 files on unmodified main on the same host at the same time gave 62 failures, against 58 with this branch. They are load flakes, and none of them touch the changed code.

馃 Generated with Claude Code

Every render parsed the whole session transcript, even when no configured
widget uses anything from it. Token metrics are the usual reason for the
scan, but the context widgets (context-length, context-percentage,
context-percentage-usable, context-bar) and the full-until-compact flex
mode only fall back to them when the status JSON lacks the matching
context_window field, and the cache widgets only read them in session
scope. The render now asks for token metrics only in those cases and
skips the scan entirely when nothing else (session clock, speed,
compaction, thinking effort, session name) needs it.

getTranscriptAnalysis gains an includeTokenMetrics option (default true,
tokenMetrics is null when it is false). A property test renders every
data-only widget with and without transcript metrics for several payload
shapes and fails if the output differs while the gate says the scan can
be skipped.

Measured (node dist, 20 interleaved passes, load1 ~57 on 6 cores), CPU
median per render: default config 1575 -> 1270 ms on a 5 MB transcript
(-19%), 2837 -> 1222 ms on 50 MB (-57%); a config with token widgets is
unchanged. Output is byte-identical on 90 config/payload pairs, including
payloads without context_window, without context_window_size, and with a
missing transcript.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
whycantfindaname pushed a commit to whycantfindaname/ccstatusline that referenced this pull request Oct 1, 2026
Merged from the jason/beta4-local-fixes Trellis build (compat-repair base):
bounded stdin reads in shared hooks (sirmalloc#590), research dispatch may write the
task research dir (sirmalloc#634), scoped archive commits (sirmalloc#622, sirmalloc#630), list filter
traversal (sirmalloc#631), remove-subtask link check (sirmalloc#632), hooks.local.json ignore
(sirmalloc#633).
Co-Authored-By: Claude Opus 5.5 (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