fix: prevent page unresponsive crashes from WebGL GPU stalls during session switch

Disable WebGL renderer during large buffer loads (>32KB) and fall back to
canvas, which handles bulk ANSI writes without synchronous GPU ReadPixels
calls. Re-enable WebGL after the buffer load completes so live terminal
streaming still benefits from GPU acceleration.

Also reduce chunk size from 128KB to 32KB and use chunked writes for
cached buffer restores instead of synchronous terminal.write().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
arkon
2026-03-04 13:18:02 +01:00
co-authored by Claude Opus 4.6
parent fc92d8a8a4
commit b1df5319d5
+27 -9
View File
@@ -542,12 +542,14 @@ class CodemanApp {
const container = document.getElementById('terminalContainer'); const container = document.getElementById('terminalContainer');
this.terminal.open(container); this.terminal.open(container);
// Activate WebGL renderer for up to 900% faster rendering (fallback to canvas on failure) // 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.
this._webglAddon = null;
if (typeof WebglAddon !== 'undefined') { if (typeof WebglAddon !== 'undefined') {
try { try {
const webglAddon = new WebglAddon.WebglAddon(); this._webglAddon = new WebglAddon.WebglAddon();
webglAddon.onContextLoss(() => { webglAddon.dispose(); }); this._webglAddon.onContextLoss(() => { this._webglAddon.dispose(); this._webglAddon = null; });
this.terminal.loadAddon(webglAddon); this.terminal.loadAddon(this._webglAddon);
} catch (_e) { /* WebGL2 unavailable — canvas renderer used */ } } catch (_e) { /* WebGL2 unavailable — canvas renderer used */ }
} }
@@ -1344,13 +1346,30 @@ class CodemanApp {
// (from historical SSE data that was stored with markers) // (from historical SSE data that was stored with markers)
const cleanBuffer = buffer.replace(DEC_SYNC_STRIP_RE, ''); const cleanBuffer = buffer.replace(DEC_SYNC_STRIP_RE, '');
// Disable WebGL during large buffer loads to prevent GPU stalls.
// The canvas renderer handles bulk writes without blocking the main thread.
// WebGL's synchronous ReadPixels calls cause "page unresponsive" on dense ANSI buffers.
const isLargeBuffer = cleanBuffer.length > chunkSize;
if (isLargeBuffer && this._webglAddon) {
try { this._webglAddon.dispose(); } catch (_e) { /* already disposed */ }
this._webglAddon = null;
}
const finish = () => { const finish = () => {
// Re-enable WebGL after buffer load completes
if (isLargeBuffer && !this._webglAddon && typeof WebglAddon !== 'undefined') {
try {
this._webglAddon = new WebglAddon.WebglAddon();
this._webglAddon.onContextLoss(() => { this._webglAddon.dispose(); this._webglAddon = null; });
this.terminal.loadAddon(this._webglAddon);
} catch (_e) { /* WebGL re-init failed — stay on canvas */ }
}
this._finishBufferLoad(); this._finishBufferLoad();
resolve(); resolve();
}; };
// For small buffers, write directly // For small buffers, write directly (WebGL stays active — small writes are fine)
if (cleanBuffer.length <= chunkSize) { if (!isLargeBuffer) {
this.terminal.write(cleanBuffer); this.terminal.write(cleanBuffer);
finish(); finish();
return; return;
@@ -1368,9 +1387,8 @@ class CodemanApp {
this.terminal.write(chunk); this.terminal.write(chunk);
offset += chunkSize; offset += chunkSize;
// Double-RAF: yields to both the browser's layout/paint AND the WebGL renderer's // Schedule next chunk on next frame
// GPU flush. Single RAF can still block if ReadPixels is synchronous. requestAnimationFrame(writeChunk);
requestAnimationFrame(() => requestAnimationFrame(writeChunk));
}; };
// Start writing // Start writing