Skip to content

perf(studio): add dev-gated timeline row virtualization - #2701

Open
miguel-heygen wants to merge 1 commit into
codex/studio-timeline-d-viewport-geometry-v2from
codex/studio-timeline-d-playhead-follow-v2
Open

perf(studio): add dev-gated timeline row virtualization#2701
miguel-heygen wants to merge 1 commit into
codex/studio-timeline-d-viewport-geometry-v2from
codex/studio-timeline-d-playhead-follow-v2

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

What

Add development-gated timeline row virtualization plus playhead-follow behavior.

Scope boundary

Row virtualization in this PR is enabled only when import.meta.env.DEV and VITE_STUDIO_TIMELINE_ROW_VIRTUALIZATION_ENABLED=1; the production/default path still mounts all rows. The playhead follow logic keeps playback inside the trailing 25% of the visible horizontal band. It does not implement a user-scroll suspension timer.

How

  • Keep the feature flag as the sole compatibility decision: flag off renders every row.
  • When the flag is on but viewport dimensions are initially zero or the timeline is hidden, render only a bounded first-row seed plus actor/focus pins.
  • Enable the virtualizer after positive dimensions arrive without ever passing through a full-row mount.
  • Keep logical focus and reveal requests independent of current DOM mounts.
  • Follow playback horizontally into the trailing-quarter band.
  • Preserve real row diagnostics on the extracted row shell.

The default-path scroll-cost fix lands in #2883, and the permanent two-state CI gate lands in #2884.

Test plan

  • Zero-size first-render integration test with 10,000 rows; mounted rows remain bounded
  • Timeline, editing, virtualization, focus-identity, and virtual-row tests
  • Full Studio suite: 291 files / 3,195 tests
  • Studio typecheck, oxfmt, and oxlint
  • Exact-tip Chrome default gate: 5/5 measured runs
  • Exact-tip Chrome 50k virtualized gate: required 4/5 quorum, memory returned

Review fixes are included at head bd0dccfd5.

@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch from 9fda27f to 9b91ca5 Compare July 27, 2026 20:51
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-viewport-geometry-v2 branch from 13367e5 to 9a2c975 Compare July 28, 2026 20:43
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch from 9b91ca5 to 8a8a531 Compare July 28, 2026 20:43
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-viewport-geometry-v2 branch from 9a2c975 to d9c532f Compare July 28, 2026 22:09
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch from 8a8a531 to 0a94dbf Compare July 28, 2026 22:10
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-viewport-geometry-v2 branch from d9c532f to eb24bd5 Compare July 28, 2026 22:35
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch 2 times, most recently from 14895e2 to e494530 Compare July 28, 2026 23:03
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-viewport-geometry-v2 branch from eb24bd5 to 684ce21 Compare July 28, 2026 23:03
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch from e494530 to 6359e3f Compare July 28, 2026 23:23
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-viewport-geometry-v2 branch from 684ce21 to 030583a Compare July 28, 2026 23:23
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch from 6359e3f to bde178d Compare July 28, 2026 23:53
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-viewport-geometry-v2 branch from 030583a to 028174c Compare July 28, 2026 23:54
@miguel-heygen
miguel-heygen changed the base branch from codex/studio-timeline-d-viewport-geometry-v2 to main July 29, 2026 02:06
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch from bde178d to 9565e75 Compare July 29, 2026 02:28
@github-actions

Copy link
Copy Markdown

Fallow audit report

Found 5 findings.

