mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix: eliminate SSE padding overhead and debounce subagent window renders
Two performance fixes for browser hanging during active agent work: 1. SSE padding (8KB per event) now only applied when Cloudflare tunnel is active — direct/Tailscale connections skip the padding entirely. Previously every broadcast event got 8KB of comment padding even on local connections, causing 40-160KB/s of wasted bandwidth during active subagent work. 2. Subagent window content renders (tool_call, progress, message, tool_result) now debounced at 100ms per agent via scheduleSubagentWindowRender(). Previously each SSE event triggered an immediate DOM rewrite, causing 10-30+ rewrites/sec that starved the terminal rendering pipeline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+39
-8
@@ -2150,9 +2150,9 @@ class CodemanApp {
|
||||
this.renderSubagentDetail();
|
||||
}
|
||||
this.renderSubagentPanel();
|
||||
// Update floating window
|
||||
// Update floating window (debounced — tool_call events fire rapidly)
|
||||
if (this.subagentWindows.has(data.agentId)) {
|
||||
this.renderSubagentWindowContent(data.agentId);
|
||||
this.scheduleSubagentWindowRender(data.agentId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2165,9 +2165,9 @@ class CodemanApp {
|
||||
if (this.activeSubagentId === data.agentId) {
|
||||
this.renderSubagentDetail();
|
||||
}
|
||||
// Update floating window
|
||||
// Update floating window (debounced)
|
||||
if (this.subagentWindows.has(data.agentId)) {
|
||||
this.renderSubagentWindowContent(data.agentId);
|
||||
this.scheduleSubagentWindowRender(data.agentId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2180,9 +2180,9 @@ class CodemanApp {
|
||||
if (this.activeSubagentId === data.agentId) {
|
||||
this.renderSubagentDetail();
|
||||
}
|
||||
// Update floating window
|
||||
// Update floating window (debounced)
|
||||
if (this.subagentWindows.has(data.agentId)) {
|
||||
this.renderSubagentWindowContent(data.agentId);
|
||||
this.scheduleSubagentWindowRender(data.agentId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2208,9 +2208,9 @@ class CodemanApp {
|
||||
if (this.activeSubagentId === data.agentId) {
|
||||
this.renderSubagentDetail();
|
||||
}
|
||||
// Update floating window
|
||||
// Update floating window (debounced)
|
||||
if (this.subagentWindows.has(data.agentId)) {
|
||||
this.renderSubagentWindowContent(data.agentId);
|
||||
this.scheduleSubagentWindowRender(data.agentId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6054,6 +6054,24 @@ class CodemanApp {
|
||||
display.textContent = '';
|
||||
display.onclick = null;
|
||||
}
|
||||
// Upload URL row
|
||||
const uploadRow = document.getElementById('tunnelUploadUrlRow');
|
||||
const uploadDisplay = document.getElementById('tunnelUploadUrlDisplay');
|
||||
if (!uploadRow || !uploadDisplay) return;
|
||||
if (url) {
|
||||
const uploadUrl = url + '/upload.html';
|
||||
uploadRow.style.display = '';
|
||||
uploadDisplay.textContent = uploadUrl;
|
||||
uploadDisplay.onclick = () => {
|
||||
navigator.clipboard.writeText(uploadUrl).then(() => {
|
||||
this.showToast('Upload URL copied', 'success');
|
||||
});
|
||||
};
|
||||
} else {
|
||||
uploadRow.style.display = 'none';
|
||||
uploadDisplay.textContent = '';
|
||||
uploadDisplay.onclick = null;
|
||||
}
|
||||
}
|
||||
|
||||
showTunnelQR() {
|
||||
@@ -9213,6 +9231,19 @@ class CodemanApp {
|
||||
}
|
||||
|
||||
|
||||
// Debounced wrapper — coalesces rapid subagent events (tool_call, progress,
|
||||
// message) into a single DOM update per 100ms per agent window.
|
||||
scheduleSubagentWindowRender(agentId) {
|
||||
if (!this._subagentWindowRenderTimeouts) this._subagentWindowRenderTimeouts = new Map();
|
||||
if (this._subagentWindowRenderTimeouts.has(agentId)) {
|
||||
clearTimeout(this._subagentWindowRenderTimeouts.get(agentId));
|
||||
}
|
||||
this._subagentWindowRenderTimeouts.set(agentId, setTimeout(() => {
|
||||
this._subagentWindowRenderTimeouts.delete(agentId);
|
||||
scheduleBackground(() => this.renderSubagentWindowContent(agentId));
|
||||
}, 100));
|
||||
}
|
||||
|
||||
renderSubagentWindowContent(agentId) {
|
||||
// Skip if this window has a live terminal (don't overwrite xterm with activity HTML)
|
||||
if (this.teammateTerminals.has(agentId)) return;
|
||||
|
||||
+15
-10
@@ -583,10 +583,12 @@ export class WebServer extends EventEmitter {
|
||||
this.sendSSE(reply, 'init', this.getLightState());
|
||||
// Flush Cloudflare tunnel buffer with padding — ensures the init event
|
||||
// (and any immediately following events) are delivered without proxy delay.
|
||||
try {
|
||||
reply.raw.write(SSE_PADDING);
|
||||
} catch {
|
||||
/* client gone */
|
||||
if (this.tunnelManager.getUrl()) {
|
||||
try {
|
||||
reply.raw.write(SSE_PADDING);
|
||||
} catch {
|
||||
/* client gone */
|
||||
}
|
||||
}
|
||||
|
||||
req.raw.on('close', () => {
|
||||
@@ -1870,11 +1872,12 @@ export class WebServer extends EventEmitter {
|
||||
this.cachedSessionsList = null;
|
||||
}
|
||||
// Performance optimization: serialize JSON once for all clients.
|
||||
// Append padding to flush Cloudflare tunnel buffers for all non-terminal events.
|
||||
// Terminal data is high-volume and already exceeds buffer thresholds naturally.
|
||||
// Only append Cloudflare tunnel padding when tunnel is actually active —
|
||||
// direct/Tailscale clients don't need 8KB padding on every event.
|
||||
const padding = this.tunnelManager.getUrl() ? SSE_PADDING : '';
|
||||
let message: string;
|
||||
try {
|
||||
message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` + SSE_PADDING;
|
||||
message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` + padding;
|
||||
} catch (err) {
|
||||
// Handle circular references or non-serializable values
|
||||
console.error(`[Server] Failed to serialize SSE event "${event}":`, err);
|
||||
@@ -2144,9 +2147,11 @@ export class WebServer extends EventEmitter {
|
||||
if (!socket || socket.destroyed || !socket.writable) {
|
||||
deadClients.push(client);
|
||||
} else {
|
||||
// Send padded SSE comment as keep-alive — the padding flushes
|
||||
// Cloudflare tunnel buffers so subsequent events arrive promptly.
|
||||
client.raw.write(':keepalive\n' + SSE_PADDING);
|
||||
// Send SSE comment as keep-alive. Only add padding when tunnel is
|
||||
// active — it flushes Cloudflare proxy buffers but wastes bandwidth
|
||||
// for direct/Tailscale connections.
|
||||
const ka = this.tunnelManager.getUrl() ? ':keepalive\n' + SSE_PADDING : ':keepalive\n\n';
|
||||
client.raw.write(ka);
|
||||
}
|
||||
} catch {
|
||||
// Error accessing socket means client is dead
|
||||
|
||||
Reference in New Issue
Block a user