#71

Fleet: 60+ updates per terminal at 1,000 live terminals

Openbettercleverwants to merge intofrom

Closes #67.

Fleet now keeps every one of 1,000 live streaming terminals at ≥ 60 parsed updates per second (p10) in one Chrome tab, whether the main thread or the workers feed them. Worker-fed, 1,500 terminals also hold it.

Measured headed at 120 Hz on an Apple M5 Max, Chrome 156, AC power, 4 workers, 3 rounds, with load recorded per run (3.4–12.3). Raw JSON and method are in docs/benchmarks/fleet/REPORT.md.

Terminals#54 f0a8486, main feed p10After, main feed p10After, worker-fed p10
256119.5–119.6119.9–120.1119.9–120.1
51283.2–99.2120.0120.0
1,00043.0–44.773.7–115.583.2–119.0
1,50030.4–31.350.2–59.163.4–74.3
2,00024.5–24.941.2–44.846.8–49.1

After the change, page rAF stayed at about 120/s at every count, with no long tasks and no Long Animation Frames. Before, page rAF fell from 82–86/s at 1,000 terminals to 41–42/s at 2,000. With 6 workers, 1,000 terminals reach p10 99.5–105.9 (main) and 105.7–109.2 (worker); 2,000 do not reach 60 in either feed. GPU-completed updates track parsed updates within a few percent; they are reported separately from display presentation.

What the profiles showed, and the fixes

CommitBottleneckFix
9338774About 2,000 per-terminal port messages per frameOne ordered command batch per worker per task, and one reply batch back
54aed01Measuring every terminal's placement every frame took about two thirds of the main threadRe-measure only after events that can move terminals (scroll, resize, ResizeObserver, mutations, CSS animations), plus a rolling 1/30-per-frame sweep
4e1ab98Worker Wasm time in wraith_row_reuse: a String allocated per cell on every scrolled row, and byte-wise memcmpBuffer-reusing clone_from and word-wise compare
f8af6941,041 compositor layers on a 256-terminal page, 768 of them hidden textareas composited as scrollersoverflow: hidden brings it to 17 layers
d7cd18dEach IME preedit span was its own compositor layerShown only while composing
0492fa0Invalidation crossing terminal edgescontain: layout paint style on terminals
96369ebPer-terminal write buffer allocations, and per-terminal stats posted every 500 ms whether observed or notOne WriteArena buffer per worker batch, stats only while onStats listeners exist, one listener per worker
4e089f1GPU process: a writeBuffer per terminal per frame and repeated pipeline bindsOne shared uniform ring written once per surface frame, pipeline set once per pass

Also in this PR:

  • 30ae285 keeps the Fleet HUD on one stable line. It is kept for its stable layout only: measured alone it slowed that page, and at the tip it makes no difference. The report shows both.
  • b7a6c79 adds a worker-fed benchmark mode and npm run profile:fleet, and 2b794b7 adds Long Animation Frame attribution.

API changes (additive, documented in both READMEs and ARCHITECTURE copies)

  • sharedSurface.tracking: 'observe' | 'frame': 'observe' is the new default; 'frame' keeps the old every-frame measurement.
  • pool.updatePlacements()
  • The PlacementTracking type.

Behaviour changes:

  • Workers send per-terminal stats only while the page has onStats listeners.
  • The display-resolution heartbeat is now one page-wide timer.

Validation

All of these pass on the merged tree (origin/main incl. #55): npm test (142, incl. batch, write-arena and placement unit tests), typecheck, build, optimize, cargo fmt --check, test:native, test:wasm, build:demo, test:fleet (38 checks: shared and per-canvas output stay pixel-identical, including a Kitty image), test:browser, test:xterm, test:accessibility, test:input, test:worker-socket, test:native-pool, test:native-kitty, test:consumer, test:xterm-consumer. Both the libghostty and xterm-compatible profiles were exercised.

Caveats

  • Unrelated work kept machine load at 3.4–12.3. The 1,000-terminal spread follows it (round at load 4–6: p10 115–119; at about 9: 74–83).
  • Worker-fed mode replays precomputed chunks inside the workers, with no network in the loop.
  • "Page rAF" counts main-thread animation frames, not physically presented frames.
  • 8 workers were not re-measured after the layer fixes.

🤖 Generated with Claude Code