Skip to content

perf(studio): the layer walk skips clips the runtime hid, so scrubbing a long film stays smooth - #6416

Merged
miguel-heygen merged 1 commit into
mainfrom
perf/studio-layer-walk-skips-hidden-clips
Oct 26, 2026
Merged

miguel-heygen merged 1 commit into
mainfrom
perf/studio-layer-walk-skips-hidden-clips

Conversation

@miguel-heygen

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

Copy link
Copy Markdown
Collaborator

What

Scrubbing a long film in Studio no longer slows down as the film gets longer. While the playhead moves, the editor's layer list skips the scenes the runtime has hidden, so each update costs what is on screen, not the whole document.

Why

Two consumers rebuild the layer list from collectDomEditLayerItems while a scrub mutates the preview:

  • the off-canvas indicators (useOffCanvasIndicators), which are mounted whenever the DOM edit overlay is, and rebuild at most every 100 ms on any DOM mutation;
  • the Layers panel, which re-collects on each playhead tick while its tab is open.

Each rebuild visited every element of every scene and called isInspectableLayerElement on it, which reads its computed style. On a film with 230 scenes of 600 elements that is about 276k style reads per rebuild, almost all of them in scenes that are not on screen.

The runtime already hides everything inside a clip its visibility pass hid: it marks the clip data-hf-clip-hidden, sets its inline visibility: hidden, and a stylesheet rule hides its contents (packages/core/src/runtime/timedClipHide.ts). Nothing inside such a clip can be a layer, so the walk can stop there.

How

  • isClipHiddenByRuntime(el) in @hyperframes/core/studio-preview-mark: the clip has the mark and an inline visibility of hidden. Both are required: older shader builds reveal a clip through its inline style only and leave the mark, and such a clip is on screen.
  • collectDomEditLayerItems returns early on such a clip.
  • CLIP_HIDDEN_ATTR moves to studio-preview-mark so the runtime and Studio read one constant; clipReveal.ts re-exports it, so runtime imports are unchanged.

What changes in output

None. This is Studio editor code only; render and snapshot are untouched. The layer list and the off-canvas indicators list the same items as before: isInspectableLayerElement already dropped everything inside a hidden clip, because its computed visibility is hidden. The early return only skips reading those styles.

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

  • domEditingLayers.test.ts, "collectDomEditLayerItems and clips the runtime hid":

    • lists what is on screen and reads the computed style of nothing inside a hidden clip with 50 children. Without the early return it reads 51.
    • still lists a marked clip whose inline style shows it.
    • Passed 3 runs in a row.
  • packages/studio src/components/editor: 155 files, 1,974 tests pass. packages/core timedClipHide and clipReveal tests pass.

No visible change

13 changed non-test lines, no .tsx, .css or .html. The Layers panel and the off-canvas indicators show the same items; only the time spent finding them drops.

Why under 100 lines: Miguel's asap P0 for the next Desktop build. One early return in one walk, plus the predicate next to the mark it reads; it shares nothing with any other open change.

…g a long film stays smooth

The off-canvas indicators (always mounted) and the Layers panel rebuild their layer list every
100 ms while a scrub mutates the preview, and each rebuild read the computed style of every element
of every scene, on screen or not. The runtime already hides everything inside a clip its visibility
pass hid, so those subtrees hold no layer: the walk now skips a clip marked data-hf-clip-hidden
with an inline visibility of hidden. The marker constant moves to studio-preview-mark so the runtime
and Studio share one owner.
@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 149 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 (0)

Unstable (1)

  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@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 · 0 important · 0 nits · head aa22718

blocker: the required Studio and player captures check fails on this head. The body has no ## Before/## After section and no ## No visible change section (scripts/check-pr-captures.mjs). This change qualifies for ## No visible change: 13 non-test lines and no .tsx/.css/.html. It is also accurate. Before this PR, isInspectableLayerElement already dropped a runtime-hidden clip and its contents, because their computed visibility is hidden, the clip through its inline style and its contents through the always-installed [data-hf-clip-hidden][data-hf-clip-hidden] * rule. The early return only skips the style reads. So the line "A hidden scene's elements no longer appear in them" describes no change. This is a body-only fix and the head stays the same.

I found no problem in the code. Every other workflow on this head is green, and domEditingLayers.test.ts passes locally (26/26).

@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 · 1 resolved, 0 open, 0 new · aa22718 → aa22718

✅ The body now has ## No visible change and no longer claims an output change. A re-run of the original run used the old event payload, so I re-ran the edited captures run. It passed, and so did every other workflow on this head.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit dc56b3d Oct 26, 2026
127 of 131 checks passed
@miguel-heygen
miguel-heygen deleted the perf/studio-layer-walk-skips-hidden-clips branch October 26, 2026 13:41
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Measured in the real Desktop app, before and after this change (the app is the only consumer that matters for the scrub target): a 23-minute film of 276,460 elements, a quiet Mac, three 6 s drags of the seek knob with a CPU profile of each.

before (app on Studio 0.8.202) after (this change, Studio 0.8.204)
seek knob drag, cold 9.3 fps 13.8 fps
seek knob drag, warm 13.0 / 14.1 fps 37.5 / 47.0 fps
film player seeking once per frame, warm 5.2 / 5.7 fps 33.5 / 35.5 fps
seek moves served in 6 s, warm 26 / 28 72 / 85

In the warm profile the layer walk went from about 65% of the sweep to about 45% (the selector index and findClosestByAttribute, addressed by #6427, plus the composition re-read, #6429), with the browser's own style and layout at about 20%.

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