Single-binary (Rust) benchmark runner for shell startup/dispatch latency
and terminal-emulator draw/input/resource characteristics. Implementation of
proposal recommendation C (docs/proposal-terminal-bench.md), MVP scope
plus the extension-1 emulator metrics (§3 items 1-3), the extension-2
compare subcommand (§6.4/§6.6), further emulator-focused metrics
(CJK/emoji rendering, loaded-vs-idle input latency, resource footprint),
the measurement-quality hardening from docs/proposal-detailed-benchmark.md
§A (settle, concurrent-run detection, warmup/outlier filtering, environment
metadata, adaptive --runs auto), the §E orchestrate subcommand for
sequential multi-emulator runs, the §C synchronized-output probe, and the
§F statistics (--keep-samples, bootstrap CI, Mann-Whitney significance).
# All scenarios, default shells (zsh,bash), 20 runs each
bin/tbench run
# One scenario only
bin/tbench run --scenario shell-startup
# Choose shells and trial count (shell-scoped scenarios only, see below)
bin/tbench run --scenario shell-startup --shells zsh,bash --runs 5
# Write JSON to a specific path (default: results/tbench-<timestamp>.json)
bin/tbench run --output results/my-report.json
# Record an explicit app/terminal identity instead of TERM_PROGRAM detection
# (wins over the TBENCH_TERMINAL_ID environment override)
bin/tbench run --app 'Ghostty nightly' --output results/ghostty-nightly.json
# List available scenarios (name, metric, unit, scope, description)
bin/tbench list
# Human-readable recap of one report (default: newest results/tbench-*.json)
bin/tbench show
bin/tbench show results/my-report.jsonOutput is a single JSON document (see schema below) printed to stdout and
also written under results/ (gitignored); run also prints a show-style
recap to stderr afterward, so stdout stays script-pipeable while a human
still gets a readable summary. list and show are read-only (no
results/.tbench.lock) and safe to run alongside an in-progress run.
For claim-safe, vendor-neutral report exchange, see the normative
docs/reporting-spec-v1.md and its versioned
Schema/validator. Validate both reports and run the eligibility gate before
using the existing comparator:
set -e
bin/tbench-validate validate results/a.json &&
bin/tbench-validate validate results/b.json &&
bin/tbench-validate compare results/a.json results/b.json && # must be COMPARABLE
bin/tbench compare results/a.json results/b.jsonThe JSON Schema is a structural subset: passing it is necessary but is not
sufficient for CONFORMANT, claim-safe, or COMPARABLE. The semantic rules
and corpus in the specification remain authoritative. On CI and non-macOS
hosts, controls that discovery cannot provide can be supplied explicitly:
TBENCH_TERMINAL_ID='ci-terminal 1.0' \
TBENCH_MACHINE_MODEL='ci-runner-class-a' \
TBENCH_POWER_SOURCE=ac \
TBENCH_LOW_POWER_MODE=false \
TBENCH_DISPLAY_REFRESH_HZ=60 \
bin/tbench run --output results/ci-report.jsonReal usage surfaced four ways the numbers themselves could be polluted;
run now guards against each (docs/proposal-detailed-benchmark.md §A):
- Settle between scenarios and variants (
--settle N, default 2s):runpausesNseconds between scenario files, andrender.throughputpauses between itsplain/ansi/cjkvariants andinput.latencybetweenidle/loaded, so one variant's leftover load (a flood still draining, a decaying CPU average) doesn't bleed into the next measurement's first samples.resource.mem/resource.cpu'sidlesampling specifically waits at least 5s regardless of--settle(macOS'sps %cpuis a decayed running average — this is the fix for an observedresource.cpu[idle]p95 of 243%, inherited from whatever ran immediately before it). - Concurrent-run detection:
runrecords its PID inresults/.tbench.lockand checks for one at startup. A live conflicting run makestbenchwarn and wait (polling, up to 120s) rather than start measuring in parallel with it — two simultaneous full-suite runs were observed to cross-pollute each other'srender.throughput(70→13 MB/s).--forceskips the wait (with a visible warning: the risk doesn't go away, it's just accepted). A lock whose PID is no longer running (a stale lock, e.g. after a crash) is reclaimed automatically. This only detects othertbenchprocesses, not unrelated system load (a concurrent Xcode build, Time Machine, etc.). - Warmup + IQR outlier filtering:
--warmup N(default 3) discarded trials run before timing starts, and samples more than 1.5×IQR outside the interquartile range are dropped before computing median/p95/stddev. The removed count is reported per result as"outliers_removed"(always present,0when not applicable —resource.*andfire.fps). - Adaptive sampling (
--runs auto, forshell.startup/cmd.overhead/render.throughput/scroll.proxyonly): instead of a fixed trial count, collects in batches of 10 and stops once a bootstrap 95% CI on the median is within 5% of the median, or 50 total trials are reached — noisier metrics (e.g.cmd.overhead, whose fork/exec cost has high relative variance) will often hit the 50-trial cap rather than converge early, which is an expected, not a failed, outcome.input.latencyandresource.*don't supportautoyet and fall back to a fixed default with a stderr note. - Failed-trial rejection: non-zero child exits and commands exceeding the 30-second per-trial safety limit are errors, never timing samples.
- Environment metadata (macOS only, best-effort — omitted, not fatal,
on failure):
meta.power_source(pmset -g batt),meta.low_power_mode,meta.machine_model/meta.cpu(sysctl), andmeta.gpu/meta.display_refresh_hz(system_profiler SPDisplaysDataType, timed — if it takes over 1s, those two fields are dropped andmeta.meta_notesexplains why, so one slow machine doesn't tax every run).
Every run sends a DECRQM query (CSI ? 2026 $ p) to the real terminal —
asking whether it recognizes DECSET mode 2026, "synchronized output"
(batch a frame instead of drawing it incrementally, so a terminal that
supports it can avoid skipping/tearing frames) — and records the reply as
meta.synchronized_output_supported: "supported", "unsupported"
(recognized the query but reported the mode as unset — rare), or
"no_reply" (no controlling tty, or the terminal didn't answer within 1s —
most terminals older than ~2023 fall here). This is not a frame-skip
detector — it doesn't know whether any given render.throughput/
scroll.proxy run actually skipped a frame, only whether the terminal
could have avoided it. proposal §C considered a marker-line +
screen-capture approach that would actually confirm the final rendered
frame, but that needs the macOS screen-recording permission and adds OCR
error risk for what proposal §C judged not to be worth it yet — this probe
is the cheap, no-permission first step: whenever
synchronized_output_supported isn't "supported", meta.frame_skip_risk_note
is added explaining that render.throughput/scroll.proxy numbers reflect
PTY-write completion, not confirmed on-screen rendering.
The probe writes to and reads from /dev/tty directly (raw mode, 1s
timeout) — the terminal emulator itself answers automatically the instant
it parses the query, so this needs a real controlling terminal but no user
keystroke. Verified directly against Ghostty ("supported") and against a
real tty with nothing listening on the other end ("no_reply" after ~1s).
Run bin/tbench run inside each terminal emulator you want to compare
(each invocation writes its own JSON, tagged with that emulator's
meta.terminal — pass --app NAME to set that tag explicitly, e.g. to
distinguish two builds of the same emulator), then feed the resulting
files to compare — or use
bin/tbench orchestrate (below) to automate the "open each emulator, run
tbench inside it, then compare" sequence instead of doing it by hand:
# Side-by-side Markdown table, 2 or more files
bin/tbench compare results/ghostty.json results/iterm2.json results/terminal-app.json
# Write the table to a file as well as stdout
bin/tbench compare results/a.json results/b.json --output results/comparison.md
# Self-contained HTML chart report (written to results/compare-<timestamp>.html
# unless --output is given; never printed to stdout)
bin/tbench compare results/a.json results/b.json --format html
# CI regression gate: exactly 2 files (baseline, latest), exit non-zero on regression
bin/tbench compare results/baseline.json results/latest.json --threshold 10%
# Keep raw per-trial samples (opt-in — see "Statistical rigor" below) so
# compare can add a bootstrap CI and a significance-aware regression check
bin/tbench run --keep-samples --output results/baseline.jsonRows are metric [variant] (shell) (unit); columns are each file's
meta.terminal (disambiguated with the timestamp if two files share the
same terminal name). Cells show the median, with the best value in a row
bolded — direction-aware, since render.throughput is
higher-is-better and every other metric is lower-is-better. A metric
missing from a given file shows as - rather than dropping the row. A
"Detail" table (p95/stddev, plus a bootstrap CI when --keep-samples data
is present — see below) follows the main comparison.
--threshold mode only accepts exactly 2 files (baseline, then latest) and
exits 0 if nothing regressed past the threshold, or 3 with a
regressions table listing metric/baseline/latest/% change if something did
— suitable for a CI gate on dotfiles/config changes. --threshold works
the same way regardless of --format.
By default run only keeps median/p95/stddev — the raw per-trial values
are discarded once those are computed, keeping report size/shape exactly
as before. run --keep-samples attaches each result's raw samples (in the
result's own unit — the same values median/p95/stddev were computed from)
as a "samples" array, opt-in specifically because it changes JSON size
(20 trials ≈ 20 extra numbers per result).
When compare finds "samples" on both files being compared for a
metric, it (stdlib-only — no SciPy, matching the rest of the project's
"no added dependencies" stance):
- Computes a bootstrap 95% CI on the median (1000 resamples, fixed-seed so the CI is reproducible from the same samples) and shows it in the Markdown detail table, the HTML report's table view and tooltip, and as a band in each per-metric chart. Without samples, the chart uses a median→p95 whisker instead.
- In
--thresholdmode, requires a Mann-Whitney U test (two-sided, normal approximation with tie correction) to also report p<0.05 before counting a threshold-exceeding row as an actual regression — a metric that moved past the threshold but whose baseline/latest distributions still overlap heavily (noise, not a real change) is reported but marked "no" under Significant?/Regression?, and doesn't fail the run. Rows where either file lacks samples for that metric fall back to the original threshold-only check (unchanged behavior — this is fully backward compatible with reports that never used--keep-samples).
Manually opening each emulator and remembering to run them one at a time
(never simultaneously — see the settle/lock-file section above) is exactly
the kind of thing that gets skipped under time pressure, which is how the
70→13 MB/s render.throughput cross-pollution that motivated the
concurrent-run lock happened in the first place. orchestrate automates
the whole sequence — launch, wait, next, then compare — so it can't happen:
bin/tbench orchestrate --apps Ghostty,iTerm,Terminal \
--scenario shell-startup --runs 20 \
--output-dir results/orchestrate-$(date +%Y%m%d) \
--format htmlFor each app in --apps (comma-separated), in order, orchestrate:
- Launches a new window/instance of that app running
tbench run(with--scenario/--runspassed through), via an app-specific adapter:open -na <App> --args -e <script>for Ghostty; AppleScriptdo script/create window with default profile commandfor Terminal/iTerm. The firstdo scripttriggers a one-time macOS Automation permission prompt for that app — accept it, since there's no unattended way around a permission macOS itself gates. - Waits (polling every 2s,
--timeoutseconds max, default 600) for that app's JSON to appear — it never launches the next app until the current one finishes, so two runs are never in flight at once. A timeout leaves that app's window open (for manual inspection) and moves on to the next app rather than aborting the whole batch. - Once every app has been tried, runs
bin/tbench compareover however many JSON files were actually collected (--formatis passed through) and prints where the report went.
An app that's already running when its turn comes up is not quit —
orchestrate only warns that its existing windows may add load to the
measurement, since silently closing a user's terminal session to get a
cleaner benchmark would be a worse trade. An app name with no adapter
(anything other than ghostty/iterm/terminal, case-insensitively) is
skipped with a warning rather than falling back to generic UI-scripting
automation, which is fragile and would need its own (separate)
Accessibility permission grant for a result that isn't guaranteed to work;
add a new adapter arm in src/orchestrate.rs (dispatch) to support
another emulator.
orchestrate itself is macOS-only — open -na/AppleScript have no
portable Linux equivalent for "launch this GUI app and run a command in a
new window."
A single self-contained file — inline SVG + vanilla JS only, no external
CDN/fonts/images — with one dot-and-interval chart per metric+variant+unit
combination (never mixing units on one axis), grouped into sections in this
order: Render Throughput, Latency, Resource, Shell. The report includes a
Wins per entity tally, a Results at a glance table showing every
entity's median and direction-aware competition rank for each metric (exact
median ties share a rank: 1, 1, 3), an Overview vs baseline, and the
per-metric charts. Each chart:
- plots every entity's (file's) median as a dot in that entity's fixed color
(same color for the same entity across every chart on the page — a legend
at the top names the mapping), labeled alongside its rank (
#N) - marks every rank-1 dot in the row with a direction-aware ★ best badge; exact ties therefore mark every tied winner
- shows a bootstrap 95% CI band on the median when
--keep-samplesdata is available; otherwise it shows a median→p95 whisker - shows a hover/keyboard-focus tooltip with rank/median/p95/stddev/runs
The report is light-only by design and ends with the same median/p95/stddev table as the Markdown detail section, augmented with each entity's direction-aware competition rank, as an accessible non-chart fallback. Missing metric cells receive no rank.
compare is snapshot-vs-snapshot; trend is compare's longitudinal
counterpart — it plots one metric's history across every results/tbench-*.json
report you've accumulated, in chronological order (by meta.timestamp), so
a terminal/shell's benchmark trajectory is visible without picking two files
by hand:
# Default: every results/tbench-*.json report, sorted by meta.timestamp
# (errors clearly if fewer than 2 are found)
bin/tbench trend
# Explicit files, Markdown to stdout (default format)
bin/tbench trend results/a.json results/b.json results/c.json
# Self-contained HTML line-chart report (written to results/trend-<timestamp>.html
# unless --output is given; never printed to stdout)
bin/tbench trend --format html --output results/trend.htmlEach results/tbench-*.json's meta.terminal is that report's series
identity — if the accumulated reports span more than one terminal (e.g. you
re-ran tbench run in Ghostty and again in iTerm over time), each terminal
becomes its own colored line/table column rather than being averaged
together. Markdown output is one table per metric [variant] (shell) (unit)
row: timestamp, terminal, median, and a Δ% vs that terminal's previous
point, annotated (better)/(worse) (direction-aware, same convention as
compare). The HTML report is one small-multiple line chart per row
(x = report index, y = median), zero-baselined by default — except a
narrow-range series (all values within ~20% of each other) gets a
non-zero, explicitly annotated baseline so the line isn't flattened to
nothing — with a median→p95 whisker and a hover/focus tooltip per point. A
report missing a metric breaks that terminal's line rather than
interpolating across the gap.
Per-metric detail — exactly what each benchmark measures, how, and how far
to trust it — lives in docs/benchmark-metrics.md;
this table and the sections below are the summary. show (and
run's recap) and compare's Markdown/HTML outputs also append a one-line
"Metric guide" for the metrics present in the report.
| Metric | Unit | Shell-scoped? | Variants | What it measures |
|---|---|---|---|---|
shell.startup |
ms | yes | — | {shell} -i -c exit wall-clock time — interactive startup including rc files |
cmd.overhead |
μs | yes | — | {shell} -c true wall-clock time — shell spawn + builtin dispatch |
render.throughput |
MB/s, lines/s | no | plain, ansi, cjk |
Producer-side completion rate for a fixed fixture written to the terminal |
scroll.proxy |
ms | no | — | Proxy: wall-clock time until a large cat producer exits |
input.latency |
ms | no | idle, loaded |
PTY round trip: write a byte into a cat spawned on a fresh pty, time until it echoes back |
resource.mem |
MB | no | idle, load |
RSS of the terminal emulator's own GUI process |
resource.cpu |
% | no | idle, load |
%CPU of the terminal emulator's own GUI process |
fire.fps |
fps | no | <columns>x<rows> |
Producer-completed frames/second of the deterministic DOOM-fire stream at the recorded region |
Each metric runs N trials (default 20, --runs) and reports median, stddev,
and p95. resource.mem/resource.cpu instead take up to 5 point-in-time
ps samples per variant (fewer if --runs < 5, since sampling a live
process 20 times isn't meaningfully different from 5). fire.fps is
duration-based rather than iteration-based: each trial runs FIRE_SECS
wall-clock seconds (env var, default 5; finite range 0.1–60), and the trial count used is
min(--runs, 5) for the same reason resource.* caps its sample count —
running it a full 20 times by default would make the suite's total
wall-clock unreasonably long.
Shell-scoped vs. not: shell.startup/cmd.overhead run once per entry
in --shells and report "shell" accordingly. render.throughput,
scroll.proxy, input.latency, and fire.fps measure the terminal
emulator itself, not any particular shell, so they ignore --shells, run
once, and report "shell": null.
render.throughput/scroll.proxy/input.latency results are only
meaningful in the context of the emulator they ran in. Each report's
meta.terminal field (from $TERM_PROGRAM/$TERM_PROGRAM_VERSION, falling
back to $TERM) identifies that emulator — to compare emulators, run
bin/tbench run inside each one and diff the resulting JSON files by
meta.terminal.
cjk mixes full-width Japanese text and emoji into the same line count as
plain/ansi. Because UTF-8 encodes those characters as 3-4 bytes each
(vs. 1 for ASCII), cjk's MB/s is not a fair byte-for-byte comparison
against plain/ansi — the same visual width costs more bytes. Use
linesps (or a cells/s metric you compute yourself: multiply CJK/emoji
runs by roughly 2 for their double-width terminal columns) for a more
apples-to-apples pace comparison across variants.
loaded runs the same PTY round-trip measurement as idle, but with a
render.throughput-style flood (the plain fixture) streaming to the
terminal concurrently in the background, to see how much terminal I/O
contention degrades echo latency and, especially, its jitter (p95). If
the flood cannot start, no loaded row is emitted; the run never relabels
an idle measurement as loaded. Flood cleanup is handled by the integrated
child-process helper in src/measure.rs.
Runs the integrated DOOM-fire workload (src/fire.rs — a faithful Rust
port of the retired vendored C tool, with identical palette, seed, frame
order, and byte stream), which repaints a truecolor half-block region every frame at the
live window size (full mode — upstream DOOM-fire-zig's full-window
condition). fps scales ~1/cell-count. The region actually rendered is
recorded as the result's <columns>x<rows> variant and workload identity,
so reports at differing geometries do not align as the same result. Without
a controlling tty the metric is skipped. Like
render.throughput, its write() blocks on pty flow control, so
frames/second approximates the terminal's drain rate, not a confirmed
on-screen frame rate — see the proxy caveats below. No C compiler is
needed anymore.
The workload uses an overlapped producer (fire-v2-overlapped, recorded
in the report row's backend field): a writer thread drains frame N into
the pty while the main thread composes frame N+1, hiding compose time
behind the drain; throughput remains limited by whichever of composition
or PTY writing is slower. The byte stream and
producer design are identical to the C tool, so fps from the two is
directly comparable; the retired serial (v1) producer's numbers are not —
check backend before comparing fire.fps values across reports.
These target the terminal emulator's own GUI process, not the shell or
tbench itself. It's found by walking the process ancestry from tbench
upward to the first executable living inside a macOS .app bundle
(.../Contents/MacOS/<bin>). If none is found — inside tmux, over SSH, or
on non-macOS — the scenario is skipped with a stderr warning rather
than misattributing resource usage to the wrong process.
macOS's ps -o %cpu is a decayed running average, not an instantaneous
sample. tbench mitigates this by taking several samples per variant and
reporting the median, but that's a mitigation, not a fix — treat
resource.cpu as directional, not a precise instant-CPU reading.
scroll.proxyhas no native scroll or redraw-event API behind it. It is literally the wall-clock time for a largecatproducer to finish writing — it does not observe terminal settling and is not a true redraw-event measurement (proposal §3 item 3).input.latencymeasures a pseudo-terminal round trip (write a byte into acatspawned on a fresh pty, time until it's read back), not real keypress-to-screen-photon latency. True input latency needs GUI automation, which is out of scope (proposal §6.6 extension 3). It also requires opening a pty; if none is available (e.g. some sandboxed/CI environments) the scenario is skipped with a stderr warning rather than failing the whole run. A timeout or missing echo invalidates the variant; the PTY line discipline itself may provide the observed echo.render.throughput/scroll.proxy/fire.fpsneed a controlling terminal (/dev/tty) to measure the registered PTY workload. Without one (non-interactive/CI contexts), those metrics are skipped; no/dev/nullsubstitute is reported under the same profile.render.throughputandscroll.proxytime the completecatprocess, including process creation and fixture reads; page-cache state is therefore part of these producer-side proxy results.fire.fpsreports frames/second, but — likerender.throughput— it is a producer-side PTY-write-completion proxy, not a confirmed on-screen frame rate; see the caveat above.resource.mem/resource.cpuare unavailable outside a directly-launched macOS terminal emulator (see caveat above).
Every result carries these caveats inline via an optional "notes" field
in the JSON (see schema below) so they travel with the data.
Process-timed metrics use a built-in monotonic timer (std::time::Instant —
no fork per sample), with warmup (default 3 discarded trials) and 1.5×IQR
outlier rejection, reported as backend "rust-timer". This replaces both
the previous zsh EPOCHREALTIME timer and the optional hyperfine
dependency — warmup + outlier handling, hyperfine's main advantages over
the old fallback, are now built in.
Each result object includes a "backend" field ("rust-timer",
"rust-pty" for input.latency, "ps" for resource.*,
"fire-v2-overlapped" for fire.fps) so reports are traceable to the
backend that produced them. Reports from the earlier backends
(hyperfine, zsh-timer, zsh-zpty) remain conformant under Reporting
Spec v1; as always, the pairwise gate treats differing backends as
NOT_COMPARABLE, so compare like with like.
make help # list targets
make build # release build (bin/tbench also builds on demand)
make test # cargo unit tests + tests/report-v1.sh (Reporting Spec v1 suite)
make lint # rustfmt --check + clippy -D warnings
make check # lint + test — what CI runsCI (.github/workflows/ci.yml) runs make lint, make unit, and the
conformance suite on a macOS runner with the explicit TBENCH_* controls
from the section above.
- A Rust toolchain to build (
cargo build --release); the result is one self-contained binary with no runtime dependencies - the shells being benchmarked (
zsh/bash/... ) onPATH - a controlling terminal, for
render.throughput/scroll.proxy/fire.fps, and a usable pty, forinput.latency— unavailable metrics are skipped with an actionable warning rather than replaced by a different workload
No runtime dependencies: hyperfine, python3, and a C compiler are no
longer needed (compare/show/trend/validate are built in, and the
DOOM-fire workload is an integrated port of the retired C tool with an
identical byte stream). YAML scenario files keep the fixed flat
key: value structure, parsed by the built-in reader. Fixtures for
render.throughput/scroll.proxy are generated lazily on first use
(same content as the retired fixtures/gen-fixtures.sh) and gitignored.
bin/tbench # thin launcher: builds (if needed) and execs the Rust binary
bin/tbench-validate # thin launcher for `tbench validate` (Reporting Spec v1 CLI)
src/main.rs # CLI dispatch (run/list/show/compare/trend/orchestrate/validate)
src/stats.rs # median/p95/stddev, IQR, bootstrap CI, Mann-Whitney
src/measure.rs # rust-timer backend, adaptive sampling, output flood
src/scenario.rs # scenarios/*.yaml loading (flat key: value reader)
src/runner.rs # scenario kind dispatch, fixtures, pty latency
src/lock.rs # results/.tbench.lock concurrent-run detection
src/resource.rs # terminal-PID discovery, resource.mem/resource.cpu
src/fidelity.rs # DECSET 2026 synchronized-output probe
src/fire.rs # integrated DOOM-fire workload (port of the retired C tool)
src/report.rs # Reporting Spec v1 JSON assembly + env metadata
src/show.rs / compare.rs / trend.rs # report views (recap, tables/HTML, history)
src/orchestrate.rs # per-app launch adapters (macOS)
src/validate.rs # Reporting Spec v1 reference validator
scenarios/ # *.yaml scenario definitions
schemas/ # tbench-report-v1.schema.json (Reporting Spec v1)
tests/report-v1.sh # Reporting Spec v1 conformance suite (make conformance)
docs/ # proposals, metric guide, reporting spec
fixtures/ # generated *.txt (gitignored) + conformance/ corpus
results/ # JSON/HTML output + .tbench.lock (gitignored)
The normative format is defined by the
Reporting Specification v1. Use the
JSON Schema for structural checks
and the valid minimal fixture
as a non-normative illustration. A Schema-only pass does not establish
semantic conformance, claim safety, or pairwise comparability; use
bin/tbench-validate and the versioned corpus for those decisions.
bin/tbench runstill only emits JSON (no Markdown report fromrunitself —compareproduces Markdown/HTML from existing JSON files); no fish shell support; noprompt.rendermetric — deferred to later phases (proposal §6.6 extension 2/3).--runs autoonly converges forshell.startup/cmd.overhead/render.throughput/scroll.proxy;input.latency/resource.*/fire.fpsfall back to a fixed trial count with a stderr note.- The concurrent-run lock only detects other
tbench runprocesses, not unrelated system load (a build, backups, etc.) — see caveat above. orchestrateis macOS-only, supportsghostty/iterm/terminalonly (see above for adding an adapter), and can't guarantee the new window actually has keyboard/tty focus — that's an emulator implementation detail, and an unfocused window could skewrender.throughputetc.- The DECSET 2026 probe only reports whether the terminal recognizes synchronized output — it does not confirm any given run actually avoided frame skipping (that would need screen-capture + a permission grant, judged not worth it yet per proposal §C).
--keep-samples/bootstrap CI/Mann-Whitney significance all require both files being compared to carry"samples"for the same metric; without that,comparefalls back to the original median/p95/stddev-only, threshold-only behavior — fully backward compatible with reports from before this feature existed.scroll.proxyandinput.latencyare approximations, not true redraw-event / photon-latency measurements — see caveats above.render.throughput'scjkvariant's MB/s isn't directly comparable toplain/ansi's (different bytes-per-character) — see caveat above.resource.mem/resource.cpuonly work whentbenchruns inside a directly-launched macOS terminal emulator (not tmux/SSH/non-macOS), andresource.cpuinherits macOS's decayed-average%cpusemantics.fire.fps's--runsis duration-based (FIRE_SECS, finite range 0.1–60 seconds per trial) and capped at 5 trials regardless of--runs, unlike the iteration-based scenarios.