diff --git a/src/web/public/app.js b/src/web/public/app.js index d26e68ae..b56976fd 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -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) // ═══════════════════════════════════════════════════════════════ diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index f804098e..0549749d 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -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 {