7a1e22d2
Input-latency bench: share grid-lock across all bench modes
a73x 2026-04-18 06:55
Commit message
docs/superpowers/specs/2026-04-18-input-latency-bench-design.md
| Old | New | ||
|---|---|---|---|
| @@ -20,9 +20,11 @@ The output is two headline metrics plus a per-stage breakdown: | |||
| 20 | 20 | ||
| 21 | New env var `WAYSTTY_INPUT_BENCH={cold|hot|both}` activates input-bench mode. | 21 | New env var `WAYSTTY_INPUT_BENCH={cold|hot|both}` activates input-bench mode. |
| 22 | 22 | ||
| 23 | When active, waystty: | 23 | Grid-lock (below) is shared infrastructure — applied whenever *any* bench mode is active (`WAYSTTY_BENCH` or `WAYSTTY_INPUT_BENCH`). This retrofits the existing output-only bench so both benches have the same reproducibility guarantees. |
| 24 | 24 | ||
| 25 | 1. **Locks the grid to a known size** before spawning any child. Default 80×24, overridable via `WAYSTTY_INPUT_BENCH_COLS` / `WAYSTTY_INPUT_BENCH_ROWS`. This removes font/DPI/monitor variance so numbers are reproducible across machines and so hot-mode sentinel lifetime is a computed guarantee, not a hope. | 25 | When input-bench mode is active, waystty: |
| 26 | |||
| 27 | 1. **Locks the grid to a known size** (see Grid-lock module below). Default 80×24, overridable via `WAYSTTY_BENCH_COLS` / `WAYSTTY_BENCH_ROWS`. This removes font/DPI/monitor variance so numbers are reproducible across machines and so hot-mode sentinel lifetime is a computed guarantee, not a hope. | ||
| 26 | 2. Spawns a minimal PTY child instead of `$SHELL`: | 28 | 2. Spawns a minimal PTY child instead of `$SHELL`: |
| 27 | - `cold` → `cat > /dev/null`. Reads and discards stdin so the PTY slave's input buffer stays drained; kernel PTY line discipline echoes each byte (ECHO flag on the slave). Cat's canonical-mode buffering is irrelevant — we measure kernel echo, not cat's reads. | 29 | - `cold` → `cat > /dev/null`. Reads and discards stdin so the PTY slave's input buffer stays drained; kernel PTY line discipline echoes each byte (ECHO flag on the slave). Cat's canonical-mode buffering is irrelevant — we measure kernel echo, not cat's reads. |
| 28 | - `hot` → `sh -c 'yes "$(printf "x%.0s" {1..500})" | pv -qL 24K'`. Produces 500-char lines rate-limited to 24 KB/s, ≈ 46 lines/sec. On an 80×24 grid this gives a sentinel lifetime of ≈30 frames at 60Hz — enough time for any reasonable presentation to land — while still exercising the render pipeline (100KB/s of parsing, atlas churn on long lines, per-frame row rebuild). | 30 | - `hot` → `sh -c 'yes "$(printf "x%.0s" {1..500})" | pv -qL 24K'`. Produces 500-char lines rate-limited to 24 KB/s, ≈ 46 lines/sec. On an 80×24 grid this gives a sentinel lifetime of ≈30 frames at 60Hz — enough time for any reasonable presentation to land — while still exercising the render pipeline (100KB/s of parsing, atlas churn on long lines, per-frame row rebuild). |
| @@ -34,6 +36,20 @@ When active, waystty: | |||
| 34 | 36 | ||
| 35 | MAILBOX present mode is preserved throughout. The driver handles `wp_presentation_feedback.discarded` by keeping feedback listeners live on subsequent frames, since a discarded frame's sentinel is still in the grid on the next frame. | 37 | MAILBOX present mode is preserved throughout. The driver handles `wp_presentation_feedback.discarded` by keeping feedback listeners live on subsequent frames, since a discarded frame's sentinel is still in the grid on the next frame. |
| 36 | 38 | ||
| 39 | ## Module 0: Grid-lock (shared across all bench modes) | ||
| 40 | |||
| 41 | Applied whenever `WAYSTTY_BENCH` or `WAYSTTY_INPUT_BENCH` is set. | ||
| 42 | |||
| 43 | 1. Size the initial window to `cols × cell_w` / `rows × cell_h` as today. | ||
| 44 | 2. Advertise `xdg_toplevel.set_min_size(w, h)` and `set_max_size(w, h)` to signal that the window should not be resized. Compositors that honor these hints (most floating compositors) will leave the window alone. | ||
| 45 | 3. In the main-loop resize observer (`src/main.zig:409`), if bench mode is active and the compositor forces a different size, **abort** the bench with a diagnostic on stderr: | ||
| 46 | |||
| 47 | > `waystty bench: compositor sized window to WxH, expected CxR grid. Run in a floating window or a non-tiling compositor for reproducible benchmarks.` | ||
| 48 | |||
| 49 | 4. Print the achieved grid size as the first line of any bench stats output, so it's always visible alongside the numbers. | ||
| 50 | |||
| 51 | This retrofits the existing `WAYSTTY_BENCH` mode — today it starts at 80×24 but silently accepts compositor resize, so numbers are already compositor-dependent. After this change, existing output-bench numbers are guaranteed reproducible or it fails loudly. | ||
| 52 | |||
| 37 | ## Module 1: Sentinel allocation | 53 | ## Module 1: Sentinel allocation |
| 38 | 54 | ||
| 39 | Each sample uses a unique codepoint from the Unicode Private Use Area U+E000…U+EFFF (4096 distinct sentinels). PUA is chosen because it never appears in normal output from `cat`, `yes`, or `pv`, so a grid scan for a specific codepoint cannot collide with unrelated output. Only one sample is in flight at a time, so wraparound at 4096 is safe. | 55 | Each sample uses a unique codepoint from the Unicode Private Use Area U+E000…U+EFFF (4096 distinct sentinels). PUA is chosen because it never appears in normal output from `cat`, `yes`, or `pv`, so a grid scan for a specific codepoint cannot collide with unrelated output. Only one sample is in flight at a time, so wraparound at 4096 is safe. |
| @@ -144,7 +160,7 @@ Mirrors the existing `bench` / `profile` targets: `OPT` defaults to `ReleaseFast | |||
| 144 | 160 | ||
| 145 | - `src/bench_input.zig` — new. `BenchDriver` struct, sentinel allocator, in-flight sample state, pending-feedback map, stats printer, WSI fallback trigger. | 161 | - `src/bench_input.zig` — new. `BenchDriver` struct, sentinel allocator, in-flight sample state, pending-feedback map, stats printer, WSI fallback trigger. |
| 146 | - `src/bench_stats.zig` — add `frame_counter: u64` to `FrameTiming`; increment on each rendered frame. | 162 | - `src/bench_stats.zig` — add `frame_counter: u64` to `FrameTiming`; increment on each rendered frame. |
| 147 | - `src/main.zig` — env parsing, grid-size lock, driver init/tick hookup, PTY child switcheroo, termios verification, scenario sequencer with teardown, sentinel scan in the post-frame hook, bench-keyboard `.key`-suppression gate. | 163 | - `src/main.zig` — env parsing, grid-size lock (shared across bench modes, retrofits existing `WAYSTTY_BENCH`), `xdg_toplevel` min/max size hints, resize-observer abort on mismatch, driver init/tick hookup, PTY child switcheroo, termios verification, scenario sequencer with teardown, sentinel scan in the post-frame hook, bench-keyboard `.key`-suppression gate. |
| 148 | - `src/wayland.zig` — bind `wp_presentation` global, plumb `wp_presentation_feedback` creation and event callbacks. | 164 | - `src/wayland.zig` — bind `wp_presentation` global, plumb `wp_presentation_feedback` creation and event callbacks. |
| 149 | - `src/renderer.zig` — expose swapchain present call site so `wp_presentation.feedback(surface)` can be called immediately before `vkQueuePresentKHR`. | 165 | - `src/renderer.zig` — expose swapchain present call site so `wp_presentation.feedback(surface)` can be called immediately before `vkQueuePresentKHR`. |
| 150 | - `Makefile` — `bench-input` target. | 166 | - `Makefile` — `bench-input` target. |
| @@ -155,7 +171,8 @@ Mirrors the existing `bench` / `profile` targets: `OPT` defaults to `ReleaseFast | |||
| 155 | - **Unit:** sentinel allocator produces 4096 distinct codepoints before wrapping. | 171 | - **Unit:** sentinel allocator produces 4096 distinct codepoints before wrapping. |
| 156 | - **Unit:** pair-on-arrival state machine completes when events arrive in either order; advances past `discarded`. | 172 | - **Unit:** pair-on-arrival state machine completes when events arrive in either order; advances past `discarded`. |
| 157 | - **Unit:** WSI-fallback trigger fires after >10% of first 50 samples time out and not before. | 173 | - **Unit:** WSI-fallback trigger fires after >10% of first 50 samples time out and not before. |
| 158 | - **Manual:** grid is 80×24 at bench start regardless of window size; verify via logging. | 174 | - **Manual (input bench):** grid is 80×24 at bench start regardless of window size; verify via logging. |
| 175 | - **Manual (existing output bench):** `make bench` still passes after grid-lock retrofit; grid-size line appears in stats output; verify on both floating and tiling compositors (tiling should abort with the diagnostic). | ||
| 159 | - **Manual:** termios `ECHO` is set on the PTY slave after spawn; verify with a short run of `stty -a` in debug mode. | 176 | - **Manual:** termios `ECHO` is set on the PTY slave after spawn; verify with a short run of `stty -a` in debug mode. |
| 160 | - **Manual:** `make bench-input` produces cold and hot numbers with cold < hot. Sanity: cold p50 on the order of one refresh interval (wait-to-vsync plus compositor latch) is expected; hot p99 noticeably larger than cold p99 confirms contention is being observed. | 177 | - **Manual:** `make bench-input` produces cold and hot numbers with cold < hot. Sanity: cold p50 on the order of one refresh interval (wait-to-vsync plus compositor latch) is expected; hot p99 noticeably larger than cold p99 confirms contention is being observed. |
| 161 | - **Manual:** run `WAYSTTY_INPUT_BENCH=cold` against a debug build; verify via logs that each injected sentinel round-trips and matches within a few frames. | 178 | - **Manual:** run `WAYSTTY_INPUT_BENCH=cold` against a debug build; verify via logs that each injected sentinel round-trips and matches within a few frames. |