Canvas at 120 fps under window spam and fast view navigation #121

Open
opened 2026-10-09 05:07:29 +00:00 by kayg · 6 comments
Owner

Owner target for 2026-10-10: spam many new windows quickly, navigate between views, and do every canvas operation fast, with every animation at 120 fps (at least 60+ fps) on the owner's devices (iPhone ProMotion, Mac, Edge). Measure with the frame meter's logs (#116) from the owner's devices plus specs: open 20 windows as fast as possible, rapid view switches (keys, pill, switcher), split/close bursts; per-frame work ≤ 8 ms at 120 Hz; no dropped frames; input never blocked.

Owner target for 2026-10-10: spam many new windows quickly, navigate between views, and do every canvas operation fast, with every animation at 120 fps (at least 60+ fps) on the owner's devices (iPhone ProMotion, Mac, Edge). Measure with the frame meter's logs (#116) from the owner's devices plus specs: open 20 windows as fast as possible, rapid view switches (keys, pill, switcher), split/close bursts; per-frame work ≤ 8 ms at 120 Hz; no dropped frames; input never blocked.
Author
Owner

Deployed to production 2026-10-09 ~09:50 CEST: web build of dev 63b1f10e (server binary unchanged; rollback web.prev = 1c3ae5f). Gate: scripts/verify fast EXIT 0 on 63b1f10e (critical 282934/307200 gzip, deferred 49879/51200); real-server browser specs 27/27.

Deployed to production 2026-10-09 ~09:50 CEST: web build of dev 63b1f10e (server binary unchanged; rollback web.prev = 1c3ae5f). Gate: scripts/verify fast EXIT 0 on 63b1f10e (critical 282934/307200 gzip, deferred 49879/51200); real-server browser specs 27/27.
Author
Owner

Owner device, mockup 10711e02 on 47401, Mac Safari 120 Hz dpr 2, 55 windows, soft background, blur 8px, mocha: 30 dropped-frame incidents, worst 100/34/33/33/33 ms. By activity: split 12, idle 12 (likely the cursor-blink cost already reported), pan 3, close 2, flight 1. Split at 55 windows is the main target.

Owner device, mockup 10711e02 on 47401, Mac Safari 120 Hz dpr 2, 55 windows, soft background, blur 8px, mocha: 30 dropped-frame incidents, worst 100/34/33/33/33 ms. By activity: split 12, idle 12 (likely the cursor-blink cost already reported), pan 3, close 2, flight 1. Split at 55 windows is the main target.
Author
Owner

Round 3 deployed to production (web only) at e704e5b8; dev was fast-forwarded to it. The server binary is unchanged (db8c5270). Backup: /var/backups/infraternal/20261010-022143-e704e5b8. The transfer budget passes.

Root cause of the 54-session lag: the server accepts 32 terminal attachments per WebSocket, and the page put every terminal on one socket.

  • Before Mesh (55716ec1), this produced a reconnect storm: 93 sockets in 2 min, idle frame p95 66.7 ms, keydown→paint p50 80 ms.
  • On db8c5270 the storm is gone, but 26 of 54 terminals were silently left unattached while still showing "live".

Fix: ConnectionPool spreads terminals over sockets of 28 each, up to 12 sockets per page.

  • 54 shells now use 2 sockets with 54/54 attached.
  • Split frame p95: 150 → 16.8 ms.
  • On pre-Mesh dev, idle frame p95: 66.7 → 16.7 ms.

Other fixes in this round:

  • The overlay MutationObserver no longer runs querySelector over the whole screen on every terminal DOM change. That was 90–120 ms of main-thread time per second.
  • The slow-link breath restarts without a document-wide style flush (saves 20 ms/s).

Still scaling: zoom costs 176–233 ms of main-thread CPU per second at 54 terminals.

Owner question (D183): "windows are sessions on one WebSocket" now means one WebSocket per 28 windows. Rejected alternatives: raising the server cap (a security-reviewed bound), and detaching off-screen terminals (they would be blank while scrolling in).

Benchmark: REAL_SCALE=1 bun x playwright test -c playwright.real.config.ts real/scale.spec.ts.

**Round 3 deployed to production (web only)** at e704e5b8; dev was fast-forwarded to it. The server binary is unchanged (db8c5270). Backup: `/var/backups/infraternal/20261010-022143-e704e5b8`. The transfer budget passes. **Root cause of the 54-session lag:** the server accepts 32 terminal attachments per WebSocket, and the page put every terminal on one socket. - Before Mesh (55716ec1), this produced a reconnect storm: 93 sockets in 2 min, idle frame p95 66.7 ms, keydown→paint p50 80 ms. - On db8c5270 the storm is gone, but 26 of 54 terminals were silently left unattached while still showing "live". **Fix:** `ConnectionPool` spreads terminals over sockets of 28 each, up to 12 sockets per page. - 54 shells now use 2 sockets with 54/54 attached. - Split frame p95: 150 → 16.8 ms. - On pre-Mesh dev, idle frame p95: 66.7 → 16.7 ms. **Other fixes in this round:** - The overlay MutationObserver no longer runs `querySelector` over the whole screen on every terminal DOM change. That was 90–120 ms of main-thread time per second. - The slow-link breath restarts without a document-wide style flush (saves 20 ms/s). **Still scaling:** zoom costs 176–233 ms of main-thread CPU per second at 54 terminals. **Owner question (D183):** "windows are sessions on one WebSocket" now means one WebSocket per 28 windows. Rejected alternatives: raising the server cap (a security-reviewed bound), and detaching off-screen terminals (they would be blank while scrolling in). Benchmark: `REAL_SCALE=1 bun x playwright test -c playwright.real.config.ts real/scale.spec.ts`.
Author
Owner

Round 4 (069fd47b, delayed surface release + staggered restore): not shipped.

Measured on the idle perf VM: A=e704e5b8 (production), B=069fd47b, alternating A B A B A B, 54 real shells, VM load 0.8–3.0 at the start of each run. Medians:

  • Zoom CPU: 220 vs 216 ms/s, no change.
  • Zoom frame p50: 50 vs 50 ms.
  • Zoom longest task: 97 → 55 ms.
  • Zoom p95: unreliable at about 20 frames per zoom (B ranged 167–1633 ms).
  • Pan CPU: 482 → 390 ms/s.
  • Window step CPU: 128 → 168 ms/s (worse).
  • Split CPU: 167 → 199 ms/s (worse).
  • Idle and typing: equal, about 16.7 ms frames.

switch.spec: each head had one flaky failure in 40 runs, in different tests, so B introduces no regression.

The VM renders in software, so these numbers say nothing about how smooth zoom feels on the owner's Mac. Next step: production frame reports (#148) from the owner's Mac. Zoom design gets revisited on that evidence.

**Round 4 (069fd47b, delayed surface release + staggered restore): not shipped.** Measured on the idle perf VM: A=e704e5b8 (production), B=069fd47b, alternating A B A B A B, 54 real shells, VM load 0.8–3.0 at the start of each run. Medians: - Zoom CPU: 220 vs 216 ms/s, no change. - Zoom frame p50: 50 vs 50 ms. - Zoom longest task: 97 → 55 ms. - Zoom p95: unreliable at about 20 frames per zoom (B ranged 167–1633 ms). - Pan CPU: 482 → 390 ms/s. - Window step CPU: 128 → 168 ms/s (worse). - Split CPU: 167 → 199 ms/s (worse). - Idle and typing: equal, about 16.7 ms frames. switch.spec: each head had one flaky failure in 40 runs, in different tests, so B introduces no regression. The VM renders in software, so these numbers say nothing about how smooth zoom feels on the owner's Mac. Next step: production frame reports (#148) from the owner's Mac. Zoom design gets revisited on that evidence.
Author
Owner

Zoom on a real GPU (Mac VM, ANGLE Metal, hardware compositing and raster confirmed; 60 Hz display).

A = e704e5b8 (production) against B = 069fd47b, alternating A B A B A B, 54 real shells, Mac load 1.5–2.4. The two are indistinguishable:

A B
Zoom frame p50 16.9 ms 16.9 ms
Zoom frame p95 83.5 ms 83.1 ms
Zoom frame max 100.7 ms 101.1 ms
Pan p95 18.2 ms 19.3 ms
Split p95 31.8 ms 31.8 ms
Window step p95 33.0 ms 33.1 ms

Round 4 is not shipped.

Where zoom spends its time. The main thread is not the bottleneck: longest task 17–19 ms. The GPU process runs at 415 ms/s, and 238 ms/s of that is renderer raster. Removing one thing at a time:

  • terminal canvases hidden: GPU 418 → 254 ms/s, p95 83.8 → 50 ms;
  • shadows off: 350 ms/s;
  • dot layers hidden: 354 ms/s.

Round 5: below scale ~0.6, show each terminal as a static bitmap of its last frame while the camera moves; make shadows and dots cheaper during motion only. Target: zoom p95 ≤ 33 ms at 60 Hz.

Side finding: the server's standalone-TLS mode returned 401 on /auth/session after passkey enrolment. Under investigation.

**Zoom on a real GPU (Mac VM, ANGLE Metal, hardware compositing and raster confirmed; 60 Hz display).** A = e704e5b8 (production) against B = 069fd47b, alternating A B A B A B, 54 real shells, Mac load 1.5–2.4. The two are indistinguishable: | | A | B | |---|---|---| | Zoom frame p50 | 16.9 ms | 16.9 ms | | Zoom frame p95 | 83.5 ms | 83.1 ms | | Zoom frame max | 100.7 ms | 101.1 ms | | Pan p95 | 18.2 ms | 19.3 ms | | Split p95 | 31.8 ms | 31.8 ms | | Window step p95 | 33.0 ms | 33.1 ms | **Round 4 is not shipped.** **Where zoom spends its time.** The main thread is not the bottleneck: longest task 17–19 ms. The GPU process runs at 415 ms/s, and 238 ms/s of that is renderer raster. Removing one thing at a time: - terminal canvases hidden: GPU 418 → 254 ms/s, p95 83.8 → 50 ms; - shadows off: 350 ms/s; - dot layers hidden: 354 ms/s. **Round 5:** below scale ~0.6, show each terminal as a static bitmap of its last frame while the camera moves; make shadows and dots cheaper during motion only. Target: zoom p95 ≤ 33 ms at 60 Hz. Side finding: the server's standalone-TLS mode returned 401 on `/auth/session` after passkey enrolment. Under investigation.
Author
Owner

Zoom round 5 deployed (web only) at d82d0fb9, with dev fast-forwarded to it. Backup: /var/backups/infraternal/20261010-090757-d82d0fb9.

Changes:

  • During camera motion each window becomes its own compositor layer. The hint is removed at rest.
  • Below scale 0.6, every terminal except the active one parks its render loop and draws once when the camera rests. Same pixels, so no flash and zero extra memory.

Real GPU on the Mac VM, 2502×1326 @2×, 54 real shells, alternating A=7e8545c4 / B=d82d0fb9, 3 runs each:

A B
Zoom p95 66.6 ms 34.9 ms
Zoom max 84 ms 50 ms
GPU process 468 ms/s 208 ms/s

Pan, split, window step, idle and typing are unchanged within noise. At 1440×900 @2×: zoom p95 50.1 → 33.9 ms.

The only cost: pan's longest task is about 4 ms higher, with no effect on frame times. The new real-server spec real/freeze.spec.ts fails without the change.

**Zoom round 5 deployed (web only)** at d82d0fb9, with dev fast-forwarded to it. Backup: /var/backups/infraternal/20261010-090757-d82d0fb9. Changes: - During camera motion each window becomes its own compositor layer. The hint is removed at rest. - Below scale 0.6, every terminal except the active one parks its render loop and draws once when the camera rests. Same pixels, so no flash and zero extra memory. Real GPU on the Mac VM, 2502×1326 @2×, 54 real shells, alternating A=7e8545c4 / B=d82d0fb9, 3 runs each: | | A | B | |---|---|---| | Zoom p95 | 66.6 ms | 34.9 ms | | Zoom max | 84 ms | 50 ms | | GPU process | 468 ms/s | 208 ms/s | Pan, split, window step, idle and typing are unchanged within noise. At 1440×900 @2×: zoom p95 50.1 → 33.9 ms. The only cost: pan's longest task is about 4 ms higher, with no effect on frame times. The new real-server spec `real/freeze.spec.ts` fails without the change.
Sign in to join this conversation.
No labels
launch-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/infraternal#121
No description provided.