hermes-agent/bench
alt-glitch e3973050df bench: post-fix otui results — crash eliminated
Re-ran the cells that crashed, against the fixed binary (a939c9a):

- mem3000 otui-capped:   crashed_after_stream exit 7 @ ~900MB
                       → completed exit 0, vmhwm 859MB
- mem3000 otui-uncapped: crashed_after_stream exit 7
                       → completed exit 0, vmhwm 834MB
- slope10000 otui-uncapped: died exit 7 at 3,000 msgs (197d499 result)
                       → completed ALL 10,000 msgs, exit 0, vmhwm 1.57GB —
                         under the 2GB cgroup cap, no cap-hit, no crash
- fresh ink baselines for both cells (mem3000 257MB / slope10k 328MB).

No "Failed to create SyntaxStyle" anywhere; the store cap now binds at the
handle-safe ceiling (1000 rows) long before the native 65,534-slot handle
table exhausts. report.html + report-assets regenerated via render.mjs
(results are append-only; pre-fix runs remain as the baseline).
2026-06-11 04:09:27 +05:30
..
report-assets bench: post-fix otui results — crash eliminated 2026-06-11 04:09:27 +05:30
results bench: post-fix otui results — crash eliminated 2026-06-11 04:09:27 +05:30
.gitignore bench: fake-gateway + PTY harness + matrix runner (methodology in docs/plans) 2026-06-11 02:29:37 +05:30
fake-gateway.mjs bench: fake-gateway + PTY harness + matrix runner (methodology in docs/plans) 2026-06-11 02:29:37 +05:30
fixture-stream.mjs bench: fake-gateway + PTY harness + matrix runner (methodology in docs/plans) 2026-06-11 02:29:37 +05:30
harness.mjs bench: E1/E3 results + report render 2026-06-11 03:22:27 +05:30
package-lock.json bench: fake-gateway + PTY harness + matrix runner (methodology in docs/plans) 2026-06-11 02:29:37 +05:30
package.json bench: fake-gateway + PTY harness + matrix runner (methodology in docs/plans) 2026-06-11 02:29:37 +05:30
README.md bench: E1/E3 results + report render 2026-06-11 03:22:27 +05:30
render.mjs bench: E1/E3 results + report render 2026-06-11 03:22:27 +05:30
report.html bench: post-fix otui results — crash eliminated 2026-06-11 04:09:27 +05:30
run-e3-inner.mjs bench: fake-gateway + PTY harness + matrix runner (methodology in docs/plans) 2026-06-11 02:29:37 +05:30
run-e3.sh bench: fake-gateway + PTY harness + matrix runner (methodology in docs/plans) 2026-06-11 02:29:37 +05:30
run.mjs bench: fake-gateway + PTY harness + matrix runner (methodology in docs/plans) 2026-06-11 02:29:37 +05:30

TUI benchmark suite — Ink (ui-tui) vs OpenTUI (ui-opentui)

Methodology (settled, binding): docs/plans/opentui-bench-suite.md. This directory is the implementation: real binaries over a real node-pty PTY (120×40, xterm-256color), a fake gateway substituted via HERMES_PYTHON (ZERO changes to either UI), external /proc sampling, cgroup-v2 memory caps. No tmux anywhere in measurement.

Pieces

