Skip to content

fix(engine): keep BeginFrame time monotonic across frame rates - #3011

Merged
miguel-heygen merged 1 commit into
heygen-com:mainfrom
zihanyang-dev:fix/beginframe-monotonic-fps
Aug 5, 2026
Merged

miguel-heygen merged 1 commit into
heygen-com:mainfrom
zihanyang-dev:fix/beginframe-monotonic-fps

Conversation

@zihanyang-dev

@zihanyang-dev zihanyang-dev commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #3012

What

Fixes HeadlessExperimental.beginFrame hanging at 60fps and other high frame rates.

Why

Warmup always advances Chrome in 33ms steps, but the first capture timestamp was calculated from the output fps.

At 60fps, the last warmup tick is 1947ms, while the next commit tick used to jump back to about 1067ms. Chrome sees time moving backwards and beginFrame can stall.

How

Keep the old timestamps when they are already safe. If the commit tick would go backwards, move the capture baseline past warmup first.

The init commit and both producer probes now use the same timestamp helpers, so they cannot calculate different values.

Test plan

  • Added tests for 24, 30, 31, 32, 33, 60, 120, 240, and 59.94fps
  • Added coverage for the actual session baseline, commit parameters, and both producer probe paths
  • Engine tests: 1383 passed, 3 skipped
  • Producer unit tests passed
  • Engine and producer typechecks and builds passed
  • Manual render test

Warmup advances Chromium on a fixed 33ms clock, but the capture
baseline was derived only from the output frame interval. At rates
above 30fps this moved BeginFrame timestamps backwards and could
stall headless Chrome.

Preserve the legacy baseline whenever it is already monotonic.
Otherwise continue from the warmup clock, then add capture-rate
headroom. Centralize commit and probe tick derivation so every
producer path uses the same ordering contract.

Add locked, unlocked, integer, and NTSC regression coverage, plus
production probe argument assertions.

@miga-heygen miga-heygen 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.

R1 — Monotonic BeginFrame timing (head 22bc4d6)

Verdict: Approve

Clean fix for a real hang at high frame rates. The root cause is clear: warmup runs at 33ms intervals, but the capture baseline was calculated from the output fps. At 60fps, the commit tick falls BEFORE the last warmup tick (1067ms vs 1947ms), Chrome sees time go backwards, and beginFrame stalls.

What was verified

deriveBeginFrameTimeTicks logic — correct at all tested rates:

fps captureInterval legacyCommit lastWarmup legacy safe? result
30 33.33ms 2133ms 1947ms yes legacy (2333ms)
60 16.67ms 1067ms 1947ms no monotonic (2147ms)
120 8.33ms 533ms 1947ms no monotonic (2063ms)
240 4.17ms 267ms 1947ms no monotonic (2022ms)

The crossover is at ~33fps (where captureInterval matches warmupInterval). Everything at or below ~33fps preserves the legacy baseline (backward-compatible). Everything above gets rebased past warmup. The invariant warmup < commit < probe < capture holds in all cases.

SSOT improvements:

  • Probe tick calculation was inline in TWO places (renderChunk.ts and probeStage.ts) — both now use deriveBeginFrameProbeTimeTicks. Good dedup.
  • Commit tick calculation was inline in initializeSession — now through preparedBeginFrameTimeline.commitParams.
  • Three magic constants (10, 6, 5) are now named: BEGIN_FRAME_CAPTURE_HEADROOM_INTERVALS, BEGIN_FRAME_COMMIT_LEAD_INTERVALS, BEGIN_FRAME_PROBE_LEAD_INTERVALS.

Tests: Strong coverage — parametric tests across 9 frame rates (24, 30, 31, 32, 33, 60, 120, 240, 59.94), monotonicity assertions, legacy-preservation assertions, unlocked warmup, and session-init wiring. Producer probe test asserts the exact frameTimeTicks passed to probeBeginFrameLiveness.

Edge cases:

  • LOCKED_WARMUP_TICKS = 0: legacy commit = 4 * captureIntervalMs > 0 → always safe. Correct.
  • Unlocked warmup with fewer ticks: explicitly tested.
  • Math.max(0, ...) in probe derivation guards against negative ticks at very low capture rates. Correct.

No findings. Ship it.


Review by Miga

@zihanyang-dev

Copy link
Copy Markdown
Contributor Author

@miguel-heygen This is ready to merge.

@miguel-heygen
miguel-heygen merged commit b390b71 into heygen-com:main Aug 5, 2026
130 of 142 checks passed
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.

BeginFrame can hang at 60fps after warmup moves Chrome time backwards

3 participants