perf(studio): virtualize timeline clip windows - #2703
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
0afcb3c to
b7a9c06
Compare
6883a5e to
99125c6
Compare
b7a9c06 to
da7adb9
Compare
99125c6 to
994baf5
Compare
da7adb9 to
4d007db
Compare
994baf5 to
ef83c66
Compare
4d007db to
6826c9f
Compare
ef83c66 to
2dd814a
Compare
6826c9f to
1964f10
Compare
2dd814a to
b09e9f9
Compare
1964f10 to
0fad6e5
Compare
b09e9f9 to
c2cae90
Compare
0fad6e5 to
0c6561c
Compare
236fc38 to
1761957
Compare
1761957 to
79f7704
Compare
c588a64 to
143d281
Compare
|
Addressed at
|
4f820c4 to
e5ce10a
Compare
76db7a2 to
46c0d25
Compare
e5ce10a to
06c0f1d
Compare
46c0d25 to
383514d
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
🔴 Changes requested at exact head 383514d48407a55d40670a926b4bf292cced6a1f.
I reviewed this as the exact #2703 delta (06c0f1df..383514d4), not as a blended stack. Two assumptions in the core windowing path do not hold.
Findings
P1 — the interval index is followed by a full-row scan on every render
TimelineLanes.tsx:128-155 queries queryTimelineClipIndex(...), converts the result to a Set, and then calls els.filter(...) across every clip in the row. That makes the hot scroll/render path O(n) in the total clips on each visible row, even though #2702 built the interval index specifically to make the same query proportional to the visible result.
The index already restores matches to original projection order (timelineClipIndex.ts:146-173, ordinal sort), so the full scan is not needed for z/DOM order. The only extra behavior in the filter is multi-drag passenger projection. That can be merged only when a multi-drag preview exists, without making every normal scroll walk the full row.
The browser gate cannot catch this shape: the 50k dense-short fixture intentionally distributes clips so each row has at most 128 roots (timelinePerformanceFixture.ts:63-69, and its test pins <= 128). A real long track with thousands of sequential clips still performs a thousands-element scan per visible row per scroll update.
Please render directly from the indexed result on the normal path, merge projected multi-drag passengers separately when active, and add a regression/perf fixture with one long row containing thousands of off-window clips.
P1 — an early reveal can be consumed before the viewport has usable geometry
useTimelineRevealClip.ts:117-133 marks a request as scrolled, focuses the pinned clip, and clears the request without requiring a non-degenerate viewport. On initial/hidden layout, clientWidth / clientHeight can still be zero. computeRevealScroll deliberately returns { left: null, top: null } for that geometry, but the reveal target is pinned so it can mount and focus successfully; the hook then clears the request. A later ResizeObserver / viewportVersion update has nothing left to retry, so the requested clip remains off-screen.
The new test sets clientWidth=300 and clientHeight=100 synchronously in the ref callback, so it never exercises the zero-size → ready transition.
Please keep the request pending until the relevant viewport axes are usable (or pass readiness explicitly), and add a regression test that starts at zero dimensions, mounts/focuses the pinned clip, then resizes and verifies the reveal scroll occurs before consumption.
Verification
git diff --checkpasses for the exact delta.- Current exact-head checks have no active failure, but the stacked branch only exposes the preview/regression subset; this review does not treat that as proof of the two missing shapes above.
Coverage ledger
Audited: all 16 changed files — Timeline.tsx, Timeline.virtualization.test.tsx, TimelineCanvas.tsx, TimelineClip.tsx, TimelineLanes.test.tsx, TimelineLanes.tsx, timelineClipDragGestureLifecycle.ts, timelineLaneProps.ts, useTimelineActiveClips.test.ts, useTimelineActiveClips.ts, useTimelineClipRenderWindow.ts, useTimelineRevealClip.test.tsx, useTimelineRevealClip.ts, useTimelineTicks.ts, useTimelinePlayer.seek.test.ts, useTimelinePlayer.ts.
Trusting: none.
— Magi
9b5b8b2 to
b8c4dd9
Compare
06c0f1d to
83f737a
Compare
b8c4dd9 to
2922d03
Compare
3a8a488 to
e2d7d99
Compare
2922d03 to
027a269
Compare
|
Addressed both exact-delta blockers on the current head
Verification: 84 focused tests pass, Studio typecheck passes, and the exact stacked 50k browser gate mounts 160 clip roots / 707 timeline descendants (4/5 hosted-runner quorum; all DOM budgets pass). Exact job: https://github.com/heygen-com/hyperframes/actions/runs/30512973405/job/90776821797 |
027a269 to
df935c5
Compare
029971e to
65a82ab
Compare
df935c5 to
20c1f09
Compare
The base branch was changed.
Fallow audit reportFound 6 findings. Duplication (5)
Health (1)
Generated by fallow. |
20c1f09 to
5f54d3f
Compare

What
Render only visible clip roots and defer horizontal reveal until virtual scroll geometry is ready.
Why
The final scalability boundary must bound clips as well as rows without hiding selected or active clips, losing reveal requests, shifting ticks, or allowing an early reveal to use stale scroll geometry.
How
This is D8 of the Family D draft review sequence. It targets current main after Family C landed; its Family D patch was replayed without semantic changes. It supersedes legacy PR #2640, which remains open for audit until landing. The unpublished local scroll-deferral source was folded here because the combined diff is 971 changed lines and both changes own the same lifecycle boundary.
Test plan
Validated on the exact Family D tip with virtualization, active-clip, reveal, and player-seek coverage; the full Studio suite (2,939 passed; 18 todos); Studio Server files tests (67 passed); both typechecks; formatting/lint/file-size/Fallow gates; and two consecutive strict 50,000-element Chromium passes bounded to 207 mounted clip roots.
2026-07-29 rebase verification
No merge was performed; this PR remains a draft.