From 60c723dcd12ec4a3422e4ce7513fcfc6c9f74408 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 23 Apr 2026 14:16:31 +0000 Subject: [PATCH] perf(producer): revert webp extraction default (20x slower than jpg on this host) --- .../producer/scripts/validate-perf-stack.ts | 110 ++++++++++++++++++ .../src/services/renderOrchestrator.ts | 21 ++-- 2 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 packages/producer/scripts/validate-perf-stack.ts diff --git a/packages/producer/scripts/validate-perf-stack.ts b/packages/producer/scripts/validate-perf-stack.ts new file mode 100644 index 000000000..7b0e86d42 --- /dev/null +++ b/packages/producer/scripts/validate-perf-stack.ts @@ -0,0 +1,110 @@ +/** + * Validation driver for the 5-PR perf stack. + * + * Runs a producer test fixture through `executeRenderJob` and prints the + * resulting `RenderPerfSummary` as JSON. Designed to be called multiple + * times with different env flags so we can compare HWaccel on/off, cache + * miss vs hit, etc., without bisecting git history. + * + * Usage: + * tsx scripts/validate-perf-stack.ts [--label ] + * + * Honors: HYPERFRAMES_EXTRACT_CACHE_DIR, PRODUCER_HWACCEL_SDR_DECODE, + * PRODUCER_HWACCEL_MIN_DURATION_SECONDS — same env surface as a real + * producer render. + */ + +import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { createRenderJob, executeRenderJob } from "../src/services/renderOrchestrator.js"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); + +async function main() { + const args = process.argv.slice(2); + if (args.length === 0) { + console.error("usage: validate-perf-stack.ts [--label ]"); + process.exit(1); + } + const fixtureName = args[0]!; + const labelIdx = args.indexOf("--label"); + const label = labelIdx >= 0 ? args[labelIdx + 1] : undefined; + + const fixtureRoot = resolve(__dirname, "..", "tests", fixtureName); + const srcDir = join(fixtureRoot, "src"); + if (!existsSync(srcDir)) { + console.error(`Fixture not found: ${srcDir}`); + process.exit(1); + } + + const metaPath = join(fixtureRoot, "meta.json"); + const meta = existsSync(metaPath) + ? JSON.parse(await Bun.file(metaPath).text()) + : { renderConfig: { fps: 30 } }; + + const tempRoot = mkdtempSync(join(tmpdir(), "hf-perf-validate-")); + const tempSrc = join(tempRoot, "src"); + cpSync(srcDir, tempSrc, { recursive: true }); + + const outDir = join(tempRoot, "out"); + mkdirSync(outDir, { recursive: true }); + const outputFormat = meta.renderConfig?.format ?? "mp4"; + const outputPath = join(outDir, `output.${outputFormat}`); + + const job = createRenderJob({ + fps: meta.renderConfig?.fps ?? 30, + quality: "high", + format: outputFormat, + workers: meta.renderConfig?.workers ?? 1, + useGpu: false, + debug: false, + hdr: meta.renderConfig?.hdr ?? false, + }); + + const started = Date.now(); + try { + await executeRenderJob(job, tempSrc, outputPath); + } catch (err) { + console.error( + JSON.stringify({ + event: "render_failed", + fixture: fixtureName, + label, + error: err instanceof Error ? err.message : String(err), + elapsedMs: Date.now() - started, + }), + ); + rmSync(tempRoot, { recursive: true, force: true }); + process.exit(2); + } + + const summary = job.perfSummary; + console.log( + JSON.stringify( + { + event: "render_complete", + fixture: fixtureName, + label, + elapsedMs: Date.now() - started, + envToggles: { + HYPERFRAMES_EXTRACT_CACHE_DIR: process.env.HYPERFRAMES_EXTRACT_CACHE_DIR ?? null, + PRODUCER_HWACCEL_SDR_DECODE: process.env.PRODUCER_HWACCEL_SDR_DECODE ?? null, + PRODUCER_HWACCEL_MIN_DURATION_SECONDS: + process.env.PRODUCER_HWACCEL_MIN_DURATION_SECONDS ?? null, + }, + perfSummary: summary, + }, + null, + 2, + ), + ); + + rmSync(tempRoot, { recursive: true, force: true }); +} + +main().catch((err) => { + console.error(err); + process.exit(3); +}); diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts index f52ede529..4ca29252f 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -1111,17 +1111,16 @@ export async function executeRenderJob( extractionResult = await extractAllVideoFrames( composition.videos, projectDir, - { - fps: job.config.fps, - outputDir: join(workDir, "video-frames"), - // WebP gives smaller files than JPEG at equivalent visual quality - // AND handles alpha natively — used by the producer as the single - // extraction format so we don't have a jpg/png split. HDR pixel - // paths still produce PNG on their own out-of-band codepath (see - // the HDR pre-extract block below); this setting only affects - // SDR frames that flow through the `` injector. - format: "webp", - }, + // Default format is "jpg" (set by extractAllVideoFrames). WebP is + // available as an opt-in via `ExtractionOptions.format: "webp"` and + // handles alpha natively, but on typical content libwebp encode is + // 16-20× slower than libjpeg-turbo — a hot-path regression that the + // architecture review's validation plan caught against these + // fixtures. WebP stays in the extractor/injector/cache for callers + // who want it (e.g. future alpha-input paths), but the producer's + // default stays JPEG until an AVIF/hardware-WebP encoder erases + // the cost gap. + { fps: job.config.fps, outputDir: join(workDir, "video-frames") }, abortSignal, // Forward extractCacheDir (when configured) so repeat renders of // the same source+window+fps+format pair skip Phase 3 entirely.