bench(web-ui): transcript replay harness: acpmux logs at top speed, shift, per-token slope, long tasks, heap, idle

Merged
TE
1d ago
+1025-1

Leo, 2026-11-03, asking for fast simulations in place of more compute: "play things back at, like, 30,000 times speed" (point 3 of manaflow-ai/cmuxterm-hq#6740).

What

A replay harness for the agent transcript, as one more bench of the shared web UI perf harness (webviews/bench/web-ui-suite.ts, the perf cron's advisory web-ui part, #31704). It adds no new harness, job, gate or runs-on.

  • bench/replayLogs.ts: acpmux session logs, one EventRecord per record as _acpmux/attach and _acpmux/event carry them. Each log is keyed by seq (1, 2, 3... per session, like acpmux's SessionEvent), and checkSeq refuses a log with a gap, a second session or time running back, so a recorded daemon log or a simulated agent's log lines up record for record. The logs:
    • every seeded ?mock chat (mockFixture.ts: the worked turn and each session's history);
    • the recorded chat the replay screen and gallery use (replay/replayFixture.ts);
    • a long, real-shaped session: 600 loadFixture.ts turns (reads, large edits, long shell logs), 4,196 records and 2,716 rows, with a streamed turn at six checkpoints. Each streamed turn has 80 reply tokens and a shell call whose output streams in over 30 updates;
    • --log FILE: any recorded log (JSON lines of SessionEvents, or {events: [...]}).
  • bench/transcript-replay.bench.ts: plays each log into the production pane (a vite build of the pane's entry, joined to a stand-in acpmux) as fast as the page can take it. A replay clock makes Date read the time of the newest record played, so elapsed labels read what they read live, and no step waits on wall time. Each log is played in two phases:
    • open: the whole log arrives on attach. Every row in view is sampled each painted frame, from first paint until the view rests. The first frame should be the final frame.
    • live: the session starts empty and the records arrive as live events, 25 per frame, with each measured burst played one record per frame.
  • Rows and budgets (one rule each, never today's number):
rowbudget
open.cls, live.cls0.01
open.moved_px (a row in view at first paint that moved; a row that left counts the view height)1 px
live.long_tasks during bursts0
long.token_ms, long.tool_output_ms (main-thread task time per record)10 ms (a frame's work)
long.*_slope_ms_per_krow (least-squares slope over the checkpoints, so O(rows) per token shows as a slope)0.1 ms per 1000 rows
long.heap_mb (after GC, compared with the empty pane)64 MB
long.idle_ms_per_s (after the replay settles)1 ms/s
  • When a budget is broken, the bench prints a BUDGET MISS line naming the row, the value, the budget and the cause: the elements that shifted (summed by source), or the per-checkpoint costs behind a slope. With --strict it exits 1. The suite runs it without --strict, so in CI it warns through the cron's regression issue and never fails a check.
  • PROFILE=1 builds the pane unminified, profiles every burst, and prints the functions whose self time per record grew most between the first and the last checkpoint. These are where the O(rows) work is.
  • Suite: transcript-replay has a 150 s timeout (the run below took about 75 s). thread-rail-jump's timeout drops from 300 s to 150 s, so the timeouts still sum to 29.5 minutes, under perf-cron's 30-minute cap. That bench ran in well under that in #31704's runs.

First numbers

Fleet Linux, 8 vCPU, headless Chromium, run 41743692374, via bun bench/web-ui-suite.ts --only transcript-replay: 90 s, exit 0, failed []:

rowp50p95budget
transcript_replay.long.token_ms6.58 ms7.64 ms10
transcript_replay.long.token_slope_ms_per_krow0.7020.7020.1
transcript_replay.long.tool_output_ms9.07 ms11.13 ms10
transcript_replay.long.tool_output_slope_ms_per_krow0.6390.6390.1
transcript_replay.live.cls0.00030.14550.01
transcript_replay.live.long_tasks000
transcript_replay.open.cls00.00010.01
transcript_replay.open.moved_px00.3 px1
transcript_replay.long.heap_mb39.539.564
transcript_replay.long.idle_ms_per_s0.750.751

Per-token cost at each checkpoint: 11 rows 5.70 ms, 545 rows 5.92, 1087 rows 6.58, 1629 rows 6.99, 2171 rows 7.64, 2709 rows 7.24.

Performance

This PR changes no product code. These are the bench's first values on this head, so there is no before.

metricbeforeafterdeltaharness or run link
per-token main-thread ms at 2,709 rowsn/a7.24new rowtranscript-replay.bench.ts, run 41743692374
per-token growth, ms per 1000 rowsn/a0.70new rowsame
tool output update, ms per 1000 rowsn/a0.64new rowsame
live CLS, long logn/a0.1455new rowsame

Found

What the first runs found (filed as follow-ups, with the PRs from git log -S):

  • O(rows) work per streamed token and per tool output update. About 0.7 ms more per 1000 rows in each of three runs. Profiles (PROFILE=1) name withoutQuietThoughts (a walk of every row per snapshot, from #23084), the inbox projection's prefix copy projectInboxRows/projectAllInboxRows (from #29829 and #32720), rowPresence, and layoutScrollTop. At 2,700 rows a tool output update costs 11-14 ms, over the 10 ms frame budget.
  • Rows already painted shift while a long chat streams: the live turn's "Worked for" line (cv-worked/cv-shimmer__band), code blocks of earlier answers (cv-codeblock), and the minimap rail. The live CLS of the long log was 0.02 to 0.26 across four runs.
  • Opening mock-home-screen shifted the dock and the header tools by 0.0458 in two of three runs. The third run showed 0, so this is intermittent.

Verification

  • bun test bench/replayLogs.test.ts: 8 pass (seq keys of every log, a gap named, burst ranges, recorded log parsing, rows, slope, judging and the miss message).
  • bun run check on the fleet: no type or lint errors. Its one red, check:design-tokens on subagents/subagents.css, is a file this PR does not touch.
  • vp fmt --check bench/: clean.
  • bun bench/web-ui-suite.ts --only transcript-replay: the bench's rows were collected and the bench was not listed as failed (fast-checks runs 41741180064, 41742499741 and 41743692374).

Observed / Not run

  • Observed: the runs above.
  • Not run: the perf cron itself. Its first tick on feat-cmux-next after the merge runs the bench as part web-ui.

Changelog

none

Agent session: agent session

๐Ÿค– Generated with Claude Code


Autofix with [code]smith
Open in [code]smith ยท Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Adds a transcript replay benchmark to the web UI perf suite that plays recorded and generated acpmux session logs into the production transcript pane as fast as the page can take them, measuring layout shift, main-thread cost per streamed token and tool output update as the transcript grows, long tasks, heap, and idle CPU. A replay clock makes Date read the time of the newest record, so a session hours long replays in seconds. The suite adds a 150 s timeout for transcript-replay and drops thread-rail-jump's from 300 s to 150 s. No product code changes; this is the bench's first run on this head, so there are no before values.

First numbers

Fleet Linux: token cost 6.58 ms p50 growing 0.70 ms per 1000 rows, tool output update 9.07 ms growing 0.64 ms per 1000 rows, live CLS 0.0003 p50, heap +39.5 MB, idle 0.75 ms/s โ€” budgets are 0.1 for slope, 10 ms per record, 0.01 CLS, 64 MB heap, 1 ms/s idle.

Found

  • Per streamed token and per tool output update the pane spends ~0.7 ms more per 1000 rows already in the transcript; at 2,700 rows a tool output update costs 11โ€“14 ms, over the 10 ms frame budget. Profiles name withoutQuietThoughts, the inbox projection's prefix copy, rowPresence, and layoutScrollTop.
  • Rows already painted shift while a long chat streams (live CLS 0.02โ€“0.26): the live turn's "Worked for" line, earlier code blocks, and the minimap rail.
  • Opening mock-home-screen intermittently shifts the dock and header tools by 0.0458.

Budgets

  • Each row has one budget; an overrun prints a BUDGET MISS line naming the row, value, budget, and cause, and --strict exits 1. The suite runs without --strict, so CI warns through the perf cron's regression issue.
  • Each log plays twice: open (the whole log arrives on attach) and live (records stream in). PROFILE=1 builds the pane unminified and diffs profiles across checkpoints; --log FILE replays any recorded daemon log (JSON lines or {events: [...]}).