diff --git a/packages/engine/src/services/browserManager.test.ts b/packages/engine/src/services/browserManager.test.ts index 867169d4b1..52caae0576 100644 --- a/packages/engine/src/services/browserManager.test.ts +++ b/packages/engine/src/services/browserManager.test.ts @@ -714,6 +714,21 @@ describe("resolveBrowserGpuMode", () => { expect(close).toHaveBeenCalledOnce(); }); + it("does not keep a probe its own caller cancelled as the answer for the process", async () => { + setMockWebGlProbe({ hasWebGL: true, vendor: "NVIDIA", renderer: "NVIDIA" }); + const cancelled = { + launch: async () => { + throw new DOMException("This operation was aborted", "AbortError"); + }, + close: vi.fn(), + }; + const first = resolveBrowserGpuMode("auto", { probeLifecycle: cancelled }); + const second = resolveBrowserGpuMode("auto"); + await expect(second).resolves.toBe("hardware"); + await first; + await expect(resolveBrowserGpuMode("auto")).resolves.toBe("hardware"); + }); + it("passes 'software' through unchanged without probing", async () => { const mode = await resolveBrowserGpuMode("software"); expect(mode).toBe("software"); diff --git a/packages/engine/src/services/browserManager.ts b/packages/engine/src/services/browserManager.ts index f1da7d9a3c..3bd9bfe242 100644 --- a/packages/engine/src/services/browserManager.ts +++ b/packages/engine/src/services/browserManager.ts @@ -551,6 +551,7 @@ export const _closeBrowserAfterFailedProbeForTests = closeBrowserAfterFailedProb interface GpuProbeOutcome { mode: "software" | "hardware"; cause?: "no-gpu" | "probe-error"; + aborted?: true; /** Under WSL, what we checked on its GPU path (see describeWslGpuAccess). */ wslAccess?: string[]; /** Under WSL, Chrome reached the GPU only without WSLg's WAYLAND_DISPLAY. */ @@ -649,7 +650,8 @@ async function probeWebGlMode( return resolved === "hardware" ? { mode: "hardware" } : { mode: "software", cause: "no-gpu" }; } catch (err) { logResolvedBrowserGpuMode("software", label + formatProbeFailure(err)); - return { mode: "software", cause: "probe-error" }; + const aborted = err instanceof Error && err.name === "AbortError"; + return { mode: "software", cause: "probe-error", ...(aborted && { aborted }) }; } } @@ -717,15 +719,25 @@ function wslAccess(options: GpuProbeOptions): Pick * safe failure mode; misclassifying toward hardware would error on the real * render. */ +async function sharedGpuProbe(options: GpuProbeOptions): Promise { + const shared = (_autoBrowserGpuModeCache ??= probeAutoBrowserGpuMode(options)); + const outcome = await shared; + if (!outcome.aborted) return outcome; + if (_autoBrowserGpuModeCache === shared) _autoBrowserGpuModeCache = undefined; + const own = await probeAutoBrowserGpuMode(options); + if (!own.aborted) _autoBrowserGpuModeCache ??= Promise.resolve(own); + return own; +} + export function resolveBrowserGpuMode( mode: EngineConfig["browserGpuMode"], options: GpuProbeOptions = {}, ): Promise<"software" | "hardware"> { if (mode === "software") return Promise.resolve(mode); - _autoBrowserGpuModeCache ??= probeAutoBrowserGpuMode(options); + const probe = sharedGpuProbe(options); if (mode === "auto") { - return _autoBrowserGpuModeCache.then((probed) => { + return probe.then((probed) => { if (probed.mode === "software" && probed.wslAccess && !_wslSoftwareNoteLogged) { _wslSoftwareNoteLogged = true; console.error(buildWslSoftwareNote(probed)); @@ -734,7 +746,7 @@ export function resolveBrowserGpuMode( }); } - return _autoBrowserGpuModeCache.then((probed) => { + return probe.then((probed) => { // Warn once per cache lifetime, not once per caller: `createCaptureSession` // resolves the mode for the probe browser AND every parallel worker, so // an un-deduplicated warning prints N+1 times and buries itself.