Duplication (4)
Severity Rule Location Description
minor fallow/code-duplication packages/studio/src/player/components/timelineClipDragGestureLifecycle.ts:56 Code clone group 1 (25 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/useTimelineClipDrag.ts:359 Code clone group 1 (25 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/useTimelineTrackLayout.test.ts:26 Code clone group 2 (25 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/useTimelineTrackLayout.test.ts:73 Code clone group 2 (25 lines, 2 instances)
Health (1)
Severity Rule Location Description
minor fallow/high-crap-score packages/studio/src/player/hooks/useTimelinePlayer.ts:69 'syncTimelineElements' has CRAP score 31.6 (threshold: 30.0, cyclomatic 10)

Generated by fallow.

@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch from 9565e75 to ee01158 Compare July 29, 2026 14:46
@miguel-heygen
miguel-heygen changed the base branch from main to codex/studio-timeline-d-viewport-geometry-v2 July 29, 2026 15:03
@miguel-heygen
miguel-heygen marked this pull request as ready for review July 29, 2026 15:06
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-viewport-geometry-v2 branch from eab22e6 to 9e92dbc Compare July 29, 2026 23:35
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch from ee01158 to 108bd87 Compare July 29, 2026 23:35

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: COMMENTED — grade B — rubric PARTIAL

Role in stack: D6 behavior — playhead follow across virtualized rows. Ships (a) horizontal playback follow unconditionally to prod, (b) @tanstack/react-virtual-backed vertical row windowing behind import.meta.env.DEV && VITE_STUDIO_TIMELINE_ROW_VIRTUALIZATION_ENABLED === "1" — dev-only.

Root-cause fit: useTimelinePlayhead subscribes to liveTime (imperative store, not state re-render) and per-tick writes scroll.scrollLeft via getTimelinePlaybackFollowScrollLeft. Row virtualization touches only scrollTop; follow touches only scrollLeft — orthogonal axes, so "across virtualized rows" holds mechanically (follow never queries row DOM). focusIdentity resolves off logical TimelineElement.track, not mount state; pinnedRowKeys extractor unions overscan with focused/reveal/dragged/menu keys so out-of-band actors stay mounted.

Claims verified (file:line at HEAD 108bd87e):

  • Follow trigger + gates: useTimelinePlayhead.ts:124-149 (isPlaying && !isDragging && zoomMode !== "fit"). Instant scrollLeft assign — no smooth-scroll, so reduced-motion is a non-issue.
  • Bounded mount: Timeline.virtualization.test.tsx:76-79 asserts ≤16 rows over 1,000 elements; focus preserved after scroll to row 500 (:89-98).
  • Hysteresis: timelineLayout.ts:434-465 — advance zone x > followLine (0.75), pull-back zone x < visibleStart; disjoint → no oscillation. Clamped max(0, min(max, next)).
  • Pins survive session-epoch reset: useTimelineVirtualRows.ts:78-86 dispatches synthetic scroll so virtualizer re-observes the DOM offset Timeline just reset.

Adversarial findings:

  • P2 — body-vs-code mismatch on "respecting explicit user scrolling": PR body says "Follow playback horizontally while respecting explicit user scrolling." Code has no user-scroll gate — the liveTime tick corrects scrollLeft continuously. During playback the user cannot peek ahead or scrub-inspect earlier time without the next tick yanking the viewport. Only slack is the [visibleStart..75%] band. Either drop the claim or add a "user recently scrolled" ref (a lastUserScrollTs checked before the scrollLeft assign in useTimelinePlayhead.ts:139-148).
  • Nit — useMountEffect subscribes once; isPlaying sampled via getState() each tick — fine, but a hot-reload / provider swap of usePlayerStore won't re-bind. Acceptable given store lifetime.
  • Nit — Row virtualization is DEV-only via import.meta.env.DEV. That is intentional per the flag comment ("Disabled until horizontal windowing and stable gesture lifetime land") but the PR title "follow playhead across virtualized rows" oversells what ships to prod.

CI state: Preflight + all perf shards + Preview parity + player-perf + preview-regression + regression all pass. Graphite mergeability pending. Mergeable=MERGEABLE.

Suggested next step: Reconcile the body claim about "respecting explicit user scrolling" — either land a scroll-suspend gate or soften the claim to "advances the viewport to keep the playhead in the trailing 25%." Otherwise sound.

Review by Via

@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch from 108bd87 to e454d16 Compare July 30, 2026 00:23
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-viewport-geometry-v2 branch from 9e92dbc to b15627d Compare July 30, 2026 00:23
@miguel-heygen miguel-heygen changed the title perf(studio): follow playhead across virtualized rows perf(studio): add dev-gated timeline row virtualization Jul 30, 2026
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Addressed at e454d16a2.

  • Renamed the PR to say row virtualization is dev-gated.
  • The body now states the exact activation condition and that the production/default path still mounts all rows.
  • Removed the unsupported “respects explicit user scrolling” claim; the documented behavior is now exactly what the code does: keep playback inside the trailing 25% band.
  • The extracted virtual row shell preserves D1’s real-row diagnostics.

@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch from e454d16 to 32554de Compare July 30, 2026 01:31
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-viewport-geometry-v2 branch from b15627d to 02ca243 Compare July 30, 2026 01:31

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-delta review at 32554de65c (additive to the existing review).

Important — the enabled path is unbounded precisely while the viewport is unmeasured or hidden. useTimelineRowVirtualization.ts:51-54 folds clientWidth/clientHeight > 0 into enabled; useTimelineVirtualRows.ts:82-86 then returns allRows whenever that value is false. The initial viewport snapshot is zero-sized, so a flagged 10k-row project first mounts all 10k rows before the ref/ResizeObserver can enable virtualization. A collapsed or transiently zero-height timeline does the same again. The integration test masks this by globally stubbing positive dimensions before the first render (Timeline.virtualization.test.tsx:30-40).

Please distinguish “feature flag disabled” (compatibility: all rows) from “flag enabled but geometry not ready” (bounded seed/pins or synchronous measurement). Add a zero-size-first-render test that proves the flagged path never mounts O(total rows).

The rest of the exact delta is coherent: actor/focus pins are identity-based, row height and scroll-margin geometry agree, epoch resets have a single writer, and listener/effect cleanup is balanced. I did not repeat the already-documented lack of a user-scroll suspension timer.

Verdict: REQUEST CHANGES
Reasoning: Steady-state virtualization is correct, but its initial/hidden-state fallback can perform the full mount that this mechanism is intended to prevent.

— Magi

@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch from 32554de to bd0dccf Compare July 30, 2026 02:19
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Resolved at bd0dccfd5.

  • The feature flag is now the only compatibility switch. Flag off still returns every row.
  • Positive viewport dimensions are a separate readiness condition owned inside useTimelineVirtualRows.
  • Flag on plus zero dimensions returns a bounded seed derived from the existing overscan budget, unioned with valid actor/focus pins; it never constructs or mounts the complete compatibility row array.
  • The virtualizer activates once dimensions become positive.
  • A zero-size-first-render integration test loads 10,000 tracks and asserts the first flagged render mounts a non-empty range of at most 16 rows.

Focused virtualization tests pass, the full Studio suite passes (291 files / 3,195 tests), and typecheck/format/lint pass. At the restacked tip, the default Chrome gate passed 5/5 and the 50k virtualized gate passed its required 4/5 quorum with memory returned.

@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-viewport-geometry-v2 branch from 02ca243 to eef5fb4 Compare July 30, 2026 03:15
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch 2 times, most recently from 0690067 to 9f3ef4a Compare July 30, 2026 03:47
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-viewport-geometry-v2 branch 2 times, most recently from 0546ad3 to 3702ced Compare July 30, 2026 04:05
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch 2 times, most recently from a451e13 to ca7d212 Compare July 30, 2026 15:25
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-viewport-geometry-v2 branch from 3702ced to 7a90b73 Compare July 30, 2026 15:25
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-playhead-follow-v2 branch from ca7d212 to 5fc5ef6 Compare July 30, 2026 17:02
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-viewport-geometry-v2 branch from 7a90b73 to 1aa53ce Compare July 30, 2026 17:02
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.

2 participants