Skip to content

perf(core): scene cuts stop re-reading every clip's position - #6311

Closed
miguel-heygen wants to merge 1 commit into
mainfrom
perf/in-flow-cache-outlives-timed-set
Closed

miguel-heygen wants to merge 1 commit into
mainfrom
perf/in-flow-cache-outlives-timed-set

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

What

A scene cut in the preview no longer makes the runtime re-read every hidden clip's position. The cache of which timed clips sit in normal flow now survives changes to the set of timed elements and is refreshed only when the clip's own inline position or classes change.

Why

The visibility pass hides an in-flow timed clip with display: none and a positioned one with visibility: hidden, and asks isTimedClipInFlow which one it is. That reads getComputedStyle(el).position, which forces a style recalc, right after the pass wrote the clip's visibility. The answer was cached per element, but postTimeline dropped the whole cache whenever computeClipTreeSignature changed.

Scene parking (#6189) changes that signature at every scene cut, so at each cut the pass re-read the position of every clip it hid, one forced recalc after another. On a 50 s 3840x2160 film with 7 scenes and 112 short video clips, that was the largest part of the task at the first cut.

The signature cannot tell the cache anything about position: an element's computed position does not change because other timed elements came or went, and a replaced element is a new object, so a WeakMap keyed on it misses anyway. It changes when the element's own inline position or classes change, which the old cache did not notice until the next signature change. The leaf cache does depend on the timed set (a clip gains or loses a nested [data-start]), so that one is still dropped.

Scope next to #6189

This PR removes one forced read: getComputedStyle(el).position in isTimedClipInFlow (packages/core/src/runtime/init.ts), which the visibility pass repeated for every hidden timed clip after each change of the timed-element set, so at every scene cut once #6189 parks scenes. It does not touch #6189's own reads at a cut, getComputedStyle(host).position and offsetWidth/offsetHeight in farScenes.ts (measureLeaving), and it does not touch farScenes.ts at all. The per-cut numbers below are measured with this PR alone, #6189 alone and both, so neither claims the other's frames.

How

  • Each in-flow entry stores the inputs it was read from: the clip's inline style.position and its class attribute, both read without a recalc. An entry is reused while those match and re-read when either changes. That covers the runtime's own applyClipLayout writing position: absolute and Studio edits that set position inline or through a class.
  • invalidateTimedClipCaches now drops only the leaf cache.
  • Not covered, as before: a stylesheet edit that alone moves a clip between in-flow and positioned is not noticed until the clip's own position or classes change.

Measured

That film, previewed in <hyperframes-player> at 1x, headless Chrome 152 on an Apple silicon Mac with GPU compositing and rasterization. Base is main with #6189 at its current head on top; "after" adds this change. Each run started with the host's 1-minute load under 10, with builds in rotating order.

Profiled (unminified runtime, CPU profiler on), playing 0.5 s to 10 s, three runs each:

before after
main-thread task at the 8.5 s cut 36.4, 28.0, 28.7 ms 19.0, 16.7, 18.8 ms
isTimedClipInFlow in that task 16.3, 16.3 ms (one run's samples landed in (program)) 5.8, 5.0, 5.2 ms
style recalc over the whole window 119.2, 115.6, 125.8 ms 118.8, 112.8, 119.0 ms

What isTimedClipInFlow still costs at the cut is clips hidden for the first time, whose position has never been read.

Without the profiler (minified runtime), playing 0 s to 9.8 s, six runs each in both orders: no film frames were missed either way, and the longest frame (18 to 30 ms in both builds) did not separate. The first scene's own frames are that long, so the cut is no longer the longest frame on this film either way.

Per cut, next to #6189

Longest frame of main-thread work from just before each scene cut to 0.5 s after it, whole film at 1x, minified runtime, no profiler. Four builds on the same main commit (main, this PR alone, #6189 alone, both), three rotations in changing order, each run started with the host's 1-minute load under 10. Values are the three runs, in ms.

cut main this PR #6189 both
8.5 s 49.3 / 49.6 / 33.1 49.1 / 49.6 / 33.5 14.8 / 30.2 / 26.3 28.2 / 18.5 / 29.3
17.46 s 76.3 / 89.1 / 51.0 84.0 / 77.4 / 53.2 28.8 / 39.1 / 33.7 29.5 / 29.6 / 29.9
23.17 s 64.5 / 65.5 / 50.1 88.5 / 62.3 / 43.6 24.6 / 127.1 / 121.3 129.2 / 116.8 / 131.4
30.5 s 47.8 / 48.7 / 32.1 46.4 / 46.9 / 31.8 61.6 / 75.4 / 72.5 64.3 / 64.2 / 62.2
40.04 s 60.1 / 61.4 / 45.1 64.2 / 59.4 / 45.1 16.8 / 16.9 / 19.1 18.5 / 20.8 / 17.0
45 s 58.1 / 57.3 / 31.0 55.4 / 59.1 / 28.8 5.9 / 5.2 / 5.6 5.2 / 5.6 / 5.1
film frames missed, of 1,201 87 / 91 / 16 77 / 96 / 15 0 / 2 / 3 2 / 1 / 2

This PR alone does not move any cut: without #6189 the timed set does not change at cuts. With #6189, adding it does not separate from #6189 alone at any cut, including 8.5 s; the profiled saving at that cut (above) is smaller than the run-to-run spread of the cut frame. The large frames at 23.17 s and 30.5 s in both #6189 columns are #6189's measureLeaving reads and are not addressed here.

Test plan

  • Unit tests added/updated

  • Manual testing performed

  • Documentation updated (if applicable)

  • Comments follow CONTRIBUTING.md "Comments": they say why, not what, and a bug fix says what the code must do and how to reproduce the bug

  • init.test.ts: after another timed clip is added (the clip tree signature changes), hiding three positioned clips again reads none of their computed styles (fails on main: 3 reads). A clip whose inline position changes from absolute to static is taken out of the flow on its next hide (fails on main: it keeps the stale positioned reading). A clip whose class change drops it from a positioning rule is taken out of the flow (fails with the class left out of the cache inputs).

  • The new tests pass 3 runs in a row. Full packages/core/src/runtime suite: 98 files, 2,434 tests, exit 0. Pre-commit lint, format, typecheck and fallow pass; check-comment-citations and comment-ratchet pass.

@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 · head 53129a4b

  • nit: packages/core/src/runtime/init.ts:3637 still says "leaf/in-flow caches may be stale" above invalidateTimedClipCaches(), which now drops only the leaf cache.
Scope

Read the full diff and the visibility pass that calls isTimedClipInFlow (init.ts:3160), which is its only caller; invalidateTimedClipCaches (init.ts:3639) is the only invalidation site. A WeakMap keyed on the element already misses for a replaced element, so dropping the in-flow map on signature change bought nothing. Runtime writes to clip classes (__hf-flash, __hf-pick-highlight) or inline position (applyClipLayout) cost one re-read each, not one per frame. Open #6189 doesn't touch this code. Ran locally: the 3 new tests fail on main's init.ts and pass at this head; src/runtime has 98 files and 2,434 tests passing. CI was still running at review time, with no failures so far.

@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy did not run: 0 of 5 shards reported results; see the shard jobs.

Nothing was compared with the base branch.

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Closing: no measurable frame win on the film this targeted.

Longest frame of main-thread work from just before each scene cut to 0.5 s after it, ms, three runs per build. Whole 50 s 3840x2160 film at 1x, minified runtime, four builds on the same main commit in rotating order, each run started with the host's 1-minute load under 10.

cut main this PR #6189 both
8.5 s 49.3 / 49.6 / 33.1 49.1 / 49.6 / 33.5 14.8 / 30.2 / 26.3 28.2 / 18.5 / 29.3
17.46 s 76.3 / 89.1 / 51.0 84.0 / 77.4 / 53.2 28.8 / 39.1 / 33.7 29.5 / 29.6 / 29.9
23.17 s 64.5 / 65.5 / 50.1 88.5 / 62.3 / 43.6 24.6 / 127.1 / 121.3 129.2 / 116.8 / 131.4
30.5 s 47.8 / 48.7 / 32.1 46.4 / 46.9 / 31.8 61.6 / 75.4 / 72.5 64.3 / 64.2 / 62.2
40.04 s 60.1 / 61.4 / 45.1 64.2 / 59.4 / 45.1 16.8 / 16.9 / 19.1 18.5 / 20.8 / 17.0
45 s 58.1 / 57.3 / 31.0 55.4 / 59.1 / 28.8 5.9 / 5.2 / 5.6 5.2 / 5.6 / 5.1
film frames missed, of 1,201 87 / 91 / 16 77 / 96 / 15 0 / 2 / 3 2 / 1 / 2

This PR alone matches main, and on top of #6189 it does not separate from #6189 alone at any cut. The profiled saving at the 8.5 s cut (the cut task went from 28 to 36 ms down to 17 to 19 ms) is smaller than that cut frame's run-to-run spread. The staleness it fixes (an inline position edit not re-read) only matters once the cache is kept longer, and main drops it at every timed-set change anyway.

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