Skip to content

perf(engine): webp extraction format + injector MIME routing#434

Open
jrusso1020 wants to merge 1 commit intoperf/producer-extraction-cachefrom
perf/producer-webp-extraction-format
Open

perf(engine): webp extraction format + injector MIME routing#434
jrusso1020 wants to merge 1 commit intoperf/producer-extraction-cachefrom
perf/producer-webp-extraction-format

Conversation

@jrusso1020
Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 commented Apr 23, 2026

What

Adds "webp" as a third option for ExtractionOptions.format and makes the producer use it for the SDR frame extraction path.

  • New format: "webp" branch in the extractor's ffmpeg args (-c:v libwebp -quality N -lossless 0).
  • New MIME-detection helper in the injector so WebP frames are tagged image/webp in the data URI.
  • Extraction-cache schema bumped from v1 → v2 (see below).
  • Producer explicitly opts into format: "webp" in its one extractAllVideoFrames call.

External callers using "jpg" or "png" keep working — this PR is additive, not a rename.

Why

PR 4 of the 5-PR stack from hyperframes-notes/producer-render-architecture-review-2026-04-21.md:

WebP as the single extraction format. Today's split is JPEG-for-opaque / PNG-for-alpha. WebP handles both, losslessly if we want, is smaller than PNG at equivalent quality, and Chrome decodes it natively.

For the SDR extraction path this shows up in two places in the RenderPerfSummary from PR 1:

  • tmpPeakBytes — WebP frames are smaller than JPEG at equivalent quality, noticeably on text-heavy content where JPEG's DCT struggles.
  • videoExtractBreakdown.extractMs — libwebp's lossy path isn't dramatically faster than libjpeg-turbo, but the smaller files mean shorter writes to disk, and we amortize that over ~K frames per render.

The bigger architectural win the doc describes (unifying an if (hasAlpha) branch) isn't visible in the SDR extractor today because the producer never branched on hasAlpha for extraction in the first place — but the groundwork this PR lays (WebP handles both opaque and alpha) is what lets future alpha-input work skip the format fork entirely.

How

Extractor

-c:v libwebp -quality $q -lossless 0 on the WebP branch. libwebp's -quality is 0-100 with higher-is-better, which is the inverse of JPEG's -q:v scale, so the mapping is just pass-through of the ExtractionOptions.quality param (default 95).

Injector

mimeTypeForFramePath replaces the inline framePath.endsWith(".png") ? "image/png" : "image/jpeg" check. Chrome has decoded WebP natively since 2014 so the data URI "just works" in a page-level <img>.

Cache schema

Bumped CACHE_SCHEMA_VERSION from 1 to 2. Reason: v1 cache dirs only hold .jpg or .png frames; a v2 extraction with format: "webp" writes .webp frames. lookupCacheEntry filters by file extension, but the schema bump defends against hash collisions with the old keyspace and makes future format changes explicit.

v1 and v2 keys can coexist under the same cache root without cross-serving — an old jpg entry can't be accidentally returned for a webp request because the key itself encodes the version.

Producer

Explicit format: "webp" in extractAllVideoFrames. No other call sites change.

Test plan

  • 4 new MIME-detection tests in videoFrameInjector.test.ts — asserts .webp/.png/.jpg/unknown extensions each produce the correct data:image/…;base64, prefix.
  • 2 new integration tests in videoFrameExtractor.test.ts:
    • format: "webp" produces .webp files on disk and hits the cache on a second call.
    • Cache DOES NOT cross-serve a jpg entry for a webp request (format is part of the key).
  • Existing VFR/HDR tests still use .jpg and continue to pass — the extractor still supports "jpg" and "png".
  • Full engine suite: 344 tests pass (was 338).
  • Typecheck clean in engine + producer.
  • Lint + format clean.

Stack

Built on #433#432#430. Each PR in the stack adds to RenderPerfSummary; PR 1's tmpPeakBytes and videoExtractBreakdown.extractMs are the primary signals for this PR's win.

Follow-ups

  1. Hardware-accelerated SDR decode, gated on !hasAlpha and segment-length floor — the last PR in the stack.

Copy link
Copy Markdown
Collaborator Author

jrusso1020 commented Apr 23, 2026

@qodo-ai-reviewer
Copy link
Copy Markdown

Hi, Bumping CACHE_SCHEMA_VERSION to 2 makes all existing v1 cache entries unreachable, so callers extracting jpg/png will incur full cache misses after upgrade even though the on-disk jpg/png frame layout and lookup logic still match.

Severity: remediation recommended | Category: performance

How to fix: Preserve v1 keys for jpg/png

Agent prompt to fix - you can give this to your LLM of choice:

Issue description

CACHE_SCHEMA_VERSION was bumped to 2, which changes the cache key prefix for all formats and makes any existing v1 extraction cache entries unreachable. This causes one-time, potentially large, re-extraction work for users who were already caching jpg/png frames.

Issue Context

computeExtractionCacheKey() already includes format in the hashed inputs and lookupCacheEntry() filters by file extension, so introducing webp can avoid cross-serving without a global schema bump.

Fix Focus Areas

  • packages/engine/src/services/extractionCache.ts[37-91]

What to change

  • Option A (simplest): keep CACHE_SCHEMA_VERSION = 1 and rely on format in the key to prevent collisions.
  • Option B (format-scoped bump): derive the schema prefix from the format (e.g., schemaVersionForFormat(format) returning 1 for jpg/png and 2 for webp) so legacy caches continue to hit while webp entries live under a new prefix.
  • If you intentionally want the one-time invalidation, add an explicit comment explaining that this is a deliberate cache flush for all formats (not just webp) so operators understand the impact.

Found by Qodo. Free code review for open-source maintainers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants