[eric] crash-safety: cap parallel CDP detach at 10 concurrent (unbounded could flood the debugger host at pathological card counts and leave one attached at unmount)

This commit is contained in:
ciregenz
2026-07-29 01:05:33 -07:00
parent 8fedff40c1
commit cc24f5adaf
2 changed files with 15 additions and 8 deletions
+13 -4
View File
@@ -39,14 +39,23 @@ export async function removeBrowserCardCleanly(
dispatch(removeBrowserCard(browserId));
}
// Batch remove: detach every browser's CDP in PARALLEL (order-independent, the only invariant is
// all debuggers gone before any unmount), THEN remove them. Serial detach cost ~72ms/browser, so a
// 20-card multi-select delete lagged over a second; this keeps the crash-safe ordering but flat.
// Detach many browsers' CDP concurrently but CAPPED (order-independent, the only invariant is all
// debuggers gone before any unmount). Serial cost ~72ms/browser (a 20-card teardown lagged >1s);
// unbounded parallel risks flooding the debugger host at pathological counts and leaving one
// attached at unmount. Batches of DETACH_CONCURRENCY are the middle: flat-ish latency, no flood.
const DETACH_CONCURRENCY = 10;
export async function detachBrowsersCdpBounded(browserIds: string[]): Promise<void> {
for (let i = 0; i < browserIds.length; i += DETACH_CONCURRENCY) {
await Promise.allSettled(browserIds.slice(i, i + DETACH_CONCURRENCY).map((id) => detachBrowserCdp(id)));
}
}
// Batch remove: bounded-parallel detach, THEN remove. Keeps the crash-safe ordering but flat.
export async function removeBrowserCardsCleanly(
browserIds: string[],
dispatch: Dispatch,
): Promise<void> {
await Promise.allSettled(browserIds.map((id) => detachBrowserCdp(id)));
await detachBrowsersCdpBounded(browserIds);
for (const id of browserIds) {
forgetBrowser(id);
dispatch(removeBrowserCard(id));
@@ -1,7 +1,7 @@
import { getAllViewOutputIds } from '@/shared/viewWebviewRegistry';
import { getAllBrowserIds } from '@/shared/browserRegistry';
import { quiesceViewWebview } from '@/shared/viewTeardown';
import { detachBrowserCdp } from '@/shared/browserTeardown';
import { detachBrowsersCdpBounded } from '@/shared/browserTeardown';
// Switching dashboards clears every card from the store in ONE reducer (resetLayout), so React
// unmounts all the outgoing app + browser <webview>s in a single frame. Ripping several live GPU
@@ -20,7 +20,5 @@ export async function prepareDashboardSwitch(keepBrowserIds: string[]): Promise<
for (const outputId of getAllViewOutputIds()) {
await quiesceViewWebview(outputId);
}
await Promise.allSettled(
getAllBrowserIds().filter((b) => !keep.has(b)).map((b) => detachBrowserCdp(b)),
);
await detachBrowsersCdpBounded(getAllBrowserIds().filter((b) => !keep.has(b)));
}