a73x

7a1e22d2

Input-latency bench: share grid-lock across all bench modes

a73x   2026-04-18 06:55

Commit message
Input-latency bench: share grid-lock across all bench modes

Retrofit existing WAYSTTY_BENCH to use the same grid-lock mechanism
as the new input-latency bench. Output-bench numbers today are
silently compositor-dependent (accepts tiling resize); after this
change, any bench mode either runs at the configured grid size
(default 80x24, overridable via WAYSTTY_BENCH_COLS/ROWS) or aborts
with a diagnostic pointing at tiling-compositor resize as the cause.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

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.