Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ takes nothing, because neither shells nor agents use Cmd.
| `^R` | Search command history |
| `⌘N` / `⌘D` / `⌘E` | New / fork / fork with a chosen profile |
| `⌘F` | Find in the screen and scrollback |
| `⌘⇧F` | Search every session's screen and scrollback from the left pane; `Enter` jumps to the hit |
| `⌘I` (or `⌘⇧R`) | Rename the session |
| `⌘K` | Clear the screen and scrollback |
| `⌘[` / `⌘]` | Select the previous / next session |
Expand All @@ -132,6 +133,20 @@ takes nothing, because neither shells nor agents use Cmd.

Everything else goes to the child process untouched.

**Searching across sessions** (`⌘⇧F`) walks every session on a worker thread,
so typing never waits on it. It has one hole: a session sitting in the
alternate screen (`vim`, `less`, or any other full-screen program) can only be
searched over the screen it is showing right now. Its real scrollback is parked
in alacritty's `inactive_grid`, and nothing outside the crate can read it. The
history comes back to the search when the program exits. A hit on such a
screen switches to the session and hands the word to `⌘F`; it cannot jump to
the line.

It reads one grid row at a time, so a word broken across a wrap — the line ran
past the right edge and continued on the next row — is invisible to it. `⌘F`
walks the grid as a stream and does find such a word, so the same search finds
it once you are in the session.

**The dot on a row** is filled while the session is alive and hollow once it
has ended. Its colour is the session's state:

