Skip to content

feat(core): motion blur reports what a composition holds, as counts - #7306

Merged
vanceingalls merged 2 commits into
mainfrom
feat/blur-fingerprint-telemetry
Nov 6, 2026
Merged

vanceingalls merged 2 commits into
mainfrom
feat/blur-fingerprint-telemetry

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

What

Motion blur now reports WHAT a composition holds, as counts, in the render log and in render telemetry, so the next failure class shows up as a rate ("films with a mover under a panel") before anyone files a bug. Five of the six blur bugs so far came from a composition feature no fixture held.

  • MotionBlurFingerprint (one definition in core/src/motionBlurFingerprint.ts, imported by engine and cli): canvas2d, webgl, video, iframe, customElement, svgClip, mask, scrollContainer, threeD, overflowingMover, frontOfMover, blendMode, fixedInMover, pseudoOutside, all numbers. Computed once per scan in discover on both tiers, never per frame; the element walk is capped at 4096 nodes and the per-mover descendant walk at 256, so the counts are lower bounds. holdsFixedChild (now in frames.ts) and pseudoBox are reused. Report.fingerprint (null before a scan) and Report.readEvents.
  • frontOfMover counts movers that something else inside the composition root sits above at any sampled scan step (elementsFromPoint at the mover's centre every ceil(steps/24) steps, at most 25 samples per candidate, a counted mover is not tested again; the tier's own canvases and the filter svg sit outside the root so never count). The first version sampled once at the restore pose and read 0 for anything entering after t=0: reviewed and changed before this PR. Real Chrome 153 on a mover passing behind a panel (behind-panel): the old code printed frontOfMover 0 (and, with two capture workers, a worker-dependent 0 or 1); this code prints frontOfMover:1 in every case.
  • Log line: formatPageMotionBlur appends readEvents=true (only when true) and fp=webgl:1,video:2 (non-zero counts, field order); the existing [motion-blur] on reasonCode=... movers=... failedFrames=... tier=... text is byte-identical when there is no fingerprint.
  • Render result: RenderPerfSummary.motionBlur?: { tier, reasonCode, movers, failedFrames, readEvents, fingerprint }: first reporting worker wins, failedFrames summed. failedFrames is fresh at session end because the session holds one pageMotionBlur object that getCapturePerfSummary hands out and closeCaptureSession updates in place.
  • Telemetry: trackRenderComplete (CLI renders and Studio renders) sends motion_blur_tier, motion_blur_movers, motion_blur_failed_frames and one motion_blur_fp_<snake_case_field> per non-zero count, nothing when blur never ran. readEvents is not sent.

Privacy (please check this part)

Counts only: the runtime only increments numbers; the engine rebuilds the object from the 14 known keys with Number(x) || 0 (a test feeds id: "secret" and video: "many" and neither gets through); the CLI iterates the same field list. moverNames is never forwarded and reasonCode is not sent to telemetry. The existing opt-out (trackEvent returns when !shouldTrack(): HYPERFRAMES_NO_TELEMETRY, DO_NOT_TRACK) and the Studio telemetryOptOut path cover the new properties without extra code. Cardinality: 3 scalars plus at most 14 small integers under fixed names.

Tests

core captureMotionBlur 242 pass (fingerprint: one composition per field, carries no names, the cap, the sampler cases, onFast called once per scan step on the first pass only), engine 80, producer 64 (vitest) + 86 (bun test), cli 219; tsc clean in all four packages. A real producer render of webgl-canvas logs [motion-blur] on reasonCode=tier-auto movers=1 failedFrames=0 tier=filter fp=webgl:1; a forced drawElementImage failure across two workers gives failedFrames 30 and 31 at session end, summed to 61 in the perf summary and in the telemetry payload (mapping run offline; the durable proof is the two-worker aggregation test and the in-place close test). Per-file flake timings 1 to 6 s.

Limits

  • frontOfMover is a lower bound: a covering overlay with pointer-events: none (vignette, light leak) is not in elementsFromPoint; a mover passing behind a thin cover between two samples is missed; a transparent full-size container with pointer events counts as covering.
  • Past the 4096-node cap the counts undercount silently.
  • Cost on a 10,000-node page is not measured; the sampler is bounded at candidates x 25 calls (2650 for a 106-mover film).
  • Deferred minors from review: canvas2d includes the capture tier's own canvases on a rescan while wrapped; the recorder keeps the latest failedFrames rather than the largest (a capture-to-filter hand-off mid-session resets it to 0).

