perf: Operation Lightspeed — 5 parallel performance optimizations

1. Session-scoped SSE subscriptions: server filters events by session ID,
   clients can subscribe via ?sessions=id1,id2 (backwards-compatible)
2. Lazy xterm.js for subagent windows: terminals created on restore,
   disposed on minimize — saves ~3.75MB DOM at 50 agents
3. Targeted badge updates: badge count changes update the <span> directly
   instead of rebuilding the entire session tab sidebar (O(1) vs O(n))
4. Conditional SSE padding: 8KB Cloudflare padding only on session:terminal
   and session:needsRefresh, not every event (~70% bandwidth reduction)
5. Canvas renderer on mobile: skip WebGL addon on mobile devices to reduce
   GPU pressure and prevent context loss on weaker mobile GPUs

All 5 implemented in parallel via isolated git worktrees, merged conflict-free.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-03-07 06:14:16 +01:00
co-authored by Claude Opus 4.6
parent 415f02e680
commit 1f30ed445c
5 changed files with 1138 additions and 37 deletions
+59 -9
View File
@@ -624,7 +624,8 @@ class CodemanApp {
// oversized terminal.write() calls that triggered the stalls.
// Disable with ?nowebgl URL param if GPU issues return.
this._webglAddon = null;
if (!new URLSearchParams(location.search).has('nowebgl') && typeof WebglAddon !== 'undefined') {
const isMobile = MobileDetection.getDeviceType() === 'mobile';
if (!isMobile && !new URLSearchParams(location.search).has('nowebgl') && typeof WebglAddon !== 'undefined') {
try {
this._webglAddon = new WebglAddon.WebglAddon();
this._webglAddon.onContextLoss(() => {
@@ -3198,15 +3199,38 @@ class CodemanApp {
return;
}
// Update subagent badge - check if count changed
// Update subagent badge - targeted update without full rebuild
const subagentBadgeEl = tab.querySelector('.tab-subagent-badge');
const minimizedAgents = this.minimizedSubagents.get(id);
const minimizedCount = minimizedAgents?.size || 0;
const currentCount = subagentBadgeEl ? parseInt(subagentBadgeEl.querySelector('.subagent-count')?.textContent || '0') : 0;
if (minimizedCount !== currentCount) {
// Count changed - need full rebuild for dropdown update
this._fullRenderSessionTabs();
return;
if (minimizedCount > 0 && subagentBadgeEl) {
// Badge exists and still has agents - update label and dropdown in-place
const labelEl = subagentBadgeEl.querySelector('.subagent-label');
const newLabel = minimizedCount === 1 ? 'AGENT' : `AGENTS (${minimizedCount})`;
if (labelEl && labelEl.textContent !== newLabel) {
labelEl.textContent = newLabel;
}
// Rebuild dropdown items (agent list may have changed)
const dropdownEl = subagentBadgeEl.querySelector('.subagent-dropdown');
if (dropdownEl) {
const newBadgeHtml = this.renderSubagentTabBadge(id, minimizedAgents);
const temp = document.createElement('div');
temp.innerHTML = newBadgeHtml;
const newDropdown = temp.querySelector('.subagent-dropdown');
if (newDropdown) {
dropdownEl.innerHTML = newDropdown.innerHTML;
}
}
} else if (minimizedCount > 0 && !subagentBadgeEl) {
// Need to add badge - insert before gear icon
const badgeHtml = this.renderSubagentTabBadge(id, minimizedAgents);
const gearEl = tab.querySelector('.tab-gear');
if (gearEl) {
gearEl.insertAdjacentHTML('beforebegin', badgeHtml);
}
} else if (minimizedCount === 0 && subagentBadgeEl) {
// Count went to 0 - remove badge
subagentBadgeEl.remove();
}
}
} else {
@@ -9599,10 +9623,16 @@ class CodemanApp {
// Show window (unless it was minimized by user)
if (!windowInfo.minimized) {
windowInfo.element.style.display = 'flex';
// Lazily re-create teammate terminal if it was disposed when hidden
if (windowInfo._lazyTerminal) {
this._restoreTeammateTerminalFromLazy(agentId);
}
}
windowInfo.hidden = false;
} else {
// Hide window (but don't close it)
// Dispose teammate terminal to free memory while hidden on inactive tab
this._disposeTeammateTerminalForMinimize(agentId);
windowInfo.element.style.display = 'none';
windowInfo.hidden = true;
}
@@ -9685,6 +9715,8 @@ class CodemanApp {
minimizeSubagentWindow(agentId) {
const windowData = this.subagentWindows.get(agentId);
if (windowData) {
// Dispose teammate terminal on minimize to free DOM/memory (lazy re-creation on restore)
this._disposeTeammateTerminalForMinimize(agentId);
windowData.element.style.display = 'none';
windowData.minimized = true;
this.updateConnectionLines();
@@ -9695,6 +9727,10 @@ class CodemanApp {
// Debounced wrapper — coalesces rapid subagent events (tool_call, progress,
// message) into a single DOM update per 100ms per agent window.
scheduleSubagentWindowRender(agentId) {
// Skip DOM updates for windows with lazy (disposed) terminals — they're minimized
const windowData = this.subagentWindows.get(agentId);
if (windowData?.minimized) return;
if (!this._subagentWindowRenderTimeouts) this._subagentWindowRenderTimeouts = new Map();
if (this._subagentWindowRenderTimeouts.has(agentId)) {
clearTimeout(this._subagentWindowRenderTimeouts.get(agentId));
@@ -9708,6 +9744,9 @@ class CodemanApp {
renderSubagentWindowContent(agentId) {
// Skip if this window has a live terminal (don't overwrite xterm with activity HTML)
if (this.teammateTerminals.has(agentId)) return;
// Skip if this window has a lazy (disposed) terminal — it will be re-created on restore
const windowData = this.subagentWindows.get(agentId);
if (windowData?._lazyTerminal) return;
const body = document.getElementById(`subagent-window-body-${agentId}`);
if (!body) return;
@@ -10110,8 +10149,19 @@ class CodemanApp {
resizeObserver.observe(win);
this.subagentWindows.get(windowId).resizeObserver = resizeObserver;
// Init the xterm.js terminal
this.initTeammateTerminal(windowId, paneData, win);
// Init the xterm.js terminal (lazy if hidden)
if (shouldHide) {
// Window starts hidden — defer terminal creation until visible (lazy init)
const windowEntry = this.subagentWindows.get(windowId);
if (windowEntry) {
windowEntry._lazyTerminal = true;
windowEntry._lazyPaneTarget = paneData.paneTarget;
windowEntry._lazySessionId = paneData.sessionId;
windowEntry._lazyBuffer = '';
}
} else {
this.initTeammateTerminal(windowId, paneData, win);
}
// Animate in
requestAnimationFrame(() => {
+149 -18
View File
@@ -121,7 +121,7 @@ Object.assign(CodemanApp.prototype, {
if (!windowData.minimized) {
openWindows.push({
agentId,
position: windowData.position || null
position: windowData.position || null,
});
}
}
@@ -163,8 +163,7 @@ Object.assign(CodemanApp.prototype, {
// Use the PERSISTENT parent map (THE source of truth)
// Fall back to saved sessionId only if it exists in current sessions
const parentFromMap = this.subagentParentMap.get(agentId);
const correctSessionId = parentFromMap ||
(this.sessions.has(savedSessionId) ? savedSessionId : null);
const correctSessionId = parentFromMap || (this.sessions.has(savedSessionId) ? savedSessionId : null);
if (correctSessionId) {
// Ensure the parent map has this association
@@ -184,7 +183,7 @@ Object.assign(CodemanApp.prototype, {
// Restore open windows (for recent, non-completed agents only)
const now = Date.now();
const maxAgeMs = 10 * 60 * 1000; // 10 minutes - don't restore windows for old agents
for (const { agentId, position } of (states.open || [])) {
for (const { agentId, position } of states.open || []) {
const agent = this.subagents.get(agentId);
// Only restore window if agent exists, is recent, and is still active/idle
const agentAge = agent?.startedAt ? now - agent.startedAt : Infinity;
@@ -458,6 +457,113 @@ Object.assign(CodemanApp.prototype, {
}
},
// ═══════════════════════════════════════════════════════════════
// Lazy Terminal Lifecycle
// ═══════════════════════════════════════════════════════════════
//
// Teammate terminal windows use xterm.js Terminal instances that consume
// ~75KB of DOM memory each. With 50 agents minimized, that's ~3.75MB of
// invisible terminals. To avoid this, we dispose the Terminal when a
// window is minimized and lazily re-create it when restored.
//
// Flow:
// minimize → _disposeTeammateTerminalForMinimize() → sets _lazyTerminal flag
// restore → _restoreTeammateTerminalFromLazy() → re-creates Terminal
// create (hidden/minimized) → skip initTeammateTerminal, set _lazyTerminal
//
// The pane buffer is always re-fetched from the API on restore, so no
// client-side buffer accumulation is needed (the tmux pane is the source
// of truth). Regular (non-teammate) subagent windows use activity HTML
// and are unaffected by this optimization.
/** Max bytes to buffer for a minimized teammate terminal (256KB). */
_LAZY_TERMINAL_BUFFER_CAP: 256 * 1024,
/**
* Dispose a teammate terminal when its window is minimized.
* Saves pane metadata so the terminal can be re-created on restore.
* No-op if the window has no teammate terminal.
*/
_disposeTeammateTerminalForMinimize(agentId) {
const termData = this.teammateTerminals.get(agentId);
if (!termData) return; // Not a teammate terminal window
const windowData = this.subagentWindows.get(agentId);
// Save pane metadata needed to re-create the terminal on restore
if (windowData) {
windowData._lazyTerminal = true;
windowData._lazyPaneTarget = termData.paneTarget;
windowData._lazySessionId = termData.sessionId;
// Buffer for any data that arrives while minimized (from pendingData or future writes)
windowData._lazyBuffer = '';
}
// Dispose the resize observer
if (termData.resizeObserver) {
termData.resizeObserver.disconnect();
}
// Dispose the xterm.js Terminal instance (frees DOM nodes and internal buffers)
if (termData.terminal) {
try {
termData.terminal.dispose();
} catch {}
}
// Remove from teammateTerminals map so renderSubagentWindowContent won't skip this window
// (the activity HTML can serve as a lightweight placeholder while minimized)
this.teammateTerminals.delete(agentId);
},
/**
* Re-create a teammate terminal when its window is restored from minimized state.
* Fetches the current pane buffer from the API (tmux is the source of truth).
* No-op if the window doesn't have the _lazyTerminal flag.
*/
_restoreTeammateTerminalFromLazy(agentId) {
const windowData = this.subagentWindows.get(agentId);
if (!windowData || !windowData._lazyTerminal) return;
const paneTarget = windowData._lazyPaneTarget;
const sessionId = windowData._lazySessionId;
// Clear lazy state
windowData._lazyTerminal = false;
windowData._lazyPaneTarget = null;
windowData._lazySessionId = null;
windowData._lazyBuffer = null;
if (!paneTarget || !sessionId) return;
// Re-create the terminal using the same initTeammateTerminal flow
const paneInfo = { paneTarget, sessionId };
this.initTeammateTerminal(agentId, paneInfo, windowData.element);
},
/**
* Append terminal data to a minimized teammate terminal's lazy buffer.
* Called when SSE data arrives for a minimized window. Caps at _LAZY_TERMINAL_BUFFER_CAP.
* Returns true if the data was buffered, false if the window is not in lazy mode.
*/
_bufferLazyTerminalData(agentId, data) {
const windowData = this.subagentWindows.get(agentId);
if (!windowData || !windowData._lazyTerminal) return false;
if (windowData._lazyBuffer === null || windowData._lazyBuffer === undefined) {
windowData._lazyBuffer = '';
}
// Append data, capping total size
windowData._lazyBuffer += data;
if (windowData._lazyBuffer.length > this._LAZY_TERMINAL_BUFFER_CAP) {
// Keep only the tail to stay under cap
windowData._lazyBuffer = windowData._lazyBuffer.slice(-this._LAZY_TERMINAL_BUFFER_CAP);
}
return true;
},
// ═══════════════════════════════════════════════════════════════
// Subagent Floating Windows
// ═══════════════════════════════════════════════════════════════
@@ -495,7 +601,7 @@ Object.assign(CodemanApp.prototype, {
// Only open windows for agents that belong to a Codeman-managed session tab.
// Agents from external Claude sessions (not tracked by Codeman) should not pop up.
if (agent.sessionId) {
const hasMatchingTab = Array.from(this.sessions.values()).some(s => s.claudeSessionId === agent.sessionId);
const hasMatchingTab = Array.from(this.sessions.values()).some((s) => s.claudeSessionId === agent.sessionId);
if (!hasMatchingTab) return;
}
@@ -600,9 +706,7 @@ Object.assign(CodemanApp.prototype, {
}
// Get parent TAB element for spawn animation
const parentTab = parentSessionId
? document.querySelector(`.session-tab[data-id="${parentSessionId}"]`)
: null;
const parentTab = parentSessionId ? document.querySelector(`.session-tab[data-id="${parentSessionId}"]`) : null;
// Create window element
const win = document.createElement('div');
@@ -611,17 +715,19 @@ Object.assign(CodemanApp.prototype, {
win.style.zIndex = ++this.subagentWindowZIndex;
// Build parent header if we have parent info
const parentHeader = parentSessionId && parentSessionName
? `<div class="subagent-window-parent" data-parent-session="${parentSessionId}">
const parentHeader =
parentSessionId && parentSessionName
? `<div class="subagent-window-parent" data-parent-session="${parentSessionId}">
<span class="parent-label">from</span>
<span class="parent-name" onclick="app.selectSession('${escapeHtml(parentSessionId)}')">${escapeHtml(parentSessionName)}</span>
</div>`
: '';
: '';
const teammateInfo = this.getTeammateInfo(agent);
const windowTitle = teammateInfo ? teammateInfo.name : (agent.description || agentId.substring(0, 7));
const windowTitle = teammateInfo ? teammateInfo.name : agent.description || agentId.substring(0, 7);
const maxTitleLen = isMobile ? 30 : 50;
const truncatedTitle = windowTitle.length > maxTitleLen ? windowTitle.substring(0, maxTitleLen) + '...' : windowTitle;
const truncatedTitle =
windowTitle.length > maxTitleLen ? windowTitle.substring(0, maxTitleLen) + '...' : windowTitle;
const modelBadge = agent.modelShort
? `<span class="subagent-model-badge ${agent.modelShort}">${agent.modelShort}</span>`
: '';
@@ -695,7 +801,19 @@ Object.assign(CodemanApp.prototype, {
// Render content — check if this teammate has a tmux pane
const paneInfo = teammateInfo ? this.teammatePanesByName.get(teammateInfo.name) : null;
if (paneInfo) {
this.initTeammateTerminal(agentId, paneInfo, win);
if (shouldHide) {
// Window starts hidden — defer terminal creation until visible (lazy init).
// Saves ~75KB of DOM memory per hidden teammate terminal window.
const windowEntry = this.subagentWindows.get(agentId);
if (windowEntry) {
windowEntry._lazyTerminal = true;
windowEntry._lazyPaneTarget = paneInfo.paneTarget;
windowEntry._lazySessionId = paneInfo.sessionId;
windowEntry._lazyBuffer = '';
}
} else {
this.initTeammateTerminal(agentId, paneInfo, win);
}
} else {
this.renderSubagentWindowContent(agentId);
}
@@ -755,6 +873,10 @@ Object.assign(CodemanApp.prototype, {
this.setAgentParentSessionId(agentId, parentSessionId);
}
// Dispose teammate terminal on minimize to free DOM/memory (~75KB per instance).
// The terminal will be lazily re-created on restore via initTeammateTerminal().
this._disposeTeammateTerminalForMinimize(agentId);
// Always minimize to tab
windowData.element.style.display = 'none';
windowData.minimized = true;
@@ -829,7 +951,9 @@ Object.assign(CodemanApp.prototype, {
for (const [, termData] of this.teammateTerminals) {
if (termData.resizeObserver) termData.resizeObserver.disconnect();
if (termData.terminal) {
try { termData.terminal.dispose(); } catch {}
try {
termData.terminal.dispose();
} catch {}
}
}
this.teammateTerminals.clear();
@@ -959,6 +1083,13 @@ Object.assign(CodemanApp.prototype, {
windowData.hidden = false;
}
windowData.minimized = false;
// Lazily re-create teammate terminal if it was disposed on minimize.
// Only re-create when the window is actually becoming visible.
if (shouldShow && windowData._lazyTerminal) {
this._restoreTeammateTerminalFromLazy(agentId);
}
this.updateConnectionLines();
// Restack all visible mobile windows so restored ones don't overlap
this.relayoutMobileSubagentWindows();
@@ -1067,7 +1198,7 @@ Object.assign(CodemanApp.prototype, {
if (!dropdown || dropdown.classList.contains('open')) return;
// Close other dropdowns first
document.querySelectorAll('.subagent-dropdown.open').forEach(d => {
document.querySelectorAll('.subagent-dropdown.open').forEach((d) => {
d.classList.remove('open', 'pinned');
if (d.parentElement === document.body && d._originalParent) {
d._originalParent.appendChild(d);
@@ -1089,8 +1220,8 @@ Object.assign(CodemanApp.prototype, {
// Schedule hide after delay (allows moving mouse to dropdown)
scheduleHideSubagentDropdown(badgeEl) {
this._subagentHideTimeout = setTimeout(() => {
const dropdown = badgeEl?.querySelector?.('.subagent-dropdown') ||
document.querySelector('.subagent-dropdown.open');
const dropdown =
badgeEl?.querySelector?.('.subagent-dropdown') || document.querySelector('.subagent-dropdown.open');
if (dropdown && !dropdown.classList.contains('pinned')) {
dropdown.classList.remove('open');
if (dropdown._originalParent) {
+62 -10
View File
@@ -210,7 +210,12 @@ export class WebServer extends EventEmitter {
// Store session listener references for explicit cleanup (prevents memory leaks)
private sessionListenerRefs: Map<string, SessionListenerRefs> = new Map();
private scheduledRuns: Map<string, ScheduledRun> = new Map();
private sseClients: Set<FastifyReply> = new Set();
/**
* SSE clients mapped to their session subscription filter.
* Value is a Set of session IDs the client wants events for,
* or `null` meaning "receive all events" (backwards-compatible default).
*/
private sseClients: Map<FastifyReply, Set<string> | null> = new Map();
/** Clients with backpressure — skip writes until 'drain' fires */
private backpressuredClients: Set<FastifyReply> = new Set();
private store = getStore();
@@ -590,6 +595,18 @@ export class WebServer extends EventEmitter {
return;
}
// Parse optional session subscription filter from query parameter.
// /api/events?sessions=id1,id2 — client only receives events for those sessions.
// /api/events (no param) — client receives all events (backwards-compatible).
const query = req.query as { sessions?: string };
let sessionFilter: Set<string> | null = null;
if (query.sessions) {
const ids = query.sessions.split(',').filter(Boolean);
if (ids.length > 0) {
sessionFilter = new Set(ids);
}
}
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
@@ -597,7 +614,7 @@ export class WebServer extends EventEmitter {
'X-Accel-Buffering': 'no', // Disable nginx buffering
});
this.sseClients.add(reply);
this.sseClients.set(reply, sessionFilter);
// Send initial state
// Use light state for SSE init to avoid sending 2MB+ terminal buffers
@@ -1972,9 +1989,15 @@ export class WebServer extends EventEmitter {
this.cachedSessionsList = null;
}
// Performance optimization: serialize JSON once for all clients.
// Only append Cloudflare tunnel padding when tunnel is actually active —
// direct/Tailscale clients don't need 8KB padding on every event.
const padding = this._isTunnelActive ? SSE_PADDING : '';
// Only append Cloudflare tunnel padding for latency-sensitive events —
// high-frequency terminal data and recovery events need immediate proxy flush,
// but low-frequency metadata events (session:created, ralph:*, respawn:*, etc.)
// are small and infrequent enough that proxy buffering doesn't matter.
// Note: session:terminal bypasses broadcast() via flushSessionTerminalBatch(),
// but is included here for completeness in case the path changes.
const needsPadding =
this._isTunnelActive && (event === SseEvent.SessionTerminal || event === SseEvent.SessionNeedsRefresh);
const padding = needsPadding ? SSE_PADDING : '';
let message: string;
try {
message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` + padding;
@@ -1983,11 +2006,35 @@ export class WebServer extends EventEmitter {
console.error(`[Server] Failed to serialize SSE event "${event}":`, err);
return;
}
for (const client of this.sseClients) {
// Extract sessionId from event data for subscription filtering.
const eventSessionId = this.extractSessionId(event, data);
for (const [client, filter] of this.sseClients) {
// No filter (null) = receive everything. Otherwise, skip if event is
// session-scoped and the session isn't in the client's subscription set.
if (filter && eventSessionId && !filter.has(eventSessionId)) continue;
this.sendSSEPreformatted(client, message);
}
}
/**
* Extract the session ID from an event's data payload for subscription filtering.
* Returns the sessionId string if the event is session-scoped, or null for global events.
*/
private extractSessionId(event: string, data: unknown): string | null {
if (data == null || typeof data !== 'object') return null;
const record = data as Record<string, unknown>;
// Most session-scoped events use `sessionId`
if (typeof record.sessionId === 'string') return record.sessionId;
// Session lifecycle events (session:*) use `id` from the session state object
if (typeof record.id === 'string' && event.startsWith('session:')) return record.id;
// No session ID found — treat as global event (sent to all clients)
return null;
}
// Batch terminal data for better performance (60fps)
// Uses per-session timers with adaptive intervals to prevent thundering herd:
// each session flushes independently rather than all sessions flushing in one burst.
@@ -2066,8 +2113,13 @@ export class WebServer extends EventEmitter {
// Fast path: build SSE message directly without JSON.stringify on wrapper object.
// Only the terminal data string needs escaping; sessionId is a UUID (safe to template).
const escapedData = JSON.stringify(syncData);
const message = `event: session:terminal\ndata: {"id":"${sessionId}","data":${escapedData}}\n\n`;
for (const client of this.sseClients) {
// Append tunnel padding for immediate Cloudflare proxy flush —
// terminal data is high-frequency and latency-sensitive.
const padding = this._isTunnelActive ? SSE_PADDING : '';
const message = `event: session:terminal\ndata: {"id":"${sessionId}","data":${escapedData}}\n\n` + padding;
for (const [client, filter] of this.sseClients) {
// Skip clients that have a session filter and aren't subscribed to this session
if (filter && !filter.has(sessionId)) continue;
this.sendSSEPreformatted(client, message);
}
}
@@ -2240,7 +2292,7 @@ export class WebServer extends EventEmitter {
private cleanupDeadSSEClients(): void {
const deadClients: FastifyReply[] = [];
for (const client of this.sseClients) {
for (const [client] of this.sseClients) {
try {
// Check if the underlying socket is still writable
const socket = client.raw.socket;
@@ -2640,7 +2692,7 @@ export class WebServer extends EventEmitter {
this.cleanup.dispose();
// Gracefully close all SSE connections before clearing
for (const client of this.sseClients) {
for (const [client] of this.sseClients) {
try {
// Send a final event to notify clients of shutdown
this.sendSSE(client, 'server:shutdown', { reason: 'Server stopping' });