perf(studio): resolve manifest clips through one pass-scoped DOM index - #2941
Draft
miguel-heygen wants to merge 1 commit into
Draft
perf(studio): resolve manifest clips through one pass-scoped DOM index#2941miguel-heygen wants to merge 1 commit into
miguel-heygen wants to merge 1 commit into
Conversation
Opening a project ran up to three whole-document scans per clip. The manifest identifies clips with `stableClipId` (`el.id` OR `data-hf-id`) while the studio lookup asked `getElementById`, which never matches the second half — so every hf-id clip missed the fast path and fell through to `getTimelineDomNodes`, a fresh `[data-start]` scan whose `isTimelineIgnoredElement` filter walks `closest()` over every node. At 3,000 clips that is 37M ancestor walks rejecting nothing. Build one index per `processTimelineMessage` pass and thread it to all three emitters: node resolution, the source-scoped selector-occurrence index, and the composition branch that re-resolves a host when the manifest carries no `compositionSrc`. The index is derived from a single document walk and is discarded with the pass, so an edit that mutates the preview DOM cannot resolve against stale nodes. Identity is unchanged. `usedNodes` claiming still runs through the resolver, the existing `data-composition-id` / class / attribute-match fallback chain is intact for clips the index cannot resolve, the source file of an element is derived to match `closest()` exactly, and `getSourceScopedSelectorIndex` keeps its indexless signature for callers that have no index. The load-bearing test runs the FULL derivation twice over a generated 3,000-clip project — node resolution included, not a pre-resolved host — and asserts every `id` and `key` is byte-identical, covering the clips whose `clip.id` is null. A divergence there would silently drop selection, expanded state, the keyframe cache and lint findings.
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.
PR 2 of 5 · base
perf/studio-load-and-open(#2940)Opening a 3,000-clip project spent 2,880 ms turning the clip manifest into timeline elements. This makes it 10 ms.
The root cause: two ideas of what a clip's identity is
The runtime mints clip ids as
stableClipId(el) = el.id ?? el.getAttribute("data-hf-id"). Studio looked them up withdoc.getElementById(id)— which matches theidattribute and neverdata-hf-id. In a real sample only 13 of 58 authored clips carry a DOMid, so for most clips the indexed lookup missed and fell through to a whole-document scan, per clip, at three separate call sites.What the profile actually said
Not what I expected.
isTimelineIgnoredElementwas 5,292 ms of self time — 40% of all CPU during open. It runsel.closest()against a four-selector list, andgetTimelineDomNodesapplies it to every[data-start]node, roughly four times per clip. Counted by patchingElement.prototype.closestin the page:The same work done once measures 1.3 ms in the live page.
The change
One
TimelineDomIndexbuilt perprocessTimelineMessagepass from a single document walk, threaded to all three emitters: node resolution (now keyed ondata-hf-idas well asid), the source-scoped selector-occurrence index, and the composition branch that re-resolves a host whencompositionSrcis absent. Pass-scoped, never memoised on the document — the preview DOM mutates during editing and a stale index would resolve clips to detached nodes.usedNodesclaiming and the full fallback chain are preserved.Two constraints a naive index gets wrong
timeline.tscomputesid: stableClipId(node) ?? nodeCompositionId ?? nullwith no ordinal fallback, unlikeclipTree.ts. Clips with noid, nodata-hf-idand no composition id arrive withclip.id === nulland genuinely need the selector-occurrence path. An id-map-only index silently breaks exactly those.mergeTimelineElementsPreservingDowngradesandtimelineElementsChangeddiff purely onkey ?? id. A different identity reads as element replacement and silently drops selection,expandedClipIds, the keyframe cache and lint findings — a data-loss bug wearing a performance fix's clothes.The parity test guards that second one, and it is worth knowing what it cost: an earlier revision passed
hostElin directly, so it compared only the identity builder and could not have caught the indexed path resolving a clip to a different node — the exact failure it exists to detect. Both arms now run the full derivation. That took the test from 5 s to ~85 s, because the un-indexed control arm is the quadratic. It gets cheaper only if someone reintroduces the quadratic, which is itself the signal.Result
Same machine, control run at
HEAD~1reproducing the recorded baseline within 1%, which is what makes the delta attributable rather than machine luck:hf.studio.manifest-derivehf.studio.project-openVirtualization-off arm: derive 2,926 → 10.3 ms, open 4,915 → 2,250 ms. Independently reproduced: derive 10.2 ms, open 1,403 ms.
The query-count test asserts a 500-clip derivation (including a composition clip with no
compositionSrc) callsdoc.querySelectorAllexactly once anddoc.querySelectorzero times.timeline-virtualization.mjspasses both arms unchanged.stableClipIdinpackages/coreis untouched.Preview is now 91% of open time — PR 3 addresses it.