Size budget. @hyperframes/inspector unpacked goes 3,645,432 to 3,684,092 B in scripts/package-size-budgets.json: the runtime fingerprint code is bundled into inspector, which put the build 2.2 KB past the 1% tolerance. docs/packages/cli.mdx now lists the composition counts under what telemetry collects.

🤖 Generated with Claude Code

The runtime scans a composition once per blur scan, on both tiers, and reports
counts of what it holds (canvases, video, frames, custom elements, clips, masks,
scroll containers, 3D, blend modes, and movers that overflow, sit under other
content, hold fixed children or carry pseudo-elements outside their box), plus
whether pose reads need callbacks. Counts only: no ids, classes, text or URLs.

The engine puts the non-zero counts on its [motion-blur] line and hands the
parsed report to the perf summary; the producer aggregates workers (first report
wins, failed frames sum) and the CLI sends motion_blur_* properties on
render_complete, from the CLI and from Studio renders.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 107 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES · 1 blocker · 1 important · 1 nit

Privacy holds: only counts reach render_complete, the engine rebuilds the 14 keys, tier is narrowed to capture/filter, and both CLI and Studio go through shouldTrack/telemetryOptOut. No names or reasonCode leave.

  • blocker: CI Build is red at verify:packed-manifests. @hyperframes/inspector unpacked is 3,684,092 B against a 3,645,432 B budget, about 2.2 KB past the 1% tolerance. Main's Build is green, so this PR causes it. Raise scripts/package-size-budgets.json and give the reason in the body (the runtime fingerprint code is bundled into inspector), or trim.
  • important: docs/packages/cli.mdx:2267 lists each telemetry category. Counts of what a composition holds (videos, iframes, canvases, custom elements…) is a new category. Add it there so the disclosure matches what the CLI sends.
Scope

Read the full non-test diff and the CLI/engine/producer tests. Locally, core captureMotionBlur (181), engine pageMotionBlur (20) and cli renderMotionBlur + studioRenderTelemetry (20) pass; I did not run producer. The Docker CLI path sends no blur properties because it has no perf summary. The producer server's HTTP response and log.info carry the full summary, reasonCode included; that is not telemetry. I am deferring to a CLI-telemetry owner on the second-eyes ask. CodeQL, regression and Windows were still running.

— Genesis


function parseFingerprint(raw: unknown): MotionBlurFingerprint | null {
if (!raw || typeof raw !== "object") return null;
const count = (field: string): number => Number(Reflect.get(raw, field)) || 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Number(x) || 0 still lets page-supplied Infinity, negatives and fractions through. movers (line 82) gets no sanitizing at all, so NaN can reach motion_blur_movers. Clamp both to finite non-negative integers so "small integers" holds at this layer too:

Suggested change
const count = (field: string): number => Number(Reflect.get(raw, field)) || 0;
const count = (field: string): number => {
const n = Math.trunc(Number(Reflect.get(raw, field)));
return Number.isFinite(n) && n > 0 ? n : 0;
};

Do the same for movers.

The runtime fingerprint code is bundled into @hyperframes/inspector, which
puts its unpacked size 2.2 KB past the 1% tolerance. The telemetry docs now
list the composition counts the CLI sends with a blur render.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@mintlify

mintlify Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Nov 6, 2026, 9:53 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE · 0 blocker · 0 important · 1 nit · 2 resolved, 1 open (nit) · 6ae6059 → 473b458

✅ Inspector budget raised to the measured 3,684,092 B, with the reason in the body. CI / Build is green on 473b458.
✅ docs/packages/cli.mdx now lists the composition counts under telemetry.

The Number(x) || 0 clamp nit on pageMotionBlur.ts:63 is still open and optional.

— Genesis

@vanceingalls
vanceingalls added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit a874a63 Nov 6, 2026
177 of 178 checks passed
@vanceingalls
vanceingalls deleted the feat/blur-fingerprint-telemetry branch November 6, 2026 15:23
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Nov 6, 2026
…view to heygen-com#7258

Adopts heygen-com#7306 (motion-blur composition counts in render_complete telemetry,
adapted to discoverMovers; readEvents stays false without heygen-com#7191) and heygen-com#7318
(a set that interrupts a tween holds at any worker count) together with its
merged prerequisite heygen-com#6851. PR axis advances to heygen-com#7258; heygen-com#7253 and heygen-com#7257 are open
and pinned as monitored (97->99).

Co-authored-by: Vance Ingalls <vance@heygen.com>
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant