Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion src/web/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -617,14 +617,57 @@ class CodemanApp {
this._webglAddon = new WebglAddon.WebglAddon();
this._webglAddon.onContextLoss(() => {
console.error('[CRASH-DIAG] WebGL context LOST — falling back to canvas renderer');
this._webglAddon.dispose();
_crashDiag.log('WEBGL_LOST');
this._disableWebGLSticky('context-lost');
this._webglAddon?.dispose();
this._webglAddon = null;
});
this.terminal.loadAddon(this._webglAddon);
console.log('[CRASH-DIAG] WebGL renderer enabled');
this._installWebGLLongTaskGuard();
} catch (_e) { /* WebGL2 unavailable — canvas renderer used */ }
}

/**
* Watch for sustained main-thread stalls that indicate WebGL/GPU trouble.
* After 3 long tasks (>=200ms each) within 30s, dispose the WebGL addon and
* persist a sticky disable so subsequent reloads also use the DOM renderer.
* 5s grace period skips initial-load stalls. Force-re-enable: ?webgl=force.
*/
_installWebGLLongTaskGuard() {
if (typeof PerformanceObserver === 'undefined' || this._webglLongTaskObserver) return;
const installedAt = performance.now();
const recent = [];
try {
this._webglLongTaskObserver = new PerformanceObserver((list) => {
if (!this._webglAddon) return;
const now = performance.now();
if (now - installedAt < 5000) return;
for (const entry of list.getEntries()) {
if (entry.duration >= 200) recent.push(entry.startTime);
}
while (recent.length && now - recent[0] > 30000) recent.shift();
if (recent.length >= 3) {
console.warn(`[CRASH-DIAG] WebGL long-task threshold (${recent.length} stalls/30s) — falling back to canvas renderer`);
_crashDiag.log(`WEBGL_FALLBACK: ${recent.length}`);
this._disableWebGLSticky('long-tasks');
this._webglAddon?.dispose();
this._webglAddon = null;
try { this._webglLongTaskObserver.disconnect(); } catch {}
this._webglLongTaskObserver = null;
try { this.terminal.refresh(0, this.terminal.rows - 1); } catch {}
}
});
this._webglLongTaskObserver.observe({ type: 'longtask', buffered: false });
} catch { /* longtask not supported */ }
}

_disableWebGLSticky(reason) {
try {
localStorage.setItem('codeman-webgl-disabled', JSON.stringify({ reason, at: Date.now() }));
} catch {}
}

// ═══════════════════════════════════════════════════════════════
// Event Listeners (Keyboard Shortcuts, Resize, Beforeunload)
// ═══════════════════════════════════════════════════════════════
Expand Down
30 changes: 28 additions & 2 deletions src/web/public/terminal-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,36 @@ Object.assign(CodemanApp.prototype, {
// but the 48KB/frame flush cap in flushPendingWrites() now prevents
// oversized terminal.write() calls that triggered the stalls.
// Disable with ?nowebgl URL param if GPU issues return.
// Auto-fallback: _initWebGL installs a long-task watchdog that disables
// WebGL sticky in localStorage after repeated GPU stalls (see app.js).
// Force re-enable after sticky disable with ?webgl=force.
// Lazy-loaded: script downloaded only on desktop (saves 244KB on mobile).
this._webglAddon = null;
const skipWebGL = MobileDetection.getDeviceType() !== 'desktop';
if (!skipWebGL && !new URLSearchParams(location.search).has('nowebgl')) {
const _params = new URLSearchParams(location.search);
if (_params.get('webgl') === 'force') {
try { localStorage.removeItem('codeman-webgl-disabled'); } catch {}
}
const _stickyDisabled = (() => {
try {
const raw = localStorage.getItem('codeman-webgl-disabled');
if (!raw) return false;
const { at } = JSON.parse(raw);
// Auto-expire after 7 days so we retry (driver may have been fixed)
if (Date.now() - at > 7 * 24 * 60 * 60 * 1000) {
localStorage.removeItem('codeman-webgl-disabled');
return false;
}
return true;
} catch { return false; }
})();
const skipWebGL =
MobileDetection.getDeviceType() !== 'desktop' ||
_params.has('nowebgl') ||
_stickyDisabled;
if (_stickyDisabled) {
console.log('[CRASH-DIAG] WebGL sticky-disabled from prior stalls — DOM renderer in use. Re-enable: ?webgl=force');
}
if (!skipWebGL) {
if (typeof WebglAddon !== 'undefined') {
this._initWebGL();
} else {
Expand Down