Noa というターミナルアプリを自作しています。開発していると、避けて通れない問いが 2 つ出てきます。この変更で描画は速くなったのか(遅くなっていないか)、そして競合のターミナルアプリと比べて今どの位置にいるのか。どちらも体感では答えられない。数値で答えるために書いたのが TerminalBench (tbench) です。
TL;DR
- シェルの起動と、ターミナルエミュレータの描画・入力・リソースを 1 コマンドで計測し、他のターミナルと横並びで比較できる Rust 製の単一バイナリです。
- 一部の値は近似(proxy)で、ツールが JSON の
notesに但し書きを出します。- まずは
bin/tbench runしてからbin/tbench show。
※ 名前は似ていますが、LLM エージェント評価の Terminal-Bench(laude-institute)とは別のプロジェクトです。
TerminalBench とは
tbench は、シェルとターミナルエミュレータの速さをまとめて測るベンチマークランナーです。実行時の追加依存はなく、cargo build --release で 1 個のバイナリになります。
測定対象は大きく 4 系統です。シェル起動(shell.startup / cmd.overhead)、描画(render.throughput / scroll.proxy / fire.fps)、入力(input.latency)、リソース(resource.mem / resource.cpu)。このうちシェル起動系だけが --shells zsh,bash の各シェルで計測され、残りはシェル非依存("shell": null)として記録されます。ただし input.latency はエミュレータ自体ではなく PTY 往復を測る近似です(詳しくは後述)。
出力は 1 個の JSON ドキュメント。標準出力に流しつつ results/ にも保存し、run の後は人間向けサマリを stderr に出します。stdout はそのままスクリプトにパイプでき、目視でも確認できます。
何が測れるか — メトリクス 8 種
tbench list でも一覧できますが、全体像は次の表にまとめました。8 種はいずれも先述の 4 系統に対応します。
| Metric | Unit | Shell依存 | Variants | 測るもの |
|---|---|---|---|---|
shell.startup |
ms | あり | — |
{shell} -i -c exit の実時間。rc ファイル込みの対話起動時間 |
cmd.overhead |
μs | あり | — |
{shell} -c true の実時間。プロセス spawn + builtin dispatch(組み込みコマンドの解決) |
render.throughput |
MB/s, lines/s | なし |
plain / ansi / cjk
|
固定フィクスチャを書き込んだときの producer 側の完了レート |
scroll.proxy |
ms | なし | — | 大きな cat producer が終了するまでの実時間(proxy) |
input.latency |
ms | なし |
idle / loaded
|
PTY 往復。fresh pty 上の cat にバイトを書き、エコーが返るまで |
resource.mem |
MB | なし |
idle / load
|
ターミナルエミュレータ自身の GUI プロセスの RSS |
resource.cpu |
% | なし |
idle / load
|
同 GUI プロセスの %CPU |
fire.fps |
fps | なし | <columns>x<rows> |
DOOM-fire ストリームの producer 完了フレーム/秒 |
各メトリクスはデフォルト 20 試行(--runs)で median / p95 / stddev を出します。ただし resource.* は 1 variant あたり最大 5 サンプルの点計測、fire.fps は時間ベース(FIRE_SECS 秒、デフォルト 5 秒)で試行数は min(--runs, 5) に丸められます。ライブプロセスは 5 サンプルで十分ですし、fire は 20 回だとスイートが長くなりすぎるからです。
shell.startup の -i がポイントで、rc ファイルをロードした状態の起動時間を測ります。「プラグインマネージャを入れ替えたら起動がどれだけ変わったか」を見るのはこの指標です。
インストールと基本的な使い方
動かすには Rust ツールチェイン(cargo)が要ります。ビルド済みバイナリの配布はなく、bin/tbench がソースからビルドして実行します。
# 取得してディレクトリへ
git clone https://github.com/simota/TerminalBench && cd TerminalBench
# 全シナリオ、デフォルトシェル(zsh,bash)、各 20 試行
bin/tbench run
# シナリオを 1 つだけ
bin/tbench run --scenario shell-startup
# シェルと試行数を指定(シェル依存シナリオのみ効く)
bin/tbench run --scenario shell-startup --shells zsh,bash --runs 5
# 利用可能なシナリオ一覧(name / metric / unit / scope / description)
bin/tbench list
# 直近レポートの人間向けリキャップ
bin/tbench show
run(や show)を叩くと、stderr にこんなリキャップが出ます。
Terminal: App-A
Shell
Metric Median P95 Stddev Runs Unit Dir
-------------------- ------ ------ ------ ---- ---- ---
shell.startup (zsh) 480.54 510.49 17.87 5 ms ↓
shell.startup (bash) 6.22 7.08 0.44 5 ms ↓
--app で計測対象の識別名を付けられます。同じエミュレータの 2 ビルドを区別したいときに使います。
bin/tbench run --app 'App A' --output results/app-a.json
list と show は読み取り専用でロックを取らないので、run 実行中に横から叩いても安全です。
エミュレータを比較する — compare / orchestrate / HTML レポート
比べたいエミュレータそれぞれの中で bin/tbench run を回し、出てきた JSON を compare に渡します。各レポートは meta.terminal($TERM_PROGRAM 由来)でタグ付けされます。
# 2 つ以上のファイルを Side-by-side の Markdown 表に
bin/tbench compare results/app-a.json results/app-b.json results/app-c.json
# 自己完結の HTML チャートレポート(外部 CDN/フォント/画像なし)
bin/tbench compare results/a.json results/b.json --format html
# CI 回帰ゲート: ちょうど 2 ファイル(baseline, latest)、回帰で非ゼロ終了
bin/tbench compare results/baseline.json results/latest.json --threshold 10%
行は metric [variant] (shell) (unit)、列は各ファイルの meta.terminal。セルは median で、行内のベスト値が太字になります。方向も考慮していて、render.throughput と fire.fps は higher-is-better、他は lower-is-better です。レポート同士が厳密に比較可能か(同じ環境・条件か)は bin/tbench-validate で検証でき、突き合わせ可否を COMPARABLE 判定として返します。
以下は手元の Mac での一例です(2026 年 7 月時点)。Noa の中で bin/tbench run --scenario shell-startup --runs 5 を 2 回実行し(--app タグで Noa-A / Noa-B と区別)、compare にかけた結果です。
| Metric | Noa-A | Noa-B |
|---|---|---|
| shell.startup (zsh) (ms) | 480.54 | 472.97 |
| shell.startup (bash) (ms) | 6.22 | 5.67 |
Detail (p95 / stddev)
| Metric | Noa-A p95 / stddev | Noa-B p95 / stddev |
|---|---|---|
| shell.startup (zsh) (ms) | 510.49 / 17.87 | 488.26 / 11.63 |
| shell.startup (bash) (ms) | 7.08 / 0.44 | 5.81 / 0.08 |
同一環境の 2 回実行なので、太字のベスト値の差は実力差ではなく run 間のばらつきです。この環境では zsh がプラグイン込みで 500ms 前後、bash が 6ms 前後という桁違いも一目で分かります。
手でエミュレータを 1 つずつ開いて回すのは、忙しいと真っ先にサボります。かといって同時に 2 つ走らせると、計測が互いに干渉します(次の節で触れます)。これを構造的に防ぐのが orchestrate です(macOS 限定)。
bin/tbench orchestrate --apps Noa,AppB,AppC \
--scenario shell-startup --runs 20 \
--output-dir results/orchestrate-$(date +%Y%m%d) \
--format html
orchestrate は各アプリの新規ウィンドウで tbench run を起動し、JSON が出るまで待ってから次へ進みます。通常経路では、2 つの run が重なることはない。ただしタイムアウトした run だけは、前のウィンドウが残ったまま次へ進むことがあります。全アプリを回し終えたら compare まで自動でかけます。アダプタ方式で、自作の noa を含む主要なターミナルアプリ 4 つに対応しています(対応名は README に)。AppleScript を使うアダプタでは、初回に macOS の Automation 許可プロンプトが出ます。すでに起動中のアプリは終了させません。勝手にセッションを閉じるほうが害が大きいからです。
HTML レポートはインライン SVG + バニラ JS だけの 1 ファイルで、メトリクス+variant+unit ごとに dot-and-interval チャートを出します。「Wins per entity」集計、方向を考慮した competition rank、rank 1 への ★ best バッジ、ツールチップ付きです。
測定品質を保つ仕組み
開発中、計測値が実態とずれるケースに何度か当たりました。idle のはずのターミナルで CPU が 243% と記録されたり、描画スループットが 70 MB/s から 13 MB/s に落ちたり。前者は macOS の CPU 使用率が直近の負荷を含む移動平均であること、後者は 2 つの計測を同時に走らせたことが原因でした。
run にはこうしたずれを防ぐ仕組みが入っています。計測の間に休止を挟む、別の計測が走っていれば完了を待つ、ウォームアップ分を捨てる、外れ値を除いてから統計を取る、数値が安定するまで試行数を自動で増やす、といった内容です。詳しいパラメータやアルゴリズムは README にあります。
変更が本物の改善かどうかの判定も tbench 側でできます。両方のレポートを --keep-samples 付きで取り --threshold で比べると、compare が 2 つの分布を統計的に比較し、差がばらつきの範囲なら「有意ではない」と報告します(Mann-Whitney U 検定)。サンプルがない場合は従来どおり閾値のみの判定です。
この数値、どこまで信じていいのか
tbench の数値のいくつかは**近似(proxy)**で、実測そのものではありません。どこが近似なのかは、ツール自身が JSON の notes に書き出します。
たとえば入力遅延(input.latency)。これはキーを押してから画面に文字が出るまでの本当の遅延ではなく、疑似端末(PTY)にバイトを書いて返ってくるまでの往復時間です。本物のキー入力から画面表示までを測るには GUI の自動操作が要るので、そこはスコープ外にしています。
描画系(render.throughput など)も性格は同じです。測っているのは「ターミナルへの書き込みが完了したレート」で、「画面に実際に描かれたフレームレート」ではありません。書き込みが終わっても、GUI が本当に何 fps で描けているかまでは踏み込んでいない。
残りの近似(スクロール、同期描画の検出)も同じで、それぞれ何が近似なのかは docs にまとめてあります。
環境によって走らないものもあります。非 macOS では resource.* はスキップされ、orchestrate はそもそもエラーで止まります。制御端末(controlling tty)がない CI などでは、描画・スクロール・fire 系もスキップされます。そうした環境では、端末名やマシン種別を TBENCH_* の環境変数で補えます。ただしこれはメタデータを補うだけで、足りない tty を用意するわけではありません。
TBENCH_TERMINAL_ID='ci-terminal 1.0' \
TBENCH_MACHINE_MODEL='ci-runner-class-a' \
TBENCH_POWER_SOURCE=ac \
bin/tbench run --output results/ci-report.json
cjk variant の MB/s は少しクセがあります。全角日本語や絵文字は UTF-8 で 1 文字 3〜4 バイトなので、同じ見た目の幅でも plain/ansi より多くのバイトを食います。variant 間の比較には linesps のほうがフェアです。
まとめ
tbench の数値は、単体で眺める絶対値ではなく、比べるための相対値です。一部は近似なので、1 つの数字そのものに意味を求めるより、同じ環境・同じ条件で並べたときの差に意味があります。変更の前後で比べれば回帰が分かり(compare --threshold)、他のターミナルアプリと並べれば立ち位置が分かる(orchestrate)。自分も Noa の開発では、そういう使い方をしています。
まずは自分のターミナルの中で動かしてみてください。
bin/tbench run
bin/tbench show
リポジトリと公開サイトはこちら。
- GitHub: simota/TerminalBench
- Website: https://simota.github.io/TerminalBench/
ターミナルアプリを開発している人も、使っているターミナルの実力が気になる人も、体感を数値で答え合わせしてみてください。