Repository navigation
perf(engine): wait for a missing composition timeline once per render, not once per browser - #5227
Open
miguel-heygen wants to merge 3 commits into
Open
miguel-heygen wants to merge 3 commits into
miguel-heygen wants to merge 3 commits into
Conversation
…, not once per browser A composition host that never registers a timeline (CSS, canvas or rAF animation without data-no-timeline) made every capture session of a render wait the full player-ready timeout: calibration, then the workers, then each static-frame check page. The first session that times out now records the host ids in a memo shared by the render's sessions; later sessions report the same timeout warning without waiting again.
Edit accuracy: accurate 2059 (base branch 2059), smooth 1601 of thoseThe gate passes. Quarantined, measured but not gated (0) |
…d chunks share one memo The verification page now gets the session's failed-script list, so it bails after the short grace like the main page instead of waiting the full timeout. Distributed chunks share one timeline-wait memo across their sessions. The poll takes an options object instead of eight positional parameters.
… still fails the render A session that skipped the wait classified it at once, before a late page error could arrive, so a render that used to stop on a script failure could ship. The memo is now written only by a wait that ran its full length with no page errors and no failed script loads; a replaying session that already sees a failed script load reports a script failure.
miguel-heygen
marked this pull request as ready for review
October 8, 2026 16:05
5 tasks done
This branch has not been deployed
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.
What
A composition whose animation is not a GSAP timeline (CSS keyframes, canvas,
requestAnimationFrame) and whose host lacksdata-no-timelinenow pays the timeline wait once per render instead of once per browser session.Why
The engine waits up to the player-ready timeout (45 s by default) for every
[data-composition-id]host to registerwindow.__timelines[id]. A host that never registers costs that full wait in every session of a render: the calibration session, then the capture workers, and each static-frame verification page. The outcome cannot change between sessions of the same render, so every wait after the first is spent on a result already known.A 5-second CSS-only composition (the
css-spinner-render-compatfixture withdata-no-timelineremoved) took 93.8 s to render on a Mac: 45.6 s in calibration and 47.5 s in capture.Related work
Builds on the fail-fast for failed script loads (
scriptLoadFailures) in the same poll. Does not change the timeout, the warning, or the opt-out.How
SubTimelineWaitMemo(enginetypes.ts) is a render-scoped record of host ids whose wait already timed out. The producer creates one per render and passes it through the probe options andbuildCaptureOptions, so every session of the render shares it. A session created without one gets its own.waitForSubCompositionTimelinesreplaces the two identical wait blocks ininitializeSession. It passes the memo's ids topollSubCompositionTimelines, and records the pending ids after a real timeout (not after a VFX-failure stop).pollSubCompositionTimelinesskips known ids while polling, then lists hosts that are still unregistered. If a known host is still missing, the session reportstimeoutwith the same pending ids and the samesub_timeline_readiness_timeoutwarning, and it skips the rebind, exactly like a session that waited. If it registered since, the session reportsreadyand rebinds.sub_timeline_script_failurestill reaches the render policy and still fails the render. A replaying session that already sees a failed script load reportsscript_failure, nottimeout.pollSubCompositionTimelinestakes an options object instead of eight positional parameters; existing callers and tests are updated.Test plan
frameCapture-subTimelineMemo.test.tsruns the real page-side check scripts against a fake DOM on fake timers:timeoutplus the pending id;readyand rebinds.frameCapture-staticDedupVerificationPage.test.ts: the verification page settles with no timer advance for a memo host, and settles between 1.999 s and 2.5 s when a script failed to load.frameCapture-subTimelineMemo.test.ts: no memo after a page error, no memo after a VFX stop, and a replaying session with a failed script load reportsscript_failure.Known residual differences (accepted)
ffmpeg -f framemd5.Before / After
Linux, 8 cores, software GPU (screenshot capture), Chrome headless shell 152. Median of 5 runs (min-max), same box, load1 under 8 throughout;
many-cutsis 3 runs. Output compared by decoded frame hash (ffmpeg -map 0:v -f framemd5) and audio hash (-f md5).css-spinner-render-compatwithoutdata-no-timeline), 5 smany-cuts(GSAP, every host registers)Re-run at this head (2 runs per composition): video and audio hashes are identical to origin/main for all three.
Where it went: capture setup (the first session's wait) is unchanged at ~45.7 s. Frame capture drops from 46.8-49.8 s to 1.8-3.1 s because the workers no longer wait again.
The first 45 s wait per render is unchanged on purpose. Skipping it would mean guessing from the page that no timeline will ever arrive, which changes the documented
data-no-timelinecontract.Independent review
An adversarial review at this head found no blocking or major issues. An earlier round found one blocker, now fixed: a replaying session could downgrade a script failure to a plain timeout. Minor items accepted: