Skip to content

perf(producer): gate per-frame debug meta via optional isLevelEnabled#383

Open
vanceingalls wants to merge 47 commits intovance/hdr-benchmark-harnessfrom
vance/logger-level-gating
Open

perf(producer): gate per-frame debug meta via optional isLevelEnabled#383
vanceingalls wants to merge 47 commits intovance/hdr-benchmark-harnessfrom
vance/logger-level-gating

Conversation

@vanceingalls
Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls commented Apr 21, 2026

Summary

Add an optional isLevelEnabled(level) method to ProducerLogger and use it to short-circuit per-frame HDR composite metadata construction in renderOrchestrator when the log level is above debug.

Closes Chunks 8C and 8D from plans/hdr-followups.md.

Why

Chunk 8C of plans/hdr-followups.md. The per-frame HDR composite snapshot (every 30 frames) was building an Array.find + toFixed + struct allocation unconditionally and handing it to a debug logger that immediately discarded it at level="info". On long renders, this is allocation pressure and CPU time wasted on log meta nobody reads.

Chunk 8D was investigated in the same pass and found to already be guarded — see below.

What changed

  • New optional isLevelEnabled(level: ProducerLogLevel): boolean on ProducerLogger.
  • createConsoleLogger implements it.
  • renderOrchestrator.ts per-frame HDR composite snapshot is now gated on i % 30 === 0 && (log.isLevelEnabled?.("debug") ?? true) — production runs at level="info" skip the meta-object construction entirely; custom loggers without the new method keep their existing behavior thanks to the ?? true fallback.
  • New packages/producer/src/logger.test.ts (17 tests) covering level filtering, meta formatting, the isLevelEnabled path, a hot-loop call-site simulation that asserts zero builder invocations at info level, and the ?? true fallback for loggers that omit the method.
  • docs/packages/producer.mdx gains a new "Logging" section documenting ProducerLogger, createConsoleLogger, defaultLogger, and the isLevelEnabled gating pattern.

8D resolution (no code change). countNonZeroAlpha / countNonZeroRgb48 calls live behind shouldLog = debugDumpEnabled && debugFrameIndex >= 0, where debugDumpEnabled is itself driven by KEEP_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 test in producer — 17/17 logger tests pass; existing service tests unchanged.
  • Hot-loop call-site simulation asserts the meta builder is invoked zero times at level="info".
  • ?? true fallback preserves prior behavior for custom logger implementations that don't define the method.
  • Re-ran the HDR benchmark from Chunk 8A — no regression on wall-clock, peak heap unchanged at info level.

Stack

Chunks 8C + 8D of plans/hdr-followups.md. Sits on top of the benchmark harness PR (Chunk 8A) so the optimization is measurable.

Copy link
Copy Markdown
Collaborator Author

vanceingalls commented Apr 21, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

…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
@vanceingalls vanceingalls force-pushed the vance/hdr-benchmark-harness branch from 8610f7e to 0d2175b Compare April 22, 2026 22:13
@vanceingalls vanceingalls force-pushed the vance/logger-level-gating branch from 55a41a8 to 349f0f6 Compare April 22, 2026 22:13
@vanceingalls vanceingalls force-pushed the vance/hdr-benchmark-harness branch from 0d2175b to cd40e4b Compare April 22, 2026 22:53
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