Expand Down
139 changes: 139 additions & 0 deletions docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,9 @@ alacritty の [vtebench](https://github.com/alacritty/vtebench) は端末の中

163 桁 x 47 行、履歴 10,000 行、1 種類あたり 16MiB。

次の表は `Counting`(行の数え上げ)が読みの経路に入る**前**の数字である。
入ったあとの測り直しは 7.16 にある。

| 負荷 | 合計 | 走査 | 解釈 | 速さ |
|---|---|---|---|---|
| light_cells(短い行) | 153ms | 0.3ms | 153ms | 104 MiB/s |
Expand Down Expand Up @@ -578,6 +581,141 @@ glyphon の `prepare` は `impl IntoIterator<Item = TextArea>` を取る。

13〜16% 減。1 行に収まる変更である。

## 7.15 横断検索の走査を測った(2026-10-29 追記)

`⌘⇧F` は全セッションの画面と履歴を走る。仕様 §10 は「先に測る」を求めているので、
`--bench` に走査の節を足した。

**測り方。** 14 本の `Term` を、シェルを起動せずに直接組み立てる(14 本ぶんの本物の出力は遅すぎる)。
1 本は 80 桁 x 24 行、履歴 10000 行を満たしてある。行は 80 桁に収まり、本ごと・行ごとに中身が違う。
語は 1 本に 1 つだけ置いた。`Redactor` は既定の式を入れてある。
`scan::scan` を `Limits::default()`(1 本 20 件、全体 200 件、chunk 256)で呼び、1 回の所要時間を測る。
最初の 2 回を捨て、15 回の中央と p90 を出す。1 回が約 50ms かかるので、フレームの測定(20 回)より少なくした。

**数字。** M4、macOS 26.5.1、`--release`。3 回走らせた。

| 走り | 中央 | p90 |
|---|---|---|
| 1 | 49.34ms | 50.35ms |
| 2 | 49.04ms | 50.16ms |
| 3 | 48.41ms | 49.47ms |

14 本 x 10000 行 = 14 万行を約 49ms で歩く。1 行あたり約 0.35µs である。

**この数字は最悪の場合である。** 当たりは 14 件しかなく、全体の上限 200 に届かないので、走査は早じまいせず
全部の行を歩く。普通は当たりが多く、1 本 20 件に達した本は、そこで遡りを止める。
語が全く当たらない場合も同じ費用になる。当たった行だけ `Redactor` を通すので、
当たりが多いほど 1 行の費用は上がるが、採用は 200 件までに収まる。

**フレームは落とさないはずである。ただし実機では未確認。** 約 49ms は 1 フレーム(16.7ms)より長い。
だから走査は別スレッドで走る。落とさない理由は、コードに次の 3 つがあることである。

1. **スレッドへ渡すのは `Arc` の複製と短い文字列だけ。** `start_scan`(`src/main.rs:2270`)が組む `Target` は、
`term`・`pushed`・`ticks` の `Arc` を複製し、名前・題名・枝の文字列と語を複製する(`src/main.rs:2295-2310`)。
`Redactor` も `Arc` の複製である(`src/main.rs:2310`)。グリッドは複製しない。
スレッドを作るのは `std::thread::Builder::spawn` の 1 回で、描画の経路に走査の中身は入らない(`src/main.rs:2311`)。
2. **描画の関数は走査も `Term` の錠も呼ばない。** 描画は結果の `Vec<Hit>`(最大 200 件)のうち、
窓に見える数件だけを組む(`across_blocks`、`src/main.rs:2732-2750`)。
`draw_across_hits`(`src/main.rs:3083`)から `draw_sidebar`(`src/main.rs:3201`)の範囲に、
`scan::scan` の呼び出しも `term.lock()` も無い。履歴の長さに比例する仕事は無い。
`start_scan` はイベントループがフレームの最後に呼ぶ(`src/main.rs:2454`)が、することはスレッドへ仕事を渡すだけで、1 がその費用を述べている。
3. **走査は `Limits::chunk` 行ごとに `Term` の錠を放す。** `scan_lines`(`src/scan.rs:214`)は、
錠を取る `let term = target.term.lock()`(`src/scan.rs:242`)を塊ごとの内側の区画に入れ、
`walked < chunk` で歩く行数を切る(`src/scan.rs:274`)。区画を抜けると錠が放れ、
`ticks` はその後で取る(`src/scan.rs:300`)。1 本の履歴を取りっぱなしで走ることは無い。
1 塊は 256 行で、塊は本ごとに切るので、全体で 14 x ceil(10024 / 256) = 560、約 550 塊になる。約 49ms を約 550 で割ると、1 塊は約 90µs になる。
これは割り算の見積もりで、錠の保持時間を直接測ったものではない。

**未了:実際のウィンドウでフレームの記録を見る確認。** この節を書いたのは窓も画面も無い環境で、
`TERMIT_FRAME_LOG` を実機で見ていない。上の 3 つはコードを読んだ論証であり、
「フレームが落ちなかった」という観測ではない。閉じるには、人が次を実行する。

```sh
RUST_LOG=info TERMIT_FRAME_LOG=1 cargo run --release
```

セッションを数本開き、それぞれに数千行を出してから `⌘⇧F` を押し、語を打って検索する。
走査が走っているあいだの毎フレームの記録に、落ちたフレームが無いことを見る。
あわせて、検索中も各セッションの出力が止まらないことを目で確かめる。

## 7.16 行の数え上げが読みの経路に足した費用を測った(2026-10-29 追記)

`Counting`(`src/lines.rs`)は、横断検索が「新しい順」を出すために履歴へ押し出した行を数える。
置いた場所がパーサと `Term` のあいだなので、**誰も検索しなくても全セッションの読みが必ず通る**。
7.13 の数字はこれが入る前のものなので、測り直した。

**測り直した理由。** 最初の査読は `--throughput` を 3 回走らせて `scrolling` が
202.6 / 196.5 / 187.7ms(7.13 の記録は 179ms)になることを見たが、同じ 3 回で
`light_cells` が 114〜136ms に振れていた。振れが差を飲んでしまい、
「遅くなった」とも「変わらない」とも言えなかった。記録が無いことが問題で、
遅くなったことが分かっていたわけではない。

**測り方。** 同じ機械・同じ条件で比べるため、`main` を別の worktree に出し、両方を
`--release` で建てた。`--throughput` を 2 本のバイナリで**交互に**走らせる
(片方を全部走らせてから他方、ではない。熱のずれが片側に乗らないようにする)。
11 周を 2 セット、計 22 組。組ごとの差を取るので、周のあいだの機械の状態は差に乗らない。
`main` 側の `throughput.rs` は `parser.advance(&mut term, ..)`、枝側は
`Counting` で包む。harness の違いはそこだけである。

**数字。** M4、macOS 26.5.1、`--release`、22 組の中央値。
「遅い組」は、枝のほうが遅かった組の数である。

| 負荷 | `main` | この枝 | 差 | 割合 | 遅い組 | 組ごとの差の四分位範囲 |
|---|---|---|---|---|---|---|
| light_cells(短い行) | 110.7ms | 117.2ms | +6.5ms | +5.9% | 17/22 | 14.6ms |
| medium_cells(半幅) | 97.6ms | 101.3ms | +3.7ms | +3.8% | 18/22 | 6.4ms |
| dense_cells(コマごとに色) | 60.2ms | 58.6ms | -1.6ms | -2.7% | 2/22 | 1.5ms |
| scrolling(送りだけ) | 181.8ms | 203.3ms | **+21.5ms** | **+11.8%** | **22/22** | 7.2ms |
| scrolling_region(領域を切って送り) | 644.5ms | 659.0ms | +14.5ms | +2.2% | **22/22** | 10.0ms |
| cursor_motion(飛ばして 1 字) | 67.9ms | 67.0ms | -0.9ms | -1.3% | 0/22 | 1.1ms |
| unicode(多バイト) | 72.2ms | 70.8ms | -1.4ms | -2.0% | 3/22 | 2.1ms |
| sync_cells(DECSET 2026 で囲む) | 92.2ms | 93.9ms | +1.7ms | +1.8% | 13/22 | 4.6ms |
| osc_heavy(シェル統合の通知が毎行) | 152.8ms | 150.6ms | -2.2ms | -1.4% | 6/22 | 4.8ms |

**費用は「送り 1 回あたり」で、コマには付かない。** これが表の形をそのまま説明する。

`Counting::linefeed` は送りごとに `at_bottom()` を呼ぶ。見るのはカーソル行と画面行数の 2 つで、
比べて `hooked` を 1 増やすだけである。いっぽう `input`(コマに字を置く)は `forward!` が
`#[inline]` でそのまま渡すので、何も足さない。だから**送りの多い負荷だけが遅くなる**。

`scrolling` は 16MiB が全部素の `\n` で、送りが 16,777,216 回ある。
+21.5ms をその数で割ると **1 回あたり約 1.3ns**、3GHz なら 4 サイクルほどで、
上に書いた仕事量とちょうど合う。

`scrolling_region` は DECSTBM を張ってからカーソルを最下行へ置くので、
送りの数(約 16M 回)も 1 回あたりの仕事(`at_bottom()` は最初の送りから真)も
`scrolling` と同じはずである。それなのに差は 14.5ms で、7ms 小さい。
この 7ms は分解していない。組ごとの差の四分位範囲 10.0ms の中に収まるので、
領域送りの経路との当たり方か、測りの振れかを分けられなかった。
割合が +2.2% なのは、下地が 3.5 倍遅いからである。

`dense_cells`・`cursor_motion`・`unicode`・`osc_heavy` は 1 行が長く、送りの数が桁違いに少ないので
差は出ない(むしろ少し速いが、符号が揃っていないので配置の揺れである)。

`light_cells`(1 行 10 バイト、送り約 168 万回)と `medium_cells`(1 行 83 バイト、約 20 万回)に
1.3ns を当てると 2.2ms と 0.3ms になる。測った差(+6.5ms・+3.7ms)はそれより大きいが、
どちらも組ごとの振れの中なので、この 2 つから 1 回あたりの費用は出せない。
1 回あたりの数字が出せるのは `scrolling` だけである。

**言えること。**

- **`scrolling` の +11.8% は本物である。** 22 組すべてで枝が遅く、差 21.5ms は
組ごとの差の四分位範囲 7.2ms の 3 倍ある。89 MiB/s が 79 MiB/s になった。
- **ただしこれは最悪の形で、普通の出力の形ではない。** 1 バイトが 1 回の送りという負荷である。
字を書く負荷(`dense_cells` 以下)では差が出ない。
- **`scrolling_region` も 22 組すべてで遅い。** 送りの数も 1 回あたりの仕事も `scrolling` と
同じはずなのに、差は 14.5ms で 7ms 小さい。その 7ms は分解できていない(上に書いた)。
- **`light_cells` と `medium_cells` は向きだけ言える。** 17/22・18/22 なので遅くなる向きは
たしかだが、差(+6.5ms・+3.7ms)が組ごとの差の四分位範囲(14.6ms・6.4ms)に収まっていない/
同程度なので、**大きさは決まっていない**。最初の査読が振れに悩んだのはこの負荷である。
- 1 台の機械の数字である。

**受け入れる理由。** 79 MiB/s は、pty が実際に届けてくる速さよりずっと速い。
7.13 が書いているとおり、ここで測っているのは「パーサとグリッドが受け取れる速さ」であって、
重さの原因が読み取りだと分かっているわけではない。送り 4 サイクルを削るには、
数えを 1 文字ごとの経路へ入れるか(7.13 が `input` の 1 割として却下した道)、
行に時刻を持たせる別の仕組みが要る。横断検索の「新しい順」が要る以上、この 4 サイクルは払う。

## 8. 参考にした資料

手法ごとの採否は [references/performance-techniques.md](references/performance-techniques.md) にまとめた。
Expand All @@ -604,3 +742,4 @@ glyphon の `prepare` は `impl IntoIterator<Item = TextArea>` を取る。
| 常駐メモリ(1 ペイン) | 100MB 以内 | 84.7MB |
| 起動から子プロセスの生成まで | 200ms 以内 | 224ms |
| バイナリ | 単一ファイル | 9.4MB |
| 横断検索の走査(14 本 x 10000 行) | 別スレッド | 49ms(実機のフレーム確認は未了、7.15) |
Loading
Loading