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 p10 | After, main feed p10 | After, worker-fed p10 |
|---|---|---|---|
| 256 | 119.5–119.6 | 119.9–120.1 | 119.9–120.1 |
| 512 | 83.2–99.2 | 120.0 | 120.0 |
| 1,000 | 43.0–44.7 | 73.7–115.5 | 83.2–119.0 |
| 1,500 | 30.4–31.3 | 50.2–59.1 | 63.4–74.3 |
| 2,000 | 24.5–24.9 | 41.2–44.8 | 46.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
| Commit | Bottleneck | Fix |
|---|---|---|
| 9338774 | About 2,000 per-terminal port messages per frame | One ordered command batch per worker per task, and one reply batch back |
| 54aed01 | Measuring every terminal's placement every frame took about two thirds of the main thread | Re-measure only after events that can move terminals (scroll, resize, ResizeObserver, mutations, CSS animations), plus a rolling 1/30-per-frame sweep |
| 4e1ab98 | Worker Wasm time in wraith_row_reuse: a String allocated per cell on every scrolled row, and byte-wise memcmp | Buffer-reusing clone_from and word-wise compare |
| f8af694 | 1,041 compositor layers on a 256-terminal page, 768 of them hidden textareas composited as scrollers | overflow: hidden brings it to 17 layers |
| d7cd18d | Each IME preedit span was its own compositor layer | Shown only while composing |
| 0492fa0 | Invalidation crossing terminal edges | contain: layout paint style on terminals |
| 96369eb | Per-terminal write buffer allocations, and per-terminal stats posted every 500 ms whether observed or not | One WriteArena buffer per worker batch, stats only while onStats listeners exist, one listener per worker |
| 4e089f1 | GPU process: a writeBuffer per terminal per frame and repeated pipeline binds | One 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
PlacementTrackingtype.
Behaviour changes:
- Workers send per-terminal stats only while the page has
onStatslisteners. - 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