Skip to content

perf(desktop): Codex 等のフル再描画 TUI 実行時の重さを軽減 (#293)#299

Merged
MocA-Love merged 2 commits intomainfrom
fix/issue-293-terminal-coalesce
Apr 17, 2026
Merged

perf(desktop): Codex 等のフル再描画 TUI 実行時の重さを軽減 (#293)#299
MocA-Love merged 2 commits intomainfrom
fix/issue-293-terminal-coalesce

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

@MocA-Love MocA-Love commented Apr 17, 2026

Summary

Issue #293 対応。Codex のようなフル画面再描画型 TUI をターミナルで動かすとアプリが重くなる問題に対し、daemon 側 Session でコアレス処理を 2 段導入しました。

背景と調査

  • Codex は Rust + Ratatui の immediate mode TUI でフレーム毎に全画面を書き直す設計のため、Claude Code (Ink の差分描画) と比べて ESC シーケンス量が 2〜3 桁多い
  • apps/desktop/src/main/terminal-host/session.ts の data イベント経路で 2 つのボトルネックを確認:
    1. broadcast: PTY data イベントごとに JSON.stringify() + socket.write() が走る
    2. emulator: processEmulatorWriteQueue() が小チャンクを 1 件ずつ emulator.write() に渡し、ANSI パーサのセットアップが何度も発生
  • attached client 下流の renderer 側 xterm.write も呼び出し回数に比例して重くなる

変更点

apps/desktop/src/main/terminal-host/session.ts のみ変更。

Phase 1: broadcast data coalesce

  • data イベントを BROADCAST_COALESCE_INTERVAL_MS = 16 (ms) または BROADCAST_COALESCE_MAX_BYTES = 131_072 に達するまで内部バッファに蓄え、1 回のブロードキャストに束ねる
  • exit / error イベントを送る直前で強制 flush(順序保証)
  • attach() で既存クライアントに対して pending を flush してから新クライアントを追加(snapshot との二重送信回避)
  • dispose() / resetProcessState() でバッファを解放
  • 無効化: `SUPERSET_TERMINAL_BROADCAST_COALESCE=0`

Phase 2: emulator write coalesce

  • processEmulatorWriteQueue() で連続する小チャンクを MAX_CHUNK_CHARS (8192) まで結合してから emulator.write() を 1 回呼ぶ
  • サイズ超過 item の分割ロジックは従来通り(サロゲートペア配慮あり)
  • emulatorWriteProcessedItems は item 単位で加算し続けるため flushToSnapshotBoundary() の境界計算は変更なし
  • 無効化: `SUPERSET_TERMINAL_EMULATOR_COALESCE=0`

期待効果と副作用

  • Codex 等の高頻度 TUI 再描画時、ターミナル経路の CPU 消費が減り daemon / renderer の詰まりが緩和される見込み
  • 16ms バッファ待ちが入るが、60fps 相当なので入力エコーの体感遅延はない想定
  • 既定で有効。問題があれば env 変数で即切り戻し可能

テスト計画

  • bun test src/main/terminal-host/ 全 37 件パス
  • tsc --noEmit で本差分由来の型エラーなし
  • biome check クリア
  • 実機での Codex 長時間セッションを用いた体感確認
  • 既存フロー(attach / resize / alt screen / kill / dispose)回帰確認
  • SUPERSET_TERMINAL_BROADCAST_COALESCE=0 / SUPERSET_TERMINAL_EMULATOR_COALESCE=0 での無効化切り戻し確認

Closes #293

Summary by CodeRabbit

  • バグ修正
    • ターミナルセッションのデータ転送処理を改善し、データの順序一貫性を強化しました。
    • ターミナルエミュレーターの書き込み処理を最適化し、連続した複数のデータチャンクの処理効率を向上させました。

Codex などフル画面再描画型の TUI を走らせるとターミナルが重くなる
問題への対応。daemon 側 Session で以下 2 点を導入:

- broadcast コアレス: data イベントを 16ms or 128KB ごとに結合して
  1 回のブロードキャストに束ねる。JSON.stringify と socket.write、
  renderer 側 xterm.write の呼び出し回数を削減
- emulator write コアレス: キュー内の連続した小チャンクを
  MAX_CHUNK_CHARS まで結合してから emulator.write を 1 回呼ぶ。
  ANSI パーサのセットアップ回数を削減

順序保証のため exit/error イベント直前、attach 時、dispose 時は
必ず flush。snapshot boundary の item 単位カウントは保持。
SUPERSET_TERMINAL_BROADCAST_COALESCE=0 および
SUPERSET_TERMINAL_EMULATOR_COALESCE=0 で個別に無効化可能。
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

ターミナルセッションの処理において、PTYデータイベントと エミュレータ書き込み操作をそれぞれバッファリングして定期的にまとめて処理する仕組みを追加。ブロードキャストコアレッシングとエミュレータコアレッシングの2つの最適化を実装しました。

Changes

Cohort / File(s) Summary
Data Broadcast Coalescing
apps/desktop/src/main/terminal-host/session.ts
即座にブロードキャストしていたPTYデータイベント(data)を新しい queueBroadcastData() メソッドでバッファリング。flushPendingBroadcastData() で非データイベント(exit, error)発行前、クライアント新規接続時、プロセス状態リセット時に一括処理。
Emulator Write Coalescing
apps/desktop/src/main/terminal-host/session.ts
SUPERSET_TERMINAL_EMULATOR_COALESCE フラグ有効時に、processEmulatorWriteQueue() で連続するエミュレータチャンク を MAX_CHUNK_CHARS 制限下で単一の emulator.write() にマージ。オーバーサイズチャンクは従来通り分割処理。

Sequence Diagram(s)

sequenceDiagram
    participant PTY as PTY Data
    participant Queue as Broadcast Queue
    participant Emulator as Emulator
    participant Client as Client
    
    rect rgba(100, 150, 200, 0.5)
    Note over PTY,Client: 従来フロー:即座にブロードキャスト
    PTY->>Client: broadcastEvent("data")
    end
    
    rect rgba(150, 200, 100, 0.5)
    Note over PTY,Client: 新フロー:データコアレッシング
    PTY->>Queue: queueBroadcastData()
    Note over Queue: バッファリング
    Client->>Queue: attach() / non-data event
    Queue->>Client: flushPendingBroadcastData()
    Queue->>Client: broadcastEvent("data") x N [まとめて]
    end
Loading
sequenceDiagram
    participant Queue as Emulator Queue
    participant Processor as processEmulatorWriteQueue()
    participant Emulator as emulator.write()
    
    rect rgba(200, 100, 150, 0.5)
    Note over Queue,Emulator: エミュレータコアレッシング(フラグON時)
    Queue->>Processor: [chunk1, chunk2, chunk3, ...]
    Processor->>Processor: 連続チャンクをマージ
    Processor->>Emulator: emulator.write(merged_data)
    Note over Emulator: 単一呼び出しで処理
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 キューに溜めて、纏めて流す
データの洪水、もう怖くない
エミュレータもコアレッシング
Codexの重さも軽くなり
ターミナルはスイスイ、ウサギ喜ぶ

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed タイトルは PR の主要な変更を明確に要約しており、パフォーマンス最適化内容を適切に反映しています。
Description check ✅ Passed 説明は詳細に背景、変更点、期待効果、テスト計画を記載していますが、PR テンプレートの形式に完全には従っていません。
Linked Issues check ✅ Passed PR は #293(Codex の重さの問題)に完全に対応しており、broadcast と emulator write コアレスの導入により、ターミナル経路の高負荷を軽減しています。
Out of Scope Changes check ✅ Passed PR の変更は apps/desktop/src/main/terminal-host/session.ts に限定され、#293 で要求されるパフォーマンス最適化に完全に関連しており、スコープ外の変更はありません。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-293-terminal-coalesce

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0cf80ca49f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 1121 to 1123
this.enqueueEmulatorWrite(this.scanState.heldBytes);
this.broadcastEvent("data", {
type: "data",
data: this.scanState.heldBytes,
} satisfies TerminalDataEvent);
this.queueBroadcastData(this.scanState.heldBytes);
this.scanState.heldBytes = "";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Broadcast held shell-ready bytes immediately on exit path

When a session exits before shell readiness completes, handleSubprocessExit() calls resolveShellReady("timed_out") and then resetProcessState(). In this block, held scanner bytes are now sent via queueBroadcastData, which defers emission behind the coalescing timer; resetProcessState() then clears the pending coalesce buffer, so those final bytes are never delivered to attached clients. This is a regression from the previous immediate broadcastEvent behavior and can drop terminal output for short-lived shells/commands that terminate during startup marker scanning.

Useful? React with 👍 / 👎.

resolveShellReady may enqueue held scanner bytes via queueBroadcastData
during subprocess exit. resetProcessState previously cleared the
coalesce buffer without flushing, silently dropping those final bytes
for short-lived shells that terminate during startup marker scanning.

Replace the clear with a flush so the data reaches attached clients
before teardown completes. Matches the pre-coalescing behavior.

Addresses Codex review on #299.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/desktop/src/main/terminal-host/session.ts (1)

655-711: emulator write coalesce のロジックは正しく、emulatorWriteProcessedItems の会計も一致しています。

内側の while 条件 merged.length + queue[0].length <= MAX_CHUNK_CHARS と、その手前の oversized head 分岐(Line 664)の組み合わせにより、FIFO 順序・サロゲートペア境界・スナップショット境界カウント(itemsConsumed 相当分の加算)のいずれも pre-coalesce 時の振る舞いと等価に保たれています。itemsConsumed === 0 の防衛ブレーク(Line 699)は、oversized 分岐が先に処理される以上、実際には到達不能ですが念のための防御として残す意図は理解できます。

細かい点ですが、Line 694 の this.emulatorWriteQueue.shift() as string は、直前の条件で queue.length > 0 が保証されているので non-null 断言(!)の方が意図が明確かもしれません。必須ではありません。

♻️ 任意のリファクタ案
-				const nextChunk = this.emulatorWriteQueue.shift() as string;
+				const nextChunk = this.emulatorWriteQueue.shift()!;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/terminal-host/session.ts` around lines 655 - 711, The
review notes that inside the coalescing loop the call
this.emulatorWriteQueue.shift() as string can be made clearer by using a
non-null assertion because the while condition already guarantees queue length >
0; replace that cast with this.emulatorWriteQueue.shift()! (in the block
handling merged/itemsConsumed) to express the guaranteed non-null value while
leaving the defensive itemsConsumed === 0 check intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/desktop/src/main/terminal-host/session.ts`:
- Around line 655-711: The review notes that inside the coalescing loop the call
this.emulatorWriteQueue.shift() as string can be made clearer by using a
non-null assertion because the while condition already guarantees queue length >
0; replace that cast with this.emulatorWriteQueue.shift()! (in the block
handling merged/itemsConsumed) to express the guaranteed non-null value while
leaving the defensive itemsConsumed === 0 check intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7cd2c121-ff82-440f-befa-9a8fedbcacf4

📥 Commits

Reviewing files that changed from the base of the PR and between 2d230cb and 3081238.

📒 Files selected for processing (1)
  • apps/desktop/src/main/terminal-host/session.ts

@MocA-Love MocA-Love merged commit 75fc2f2 into main Apr 17, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Codexが重い

1 participant