diff --git a/packages/engine/src/services/frameCapture-discardWarmup.test.ts b/packages/engine/src/services/frameCapture-discardWarmup.test.ts index 677da7d91f..3f6f48e8f5 100644 --- a/packages/engine/src/services/frameCapture-discardWarmup.test.ts +++ b/packages/engine/src/services/frameCapture-discardWarmup.test.ts @@ -210,3 +210,15 @@ describe("discardWarmupCapture", () => { } }); }); + +describe("getCapturePerfSummary", () => { + it("reports the beyond-viewport value the session captured with", () => { + const session = makeFakeSession(); + try { + session.options.captureBeyondViewport = false; + expect(getCapturePerfSummary(session).captureBeyondViewport).toBe(false); + } finally { + rmSync(session.outputDir, { recursive: true, force: true }); + } + }); +}); diff --git a/packages/engine/src/services/frameCapture.ts b/packages/engine/src/services/frameCapture.ts index 693565d056..6ed92abecf 100644 --- a/packages/engine/src/services/frameCapture.ts +++ b/packages/engine/src/services/frameCapture.ts @@ -5557,6 +5557,7 @@ export function getCapturePerfSummary(session: CaptureSession): CapturePerfSumma avgSeekMs: Math.round(session.capturePerf.seekMs / frames), avgBeforeCaptureMs: Math.round(session.capturePerf.beforeCaptureMs / frames), avgScreenshotMs: Math.round(session.capturePerf.screenshotMs / frames), + captureBeyondViewport: session.options.captureBeyondViewport, p50TotalMs: medianOf(session.capturePerf.frameMs), p95TotalMs: percentileOf(session.capturePerf.frameMs, 0.95), p99TotalMs: percentileOf(session.capturePerf.frameMs, 0.99), diff --git a/packages/engine/src/types.ts b/packages/engine/src/types.ts index 606f2bbec9..44ab61ac91 100644 --- a/packages/engine/src/types.ts +++ b/packages/engine/src/types.ts @@ -272,6 +272,8 @@ export interface CapturePerfSummary { avgSeekMs: number; avgBeforeCaptureMs: number; avgScreenshotMs: number; + /** Whether the session captured beyond the viewport, after init turned the request off for a page that fits. */ + captureBeyondViewport?: boolean; /** * Median per-frame capture time — warmup-robust, unlike `avgTotalMs` * (first frames pay font/image decode + GC that swamps short renders' diff --git a/packages/producer/src/services/render/perfSummary.test.ts b/packages/producer/src/services/render/perfSummary.test.ts index 28e6c8bb94..cb893bfecc 100644 --- a/packages/producer/src/services/render/perfSummary.test.ts +++ b/packages/producer/src/services/render/perfSummary.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; -import { buildRenderPerfSummary, roundDb } from "./perfSummary.js"; +import { buildRenderPerfSummary, capturedBeyondViewport, roundDb } from "./perfSummary.js"; +import type { CapturePerfSummary } from "@hyperframes/engine"; import type { RenderJob } from "../renderOrchestrator.js"; describe("roundDb", () => { @@ -75,3 +76,21 @@ describe("buildRenderPerfSummary drawElement gate reason", () => { expect(summary.drawElement?.gateReason).toBe("css_effect:filter"); }); }); + +describe("capturedBeyondViewport", () => { + const perf = (captureBeyondViewport?: boolean) => + ({ captureBeyondViewport }) as CapturePerfSummary; + + it("reports off when every worker's session turned the requested capture off", () => { + expect(capturedBeyondViewport([perf(false), perf(false)], true)).toBe(false); + }); + + it("reports on when any worker kept it", () => { + expect(capturedBeyondViewport([perf(false), perf(true)], false)).toBe(true); + }); + + it("keeps the stage's value when no worker reported one", () => { + expect(capturedBeyondViewport([perf()], true)).toBe(true); + expect(capturedBeyondViewport([], undefined)).toBeUndefined(); + }); +}); diff --git a/packages/producer/src/services/render/perfSummary.ts b/packages/producer/src/services/render/perfSummary.ts index 8d0ef0f001..4526b7e238 100644 --- a/packages/producer/src/services/render/perfSummary.ts +++ b/packages/producer/src/services/render/perfSummary.ts @@ -54,6 +54,15 @@ export function pushWorkerDedupPerfs( } } +/** Beyond-viewport capture as the workers' sessions used it: on if any kept it, `fallback` when none reported. */ +export function capturedBeyondViewport( + perfs: ReadonlyArray, + fallback: boolean | undefined, +): boolean | undefined { + const used = perfs.flatMap((perf) => perf.captureBeyondViewport ?? []); + return used.length > 0 ? used.includes(true) : fallback; +} + /** Adds each worker's page-error console lines to `sink`, once per distinct line. */ export function pushWorkerPageErrors( results: ReadonlyArray<{ pageErrorLines?: string[] }>, diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts index 9d4abc0ba1..aef294d5b8 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -177,6 +177,7 @@ import { formatCaptureFrameName } from "../utils/paths.js"; import { findRenderHdrAutoPromotionTrigger, resolveEffectiveHdrMode } from "./render/hdrMode.js"; import { buildRenderPerfSummary, + capturedBeyondViewport, pushWorkerDedupPerfs, pushWorkerPageErrors, roundDb, @@ -5150,10 +5151,12 @@ async function executeRenderPipeline(input: { deDrainStats = streamingRes.deDrainStats; workerCount = streamingRes.workerCount; updateCaptureObservability({ workerCount }); - if (streamingRes.captureBeyondViewport !== undefined) { - updateCaptureObservability({ - captureBeyondViewport: streamingRes.captureBeyondViewport, - }); + const usedBeyondViewport = capturedBeyondViewport( + dedupPerfs, + streamingRes.captureBeyondViewport, + ); + if (usedBeyondViewport !== undefined) { + updateCaptureObservability({ captureBeyondViewport: usedBeyondViewport }); } probeSession = streamingRes.probeSession; lastBrowserConsole = streamingRes.lastBrowserConsole; @@ -5337,10 +5340,12 @@ async function executeRenderPipeline(input: { const captureFrameMs = Date.now() - captureFrameStart; workerCount = captureRes.workerCount; updateCaptureObservability({ workerCount }); - if (captureRes.captureBeyondViewport !== undefined) { - updateCaptureObservability({ - captureBeyondViewport: captureRes.captureBeyondViewport, - }); + const usedBeyondViewport = capturedBeyondViewport( + dedupPerfs, + captureRes.captureBeyondViewport, + ); + if (usedBeyondViewport !== undefined) { + updateCaptureObservability({ captureBeyondViewport: usedBeyondViewport }); } probeSession = captureRes.probeSession; lastBrowserConsole = captureRes.lastBrowserConsole;