From 4f648c81228ff8b5e17dbb30b727888a1413b14b Mon Sep 17 00:00:00 2001 From: James Date: Mon, 11 May 2026 17:54:56 +0000 Subject: [PATCH 1/2] refactor(producer): extract compileStage from executeRenderJob MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the pure compile sub-stage (`compileForRender` + `applyRenderModeHints` + `writeCompiledArtifacts` + `CompositionMetadata` build + DPR resolution) out of `executeRenderJob` into `services/render/stages/compileStage.ts`. No behavior change. The sequencer calls `runCompileStage` at the same code point with identical inputs and outputs. The following invariants are preserved verbatim: - `cfg.forceScreenshot` is still mutated by `applyRenderModeHints`. - `perfStages.compileOnlyMs` is set to the same wall-clock interval (around the `compileForRender` call only). - The "Compiled composition metadata" log line is emitted after artifact writes with the same payload shape. - The "Supersampling composition via deviceScaleFactor" log line is emitted only when `deviceScaleFactor > 1`. - `stage1Start`, `updateJobStatus(..., "Compiling composition", 5, ...)`, and `perfStages.compileMs` (set at the end of probe) remain at their current code points in the sequencer. The probe sub-stage (`if (needsBrowser)`) is unchanged — it is extracted separately in PR 1.3. `recompileWithResolutions` lives inside the probe block because it depends on browser-resolved durations. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../services/render/stages/compileStage.ts | 116 ++++++++++++++++++ .../src/services/renderOrchestrator.ts | 57 +++------ 2 files changed, 130 insertions(+), 43 deletions(-) create mode 100644 packages/producer/src/services/render/stages/compileStage.ts diff --git a/packages/producer/src/services/render/stages/compileStage.ts b/packages/producer/src/services/render/stages/compileStage.ts new file mode 100644 index 000000000..fd6cb0102 --- /dev/null +++ b/packages/producer/src/services/render/stages/compileStage.ts @@ -0,0 +1,116 @@ +/** + * compileStage — pure compile pass of `executeRenderJob`. + * + * Runs `compileForRender` on the entry HTML, applies render-mode hints + * (which may flip `cfg.forceScreenshot` on for compositions that need it), + * writes compiled artifacts to `workDir/compiled/`, builds the + * `CompositionMetadata` view of the result, and resolves the + * `deviceScaleFactor` for supersampling. + * + * The probe sub-stage (browser launch, duration discovery, recompile, + * media reconciliation) is extracted separately in PR 1.3. This stage + * stops at the point where the in-process renderer formerly entered the + * `if (needsBrowser)` branch. + * + * Hard constraints preserved verbatim from the in-process renderer: + * - `applyRenderModeHints(cfg, ...)` is allowed to mutate `cfg.forceScreenshot`. + * - `perfStages.compileOnlyMs` is set to wall-clock ms around the + * `compileForRender` call only. + * - The `log.info("Compiled composition metadata", ...)` line is emitted + * after writing artifacts, with the same payload shape as before. + * - The `log.info("Supersampling composition via deviceScaleFactor", ...)` + * line is emitted only when `deviceScaleFactor > 1`. + */ + +import { join } from "node:path"; +import type { EngineConfig } from "@hyperframes/engine"; +import type { CompiledComposition } from "../../htmlCompiler.js"; +import { compileForRender } from "../../htmlCompiler.js"; +import type { ProducerLogger } from "../../../logger.js"; +import { + applyRenderModeHints, + resolveDeviceScaleFactor, + writeCompiledArtifacts, + type CompositionMetadata, + type RenderJob, +} from "../../renderOrchestrator.js"; + +export interface CompileStageInput { + projectDir: string; + workDir: string; + /** Absolute path to the entry HTML (already resolved to standalone-entry if needed). */ + htmlPath: string; + /** The relative `entryFile` string, used only for log payloads. */ + entryFile: string; + job: RenderJob; + /** EngineConfig — may be mutated via `cfg.forceScreenshot = true`. */ + cfg: EngineConfig; + /** True when the output format requires an alpha channel (webm/mov/png-sequence). */ + needsAlpha: boolean; + log: ProducerLogger; + /** Cooperative-cancellation probe; throws `RenderCancelledError` when aborted. */ + assertNotAborted: () => void; +} + +export interface CompileStageResult { + compiled: CompiledComposition; + composition: CompositionMetadata; + deviceScaleFactor: number; + outputWidth: number; + outputHeight: number; + /** Wall-clock ms for the pure `compileForRender` call only (excludes artifact writes). */ + compileOnlyMs: number; +} + +export async function runCompileStage(input: CompileStageInput): Promise { + const { projectDir, workDir, htmlPath, entryFile, job, cfg, needsAlpha, log, assertNotAborted } = + input; + + const compileStart = Date.now(); + const compiled = await compileForRender(projectDir, htmlPath, join(workDir, "downloads")); + assertNotAborted(); + const compileOnlyMs = Date.now() - compileStart; + applyRenderModeHints(cfg, compiled, log); + writeCompiledArtifacts(compiled, workDir, Boolean(job.config.debug)); + + log.info("Compiled composition metadata", { + entryFile, + staticDuration: compiled.staticDuration, + width: compiled.width, + height: compiled.height, + videoCount: compiled.videos.length, + audioCount: compiled.audios.length, + renderModeHints: compiled.renderModeHints, + }); + + const composition: CompositionMetadata = { + duration: compiled.staticDuration, + videos: compiled.videos, + audios: compiled.audios, + images: compiled.images, + width: compiled.width, + height: compiled.height, + }; + const { width, height } = composition; + const deviceScaleFactor = resolveDeviceScaleFactor({ + compositionWidth: width, + compositionHeight: height, + outputResolution: job.config.outputResolution, + hdrRequested: job.config.hdrMode === "force-hdr", + alphaRequested: needsAlpha, + }); + const outputWidth = width * deviceScaleFactor; + const outputHeight = height * deviceScaleFactor; + if (deviceScaleFactor > 1) { + log.info("Supersampling composition via deviceScaleFactor", { + compositionWidth: width, + compositionHeight: height, + outputResolution: job.config.outputResolution, + outputWidth, + outputHeight, + deviceScaleFactor, + }); + } + + return { compiled, composition, deviceScaleFactor, outputWidth, outputHeight, compileOnlyMs }; +} diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts index d535eb2a9..22963b5a4 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -109,7 +109,6 @@ import { freemem } from "os"; import { fileURLToPath } from "url"; import { createFileServer, type FileServerHandle, VIRTUAL_TIME_SHIM } from "./fileServer.js"; import { - compileForRender, resolveCompositionDurations, recompileWithResolutions, discoverMediaFromBrowser, @@ -121,6 +120,7 @@ import { type HdrImageTransferCache, createHdrImageTransferCache, } from "./hdrImageTransferCache.js"; +import { runCompileStage } from "./render/stages/compileStage.js"; /** * Wrap a cleanup operation so it never throws, but logs any failure. @@ -2125,51 +2125,22 @@ export async function executeRenderJob( const stage1Start = Date.now(); updateJobStatus(job, "preprocessing", "Compiling composition", 5, onProgress); - const compileStart = Date.now(); - let compiled = await compileForRender(projectDir, htmlPath, join(workDir, "downloads")); - assertNotAborted(); - perfStages.compileOnlyMs = Date.now() - compileStart; - applyRenderModeHints(cfg, compiled, log); - writeCompiledArtifacts(compiled, workDir, Boolean(job.config.debug)); - - log.info("Compiled composition metadata", { + const compileResult = await runCompileStage({ + projectDir, + workDir, + htmlPath, entryFile, - staticDuration: compiled.staticDuration, - width: compiled.width, - height: compiled.height, - videoCount: compiled.videos.length, - audioCount: compiled.audios.length, - renderModeHints: compiled.renderModeHints, + job, + cfg, + needsAlpha, + log, + assertNotAborted, }); - - const composition: CompositionMetadata = { - duration: compiled.staticDuration, - videos: compiled.videos, - audios: compiled.audios, - images: compiled.images, - width: compiled.width, - height: compiled.height, - }; + let compiled = compileResult.compiled; + const composition = compileResult.composition; + const { deviceScaleFactor, outputWidth, outputHeight } = compileResult; const { width, height } = composition; - const deviceScaleFactor = resolveDeviceScaleFactor({ - compositionWidth: width, - compositionHeight: height, - outputResolution: job.config.outputResolution, - hdrRequested: job.config.hdrMode === "force-hdr", - alphaRequested: needsAlpha, - }); - const outputWidth = width * deviceScaleFactor; - const outputHeight = height * deviceScaleFactor; - if (deviceScaleFactor > 1) { - log.info("Supersampling composition via deviceScaleFactor", { - compositionWidth: width, - compositionHeight: height, - outputResolution: job.config.outputResolution, - outputWidth, - outputHeight, - deviceScaleFactor, - }); - } + perfStages.compileOnlyMs = compileResult.compileOnlyMs; const probeStart = Date.now(); const needsBrowser = composition.duration <= 0 || compiled.unresolvedCompositions.length > 0; From d1683977580e0cddd004752c30e27c3c7eace09c Mon Sep 17 00:00:00 2001 From: James Date: Mon, 11 May 2026 19:00:59 +0000 Subject: [PATCH 2/2] refactor(producer): flag cfg.forceScreenshot mutation as distributed-render TODO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `TODO(distributed-render):` comment near the `applyRenderModeHints` call documenting that this caller-owned-object mutation needs to move into the result type before `freezePlan` wires up. The mutation pattern works in-process but won't survive across processes / replays from a frozen plan — the value belongs in `LockedRenderConfig`, not on a mutated `EngineConfig`. No behavior change. Comment-only. Review feedback addressed: vanceingalls on #718. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/producer/src/services/render/stages/compileStage.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/producer/src/services/render/stages/compileStage.ts b/packages/producer/src/services/render/stages/compileStage.ts index fd6cb0102..d9d465459 100644 --- a/packages/producer/src/services/render/stages/compileStage.ts +++ b/packages/producer/src/services/render/stages/compileStage.ts @@ -70,6 +70,11 @@ export async function runCompileStage(input: CompileStageInput): Promise