Skip to content

Commit 4976b5e

Browse files
fix(producer): split capture average timing (#1657)
1 parent e9957ec commit 4976b5e

8 files changed

Lines changed: 101 additions & 4 deletions

File tree

packages/cli/src/commands/render.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,6 +1459,8 @@ function trackRenderMetrics(
14591459
stageVideoExtractMs: stages.videoExtractMs,
14601460
stageAudioProcessMs: stages.audioProcessMs,
14611461
stageCaptureMs: stages.captureMs,
1462+
stageCaptureSetupMs: stages.captureSetupMs,
1463+
stageCaptureFrameMs: stages.captureFrameMs,
14621464
stageEncodeMs: stages.encodeMs,
14631465
stageAssembleMs: stages.assembleMs,
14641466
extractResolveMs: extract?.resolveMs,

packages/cli/src/server/studioRenderTelemetry.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ const fullPerf: RenderPerfSummary = {
9393
videoExtractMs: 200,
9494
audioProcessMs: 50,
9595
captureMs: 4000,
96+
captureSetupMs: 750,
97+
captureFrameMs: 3250,
9698
encodeMs: 500,
9799
assembleMs: 150,
98100
},
@@ -161,6 +163,8 @@ describe("studioRenderTelemetry", () => {
161163
expect(p.stageVideoExtractMs).toBe(200);
162164
expect(p.stageAudioProcessMs).toBe(50);
163165
expect(p.stageCaptureMs).toBe(4000);
166+
expect(p.stageCaptureSetupMs).toBe(750);
167+
expect(p.stageCaptureFrameMs).toBe(3250);
164168
expect(p.stageEncodeMs).toBe(500);
165169
expect(p.stageAssembleMs).toBe(150);
166170
// video-extract breakdown

packages/cli/src/server/studioRenderTelemetry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ function stagesPayload(stages: Record<string, number>): Partial<RenderCompletePr
4141
stageVideoExtractMs: stages.videoExtractMs,
4242
stageAudioProcessMs: stages.audioProcessMs,
4343
stageCaptureMs: stages.captureMs,
44+
stageCaptureSetupMs: stages.captureSetupMs,
45+
stageCaptureFrameMs: stages.captureFrameMs,
4446
stageEncodeMs: stages.encodeMs,
4547
stageAssembleMs: stages.assembleMs,
4648
};

packages/cli/src/telemetry/events.test.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ vi.mock("./client.js", () => ({
55
trackEvent: (...args: unknown[]) => trackEvent(...args),
66
}));
77

8-
const { trackRenderError, trackRenderObservation, trackCommandFailure, trackCliError } =
9-
await import("./events.js");
8+
const {
9+
trackRenderComplete,
10+
trackRenderError,
11+
trackRenderObservation,
12+
trackCommandFailure,
13+
trackCliError,
14+
} = await import("./events.js");
1015

1116
describe("render telemetry events", () => {
1217
beforeEach(() => {
@@ -49,6 +54,31 @@ describe("render telemetry events", () => {
4954
);
5055
});
5156

57+
it("sends split capture-stage timing fields on render_complete", () => {
58+
trackRenderComplete({
59+
durationMs: 6000,
60+
fps: 30,
61+
quality: "standard",
62+
docker: false,
63+
gpu: false,
64+
stageCaptureMs: 5100,
65+
stageCaptureSetupMs: 1860,
66+
stageCaptureFrameMs: 3240,
67+
captureAvgMs: 27,
68+
});
69+
70+
expect(trackEvent).toHaveBeenCalledWith(
71+
"render_complete",
72+
expect.objectContaining({
73+
stage_capture_ms: 5100,
74+
stage_capture_setup_ms: 1860,
75+
stage_capture_frame_ms: 3240,
76+
capture_avg_ms: 27,
77+
}),
78+
undefined,
79+
);
80+
});
81+
5282
it("redacts render_observation messages and includes renderJobId for correlation", () => {
5383
trackRenderObservation({
5484
renderJobId: "render-123",

packages/cli/src/telemetry/events.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ export function trackRenderComplete(
129129
stageVideoExtractMs?: number;
130130
stageAudioProcessMs?: number;
131131
stageCaptureMs?: number;
132+
stageCaptureSetupMs?: number;
133+
stageCaptureFrameMs?: number;
132134
stageEncodeMs?: number;
133135
stageAssembleMs?: number;
134136
// Video-extraction breakdown (from RenderPerfSummary.videoExtractBreakdown)
@@ -176,6 +178,8 @@ export function trackRenderComplete(
176178
stage_video_extract_ms: props.stageVideoExtractMs,
177179
stage_audio_process_ms: props.stageAudioProcessMs,
178180
stage_capture_ms: props.stageCaptureMs,
181+
stage_capture_setup_ms: props.stageCaptureSetupMs,
182+
stage_capture_frame_ms: props.stageCaptureFrameMs,
179183
stage_encode_ms: props.stageEncodeMs,
180184
stage_assemble_ms: props.stageAssembleMs,
181185
extract_resolve_ms: props.extractResolveMs,

packages/producer/src/services/render/perfSummary-dedup.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import type { CapturePerfSummary } from "@hyperframes/engine";
33
import { buildRenderPerfSummary } from "./perfSummary.js";
44
import { createRenderJob } from "../renderOrchestrator.js";
55

6-
function baseInput(dedupPerfs: CapturePerfSummary[]) {
6+
function baseInput(
7+
dedupPerfs: CapturePerfSummary[],
8+
overrides: Partial<Parameters<typeof buildRenderPerfSummary>[0]> = {},
9+
) {
710
return {
811
job: createRenderJob({ fps: { num: 30, den: 1 }, quality: "high" }),
912
workerCount: dedupPerfs.length || 1,
@@ -24,6 +27,7 @@ function baseInput(dedupPerfs: CapturePerfSummary[]) {
2427
peakRssBytes: 0,
2528
peakHeapUsedBytes: 0,
2629
dedupPerfs,
30+
...overrides,
2731
};
2832
}
2933

@@ -128,3 +132,33 @@ describe("buildRenderPerfSummary static-dedup aggregation", () => {
128132
});
129133
});
130134
});
135+
136+
describe("buildRenderPerfSummary capture average attribution", () => {
137+
it("uses frame-capture time for captureAvgMs instead of setup-inclusive captureMs", () => {
138+
const summary = buildRenderPerfSummary(
139+
baseInput([], {
140+
totalFrames: 120,
141+
perfStages: {
142+
captureMs: 5_100,
143+
captureSetupMs: 1_860,
144+
captureFrameMs: 3_240,
145+
},
146+
}),
147+
);
148+
149+
expect(summary.captureAvgMs).toBe(27);
150+
});
151+
152+
it("falls back to legacy captureMs when captureFrameMs is absent", () => {
153+
const summary = buildRenderPerfSummary(
154+
baseInput([], {
155+
totalFrames: 120,
156+
perfStages: {
157+
captureMs: 5_100,
158+
},
159+
}),
160+
);
161+
162+
expect(summary.captureAvgMs).toBe(43);
163+
});
164+
});

packages/producer/src/services/render/perfSummary.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ export function buildRenderPerfSummary(input: {
122122
observability: input.observability,
123123
captureAvgMs:
124124
input.totalFrames > 0
125-
? Math.round((input.perfStages.captureMs ?? 0) / input.totalFrames)
125+
? Math.round(
126+
(input.perfStages.captureFrameMs ?? input.perfStages.captureMs ?? 0) /
127+
input.totalFrames,
128+
)
126129
: undefined,
127130
peakRssMb: Math.round(input.peakRssBytes / (1024 * 1024)),
128131
peakHeapUsedMb: Math.round(input.peakHeapUsedBytes / (1024 * 1024)),

packages/producer/src/services/renderOrchestrator.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)