diff --git a/packages/producer/src/services/render/shared.ts b/packages/producer/src/services/render/shared.ts index 8c26224d17..3f88a56381 100644 --- a/packages/producer/src/services/render/shared.ts +++ b/packages/producer/src/services/render/shared.ts @@ -252,6 +252,44 @@ export function updateJobStatus( if (onProgress) void onProgress(job, stage); } +/** + * Surface auto-worker calibration as its own stage. Calibration writes + * throwaway sample frames, so keep the final-output counter at zero until the + * real capture stage begins. + */ +export function prepareCaptureCalibration(input: { + job: RenderJob; + totalFrames: number; + htmlInCanvasDetected: boolean; + lowMemoryMode: boolean; + deInversionEligible: boolean; + deParallelRouterEligible: boolean; + onProgress?: ProgressCallback; +}): boolean { + const { + job, + totalFrames, + htmlInCanvasDetected, + lowMemoryMode, + deInversionEligible, + deParallelRouterEligible, + onProgress, + } = input; + if ( + job.config.workers !== undefined || + totalFrames < 60 || + htmlInCanvasDetected || + lowMemoryMode || + deInversionEligible || + deParallelRouterEligible + ) { + return false; + } + job.framesRendered = 0; + updateJobStatus(job, "rendering", "Calibrating capture performance", 25, onProgress); + return true; +} + /** * Build a `resolver(framePath)` closure that maps an absolute path to * a frame inside `compiledDir` into a server-relative URL the producer's diff --git a/packages/producer/src/services/renderOrchestrator.test.ts b/packages/producer/src/services/renderOrchestrator.test.ts index 818694973c..a09236f18a 100644 --- a/packages/producer/src/services/renderOrchestrator.test.ts +++ b/packages/producer/src/services/renderOrchestrator.test.ts @@ -30,6 +30,7 @@ import { shouldDiscardProbeSessionForPageSideCompositing, resolveInversionRetryPlan, resolveParallelRouterRetryPlan, + createRenderJob, resetCaptureAttemptProgress, shouldRetryViaPinnedFallback, shouldPreferParallelDrawElement, @@ -51,12 +52,66 @@ import { applyRenderModeHints, createCompiledFrameSrcResolver, materializeExtractedFramesForCompiledDir, + prepareCaptureCalibration, projectBrowserEndToCompositionTimeline, resolveDeviceScaleFactor, writeCompiledArtifacts, } from "./render/shared.js"; import { formatCaptureFrameName, toExternalAssetKey } from "../utils/paths.js"; +describe("capture calibration progress", () => { + it("reports calibration when the auto-worker branch is eligible", () => { + const job = createRenderJob({ fps: 30, quality: "high" }); + job.framesRendered = 17; + const events: Array<{ stage: string; framesRendered: number | undefined }> = []; + + const shouldCalibrate = prepareCaptureCalibration({ + job, + totalFrames: 60, + htmlInCanvasDetected: false, + lowMemoryMode: false, + deInversionEligible: false, + deParallelRouterEligible: false, + onProgress: (current) => { + events.push({ + stage: current.currentStage, + framesRendered: current.framesRendered, + }); + }, + }); + + expect(shouldCalibrate).toBe(true); + expect(events).toEqual([ + { + stage: "Calibrating capture performance", + framesRendered: 0, + }, + ]); + expect(job.progress).toBe(25); + }); + + it("leaves progress untouched when calibration is skipped", () => { + const job = createRenderJob({ fps: 30, quality: "high", workers: 2 }); + job.framesRendered = 17; + const onProgress = vi.fn(); + + const shouldCalibrate = prepareCaptureCalibration({ + job, + totalFrames: 60, + htmlInCanvasDetected: false, + lowMemoryMode: false, + deInversionEligible: false, + deParallelRouterEligible: false, + onProgress, + }); + + expect(shouldCalibrate).toBe(false); + expect(onProgress).not.toHaveBeenCalled(); + expect(job.framesRendered).toBe(17); + expect(job.currentStage).toBe("Queued"); + }); +}); + describe("extractStandaloneEntryFromIndex", () => { it("reuses the index wrapper and keeps only the requested composition host", () => { const indexHtml = ` diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts index c5d2528d3c..66c33cfbcf 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -89,7 +89,12 @@ import { VIRTUAL_TIME_SHIM, } from "./fileServer.js"; import { defaultLogger, type ProducerLogger } from "../logger.js"; -import { createMemorySampler, type MemorySampler, updateJobStatus } from "./render/shared.js"; +import { + createMemorySampler, + prepareCaptureCalibration, + type MemorySampler, + updateJobStatus, +} from "./render/shared.js"; import { buildRenderErrorDetails, cleanupRenderResources, safeCleanup } from "./render/cleanup.js"; import { OrderedRenderEventPublisher, @@ -2223,12 +2228,15 @@ export async function executeRenderJob( }); if ( - job.config.workers === undefined && - totalFrames >= 60 && - !htmlInCanvasDetected && - !cfg.lowMemoryMode && - !deInversionEligible && - !deParallelRouterEligible + prepareCaptureCalibration({ + job, + totalFrames, + htmlInCanvasDetected, + lowMemoryMode: Boolean(cfg.lowMemoryMode), + deInversionEligible, + deParallelRouterEligible, + onProgress, + }) ) { const outcome = await observeRenderStage( observability,