Both UIs were run as real binaries in a real terminal, fed the exact same scripted conversations by a fake gateway, and measured from outside the process. Every number below is the typical of repeated runs (median) unless said otherwise. 103 result files · sha 50e34713b · node v26.3.0 · generated 2026-06-11T04:05:28.897Z
| dimension | winner | the numbers |
|---|---|---|
| Memory — typical real sessions (20–300 msgs) | Ink wins, modestly | OpenTUI uses ~59–88MB more (163 vs 222MB at 100 msgs; 180 vs 268MB at 300). |
| Memory — longest real sessions (~2,000 msgs, the longest 1 in 100 = p99) | Ink wins big | 234MB vs 671MB peak — 2.9× more. Sessions this long really happen (6 of them in the DB). |
| Memory — 3,000-msg stress marathon (beyond any real session) | Ink wins | Ink levels off near 249MB; OpenTUI climbs to ~767MB (peak ~875MB), and its syntax styling degrades past ~1,400 rows. Stress test only — past the longest real session. |
| Scroll responsiveness on a long transcript | OpenTUI wins decisively | Slowest 1-in-100 scroll responses (p99): 14–17ms vs 83–101ms across repeats. Typical scrolls are ~2ms on both — the difference is the stutters. |
| Frame smoothness while streaming | OpenTUI wins | 22 vs 16 screen updates/s, and its worst pauses between updates are half as long (103ms vs 209ms, slowest 1 in 20). |
| Typing echo (keystroke → it appears) | Tie | Both answer a keystroke in 1–2ms — under any human threshold. |
| Submit → first reply paint | Ink wins | 44ms vs 107ms from pressing Enter to the first reply text on screen. |
| CPU, including the terminal-emulator (tmux) side | Tie | ~80 CPU-seconds either way for the same 800-message stream (83 vs 80s total); the tmux leg is ~0.4s for both. |
| Crash recovery (gateway shot mid-stream) | Tie | Both auto-respawn the killed gateway and end with the full transcript intact and zero orphan processes. Ink respawns in ~81ms, OpenTUI in ~1.0s. |
| Startup | Ink wins, modestly | First paint 67ms vs 127ms. Both feel instant; OpenTUI actually reaches “session ready” slightly sooner (176 vs 202ms). |
red = Ink (current UI) wins · green = OpenTUI (new engine) wins · grey = tie
One-line summary: OpenTUI is the smoother UI (scrolling, streaming) and Ink is the lighter one (memory, first paint). Everything else is a wash — including reliability, where both recover from a killed gateway with the transcript intact.
The memory debate was framed around 200–300-message sessions. The real session database says that band is the top 5–10%: the typical session is ~20 messages, 90% stay under 182, and the longest real sessions reach ~1941 messages (the longest 1 in 100 — p99) — and at that tail the memory gap widens to 2.9×.
| session size | what that means in practice | Ink peak | OpenTUI peak | difference |
|---|---|---|---|---|
| 100 msgs | a heavier-than-usual day (typical session is ~20 msgs) | 163 MB | 222 MB | OpenTUI +59 MB (1.4×) |
| 300 msgs | top ~5% of real sessions | 180 MB | 268 MB | OpenTUI +88 MB (1.5×) |
| 2,000 msgs | the longest sessions that actually occur (~1 in 100, p99) | 234 MB | 671 MB | OpenTUI +437 MB (2.9×) |
| UI | keystroke echo, typical (p50) | keystroke echo, slowest 1 in 20 (p95) | Enter → first reply text on screen | keystrokes verified |
|---|---|---|---|---|
| Ink | 1 ms | 1.5 ms | 44 ms | 30/30 |
| OpenTUI | 2 ms | 3.5 ms | 107 ms | 30/30 |
Keystroke echo is a tie — 1–2ms on both, far below anything a human can perceive. The one real difference: after pressing Enter, Ink paints the first reply text in 44ms vs OpenTUI's 107ms. (Single run per UI.)
The hypothesis that Ink's bigger output stream costs meaningfully more CPU in the terminal emulator did not hold at this workload: Ink did push more bytes (3.8MB vs 3.3MB), but the tmux server burned ~0.4 CPU-seconds either way.
Each scenario kills, freezes, or yanks something out from under the UI on a live session, then checks: did the UI survive, did the gateway come back, did the stream resume, is the final transcript identical to an undisturbed run, and is anything left running afterwards?
| what we did | Ink | OpenTUI |
|---|---|---|
| gw-kill-stream shot the gateway (kill -9) while reply text was streaming | UI survived: yes gateway respawned: yes (87ms) stream resumed: yes transcript intact: yes orphan processes: none | UI survived: yes gateway respawned: yes (1.0s) stream resumed: yes transcript intact: yes orphan processes: none |
| gw-kill-tool shot the gateway (kill -9) in the middle of a tool call | UI survived: yes gateway respawned: yes (81ms) stream resumed: yes transcript intact: yes orphan processes: none | UI survived: yes gateway respawned: yes (1.0s) stream resumed: yes transcript intact: yes orphan processes: none |
| gw-stop froze the gateway for 30 seconds mid-session, then let the UI recover | UI survived: yes gateway respawned: yes (35ms) stream resumed: yes transcript intact: yes orphan processes: none | UI survived: yes gateway respawned: yes (1.1s) stream resumed: yes transcript intact: yes orphan processes: none |
| resize-storm resized the window 30 times in 3 seconds | survived: yes transcript intact: yes orphan processes: none | survived: yes transcript intact: yes orphan processes: none |
| pty-eof closed the terminal out from under the UI (should exit cleanly, leave nothing behind) | exited cleanly: yes gateway cleaned up: yes (100ms) orphan processes: none | exited cleanly: yes gateway cleaned up: yes (101ms) orphan processes: none |
Result: a tie. Both UIs auto-respawn a killed gateway and finish with a byte-identical final transcript and zero orphan processes. Ink respawns faster (~35–87ms vs ~1.0s), but both are well within "didn't lose anything".
Everything below streams 3,000–10,000 messages into one session — past the longest session ever recorded in the real database (~1941 msgs). It shows where the engines break, not what daily use feels like.
| config | memory growth MB per 1k msgs (3,000-msg runs) | memory growth MB per 1k (10,000-msg run) | settled memory MB (plateau, end of run) | peak memory MB | scroll ms — typical / slowest 1 in 10 / slowest 1 in 100 (p50/p90/p99) | CPU ms per event (paced stream) | killed by 2GB cap? |
|---|---|---|---|---|---|---|---|
| Ink | 25.48 [middle half: 23.89–27.74] | 5.88 | 249 [middle half: 247–250] | 257 [middle half: 256–258] | 2.0 / 36.1 / 98.5 | 31.04 [middle half: 30.99–31.23] | none |
| OpenTUI | 254.74 [middle half: 250.81–265.46] | not run | 767 [middle half: 741–799] | 875 [middle half: 868–879] | 2.0 / 3.0 / 17.0 | 30.00 [middle half: 30.00–30.05] | none |
| OpenTUI (no cap) | 297.65 [middle half: 283.88–311.91] | 173.76 | 807 [middle half: 796–813] | 890 [middle half: 874–893] | 2.0 / 3.0 / 14.0 | 30.09 [middle half: 30.09–30.10] | none |
Reading guide: "typical" = median; the bracketed range is the middle half of runs (interquartile range). "slowest 1 in 100" = p99. OpenTUI's syntax styling visibly degrades past ~1,400 rendered rows in these marathons (style-handle exhaustion fallback); older OpenTUI runs also show a post-stream crash that was fixed before the latest runs.
| cell | UI | memory limit | result | msgs survived | peak memory | basis |
|---|---|---|---|---|---|---|
| e3lite-1g | Ink | 1 GB | completed | 10000 | 223 MB | — |
| e3lite-1g | OpenTUI | 1 GB | died | 3000 | 849 MB | — |
Determinism gate: each UI replayed the same input twice and must produce a pixel-identical final screen (same fingerprint). If this fails, none of the comparisons above are meaningful.
| UI | replay fingerprints (same input must give same screen) | gate |
|---|---|---|
| Ink | 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b | PASS |
| OpenTUI | d5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eac | PASS |
⚠ some runs were flagged: the test rig itself lagged (results kept, but read with care):
| run | max rig lag | lags >10ms |
|---|---|---|
| 2026-06-10T2058-50e3471-mem3000-opentui-otui-capped-r0.json | 18 ms | 2 |
| 2026-06-10T2058-50e3471-mem3000-opentui-otui-uncapped-r0.json | 22 ms | 3 |
| 2026-06-10T2059-14ee1a5-mem3000-opentui-otui-uncapped-r1.json | 33 ms | 3 |
| 2026-06-10T2100-197d499-mem3000-opentui-otui-capped-r2.json | 22 ms | 3 |
| 2026-06-10T2100-197d499-mem3000-opentui-otui-uncapped-r2.json | 19 ms | 2 |
| 2026-06-10T2107-197d499-cpu800-opentui-otui-uncapped-r0.json | 11 ms | 1 |
| 2026-06-10T2108-197d499-cpu800-opentui-otui-capped-r0.json | 15 ms | 1 |
| 2026-06-10T2116-197d499-cpu800-opentui-otui-capped-r1.json | 11 ms | 1 |
| 2026-06-10T2124-197d499-scroll2000-opentui-otui-capped-r0.json | 15 ms | 2 |
| 2026-06-10T2125-197d499-scroll2000-opentui-otui-uncapped-r0.json | 26 ms | 2 |
| 2026-06-10T2126-197d499-scroll2000-opentui-otui-capped-r1.json | 23 ms | 2 |
| 2026-06-10T2126-197d499-scroll2000-opentui-otui-uncapped-r1.json | 21 ms | 2 |
| 2026-06-10T2127-197d499-scroll2000-opentui-otui-uncapped-r2.json | 15 ms | 2 |
| 2026-06-10T2128-197d499-scroll2000-opentui-otui-capped-r2.json | 20 ms | 2 |
| 2026-06-10T2146-197d499-slope10000-opentui-otui-uncapped-r0.json | 25 ms | 4 |
| 2026-06-10T2147-e3-e3lite-1g-opentui-otui-capped-r0.json | 14 ms | 3 |
| 2026-06-10T2236-a939c9a-mem3000-opentui-otui-capped-r0.json | 24 ms | 4 |
| 2026-06-10T2237-a939c9a-mem3000-opentui-otui-uncapped-r0.json | 14 ms | 2 |
| 2026-06-10T2237-a939c9a-slope10000-opentui-otui-uncapped-r0.json | 69 ms | 17 |
| 2026-06-11T0240-805e080-mem2000-opentui-otui-capped-r0.json | 25 ms | 2 |
| 2026-06-11T0325-cbe703c-chaos-ink-ink-rgw-stop.json | 13 ms | 1 |
Methodology: docs/plans/opentui-bench-suite.md. Real binaries on a real PTY
(120×40), fake gateway via HERMES_PYTHON (zero UI changes), outside-the-process /proc sampling,
2GB cgroup caps via systemd. Instrumented diagnostic runs are flagged and never headlined.