file role
fake-gateway.mjs NDJSON JSON-RPC gateway stand-in. Both UIs spawn it as $HERMES_PYTHON -m tui_gateway.entry. Answers every startup RPC with canned results, then streams the fixture (burst / paced / load-then-idle). Never writes stderr (the UIs render gateway stderr).
fixture-stream.mjs Serializes the deterministic lumpy-turn fixture (ui-opentui/scripts/fixture.ts, imported directly via Node ≥26 type stripping — no port) to NDJSON. Cached under .cache/, sha256-stamped.
harness.mjs One scenario = one UI boot: node-pty PTY, tight drain loop (event-loop starvation probe, 10ms budget asserted), /proc/PID/{smaps_rollup,status,stat} samples on 100-msg boundaries (UI PID only), systemd-run --user --scope -p MemoryMax=… -p MemorySwapMax=0 caps, SGR wheel injection, resize-jiggle digest capture.
run.mjs The matrix runner (protocol: determinism gate first, sequential SUTs, randomized per-rep config order, 10s cooldowns, load gate).
render.mjs results/*.json → self-contained report.html (inline SVG, no CDN) + PNGs in report-assets/.

Running cells

Node 26 is required (BENCH_NODE_BIN overrides the default fnm path). Build both UIs first; results land in results/<utc>-<sha7>-<cell>-<ui>-<config>-r<rep>.json.

cd ui-opentui && node scripts/build.mjs && cd ../ui-tui && node scripts/build.mjs && cd ../bench
npm install                       # node-pty (bench-local devDep)

node run.mjs --cell gate          # determinism gate (digest replay ×2 per UI) — run FIRST
node run.mjs --cell mem3000       # clean memory runs, 3 reps × 3 configs, 2GB cap
node run.mjs --cell slope10k      # one 10k-msg slope run: ink + otui-uncapped (cap-hit IS a datapoint)
node run.mjs --cell nodes         # instrumented node counts (ink fd-3 sampler; opentui headless walk)
node run.mjs --cell cpu           # paced 30 ev/s streaming ×3
node run.mjs --cell scroll        # SGR wheel 30Hz×15s on a 3000-msg transcript ×3
node run.mjs --cell startup       # ×10, fake gateway
node render.mjs                   # report.html + report-assets/*.png

Configs: ink · otui-capped (HERMES_TUI_MAX_MESSAGES=3000, the default) · otui-uncapped (=100000). Launch parity with hermes_cli/main.py: Ink = node --expose-gc ui-tui/dist/entry.js, OpenTUI = node --experimental-ffi --no-warnings ui-opentui/dist/main.js, both with NODE_OPTIONS=--max-old-space-size=<heap> (8192 on the unconstrained host — what the launcher picks outside a container).

E3 (constrained Docker survival)

E3-lite runs the same harness inside a generic node:26 container (NOT the shipped image) with the worktree bind-mounted read-only and --memory=1g --memory-swap=1g; the whole container (UI + fake gateway + harness) shares the limit. See run-e3.sh if present, or the report's survival table for the exact invocation used.

What actually ran on 2026-06-11 (E1 host + E3-lite) — deviations from the plan

  • 3 reps for mem3000 (not 5) and scroll at 2000 msgs (not 3000): the OpenTUI engine on this tree (sha 197d499, dist built from 50e3471 tree state) crashes at ≈3000 fixture msgs — an uncaught Error: Failed to create SyntaxStyle (native handle allocation fails; every TextBufferRenderable creates one in @opentui/core 0.4.0), masked by a second Failed to create optimized buffer crash inside the renderer's uncaughtException handler. Postmortems are in each result's pty_tail; RSS at crash ≈880MB — far below the 2GB cap, so it is a handle/pool limit, not memory. This dominates every OpenTUI cell past ~3000 msgs.
  • OpenTUI headless node-count: not run. scripts/mem-bench.tsx under Node FFI dies on the first fixture turn with ERR_INVALID_ARG_VALUE … textBufferViewSetViewport (the known Bun→Node u32-coordinate class; the production binary carries the ffiSafe clamp, the headless test renderer path does not) and then hangs. The Ink fd-3 sampler ran fine.
  • Startup real-gateway variant: probed, not run as a cell. A full run would forge real sessions in the user's ~/.hermes store. Measured standalone: the real tui_gateway (venv python) emits gateway.ready in 131ms median (×10, range 130138ms) — add that to the fake-gateway startup numbers.
  • No cgroup OOM kills observed anywhere (Ink at 10k msgs peaks ~321MB; OpenTUI crashes before reaching the cap), so the cap-hit machinery (memory.events / journal fallback) never fired in anger; E3-lite classified the OpenTUI death correctly as a crash (oom_kill=0, exit 7).
  • E2 (shipped Docker image): not run — image build time prohibitive in this session; E3-lite (generic node:26) covers the constrained-memory question.
  • Drain-loop starvation: a handful of OpenTUI burst runs recorded 1118ms max event-loop lag in the harness (>10ms budget, flagged drain_ok:false in those results); all paced/scroll/startup runs stayed under 10ms.

Accounting + known deviations (by design)

  • "messages" = fixture rows (rowsPerTurn accounting, identical to ui-opentui/scripts/mem-bench.tsx), so numbers are comparable with the pre-registered expectations. ~46% of fixture rows are user/system rows.
  • User/system rows are not streamed: they are composer-local in both UIs (no wire event exists), so PTY runs mount only the assistant/tool rows — the renderable-heavy part that carries the memory claim. Consequence: the OpenTUI store cap (3000 rows) binds at ≈6.6k fixture-msgs in PTY runs.
  • Digest gate: final-screen digest after a resize-forced repaint, ANSI stripped, cut at the composer hint, up: Ns normalized (the OpenTUI status bar has a 1Hz uptime clock; the transcript region itself is deterministic).
  • The headless scripts/mem-bench.tsx numbers are diagnostic-only and flagged instrumented/diagnostic_only — never headlined.