mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 05:29:42 +02:00
fix: disable WebGL renderer to prevent Chrome page unresponsive crashes
Root cause: xterm.js WebGL addon performs synchronous GPU ReadPixels calls during terminal.write(). When heavy terminal output floods in (~1MB/4s from active Claude sessions), single-frame writes of 70-105KB block Chrome's main thread for 10+ seconds, triggering "page unresponsive" dialogs. This happens both during tab switches (buffer load + live SSE data competing) and during normal use (Ink redraw bursts). Fix: disable WebGL by default, use canvas renderer instead. Canvas handles the same workloads without GPU stalls. Re-enable with ?webgl URL param for testing. Also: gate live SSE terminal writes during the entire selectSession() buffer load sequence (not just during chunkedTerminalWrite), preventing live data from competing with historical buffer restoration. Crash investigation data (from server-side breadcrumb collection): - 27 flushes totaling 937KB in 4 seconds preceded crash - Two 105KB and one 104KB single-frame flushes observed - Crash occurred when user switched tabs during output flood - Tab froze for 2m54s before recovering - Backend always stable (0 crashes); pure frontend GPU issue Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+31
-16
@@ -618,15 +618,14 @@ class CodemanApp {
|
||||
const container = document.getElementById('terminalContainer');
|
||||
this.terminal.open(container);
|
||||
|
||||
// Activate WebGL renderer for up to 900% faster rendering (fallback to canvas on failure).
|
||||
// Store reference so we can disable during large buffer loads to prevent GPU stalls.
|
||||
// Disable with ?nowebgl=1 URL param for debugging GPU freeze issues.
|
||||
// WebGL renderer disabled — canvas renderer used instead.
|
||||
// xterm.js WebGL addon causes synchronous GPU ReadPixels calls during large
|
||||
// terminal writes (buffer loads, heavy Ink output) that block Chrome's main
|
||||
// thread for 10+ seconds, triggering "page unresponsive" crashes.
|
||||
// Canvas renderer handles the same workloads without GPU stalls.
|
||||
// Re-enable with ?webgl=1 URL param for testing.
|
||||
this._webglAddon = null;
|
||||
const _noWebGL = new URLSearchParams(location.search).has('nowebgl');
|
||||
if (_noWebGL) {
|
||||
console.warn('[CRASH-DIAG] WebGL renderer DISABLED via ?nowebgl param — using canvas renderer');
|
||||
}
|
||||
if (!_noWebGL && typeof WebglAddon !== 'undefined') {
|
||||
if (new URLSearchParams(location.search).has('webgl') && typeof WebglAddon !== 'undefined') {
|
||||
try {
|
||||
this._webglAddon = new WebglAddon.WebglAddon();
|
||||
this._webglAddon.onContextLoss(() => {
|
||||
@@ -635,6 +634,7 @@ class CodemanApp {
|
||||
this._webglAddon = null;
|
||||
});
|
||||
this.terminal.loadAddon(this._webglAddon);
|
||||
console.log('[CRASH-DIAG] WebGL renderer enabled via ?webgl param');
|
||||
} catch (_e) { /* WebGL2 unavailable — canvas renderer used */ }
|
||||
}
|
||||
|
||||
@@ -1376,11 +1376,13 @@ class CodemanApp {
|
||||
// This implements synchronized output for xterm.js which doesn't support DEC 2026 natively
|
||||
const _joinedLen = this.pendingWrites.reduce((s, w) => s + w.length, 0);
|
||||
if (_joinedLen > 16384) _crashDiag.log(`FLUSH: ${(_joinedLen/1024).toFixed(0)}KB`);
|
||||
const segments = extractSyncSegments(this.pendingWrites.join(''));
|
||||
const joined = this.pendingWrites.join('');
|
||||
this.pendingWrites = [];
|
||||
|
||||
// Write all segments in a single batch (atomic within this frame)
|
||||
// xterm.js internally batches multiple write() calls within same frame
|
||||
const segments = extractSyncSegments(joined);
|
||||
|
||||
// Write all segments in a single batch (atomic within this frame).
|
||||
// xterm.js internally batches multiple write() calls within same frame.
|
||||
// Never discard content from incomplete sync blocks — xterm.js doesn't support
|
||||
// DEC 2026 natively anyway, so strip the marker and write content regardless.
|
||||
// Discarding causes real data loss (including Ink's erase-line escapes).
|
||||
@@ -1393,7 +1395,7 @@ class CodemanApp {
|
||||
}
|
||||
}
|
||||
const _dt = performance.now() - _t0;
|
||||
if (_dt > 100) console.warn(`[CRASH-DIAG] flushPendingWrites took ${_dt.toFixed(0)}ms (${_joinedLen} bytes, ${segments.length} segments)`);
|
||||
if (_dt > 100) console.warn(`[CRASH-DIAG] flushPendingWrites took ${_dt.toFixed(0)}ms (${_joinedLen} bytes)`);
|
||||
|
||||
// Sticky scroll: if user was at bottom, keep them there after new output
|
||||
if (this._wasAtBottomBeforeWrite) {
|
||||
@@ -3578,6 +3580,12 @@ class CodemanApp {
|
||||
// filter catches most cases, but _restoringFlushedState provides a
|
||||
// belt-and-suspenders guard for any edge cases.
|
||||
this._restoringFlushedState = true;
|
||||
// Gate live SSE terminal writes for the ENTIRE buffer load sequence.
|
||||
// Without this, SSE events arriving during the fetch() gap compete with
|
||||
// the buffer write, causing 70KB+ single-frame flushes that stall WebGL.
|
||||
// chunkedTerminalWrite also sets this, but we need it before the fetch too.
|
||||
this._isLoadingBuffer = true;
|
||||
this._loadBufferQueue = [];
|
||||
try {
|
||||
// Instant cache restore — show previous buffer via chunked write to avoid WebGL GPU stalls.
|
||||
// Direct terminal.write() of large cached buffers (256KB+) can block the main thread
|
||||
@@ -3588,7 +3596,7 @@ class CodemanApp {
|
||||
this.terminal.clear();
|
||||
this.terminal.reset();
|
||||
await this.chunkedTerminalWrite(cachedBuffer);
|
||||
if (selectGen !== this._selectGeneration) { this._restoringFlushedState = false; return; }
|
||||
if (selectGen !== this._selectGeneration) { if (this._isLoadingBuffer) this._finishBufferLoad(); this._restoringFlushedState = false; return; }
|
||||
this.terminal.scrollToBottom();
|
||||
_crashDiag.log('CACHE_DONE');
|
||||
}
|
||||
@@ -3596,7 +3604,7 @@ class CodemanApp {
|
||||
_crashDiag.log('FETCH_START');
|
||||
const tailSize = 256 * 1024;
|
||||
const res = await fetch(`/api/sessions/${sessionId}/terminal?tail=${tailSize}`);
|
||||
if (selectGen !== this._selectGeneration) { this._restoringFlushedState = false; return; }
|
||||
if (selectGen !== this._selectGeneration) { if (this._isLoadingBuffer) this._finishBufferLoad(); this._restoringFlushedState = false; return; }
|
||||
const data = await res.json();
|
||||
_crashDiag.log(`FETCH_DONE: ${data.terminalBuffer ? (data.terminalBuffer.length/1024).toFixed(0) + 'KB' : 'empty'} truncated=${data.truncated}`);
|
||||
|
||||
@@ -3615,7 +3623,7 @@ class CodemanApp {
|
||||
}
|
||||
// Use chunked write for large buffers to avoid UI jank
|
||||
await this.chunkedTerminalWrite(data.terminalBuffer);
|
||||
if (selectGen !== this._selectGeneration) { this._restoringFlushedState = false; return; }
|
||||
if (selectGen !== this._selectGeneration) { if (this._isLoadingBuffer) this._finishBufferLoad(); this._restoringFlushedState = false; return; }
|
||||
// Ensure terminal is scrolled to bottom after buffer load
|
||||
this.terminal.scrollToBottom();
|
||||
}
|
||||
@@ -3633,7 +3641,13 @@ class CodemanApp {
|
||||
this.terminal.reset();
|
||||
}
|
||||
|
||||
// Buffer load complete — drop the guard so user input clears state normally
|
||||
// Buffer load complete — unblock live SSE writes and flush any queued events.
|
||||
// chunkedTerminalWrite calls _finishBufferLoad internally, but if we skipped
|
||||
// the chunked write (small buffer, cache hit, or empty), we must call it here.
|
||||
if (this._isLoadingBuffer) {
|
||||
this._finishBufferLoad();
|
||||
}
|
||||
// Drop the guard so user input clears state normally
|
||||
this._restoringFlushedState = false;
|
||||
|
||||
// Restore flushed offset and text for this session so the overlay positions
|
||||
@@ -3739,6 +3753,7 @@ class CodemanApp {
|
||||
_crashDiag.log(`SELECT_DONE: ${(performance.now() - _selStart).toFixed(0)}ms`);
|
||||
console.log(`[CRASH-DIAG] selectSession DONE: ${sessionId.slice(0,8)} in ${(performance.now() - _selStart).toFixed(0)}ms`);
|
||||
} catch (err) {
|
||||
if (this._isLoadingBuffer) this._finishBufferLoad();
|
||||
this._restoringFlushedState = false;
|
||||
console.error('Failed to load session terminal:', err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user