perf(producer): gate per-frame debug meta via optional isLevelEnabled#383
Open
vanceingalls wants to merge 47 commits intovance/hdr-benchmark-harnessfrom
Open
perf(producer): gate per-frame debug meta via optional isLevelEnabled#383vanceingalls wants to merge 47 commits intovance/hdr-benchmark-harnessfrom
vanceingalls wants to merge 47 commits intovance/hdr-benchmark-harnessfrom
Conversation
This was referenced Apr 21, 2026
Collaborator
Author
This was referenced Apr 21, 2026
Open
perf(producer): hdr benchmark harness — --tags filter, peak heap/RSS tracking, bench:hdr script
#382
Open
1d3a961 to
cadda3c
Compare
38cae8b to
723c98b
Compare
cadda3c to
aa41490
Compare
723c98b to
2f5ecdc
Compare
2f5ecdc to
57abd3a
Compare
aa41490 to
9b942fb
Compare
57abd3a to
c0d1d4e
Compare
3e3fa0c to
6d2f104
Compare
…ders The CLI flags `--crf` and `--video-bitrate` were defined and parsed in `packages/cli/src/commands/render.ts`, validated for mutual exclusivity, and threaded into `RenderConfig.crf`/`RenderConfig.videoBitrate`, but the values were silently dropped at the encoder spawn sites in `renderOrchestrator.ts`. PR #292 originally wired these through with a `baseEncoderOpts` object using `effectiveQuality`/`effectiveBitrate`; PR #268 rewrote the encode paths and reverted to `preset.quality` only. This change re-introduces the override at the three encoder spawn sites: 1. HDR streaming encoder (rgb48le path) 2. SDR streaming encoder (jpeg/png path) 3. Disk-based encode (encodeFramesFromDir / encodeFramesChunkedConcat) At each site, `quality` defaults to `preset.quality` but is overridden by `job.config.crf` when set, and `bitrate` is set from `job.config.videoBitrate`. Mutual exclusivity is enforced upstream in the CLI, so we do not need to re-check it here. Also fixes the contradictory note in `docs/packages/cli.mdx` that claimed CRF/bitrate were now driven only by `--quality`. The flags table now lists `--crf` and `--video-bitrate` consistent with `docs/guides/rendering.mdx`.
Programmatic callers can construct RenderConfig directly and bypass the CLI's mutual-exclusivity guard for --crf vs --video-bitrate. Add an orchestrator-level check that logs a warning and explicitly nulls out videoBitrate when crf is also set, so the encoder gets unambiguous inputs and downstream users aren't confused by a quietly-different bitrate than they passed in. Addresses non-blocking review feedback on PR #372.
Address jrusso1020's nit on PR #365 (non-blocking review): both READMEs now explain where the tolerance values come from. - hdr-regression/README.md: add a budget-breakdown table that derives the 30 frames from the deltas in PRs #369 (window C fix → 5) and #375 (window F fix → 0). The table doubles as a contract: if a future change forces the budget back up, exactly one bucket has regressed and the table tells you which one to investigate first. - hdr-hlg-regression/README.md: add a 'Tolerance' section explaining why 0 is the right floor (HLG is a pure pass-through path, HEVC over rgb48le is byte-deterministic on the same fixture, so any drift is a real regression). The regeneration command for generate-hdr-photo-pq.py was already documented at README lines 67-71, so no changes needed there.
Replace the old hdr-pq + hdr-image-only tests with two consolidated regression suites that exercise the full HDR pipeline. hdr-regression (PQ, BT.2020, ~20s): - 8 windows (A-H) covering clip-only video, image+video composition, wrapper opacity, direct-on-video opacity, scene transitions, transform + border-radius, mid-clip cuts, and shader transitions. - Reuses the existing hdr-clip.mp4 fixture (NOTICE.md preserved). - New hdr-photo-pq.png generated via scripts/generate-hdr-photo-pq.py (writes a cICP chunk for BT.2020/PQ/full). hdr-hlg-regression (HLG, ARIB STD-B67, ~5s): - 2 windows (A-B) covering clip-only HLG playback and HLG + opacity tween. - New hdr-hlg-clip.mp4 fixture (last 5s of a user-recorded HLG iPhone clip). Both compositions follow the documented timed-element pattern: data-start, data-duration, and class="clip" applied directly to each timed leaf element (no wrapper inheritance). CI: regression workflow's hdr shard now runs the new pair sequentially. LFS: new MP4 fixtures and golden outputs are tracked via existing rules. Goldens generated with bun run test:update --sequential. ffprobe verifies HEVC/yuv420p10le/bt2020nc/smpte2084 (PQ) and arib-std-b67 (HLG). Made-with: Cursor
Four related bugs in the opacity pipeline that interact with HDR video
compositing and GSAP-controlled fades:
1A. screenshotService injectVideoFramesBatch and syncVideoFrameVisibility
were applying `opacity: 0 !important` to native <video> elements to
hide them under the injected <img>. That stomp clobbered any
GSAP-controlled inline opacity, so the next seek read 0 from
computed style and the comp went black. We now use
`visibility: hidden !important` only — visibility hides the element
from rendering without changing its opacity, so subsequent reads
(and queryElementStacking) see the real GSAP value on every frame.
The `parseFloat(...) || 1` recovery hack at injectVideoFramesBatch
was specifically there to compensate for this stomp; it’s now
replaced with a `Number.isNaN` guard that defaults to 1 only when
parsing actually fails.
1B. queryVideoElementBounds parsed `style.opacity` and `style.zIndex`
with `parseFloat(...) || N`, which silently coerces a real opacity
of 0 into 1 (and zIndex 0 into 0 only by coincidence). Switched to
explicit `Number.isNaN` checks so opacity 0 stays 0.
1C. resolveRadius cast `el as HTMLElement` to read offsetWidth/Height.
SVG and other non-HTML elements would have crashed at runtime.
Replaced the cast with an `instanceof HTMLElement` guard, and made
the numeric fallback `Number.isNaN`-safe.
1D. The opacity walk in queryVideoElementBounds started from
`el.parentElement` for HDR videos to skip past the engine’s forced
`opacity: 0` on the element itself. Now that the engine never sets
opacity, the special case is unnecessary — always walk from `el`.
Kept the `isHdrEl` lookup because transform/border-radius logic
further down still branches on it.
Verified:
- bun run --filter @hyperframes/engine typecheck (clean)
- bun run --filter @hyperframes/engine test (308/308 passing)
- bun run --filter @hyperframes/producer typecheck (clean)
- oxlint + oxfmt --check on both touched files
Next: regenerate hdr-regression window C (the opacity-fade window) and
tighten its maxFrameFailures budget — done in a follow-up commit so the
golden churn is reviewable separately from the code fix.
…Chunk 1 fix Window C (direct <video> opacity tween) was previously listed as a known failure with a maxFrameFailures budget of 30 to absorb expected drift until Chunk 1 (opacity pipeline bugs) landed. After the Chunk 1 fix, the regression test passes against the existing golden with 0 failed frames, confirming Window C now renders correctly. Regenerating the golden produces byte-identical output (HEVC encoding is deterministic and the opacity fix doesn't perturb pixels at the PSNR ≥ 28 checkpoint threshold). Tighten maxFrameFailures from 30 → 5 to leave only a small budget for encoder noise. Window F (transform + border-radius) remains pending Chunk 4; its broken state is currently baked into the golden, so the suite is green and Chunk 4's regen will catch any drift. Update README.md to reflect Window C is fixed and the tightened budget.
…Chunk 4 fix Window F (transform + scale + border-radius on the video itself) was the remaining known-fail in the hdr-regression suite, baked into the golden so the suite stayed green while Chunk 4 was outstanding. After Chunk 4 fixed parseTransformMatrix (matrix3d support) and the shader-transitions initial-state, re-running the suite shows 0 failed frames against the existing golden — the encoder is byte-deterministic and Window F's GSAP rotation/scale happens to emit 2D matrix() rather than matrix3d(), so the same golden is still correct after the fix. Tighten maxFrameFailures from 5 → 0 to match hdr-hlg-regression, so any drift in the layered HDR compositor is caught immediately. Update the README's Window F row + Fix history section to reflect the new state.
Address jrusso1020's nit on PR #365 (non-blocking review): both READMEs now explain where the tolerance values come from. - hdr-regression/README.md: add a budget-breakdown table that derives the 30 frames from the deltas in PRs #369 (window C fix → 5) and #375 (window F fix → 0). The table doubles as a contract: if a future change forces the budget back up, exactly one bucket has regressed and the table tells you which one to investigate first. - hdr-hlg-regression/README.md: add a 'Tolerance' section explaining why 0 is the right floor (HLG is a pure pass-through path, HEVC over rgb48le is byte-deterministic on the same fixture, so any drift is a real regression). The regeneration command for generate-hdr-photo-pq.py was already documented at README lines 67-71, so no changes needed there.
… transfer
Chunk 3 of HDR follow-ups. Three independent fixes that share a common
thread: HDR config flowing correctly from the EngineConfig down through
the encoders.
3A. chunkEncoder respects options.hdr (BT.2020 + mastering metadata)
Previously buildEncoderArgs hard-coded BT.709 color tags and the
bt709 VUI block in -x265-params, even when callers passed an HDR
EncoderOptions. Today this is harmless because renderOrchestrator
routes native-HDR content to streamingEncoder and only feeds
chunkEncoder sRGB Chrome screenshots — but the contract was a lie.
Now: when options.hdr is set, the libx265 software path emits
bt2020nc + the matching transfer (smpte2084 for PQ,
arib-std-b67 for HLG) at the codec level *and* embeds
master-display + max-cll SEI in -x265-params via
getHdrEncoderColorParams. libx264 still tags BT.709 inside
-x264-params (libx264 has no HDR support) but the codec-level color
flags flip so the container describes pixels truthfully. GPU
H.265 (nvenc/videotoolbox/qsv/vaapi) gets the BT.2020 tags but no
-x265-params block, so static mastering metadata is omitted —
acceptable for previews, not HDR-aware delivery.
3B. convertSdrToHdr accepts a target transfer
videoFrameExtractor.convertSdrToHdr was hard-coded to
transfer=arib-std-b67 (HLG) regardless of the surrounding
composition's dominant transfer. extractAllVideoFrames now calls
analyzeCompositionHdr first, then passes the dominant transfer
("pq" or "hlg") into convertSdrToHdr so an SDR clip mixed into a PQ
timeline gets converted with smpte2084, not arib-std-b67.
3C. EngineConfig.hdr type matches its declared shape
The IIFE for the hdr field returned undefined when
PRODUCER_HDR_TRANSFER wasn't "hlg" or "pq", but the field is typed
as { transfer: HdrTransfer } | false. Returning false matches the
type and avoids a downstream undefined check.
Tests
- chunkEncoder.test.ts: replaced the previous "HDR options ignored"
assertions with 8 new specs covering BT.2020 + transfer tagging,
master-display/max-cll embedding, libx264 fallback behavior, GPU
H.265 + HDR (tags but no x265-params), and range conversion for
both SDR and HDR CPU paths.
- All 313 engine unit tests pass (5 new HDR specs).
Follow-ups (separate PRs):
- Producer regression suite runs in CI; not exercising HDR-tagged
chunkEncoder yet because no live caller sets options.hdr there.
…d-transfer caller error PR #370 review feedback (jrusso1020): - chunkEncoder: when codec=h264 and hdr is set, log a warning and strip hdr instead of emitting a half-HDR file (BT.2020 container tags + BT.709 VUI inside the bitstream). libx264 has no HDR support; the only honest output is SDR/BT.709. Caller is told to use codec=h265. - videoFrameExtractor: comment at the convertSdrToHdr call site clarifying that dominantTransfer is majority-wins; mixing PQ and HLG sources in a single composition is caller-error and the minority transfer's videos will be converted with the wrong curve. Render two compositions if you need both transfers. - docs/guides/hdr.mdx: limitations section now documents (a) H.264 + HDR is rejected at the encoder layer, and (b) GPU H.265 (nvenc, videotoolbox, qsv, vaapi) emits BT.2020 + transfer tags but does NOT embed master-display or max-cll SEI, since ffmpeg won't pass x265-params through hardware encoders. Acceptable for previews, not for HDR10 delivery.
Address jrusso1020's review on PR #371: add a path.win32 test suite that exercises isPathInside on Linux/macOS CI to catch accidental Unix-only assumptions (e.g. only splitting on "/") that would silently regress for Windows users. - isPathInside now accepts an optional `pathModule` (defaults to node:path) so tests can inject path.win32 / path.posix without changing call sites. - New describe block covers equality, direct/deep children, sibling-prefix rejection, traversal escapes, trailing-backslash normalization, and cross-drive rejection.
Migrates the six existing PNG fixtures (1.6 MB combined: hdr-photo-pq.png plus heygen-promo-preview-assets screenshots) onto Git LFS to mirror the existing policy for golden videos and fixture .mp4s. Without this, regression suites that grow PNG fixtures over time would bloat the working-tree history and slow shallow clones. Addresses Chunk 11C in plans/hdr-followups.md.
Address jrusso1020's nit on PR #365 (non-blocking review): both READMEs now explain where the tolerance values come from. - hdr-regression/README.md: add a budget-breakdown table that derives the 30 frames from the deltas in PRs #369 (window C fix → 5) and #375 (window F fix → 0). The table doubles as a contract: if a future change forces the budget back up, exactly one bucket has regressed and the table tells you which one to investigate first. - hdr-hlg-regression/README.md: add a 'Tolerance' section explaining why 0 is the right floor (HLG is a pure pass-through path, HEVC over rgb48le is byte-deterministic on the same fixture, so any drift is a real regression). The regeneration command for generate-hdr-photo-pq.py was already documented at README lines 67-71, so no changes needed there.
…d-transfer caller error PR #370 review feedback (jrusso1020): - chunkEncoder: when codec=h264 and hdr is set, log a warning and strip hdr instead of emitting a half-HDR file (BT.2020 container tags + BT.709 VUI inside the bitstream). libx264 has no HDR support; the only honest output is SDR/BT.709. Caller is told to use codec=h265. - videoFrameExtractor: comment at the convertSdrToHdr call site clarifying that dominantTransfer is majority-wins; mixing PQ and HLG sources in a single composition is caller-error and the minority transfer's videos will be converted with the wrong curve. Render two compositions if you need both transfers. - docs/guides/hdr.mdx: limitations section now documents (a) H.264 + HDR is rejected at the encoder layer, and (b) GPU H.265 (nvenc, videotoolbox, qsv, vaapi) emits BT.2020 + transfer tags but does NOT embed master-display or max-cll SEI, since ffmpeg won't pass x265-params through hardware encoders. Acceptable for previews, not for HDR10 delivery.
Mirrors buildSrgbToHdrLut() in Python so future intentional LUT changes (transfer-function constants, BT.709→BT.2020 matrix, OOTF, SDR-white nit reference) can regenerate the byte-exact SRGB_TO_HDR_REFERENCE table in one shot instead of hand-editing 12 inline literals. Addresses PR #377 review feedback (jrusso1020): "reference values are currently inline literals. If the LUT ever gets regenerated for a legitimate reason, updating 97 lines of literals by hand is noisy. A helper pattern — scripts/generate-lut-reference.py that emits the current LUT values as a test-fixture JSON — is worth ~5 minutes to write and would make future regenerations one-shot." Usage: python3 packages/engine/scripts/generate-lut-reference.py --probes Produces a paste-ready TS snippet matching the existing reference array. Default mode (no flag) emits full 256-entry HLG/PQ LUTs as JSON. The script uses int(math.floor(x + 0.5)) to mirror JS Math.round() for non-negative numbers, which is what guarantees byte-identical output. Verified: --probes regenerates the existing 12 reference rows exactly.
…for all shaders Existing smoke tests cover only the endpoints (p=0 ≈ from, p=1 ≈ to), which miss a class of regressions that surface specifically at the midpoint — where the transition is most visible to viewers — and let shaders silently rot in CI: • A shader becomes a no-op (returns input as-is) • A shader prematurely completes (returns target at midpoint) • A shader doesn't write to the output buffer at all • A shader loses determinism (Math.random / Date.now / leaked state) Add four invariants every shader must satisfy at p=0.5, applied via a describe loop over ALL_SHADERS so any new transition added to the registry automatically picks up the same coverage: 1. output ≠ from catches no-ops 2. output ≠ to catches premature completion 3. output is non-zero catches blank output 4. output is deterministic catches accidental non-determinism Uses two distinct uniform input colors (40000/30000/20000 vs 10000/10000/10000) so equality checks have distinct byte patterns to compare against. Even shaders that warp UVs (which would be no-ops on uniform input alone) produce mix16(from, to, 0.5) at every pixel, distinct from both inputs. 60 new tests (4 invariants × 15 shaders), all passing. Follow-up to plans/hdr-followups.md Chunk 9G.
Mock node:child_process.spawn to surface ENOENT and verify that: - extractMediaMetadata falls back to PNG cICP metadata for image inputs - extractMediaMetadata rethrows for non-image inputs lacking a still-image fallback - extractAudioMetadata + analyzeKeyframeIntervals propagate the install-hint error verbatim Closed gap from hdr-followups Chunk 9B.
Add unit tests that mock child_process.spawn so we can drive an in-memory 'ffmpeg' through the success/failure paths used by the producer's HDR encoder and by Chunk 5A's defensive close() in renderOrchestrator. Covers: - successful exit after explicit close() - non-zero exit before close() returns a failure result (no throw) - ENOENT on spawn returns a failure result (no throw) - abort signal triggers SIGTERM and a 'cancelled' result - close() is idempotent and never throws on a second call - writeFrame returns false after the encoder has exited - close() detaches the abort listener so post-close aborts don't re-kill ffmpeg These contracts are what the renderOrchestrator try/finally cleanup added in Chunk 5A relies on, and what the ffprobe-unavailable test (Chunk 9B) hinted at for the encoder side.
…in cross-job isolation
Chunk 9E. The frame-directory max-index cache lived as a private
module-scoped Map inside renderOrchestrator.ts, which made the cross-job
isolation contract added in Chunk 5B impossible to unit-test directly.
Extract the cache into packages/producer/src/services/frameDirCache.ts
behind getMaxFrameIndex / clearMaxFrameIndex / getMaxFrameIndexCacheSize
(plus a test-only __resetMaxFrameIndexCacheForTests helper). Behavior is
unchanged: callers still get the same module-scoped sharing inside a
job, and renderOrchestrator's outer finally still clears every entry it
registered so the cache cannot grow monotonically across renders.
renderOrchestrator now imports the new helpers, drops the unused
readdirSync import, and updates the inline comments to point at the new
module. Two cleanup sites that previously called
frameDirMaxIndexCache.delete now call clearMaxFrameIndex.
Add frameDirCache.test.ts (bun:test) with 11 tests covering:
- reading the max index from a populated directory
- ignoring filenames that do not match frame_NNNN.png (wrong ext,
wrong prefix, wrong case, double extension, empty index group, and
a same-named subdirectory)
- empty- and missing-directory paths returning 0 and being cached
- the intra-job invariant that subsequent readdir mutations are not
observed once a directory has been cached
- clearMaxFrameIndex forcing a re-read and returning false for paths
that were never cached
- per-directory isolation when multiple directories are registered
- the cross-job contract from Chunk 5B: the cache is empty between
well-behaved jobs, does not grow monotonically across 20 simulated
renders with 3 HDR videos each (steady-state cache size stays at 3),
and a buggy job that forgets to clear leaks exactly its own entries
rather than affecting unrelated jobs.
Made-with: Cursor
…tracking, bench:hdr script Makes the existing benchmark harness genuinely useful for HDR perf work before landing image-cache and debug-logging optimizations in the rest of Chunk 8. Three tightly-related changes: 1. **Positive --tags filter** in `benchmark.ts`. Existing harness only had `--exclude-tags` (which defaults to `slow`). Adds `--tags hdr` so HDR runs don't have to wait for unrelated SDR fixtures. Filters compose: a fixture must match `--tags` (if provided) AND must not match `--exclude-tags`. 2. **Peak heap + RSS tracking** in `executeRenderJob`. A 250ms periodic `process.memoryUsage()` sampler runs alongside every render and reports `peakRssMb` / `peakHeapUsedMb` in `RenderPerfSummary`. Wall-clock alone can't catch slow memory regressions like an unbounded image cache — peak RSS does. Sampler is `unref`'d and always cleared in `finally` so it never keeps the event loop alive or leaks across jobs. Both fields are optional on the interface for back-compat with serialized older summaries. 3. **bench:hdr convenience script** plus a perf README at `tests/perf/README.md` documenting the harness, the new flags, and the captured April-2026 HDR baseline (PQ regression: 34.5s / 272 MiB RSS, HLG regression: 11.5s / 227 MiB RSS, both 1080p / 1 worker / 1 run). The benchmark output table is widened and gains PeakRSS / PeakHeap columns. A new `avgOrNull` helper preserves `null` in the JSON when no run reported memory (avoids silently coercing missing data to 0 in older snapshots). No behavior change for non-benchmark renders — the sampler runs in every `executeRenderJob` but its overhead is a single `process.memoryUsage()` call every 250ms, well below noise. Verification: - `bunx tsc --noEmit -p packages/producer` — clean - `bunx oxlint` / `bunx oxfmt --check` on changed files — clean - `bun test src/services/` — 60/60 pass (frameDirCache, orchestrator, etc.) - `bunx tsx src/benchmark.ts --tags hdr --runs 1` — both HDR fixtures render successfully, summary table prints PeakRSS/PeakHeap columns, per-run output shows new memory line. - `bunx tsx src/benchmark.ts --tags nonexistent` — exits 1 with a helpful message naming the active filters. Refs: plans/hdr-followups.md Chunk 8A.
Add an optional `isLevelEnabled(level)` method to `ProducerLogger` so call
sites can short-circuit expensive metadata construction in hot loops
before handing it to a debug logger. Implement it in
`createConsoleLogger` and gate the per-frame HDR composite snapshot in
`renderOrchestrator` (every 30 frames) on
`log.isLevelEnabled?.("debug") ?? true`, so production runs at
`level="info"` skip the `Array.find` + `toFixed` + struct allocation
entirely while custom loggers without the new method keep their
existing behavior.
Also add unit coverage in `packages/producer/src/logger.test.ts` (17
tests) for level filtering, meta formatting, the new `isLevelEnabled`
path including a hot-loop call-site simulation that asserts zero
builder invocations at info level, and the `?? true` fallback for
loggers that omit the method.
Update `docs/packages/producer.mdx` with a new "Logging" section
documenting `ProducerLogger`, `createConsoleLogger`, `defaultLogger`,
and the `isLevelEnabled` gating pattern.
This closes 8C in `plans/hdr-followups.md`. 8D (gating the
`countNonZeroAlpha` / `countNonZeroRgb48` diagnostic counters) was
verified during the same work to already be guarded by
`shouldLog = debugDumpEnabled && debugFrameIndex >= 0`, where
`debugDumpEnabled` is itself driven by `KEEP_TEMP=1`, so the pixel
iteration is fully skipped on production runs and no code change was
needed.
Made-with: Cursor
8610f7e to
0d2175b
Compare
55a41a8 to
349f0f6
Compare
0d2175b to
cd40e4b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Add an optional
isLevelEnabled(level)method toProducerLoggerand use it to short-circuit per-frame HDR composite metadata construction inrenderOrchestratorwhen the log level is above debug.Closes Chunks 8C and 8D from
plans/hdr-followups.md.Why
Chunk 8Cofplans/hdr-followups.md. The per-frame HDR composite snapshot (every 30 frames) was building anArray.find+toFixed+ struct allocation unconditionally and handing it to a debug logger that immediately discarded it atlevel="info". On long renders, this is allocation pressure and CPU time wasted on log meta nobody reads.Chunk 8Dwas investigated in the same pass and found to already be guarded — see below.What changed
isLevelEnabled(level: ProducerLogLevel): booleanonProducerLogger.createConsoleLoggerimplements it.renderOrchestrator.tsper-frame HDR composite snapshot is now gated oni % 30 === 0 && (log.isLevelEnabled?.("debug") ?? true)— production runs atlevel="info"skip the meta-object construction entirely; custom loggers without the new method keep their existing behavior thanks to the?? truefallback.packages/producer/src/logger.test.ts(17 tests) covering level filtering, meta formatting, theisLevelEnabledpath, a hot-loop call-site simulation that asserts zero builder invocations at info level, and the?? truefallback for loggers that omit the method.docs/packages/producer.mdxgains a new "Logging" section documentingProducerLogger,createConsoleLogger,defaultLogger, and theisLevelEnabledgating pattern.8D resolution (no code change).
countNonZeroAlpha/countNonZeroRgb48calls live behindshouldLog = debugDumpEnabled && debugFrameIndex >= 0, wheredebugDumpEnabledis itself driven byKEEP_TEMP=1. The pixel iteration is fully skipped on production runs already, so 8D needed no fix — verified during the 8C work.Test plan
bun testin producer — 17/17 logger tests pass; existing service tests unchanged.level="info".?? truefallback preserves prior behavior for custom logger implementations that don't define the method.Stack
Chunks 8C + 8D of
plans/hdr-followups.md. Sits on top of the benchmark harness PR (Chunk 8A) so the optimization is measurable.