diff --git a/packages/cli/src/commands/layout-audit.browser.js b/packages/cli/src/commands/layout-audit.browser.js index 14ee9ed943..3d83317c2d 100644 --- a/packages/cli/src/commands/layout-audit.browser.js +++ b/packages/cli/src/commands/layout-audit.browser.js @@ -2209,9 +2209,11 @@ // Canvases that drew WebGL while hidden since the last audit, judged when the draw happens; the author's own // hf-seek loop decides what draws. const webglDraws = (window.__hyperframesWebglDraws ??= new Set()); + let judgedThisSample = new WeakSet(); const drawGate = Symbol.for("hyperframes.webglDrawGate"); window.__hyperframesRecordWebglDraw = (canvas) => { - if (!canvas || webglDraws.has(canvas)) return; + if (typeof canvas?.closest !== "function" || judgedThisSample.has(canvas)) return; + judgedThisSample.add(canvas); if (canvas.closest(RUNTIME_HIDDEN_CLIP) || canvas.checkVisibility?.() === false) { webglDraws.add(canvas); } @@ -2230,7 +2232,9 @@ const draw = proto[method]; if (typeof draw !== "function") continue; proto[method] = function (...args) { - if (!window[drawGate]?.skip) window.__hyperframesRecordWebglDraw(this.canvas); + try { + if (!window[drawGate]?.skip) window.__hyperframesRecordWebglDraw(this.canvas); + } catch {} return draw.apply(this, args); }; } @@ -2239,6 +2243,7 @@ function inactiveWebglIssues(root, time) { const drawn = [...webglDraws].filter((canvas) => canvas?.isConnected); webglDraws.clear(); + judgedThisSample = new WeakSet(); return drawn.map((canvas) => { const scene = canvas.closest("[data-composition-id]:not([data-width])") ?? diff --git a/packages/cli/src/commands/layout-audit.browser.test.ts b/packages/cli/src/commands/layout-audit.browser.test.ts index 14d282a247..42d0913ee3 100644 --- a/packages/cli/src/commands/layout-audit.browser.test.ts +++ b/packages/cli/src/commands/layout-audit.browser.test.ts @@ -1352,6 +1352,45 @@ describe("layout-audit.browser coordinate-frame findings", () => { expect(runAudit().filter((i) => i.code === "webgl_draws_while_inactive")).toHaveLength(1); expect(runAudit().some((issue) => issue.code === "webgl_draws_while_inactive")).toBe(false); }); + + function installFakeWebgl(): { drawn: unknown[]; context: { canvas: unknown } } { + const drawn: unknown[] = []; + class FakeContext { + canvas: unknown; + drawArrays(...args: unknown[]) { + drawn.push(args); + } + } + (window as unknown as Record).WebGLRenderingContext = FakeContext; + return { drawn, context: new FakeContext() }; + } + + it("lets an OffscreenCanvas context draw, and reports nothing for it", () => { + const { drawn, context } = installFakeWebgl(); + mountScenes(true); + context.canvas = { width: 64, height: 64 }; + + (context as unknown as { drawArrays(mode: number): void }).drawArrays(4); + + expect(drawn).toEqual([[4]]); + expect(runAudit().some((issue) => issue.code === "webgl_draws_while_inactive")).toBe(false); + }); + + it("checks a canvas's visibility once per sample, however many draws it makes", () => { + const { context } = installFakeWebgl(); + const canvas = mountScenes(false); + const closest = vi.spyOn(canvas, "closest"); + context.canvas = canvas; + + for (let i = 0; i < 50; i++) + (context as unknown as { drawArrays(mode: number): void }).drawArrays(4); + expect(closest).toHaveBeenCalledTimes(1); + + runAudit(); + closest.mockClear(); + (context as unknown as { drawArrays(mode: number): void }).drawArrays(4); + expect(closest).toHaveBeenCalledTimes(1); + }); }); describe("canvas_content_at_edge", () => {