From ecb416f6cbe68a9f1741ea7978602c0db0adbf99 Mon Sep 17 00:00:00 2001 From: Joshua Frank Date: Thu, 9 Apr 2026 14:38:18 -0500 Subject: [PATCH] perf: cache list_windows and skip redundant project scans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit list_windows() via libtmux was called 3+ times/second from independent callers at ~39ms each. Add 1s TTL cache so concurrent callers share one query. Also skip the full scan_projects() directory walk for sessions already tracked in monitor state — only scan when new sessions appear. Reduces CPU from ~34% to ~8%. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ccbot/session_monitor.py | 26 +++++++++++++++++++++----- src/ccbot/tmux_manager.py | 18 +++++++++++++++++- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/ccbot/session_monitor.py b/src/ccbot/session_monitor.py index 0a1b3186..1608c2be 100644 --- a/src/ccbot/session_monitor.py +++ b/src/ccbot/session_monitor.py @@ -272,18 +272,34 @@ async def check_for_updates(self, active_session_ids: set[str]) -> list[NewMessa Reads from last byte offset. Emits both intermediate (stop_reason=null) and complete messages. + For already-tracked sessions, uses stored file paths (fast path). + Only runs the expensive scan_projects() when there are new + session IDs not yet tracked. + Args: active_session_ids: Set of session IDs currently in session_map """ new_messages = [] - # Scan projects to get available session files - sessions = await self.scan_projects() + # Fast path: build session list from already-tracked sessions + tracked_ids = set(self.state.tracked_sessions.keys()) + sessions: list[SessionInfo] = [] + for sid in active_session_ids & tracked_ids: + tracked = self.state.get_session(sid) + if tracked and tracked.file_path: + fp = Path(tracked.file_path) + if fp.exists(): + sessions.append(SessionInfo(session_id=sid, file_path=fp)) + + # Only scan projects if there are untracked active sessions + untracked = active_session_ids - tracked_ids + if untracked: + scanned = await self.scan_projects() + for si in scanned: + if si.session_id in untracked: + sessions.append(si) - # Only process sessions that are in session_map for session_info in sessions: - if session_info.session_id not in active_session_ids: - continue try: tracked = self.state.get_session(session_info.session_id) diff --git a/src/ccbot/tmux_manager.py b/src/ccbot/tmux_manager.py index f05b4f3a..c77c4dc6 100644 --- a/src/ccbot/tmux_manager.py +++ b/src/ccbot/tmux_manager.py @@ -15,6 +15,7 @@ import asyncio import logging +import time from dataclasses import dataclass from pathlib import Path @@ -38,6 +39,9 @@ class TmuxWindow: class TmuxManager: """Manages tmux windows for Claude Code sessions.""" + # TTL for cached list_windows results (seconds) + _CACHE_TTL = 1.0 + def __init__(self, session_name: str | None = None): """Initialize tmux manager. @@ -46,6 +50,8 @@ def __init__(self, session_name: str | None = None): """ self.session_name = session_name or config.tmux_session_name self._server: libtmux.Server | None = None + self._cached_windows: list[TmuxWindow] | None = None + self._cache_time: float = 0.0 @property def server(self) -> libtmux.Server: @@ -95,9 +101,16 @@ def _scrub_session_env(session: libtmux.Session) -> None: async def list_windows(self) -> list[TmuxWindow]: """List all windows in the session with their working directories. + Results are cached for _CACHE_TTL seconds to avoid redundant + libtmux Server queries from concurrent callers (status poll, + session monitor, message queue). + Returns: List of TmuxWindow with window info and cwd """ + now = time.monotonic() + if self._cached_windows is not None and (now - self._cache_time) < self._CACHE_TTL: + return self._cached_windows def _sync_list_windows() -> list[TmuxWindow]: windows = [] @@ -135,7 +148,10 @@ def _sync_list_windows() -> list[TmuxWindow]: return windows - return await asyncio.to_thread(_sync_list_windows) + windows = await asyncio.to_thread(_sync_list_windows) + self._cached_windows = windows + self._cache_time = time.monotonic() + return windows async def find_window_by_name(self, window_name: str) -> TmuxWindow | None: """Find a window by its name.