From ae2c4b3942fda0d200d9169bda01e002a96bef2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 27 Oct 2026 10:08:51 +0000 Subject: [PATCH] fix(engine): a GPU probe cancelled by its caller no longer sets software mode for the process (#6541) The GPU probe's answer is kept for the whole process and the first caller's probe is the one every later caller waits on. When that first caller cancelled it (a prelaunched browser dropped, a stopping check), the probe died with "This operation was aborted", and that cancellation was kept as the answer: every browser the process launched after it ran in software mode, with a log line blaming the GPU probe. A cancelled probe is now forgotten and the next caller probes again. --- .../src/services/browserManager.test.ts | 15 ++++++++++++++ .../engine/src/services/browserManager.ts | 20 +++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) 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.