From 8aac6656c47d655dfad7d14f2998e7ba6d7ce232 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 10 Aug 2026 08:37:42 -0700 Subject: [PATCH] [eric] crash: app-card teardown waits for the about:blank commit event plus a settle frame instead of a 250ms fail-open timer, so a loaded machine stops destroying live GPU surfaces mid-composite --- frontend/src/shared/viewTeardown.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/frontend/src/shared/viewTeardown.ts b/frontend/src/shared/viewTeardown.ts index 7c282033..c43af725 100644 --- a/frontend/src/shared/viewTeardown.ts +++ b/frontend/src/shared/viewTeardown.ts @@ -2,18 +2,29 @@ import type { Dispatch } from '@reduxjs/toolkit'; import { removeViewCard } from '@/shared/state/dashboardLayoutSlice'; import { getViewWebview } from '@/shared/viewWebviewRegistry'; -// A wedged app must never hold a card open; cap the whole quiesce so delete stays responsive. Common case (about:blank is a trivial nav) resolves in well under this. -const QUIESCE_BUDGET_MS = 250; +// A wedged app must never hold a card open; cap the whole quiesce so delete stays responsive. The +// old 250ms timer was fail-OPEN: on a loaded machine the about:blank commit takes longer, the timer +// won, and the destroy ripped a LIVE GPU surface mid-composite, which is the whole-app crash family +// (ENG-228, Haik's close-crashes). Now we wait for the commit EVENT with a generous ceiling; the +// UI already hid the card, so the extra wait costs nothing visible. +const QUIESCE_BUDGET_MS = 1500; -// Navigate a doomed card's webview to about:blank so the running app's heavy GPU surfaces are released BEFORE React destroys the , leaving only a trivial surface to tear down. Bounded + fail-open. +// Navigate a doomed card's webview to about:blank so the running app's heavy GPU surfaces are released BEFORE React destroys the , leaving only a trivial surface to tear down. export async function quiesceViewWebview(outputId: string): Promise { const wv = getViewWebview(outputId); if (!wv) return; try { + const committed = new Promise((resolve) => { + const done = (): void => { wv.removeEventListener('did-navigate', done); resolve(); }; + wv.addEventListener('did-navigate', done); + }); + void wv.loadURL('about:blank').catch(() => {}); await Promise.race([ - wv.loadURL('about:blank').catch(() => {}), + committed, new Promise((resolve) => setTimeout(resolve, QUIESCE_BUDGET_MS)), ]); + // One settle frame after commit so the compositor lets go of the old surface before React unmounts the element. + await new Promise((resolve) => requestAnimationFrame(() => resolve())); } catch { // webview already torn down; nothing to quiesce }