@@ -293,6 +293,14 @@ export interface RenderPerfSummary {
293293 videoExtractBreakdown ?: ExtractionPhaseBreakdown ;
294294 /** Bytes on disk in the render's workDir at assembly time (sampled before cleanup). Lets callers correlate peak temp usage with render duration. */
295295 tmpPeakBytes ?: number ;
296+ /**
297+ * Average wall-clock capture time per output frame.
298+ *
299+ * Uses `stages.captureFrameMs` when present so fixed Stage 4 setup costs
300+ * (file server creation, calibration, readiness/session init, strategy
301+ * resolution) do not get amortized into a per-frame metric. Older summaries
302+ * without the split fall back to `stages.captureMs`.
303+ */
296304 captureAvgMs ?: number ;
297305 capturePeakMs ?: number ;
298306 captureCalibration ?: {
@@ -1488,6 +1496,8 @@ export async function executeRenderJob(
14881496 lastBrowserConsole = hdrRes . lastBrowserConsole ;
14891497 hdrPerf = hdrRes . hdrPerf ;
14901498 perfStages . captureMs = hdrRes . captureDurationMs ;
1499+ perfStages . captureFrameMs = hdrRes . captureDurationMs ;
1500+ perfStages . captureSetupMs = Math . max ( 0 , Date . now ( ) - stage4Start - hdrRes . captureDurationMs ) ;
14911501 perfStages . encodeMs = hdrRes . encodeMs ;
14921502 } else {
14931503 // ── Standard capture paths (SDR or DOM-only HDR) ──────────────────
@@ -1497,6 +1507,7 @@ export async function executeRenderJob(
14971507 // and we fall back to the disk path below.
14981508 let streamingHandled = false ;
14991509 if ( useStreamingEncode ) {
1510+ const captureFrameStart = Date . now ( ) ;
15001511 const streamingRes = await observeRenderStage (
15011512 observability ,
15021513 "capture_streaming" ,
@@ -1537,13 +1548,16 @@ export async function executeRenderJob(
15371548 dedupPerfs,
15381549 } ) ,
15391550 ) ;
1551+ const captureFrameMs = Date . now ( ) - captureFrameStart ;
15401552 if ( streamingRes . success ) {
15411553 streamingHandled = true ;
15421554 workerCount = streamingRes . workerCount ;
15431555 updateCaptureObservability ( { workerCount } ) ;
15441556 probeSession = streamingRes . probeSession ;
15451557 lastBrowserConsole = streamingRes . lastBrowserConsole ;
15461558 perfStages . captureMs = Date . now ( ) - stage4Start ;
1559+ perfStages . captureFrameMs = captureFrameMs ;
1560+ perfStages . captureSetupMs = Math . max ( 0 , perfStages . captureMs - captureFrameMs ) ;
15471561 perfStages . encodeMs = streamingRes . encodeMs ; // Overlapped with capture
15481562 } else {
15491563 useStreamingEncode = false ;
@@ -1554,6 +1568,7 @@ export async function executeRenderJob(
15541568
15551569 if ( ! streamingHandled ) {
15561570 // ── Disk-based capture (original flow) ────────────────────────────
1571+ const captureFrameStart = Date . now ( ) ;
15571572 const captureRes = await observeRenderStage (
15581573 observability ,
15591574 "capture_disk" ,
@@ -1580,12 +1595,15 @@ export async function executeRenderJob(
15801595 onProgress,
15811596 } ) ,
15821597 ) ;
1598+ const captureFrameMs = Date . now ( ) - captureFrameStart ;
15831599 workerCount = captureRes . workerCount ;
15841600 updateCaptureObservability ( { workerCount } ) ;
15851601 probeSession = captureRes . probeSession ;
15861602 lastBrowserConsole = captureRes . lastBrowserConsole ;
15871603
15881604 perfStages . captureMs = Date . now ( ) - stage4Start ;
1605+ perfStages . captureFrameMs = captureFrameMs ;
1606+ perfStages . captureSetupMs = Math . max ( 0 , perfStages . captureMs - captureFrameMs ) ;
15891607
15901608 const encodeRes = await observeRenderStage (
15911609 observability ,
0 commit comments