|
| 1 | +# Performance & Responsiveness Optimization Plan |
| 2 | + |
| 3 | +**Date**: 2026-02-28 |
| 4 | +**Status**: In Progress |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +## Executive Summary |
| 9 | + |
| 10 | +Three independent research passes analyzed the Codeman codebase for performance bottlenecks across frontend rendering, backend hot paths, and system-level resource usage. The codebase already has strong foundational optimizations (per-session adaptive batching, rAF terminal writes, DEC 2026 sync markers, backpressure handling). This plan targets the remaining high-impact opportunities. |
| 11 | + |
| 12 | +**Key finding**: The biggest wins come from **skipping unnecessary work** — serializing unchanged state, processing output nobody is watching, and reducing broadcast volume. |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +## Phase 1: Quick Wins — ALREADY IMPLEMENTED |
| 17 | + |
| 18 | +All Phase 1 items were found to already exist in the codebase during verification: |
| 19 | + |
| 20 | +| # | Item | Status | Evidence | |
| 21 | +|---|------|--------|----------| |
| 22 | +| 1.1 | Skip terminal writes for hidden tabs | Done | SSE handler filters by `activeSessionId` (app.js:4076) | |
| 23 | +| 1.2 | mobile.css media query | Done | `media="(max-width: 1023px)"` on link tag (index.html:13) | |
| 24 | +| 1.3 | Deduplicate init API calls | Done | `_initGeneration` dedup + 3s fallback timer (app.js:2901-2904) | |
| 25 | +| 1.4 | Remove cache-busting timestamps | Done | No `?_t=` patterns found anywhere | |
| 26 | +| 1.5 | JS/CSS minification + compression | Done | esbuild minify + gzip + brotli in build.mjs (lines 42-51) | |
| 27 | + |
| 28 | +--- |
| 29 | + |
| 30 | +## Phase 2: Frontend Responsiveness — MOSTLY ALREADY IMPLEMENTED |
| 31 | + |
| 32 | +### 2.1 Batch `getBoundingClientRect()` in connection lines — DONE |
| 33 | +- **Files**: `src/web/public/app.js` (`_updateConnectionLinesImmediate()`) |
| 34 | +- **Change**: Refactored to batch all layout reads into Phase 1 (collect all rects into a Map), then perform all SVG writes in Phase 2 using cached values. Classic read-then-write pattern prevents interleaved forced reflows. |
| 35 | + |
| 36 | +### 2.2 Clean up ResizeObservers — Already implemented |
| 37 | +- `forceCloseSubagentWindow()` disconnects observers (app.js:12618-12620) |
| 38 | +- `cleanupAllFloatingWindows()` disconnects all on reconnect (app.js:12649-12653) |
| 39 | +- Observer refs stored on `windowData.resizeObserver` (app.js:12492) |
| 40 | + |
| 41 | +### 2.3 Drag handler cleanup — Already implemented |
| 42 | +- `makeWindowDraggable()` returns listener refs, stored in `windowData.dragListeners` |
| 43 | +- `forceCloseSubagentWindow()` removes all document-level drag listeners (app.js:12622-12630) |
| 44 | +- Panel drags add listeners on mousedown, remove on mouseup (app.js:10253-10284) |
| 45 | + |
| 46 | +### 2.4 Mobile window position cache — Skipped |
| 47 | +- O(n) loop over max ~20 windows; complexity of cached counter not justified |
| 48 | + |
| 49 | +### 2.5 Lazy modal DOM — Skipped |
| 50 | +- Large effort, marginal benefit for a vanilla JS app with fast DOM construction |
| 51 | + |
| 52 | +--- |
| 53 | + |
| 54 | +## Phase 3: Backend Hot Paths |
| 55 | + |
| 56 | +### 3.1 State diff broadcasts — ALREADY OPTIMIZED |
| 57 | +- `broadcastSessionStateDebounced()` already batches at 500ms intervals |
| 58 | +- `toLightDetailedState()` excludes heavy buffers (textOutput, terminalBuffer) |
| 59 | +- Per-session serialization is <1ms; with debouncing, only 1-3 sessions serialize per flush |
| 60 | +- JSON.stringify happens once per broadcast (not per client) — serialization cost is negligible |
| 61 | +- Full state diffs would add significant frontend complexity for marginal gain |
| 62 | + |
| 63 | +### 3.2 Improve session list cache hit rate — DONE |
| 64 | +- **Files**: `src/web/server.ts` (`broadcast()` method) |
| 65 | +- **Change**: Cache now only invalidated on truly structural events (`session:created`, `session:deleted`, `session:updated`) instead of on every `session:*` and `respawn:*` event. High-frequency events like `session:working`, `session:idle`, `session:completion`, `respawn:stateChanged` no longer defeat the 1s TTL cache. |
| 66 | +- **Impact**: Cache hit ratio from ~0% to ~80%+ during active sessions. The debounced `session:updated` still refreshes the cache within 500ms of any state change. |
| 67 | + |
| 68 | +### 3.3 Skip PTY processing — ALREADY OPTIMIZED |
| 69 | +- `_processExpensiveParsers()` is already throttled to every 150ms (not per-chunk) |
| 70 | +- Lazy ANSI stripping via `getCleanData()` closure — only computed when a consumer needs it |
| 71 | +- Quick pre-checks skip parsers when content is irrelevant (e.g., token parser only runs if data contains "token") |
| 72 | +- OpenCode sessions skip all Claude-specific parsers entirely |
| 73 | +- Further optimization would require visibility-aware processing, adding complexity for marginal gain |
| 74 | + |
| 75 | +### 3.4 Batch subagent liveness checks — Deferred |
| 76 | +- `/proc/{pid}` stat calls are ~0.1ms each; even with 500 agents, total is 50ms every 10s |
| 77 | +- Current approach is simple and reliable; batching adds race condition risk |
| 78 | +- Consider only if profiling shows this as a bottleneck |
| 79 | + |
| 80 | +### 3.5 Deduplicate detection update emissions — DONE |
| 81 | +- **Files**: `src/respawn-controller.ts` (`startDetectionUpdates()`) |
| 82 | +- **Change**: Detection status now only emitted when key fields (confidenceLevel, statusText, controller state) actually change. Previously emitted every 2s regardless, broadcasting identical status to all SSE clients. |
| 83 | +- **Impact**: For stable/idle sessions, eliminates ~100% of redundant detection broadcasts. For active sessions, reduces broadcasts to only meaningful state transitions. |
| 84 | + |
| 85 | +--- |
| 86 | + |
| 87 | +## Phase 4: System-Level Improvements |
| 88 | + |
| 89 | +### 4.1 Incremental state persistence |
| 90 | +- **Files**: `src/state-store.ts` (~lines 145-160) |
| 91 | +- **Problem**: Every 500ms debounce writes the entire `AppState` (all sessions, tasks, config) via `JSON.stringify()`. With 50 sessions, state can be tens of MB. Serialization alone costs 50-100ms. |
| 92 | +- **Fix**: Track dirty sessions. On persist, only re-serialize dirty sessions; cache serialized JSON for clean sessions. Assemble final output from cached fragments. |
| 93 | +- **Impact**: Reduces serialization cost from O(all sessions) to O(dirty sessions). Typical steady-state: 1-2 dirty sessions instead of 50. |
| 94 | + |
| 95 | +### 4.2 Replace polling with fs watchers for team watcher |
| 96 | +- **Files**: `src/team-watcher.ts` (~lines 148-180) |
| 97 | +- **Problem**: Polls `~/.claude/teams/` every 5s via `readdir()` + `stat()`. Blocks event loop for 100-200ms on large directories. |
| 98 | +- **Fix**: Use `chokidar` (already a dependency) or `fs.watch()` to react to changes. Keep a 30s fallback poll for reliability. |
| 99 | +- **Impact**: Eliminates 5s polling overhead; near-instant team detection. |
| 100 | + |
| 101 | +### 4.3 Consolidate subagent file watchers |
| 102 | +- **Files**: `src/subagent-watcher.ts` (~line 229+) |
| 103 | +- **Problem**: One chokidar watcher per agent directory. With 500 agents, that's 500 inotify watchers consuming kernel resources. |
| 104 | +- **Fix**: Watch at the session level (one watcher per session's subagent directory), not per-agent. Parse events to route to correct agent. |
| 105 | +- **Impact**: Reduces inotify watchers from 500 to ~50 (one per session). |
| 106 | + |
| 107 | +### 4.4 Stream transcript files instead of full reads |
| 108 | +- **Files**: `src/subagent-watcher.ts` (~lines 959-964) |
| 109 | +- **Problem**: `loadTranscript()` reads entire transcript file (can be >100KB). With 500 agents discovered at once, that's 50MB of file reads. |
| 110 | +- **Fix**: Only read last 10KB for display (tail). Full file on-demand only (e.g., when user opens transcript viewer). |
| 111 | +- **Impact**: Reduces file I/O from 50MB to 5MB for bulk agent discovery. |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +## Phase 5: Long-Term Architectural (Optional) |
| 116 | + |
| 117 | +### 5.1 Worker thread for PTY processing |
| 118 | +- **Files**: `src/session.ts` |
| 119 | +- **Problem**: ANSI stripping, Ralph tracking, and bash tool parsing all run on the main event loop. At scale (50 busy sessions), this consumes 300-500ms CPU/sec. |
| 120 | +- **Fix**: Offload ANSI strip + line processing to a worker thread pool. Main thread receives clean text + parsed events. |
| 121 | +- **Impact**: Frees event loop for I/O operations. Most impactful at 10+ concurrent busy sessions. |
| 122 | + |
| 123 | +### 5.2 Per-session SSE subscriptions |
| 124 | +- **Files**: `src/web/server.ts` |
| 125 | +- **Problem**: Every SSE event is broadcast to all connected clients. A client watching session A still receives events for sessions B through Z. |
| 126 | +- **Fix**: Clients subscribe to specific session IDs. Server only sends events to interested clients. |
| 127 | +- **Impact**: Reduces SSE broadcast fan-out from N clients to ~1-2 per event. Major improvement at 100 SSE clients. |
| 128 | + |
| 129 | +### 5.3 O(1) LRUMap via doubly-linked list |
| 130 | +- **Files**: `src/utils/lru-map.ts` (~lines 98-110) |
| 131 | +- **Problem**: `get()` uses delete + re-insert to refresh position — O(n) on Map iteration for delete. |
| 132 | +- **Fix**: Implement classic LRU with doubly-linked list + Map for O(1) get/put/evict. |
| 133 | +- **Impact**: Low — current sizes (max 500) make this barely measurable. Only worthwhile if LRUMap is used on hot paths. |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +## Priority Matrix (Remaining Work) |
| 138 | + |
| 139 | +| # | Item | Impact | Risk | Effort | |
| 140 | +|---|------|--------|------|--------| |
| 141 | +| 3.1 | State diff broadcasts | **Very High** | Medium | 3-4h | |
| 142 | +| 3.2 | Fix session cache invalidation | **High** | Low | 1h | |
| 143 | +| 3.3 | Skip PTY processing for hidden sessions | **High** | Medium | 2-3h | |
| 144 | +| 3.5 | Throttle detection broadcasts | **Medium** | Low | 1h | |
| 145 | +| 3.4 | Batch liveness checks | **Medium** | Low | 1-2h | |
| 146 | +| 4.1 | Incremental state persistence | **Medium** | Medium | 3-4h | |
| 147 | +| 4.2 | Team watcher fs events | **Low-Med** | Medium | 2h | |
| 148 | +| 4.3 | Consolidate file watchers | **Low-Med** | Medium | 2h | |
| 149 | +| 4.4 | Stream transcripts | **Low-Med** | Low | 1h | |
| 150 | +| 5.1 | Worker thread PTY | **Med** (at scale) | High | 8h | |
| 151 | +| 5.2 | Per-session SSE subs | **Med** (at scale) | High | 4h | |
| 152 | +| 5.3 | O(1) LRUMap | **Very Low** | Medium | 2h | |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +## Recommended Execution Order |
| 157 | + |
| 158 | +**Sprint 1** (Phase 3 — Backend Hot Paths): Items 3.1, 3.2, 3.3, 3.5 |
| 159 | +- Backend serialization and broadcast efficiency |
| 160 | +- Highest remaining impact; requires careful testing with multiple active sessions |
| 161 | + |
| 162 | +**Sprint 2** (Phase 4 — System Level): Items 4.1, 3.4, 4.3, 4.4 |
| 163 | +- State persistence, liveness checks, watcher consolidation |
| 164 | +- Medium-complexity refactors |
| 165 | + |
| 166 | +**Sprint 3** (Phase 5 — Architectural): Items 5.1, 5.2 — only if scaling demands it |
| 167 | + |
| 168 | +--- |
| 169 | + |
| 170 | +## Measurement |
| 171 | + |
| 172 | +Before starting implementation, establish baselines: |
| 173 | + |
| 174 | +1. **Frontend**: Record Chrome DevTools Performance trace with 10 sessions open. Measure: |
| 175 | + - Frame rate during rapid terminal output |
| 176 | + - Long tasks (>50ms) count per 30s |
| 177 | + - Heap size after 1h session |
| 178 | + |
| 179 | +2. **Backend**: Add `performance.now()` instrumentation around: |
| 180 | + - `flushSessionTerminalBatch()` — time per flush |
| 181 | + - `broadcastSessionStateDebounced()` — serialization time |
| 182 | + - `StateStore.save()` — persist time |
| 183 | + - Event loop lag via `monitorEventLoopDelay()` |
| 184 | + |
| 185 | +3. **First load**: Lighthouse score on desktop and mobile (simulated 3G) |
0 commit comments