Skip to content

perf: 使い捨て SessionStore init の副作用排除と refresh デバウンスで常時 CPU を削減#79

Draft
sasagar wants to merge 2 commits into
feature/fix-git-process-deadlockfrom
feature/reduce-idle-power
Draft

perf: 使い捨て SessionStore init の副作用排除と refresh デバウンスで常時 CPU を削減#79
sasagar wants to merge 2 commits into
feature/fix-git-process-deadlockfrom
feature/reduce-idle-power

Conversation

@sasagar

@sasagar sasagar commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

症状

エージェント作業中にアプリの CPU が常時 30% 前後に張り付き、消費電力が高い。

実測で特定した原因

  1. 使い捨て SessionStore の暴発: ContentView@State private var store = SessionStore() の初期値式は View 再 init のたびに評価され、そのつど使い捨ての SessionStore が restore() を実行していた。restore はセッション毎に git status / gh pr view / AF_UNIX ソケット bind / hooks 注入 / 常駐スレッド起動まで行う。実測: DB 上のセッション 3 個に対し accept 待ちスレッドが 48 本(バスのスレッドが自身を retain するため掃除もされない)。
  2. refresh の多重実行: FSEvents(latency 0.4s)のコールバック毎にデバウンスも in-flight ガードもなく refresh() が走り、1 回の refresh は repo 数 × 4 + 3 個の git を起動。プロセスサンプルでは __posix_spawn と文字列パースが CPU の大半を占め、任意の瞬間に 1〜10 個の git が生存していた。
  3. 非表示セッションも監視: ZStack 常駐設計により、非アクティブセッションの FileWatcher + refresh も動き続けていた。
  4. conflict watch の 5 秒ポーリング: 無変更でもセッション数ぶんの git status を定期起動。

修正(コミット順)

1 コミット目(構造の止血)

  • SessionStore の復元・常駐監視を start() に分離し .task から 1 回だけ呼ぶ
  • WorkPaneModel.scheduleRefresh(): 0.5 秒デバウンス+in-flight 合流。start() 冪等化
  • SessionDetailViewisActive を渡し、非アクティブ時は watcher 停止(pty 常駐は維持)
  • AgentStatusBus.start() 冪等化(二重 start の runServer 並走・fd 漏れ防止)

2 コミット目(さらなる削減)

  • FileWatcher が変更パス群をコールバックへ渡すように拡張
  • WorkPaneModel.refresh を部分リフレッシュ化: 変更パスに紐づく repo だけ git を回し、残りは repo 単位キャッシュから合成。履歴(commitGraph 300 件)・ブランチバーも対象 repo 変更時のみ再取得。紐づかないパスは全量へフォールバック(安全弁)
  • conflict watch を 5 秒ポーリング → イベント駆動(セッション毎の軽量 FSEvents + 2 秒デバウンス、対象は同一 repo 複数セッションのみ)。無変更時の定常 git 起動ゼロ
  • CLAUDE.md に常用向け Release ビルド手順を追記(Debug は最適化なしでパースが数倍遅い)

実測(1 コミット目適用時点)

指標
バス accept スレッド 48 3(=セッション数)
総スレッド 68 15
CPU(リポジトリ活動あり) 29.5% 張り付き 0〜11.5%

テスト / 検証

  • swift test 全 98 テスト成功、アプリ Debug/Release 両構成でビルド成功
  • 1 コミット目は実機起動で上表を確認済み。2 コミット目の実機挙動(org での部分リフレッシュ・conflict 警告の発火)は未検証 — 現在起動中のインスタンスを再起動できないため、次回起動時に要確認

Note: #78(GitRunner デッドロック根治)の上に積んでいます。#78 マージ後に base が dev へ自動付け替えされます。

sasagar added 2 commits July 8, 2026 14:37
- ContentView の @State 初期値式は View 再 init のたびに評価され、使い捨ての
  SessionStore が restore()(セッション毎に git status / gh pr view / ソケット
  bind / hooks 注入 / 常駐スレッド起動)まで実行していた。復元と常駐監視を
  start() に分離して表示側から 1 回だけ呼ぶ(実測でバススレッド 48 本 → 3 本、
  総スレッド 68 本 → 15 本)
- WorkPaneModel: FSEvents からの refresh を 0.5 秒デバウンス+in-flight 合流に
  変更。1 回の refresh は repo 数 × 4 + 3 個の git を起動するため、エージェント
  作業中のバーストでの多重実行が定常 CPU(実測 ~30%)の主因だった。start() も
  冪等化
- 非アクティブセッションの FileWatcher/refresh を停止(ZStack 常駐は維持しつつ
  可視セッションだけ監視。アクティブ化時に 1 回再取得)
- AgentStatusBus.start() を冪等化(二重 start による runServer 並走・fd 漏れを防止)
- FileWatcher が FSEvents の変更パス群をコールバックへ渡すように拡張
- WorkPaneModel.refresh を部分リフレッシュ対応に: 変更パスに紐づく repo だけ
  git(status/numstat/listFiles)を回し、残りは repo 単位キャッシュから合成。
  履歴(commitGraph 300 件)とブランチバーも対象 repo が変わったときだけ再取得。
  パスがどの repo にも紐づかない場合は discoverRepos の上で全量へフォールバック
  (シンボリックリンク差異・新規 clone への安全弁)
- SessionStore の conflict watch を 5 秒ポーリングからイベント駆動へ: セッション毎の
  軽量 FSEvents 監視で dirty マークだけ付け、2 秒デバウンスで対象セッション
  (同一 repo に複数セッションがあるもの)だけ git status を回す。無変更時の
  定常 git 起動がゼロになる
- CLAUDE.md に常用向け Release ビルド手順を追記(Debug は最適化なしでパースが
  数倍遅い)。Release 構成でのビルドが通ることを確認済み
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.

1 participant