Padi — Typing-Echo Latency Baseline
The W1 baseline owed by padi W1 (PR 1652) before W2.2 starts — keystroke→echo latency measured through the real stack on current master, both clock points on kolu-server's websocket so the padi hop W2.2 adds is measured honestly. Method, box spec, numbers, and how to re-run for the sub-5ms added-p99 budget.
The number W2.2 has to beat. padi W2.2 slips a whole new process — padi — between kolu-server and kaval, so every keystroke gains a hop. The plan budgets that hop at < 5ms added p99 and says the ceiling is measured, not guessed: W1 (#1652) owes a baseline of today’s echo latency, and W2.2’s done-criterion (e) compares against it. This note is that baseline — what we measure, where the clock starts and stops, the box it ran on, the numbers, and the one command that re-runs it identically.
What the baseline measures
One keystroke’s full round-trip through the real stack, timed by a headless client that speaks the same wire the browser does:
- t0 — the clock starts the instant the probe dispatches
padiSurface.lifecycle.sendInput({ id, data: <char> })over kolu-server’s websocket (/rpc/ws). - The write travels kolu-server → kaval → PTY, where the shell’s line discipline echoes the typed character back.We type into a fresh shell’s line editor (readline), which echoes each character verbatim in raw mode — the same echo a user sees. The probe never sends a newline, so no command ever runs; between measurements it sends a Backspace so the input line stays one character wide and long-line redraws can’t pollute later samples.
- t1 — the clock stops when that echoed character first appears in a
padiSurface.terminalAttachdelta frame on the same websocket.
latency = t1 − t0, both read from one process’s monotonic clock
(process.hrtime.bigint()), so absolute clock skew is irrelevant — only the delta
matters.
The measured baseline
Current master (#1652 at merge — the padi domain served
in-process, no separate padi process yet), a nix-built kolu (.#default), on a
quiet ephemeral box with nothing else running. 2400 warm keystrokes (8 terminals
× 300 measured, 50 warm-up discarded per terminal):
| percentile | latency |
|---|---|
| p50 | 2.14 ms |
| p90 | 3.16 ms |
| p95 | 3.42 ms |
| p99 | 4.36 ms |
| min / mean / max | 0.16 / 2.36 / 20.9 ms |
A second independent run agreed within noise (p50 2.04, p95 3.45, p99 4.21 ms), so the percentiles are stable, not a lucky draw.Percentiles use the nearest-rank method on the ascending-sorted samples (the value at rank ⌈p/100·n⌉) — no interpolation, so the computation itself is deterministic and W2.2 compares like for like. The lone ~21 ms max is a single GC/scheduler outlier well outside p99.
The box — recorded in the result JSON, so a re-run on a different box is never
mistaken for the same conditions: pu ephemeral Incus container (NixOS), Intel
Core i9-14900K, 32 logical CPUs, 125 GiB RAM, Linux 6.12.85 x86_64; kolu’s bundled
Node v22.22.1; kolu built from master via nix build .#default.
How the probe drives the stack
The measurement is a small, reusable script — not a one-off — so W2.2 re-runs the identical method (bench + this note shipped in #1660):
packages/server/bench/typingEchoLatency.ts— the client probe. It dialsws://host:port/rpc/wswith Node’s globalWebSocket, wraps it in@kolu/surface’swebsocketLink, then for each terminal:lifecycle.create({})a bare shell, subscribe toterminalAttach(skipping the first frame, which is the scrollback snapshot), and ping single keystrokes serially — send one, wait for its echo, record, repeat. Serial by design: only one keystroke is ever in flight, so an echo can never be attributed to the wrong key.packages/server/bench/run.sh— the orchestrator. Boots a private, isolated kolu (its own kaval via a throwaway$XDG_RUNTIME_DIR+KOLU_KAVAL_SOCKET, its own state dir, a throwaway$HOME, an OS-assigned free port), waits for it to listen, runs the probe, and tears everything down. The private socket means kolu’s always-recycle can only ever reap its own kaval — it can never touch a production daemon.
Both live under packages/server/ because the probe imports @kolu/surface and
ws, which that package already depends on — no new dependency, no lockfile churn.
Re-running it
From a checkout, on a quiet box:
just bench-typing-echo
That builds .#default, boots the private server, runs the probe, prints the
percentiles, and cleans up. Knobs (all optional) are environment variables:
KOLU_BENCH_SAMPLES (measured keystrokes per terminal), KOLU_BENCH_TERMINALS,
KOLU_BENCH_WARMUP, KOLU_BENCH_INTERKEY_MS, KOLU_BENCH_OUT (write the full JSON,
raw samples included), and KOLU_BENCH_BIN (point at a pre-built bin/kolu to skip
the nix build).