Skip to content

perf(studio): define timeline viewport budgets and fixtures - #2696

Open
miguel-heygen wants to merge 1 commit into
mainfrom
codex/studio-timeline-d-performance-contract-v2
Open

perf(studio): define timeline viewport budgets and fixtures#2696
miguel-heygen wants to merge 1 commit into
mainfrom
codex/studio-timeline-d-performance-contract-v2

Conversation

@miguel-heygen

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

Copy link
Copy Markdown
Collaborator

What

Define the measurable timeline viewport contract before changing rendering behavior.

How

  • Own immutable budgets for mounted rows, clip roots, per-row clips, descendants, latency, frame cadence, long tasks, memory return, and run quorum.
  • Instrument the real TimelineLanes row shell and require exactly one mounted Timeline, so a pre-mount or crashed run cannot pass on all-zero diagnostics.
  • Keep every DOM ceiling inclusive and exclude orphan clips from the per-row maximum.
  • Load fixtures through one atomic, isolated project-state replacement so stale playback, manifests, selections, pending requests, caches, or beat state cannot leak between identical runs.
  • Reject zero or fractional measured/quorum counts.
  • Validate fixture profiles and poster states with own-key checks, not prototype-chain membership.
  • Verify full deep equality for every 50,000-element profile, not samples from only one workload.

This is D1 of Family D. The permanent two-state CI workflow lands later in the same stack in #2884.

Test plan

  • Timeline budget and diagnostics tests
  • Real row instrumentation test
  • Atomic fixture isolation tests
  • All-profile 50,000-element determinism tests
  • Full Studio suite
  • Studio typecheck, oxfmt, and oxlint

Review fixes are included at head 09b469a31.

@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-performance-contract-v2 branch 2 times, most recently from fa190d8 to d49c381 Compare July 28, 2026 20:42
@miguel-heygen
miguel-heygen changed the base branch from codex/review-baseline/studio-timeline-family-c-v2 to codex/studio-timeline-c-ease-mode-switch-v2 July 28, 2026 20:43
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-c-ease-mode-switch-v2 branch from 4762c45 to bc1c49e Compare July 28, 2026 22:01
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-performance-contract-v2 branch from d49c381 to 2540026 Compare July 28, 2026 22:09
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-c-ease-mode-switch-v2 branch from bc1c49e to 2cbd849 Compare July 28, 2026 22:35
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-performance-contract-v2 branch from 2540026 to a26435d Compare July 28, 2026 22:35
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-c-ease-mode-switch-v2 branch from 2cbd849 to 2469850 Compare July 28, 2026 23:02
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-performance-contract-v2 branch from a26435d to 5d7f12d Compare July 28, 2026 23:03
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-c-ease-mode-switch-v2 branch from 2469850 to cbd22db Compare July 28, 2026 23:22
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-performance-contract-v2 branch from 5d7f12d to b8882aa Compare July 28, 2026 23:23
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-c-ease-mode-switch-v2 branch from cbd22db to ea7b955 Compare July 28, 2026 23:53
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-performance-contract-v2 branch from b8882aa to f411080 Compare July 28, 2026 23:53
@miguel-heygen
miguel-heygen changed the base branch from codex/studio-timeline-c-ease-mode-switch-v2 to main July 28, 2026 23:55
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-performance-contract-v2 branch 2 times, most recently from 0241316 to 4033f5f Compare July 29, 2026 14:46
@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-performance-contract-v2 branch from 4033f5f to e9360d1 Compare July 29, 2026 23:35

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Clean foundation PR — pure additions of budgets + diagnostics + fixtures, no behavior changes to the timeline. The deterministic-generator contract holds up (pure indexed math, no Date.now/Math.random/async), the fixture stability test at useStudioTestHooks.test.tsx:37-64 proves round-trip equality on 50k identities, and the "128-roots-per-row envelope" math in fixtureTrack (timelinePerformanceFixture.ts:53-60) is documented and enforced by the expect(Math.max(...perTrack.values())).toBeLessThanOrEqual(128) assertion. Ready as the contract D2..D8 will be measured against.

Concerns

  • Boundary asymmetry in getTimelineResourceBudgetStatustimelinePerformanceDiagnostics.ts:101-103 bounds clipRoots and clipRootsPerRow inclusively (<=) but descendants strictly (<). The test title at :61 flags this as intentional ("including the strict descendant boundary"), and I don't think it's wrong — but the source has no comment explaining why the descendant ceiling is strict while the clip ceilings are inclusive. Someone touching these three lines later will see the mixed operators and normalize them "for consistency," silently loosening the descendant guard. A one-line source comment on line 103 documenting the intent would prevent that.

  • Dev-only-branch imports may not tree-shake cleanlyuseStudioTestHooks.ts:3-13 imports createTimelinePerformanceFixture, readTimelinePerformanceDiagnostics, and TIMELINE_VIEWPORT_BUDGETS at top level; the only consumption is inside the if (!isDev || …) return; branch wrapped in try/catch (useStudioTestHooks.ts:54-61). Vite replaces import.meta.env.DEV with false at build time so the branch dies, but with the try/catch shell and the Object.freeze side-effect calls in the budget/fixture modules, esbuild/rollup DCE is not guaranteed to elide the imports from the prod chunk. Grep confirms no other prod consumers, so a static-import graph analysis in the next PR (vite build --report or similar) would verify. Not a blocker — worst case is ~2KB of dead code in prod — but worth confirming before the file-size gate absorbs it as new baseline.

Nits

  • readNonNegativeNumber at timelinePerformanceDiagnostics.ts:24-27 silently returns 0 on unparseable input (e.g. a writer emitting data-timeline-cache-bytes="NaN" shows up as 0 bytes cached, which looks healthy to a budget check). For a diagnostics module whose purpose is exposing violations, a dev-only warn on unparseable input would help catch writer bugs before they land as false-clean budget signals.
  • countPosters at :29-42 similarly drops unknown data-timeline-poster-state values silently (if (state && state in counts) …). Same class of silent-failure as above — a typo'd poster state disappears from the counts with no signal.
  • timeOverscanViewportRatio: 0.5 at timelineViewportBudgets.ts:57 is not in the <=1 validator list at :115-119 (unlike the three other ratio fields). If that's intentional because overscan >100% is conceivable, a source comment would prevent someone from adding it "for consistency" later.
  • dense-short at elementCount: 1_000 in fixtureTrack never reaches the dense branch — all 1000 indices satisfy index < TRACK_COUNT, so track = index (one clip per track). The profile is only actually dense at 50k. A // dense-short only diverges from non-dense profiles at 50k comment at timelinePerformanceFixture.ts:54 would clarify.

Questions

  • Budget calibration derivation — the numeric values (maxMountedClipRoots: 512, interactionP95Ms: 50, frameIntervalP95Ms: 33.3, memoryReturnToleranceRatio: 0.15, etc.) look like a post-virtualization target contract, not the current-main baseline. The stack screenshot shows 3018 → 20 mounted clips post-Family-D, so the 512 ceiling is comfortably above the post-D state but well below the current-main baseline. Is the design doc explicit about the numeric derivation, or should reviewers treat these as reasonable-failure ceilings that assume Family D lands intact? (I couldn't fetch the heygenverse doc — 403 without SSO from here.)

  • CI wiring intent — no .github/workflows changes here, so these budgets are enforced by whatever test framework consumes them locally / in Fallow. Are D2..D8 planning to wire budget-hit checks into required CI (via the warmupRuns: 3, measuredRuns: 5, requiredPassingRuns: 4 quorum), or is this staying as a local-run contract?

What I didn't verify

  • Tree-shaking of the dev-only fixture module in prod builds (would need a vite build --report run).
  • The heygenverse design doc for numeric-budget derivation (auth-gated, 403).
  • Downstream stack behavior — D2..D8 haven't landed here, so budget calibration is a future check.
  • The reported perf numbers (−99.3% / −93.3% / −35.3%) come from the stack, not this PR — this PR only encodes the contract they'll be measured against.

Review by Rames D Jusso

@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: APPROVE — grade A — rubric CORRECT

Role in stack: D1 base — budgets + diagnostics + fixtures; no behavior change; contract for D2-D8.

Root-cause fit (contract fit): The budget module owns every ceiling (DOM, media, latency, memory, quorum) as a frozen singleton with resolveTimelineViewportBudgets(overrides) for per-test hard-forks — no global mutation, so downstream PRs cannot silently loosen the contract. The 512 mounted-clip / 5,000-descendant / 4-of-5 quorum ceilings sit ~45% below the -99.3% target claim, giving virtualization work headroom without letting a regression slip through.

Budget calibration check:

  • mounted rows: none (implicit via overscan=4 + 128/row) — OK
  • mounted clips: 512 total, 128/row — tight vs -99.3% target
  • descendants: 5,000 (strict <) — snug given 512×~10 = 5,120
  • interaction latency: 50/75ms constrained — matches INP guidance
  • frame cadence: 33.3/50ms — 30fps/20fps floor
  • long tasks: 50ms — PerformanceObserver default
  • memory return: 0.15 ratio — reasonable GC-noise tolerance
  • run quorum: 3 warmup + 5 measured + 4 required — validated by requiredPassingRuns > measuredRuns guard

Claims verified (file:line at HEAD e9360d10):

  • 691/-0, 7 files: 125+36+85+115+157+48+125 = 691 (files-list).
  • Deterministic fixtures: no Math.random / Date.now / crypto in timelinePerformanceFixture.ts:1-157.
  • Dense-short coverage: fixtureStart uses (index % 128) * 0.5 on 1.5s clips over 120s duration — genuinely dense (timelinePerformanceFixture.ts:63).
  • Dev-gated hook: import.meta.env.DEV === true guard + cleanup delete window.__studioTest (useStudioTestHooks.ts:52-92).
  • Tests exercise budgets: timelineViewportBudgets.test.ts:38-46 asserts override rejection for negative, NaN, quorum-inversion, ratio>1.
  • 50k determinism: useStudioTestHooks.test.tsx:36-59 compares two independent generations byte-equal.

Adversarial findings:

  • nit — getTimelineResourceBudgetStatus uses < for descendants but <= for clipRoots/perRow (timelinePerformanceDiagnostics.ts:88-92); documented as "strict boundary" in test, but asymmetry is easy to trip on downstream — worth a code comment.
  • nit — maxClipsInOneRow groups clips without a [data-timeline-row] ancestor under a shared null bucket (timelinePerformanceDiagnostics.ts:44-50), over-counting orphans; benign today, but D2-D8 must preserve the row-ancestor invariant.
  • nit — silent try/catch { isDev = false } around import.meta.env access (useStudioTestHooks.ts:56-60); defensible for non-Vite envs, but no telemetry if the DEV flag disappears in a future toolchain move.
  • Fixture cover gap — only dense-short is byte-stability-asserted at 50k (useStudioTestHooks.test.tsx:37); long-overlap / composition-heavy / remote-unsupported / keyframe-heavy-expanded get only 1k coverage.

CI state: All 27 required green. No red required checks.

Suggested next step: Land D1 as the frozen contract. Recommend a follow-up in D2 to (a) add a code comment on the intentional < for descendants, (b) extend the byte-identity 50k assertion to the other four profiles, (c) confirm D2-D8 never leave clips without a data-timeline-row ancestor so maxClipsInOneRow stays truthful.

Review by Via

@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.

Single-PR review of #2696 only at e9360d1048fb7d366c90d5a5021a687069e23354. I audited all seven changed files end-to-end against main; I did not review or rely on D2–D8.

The shape is strong where it is connected: timelineViewportBudgets.ts:49-124 gives the contract one immutable owner, timelinePerformanceDiagnostics.ts:61-93 recomputes from the DOM instead of retaining cleanup-sensitive counters, and timelinePerformanceFixture.ts:31-156 uses deterministic indexed generation without clocks/randomness. Rames/Via already covered the strict-descendant asymmetry, dev-only tree-shaking, unknown diagnostic values, and breadth of 50k profile coverage; the findings below are additive.

Blockers

  1. Live row diagnostics are disconnected from the real Timeline DOM. timelinePerformanceDiagnostics.ts:44-49,76 depends on [data-timeline-row], but the actual row wrapper at packages/studio/src/player/components/TimelineLanes.tsx:149-157 emits no such attribute. Repository-wide, the marker exists only in this reader and its synthetic unit test. A real Timeline therefore reports mountedRows: 0, and every clip is grouped into the shared null bucket, making maxMountedClipRootsInOneRow equal the total clip count. The PR also claims a mounted-row budget, but timelineViewportBudgets.ts:1-43 has no mounted-row ceiling and getTimelineResourceBudgetStatus() at timelinePerformanceDiagnostics.ts:96-104 has no row result. Add the production marker (or derive from real structure), an enforceable row budget/status, and an integration test that mounts the real Timeline rather than hand-authoring the marker.

  2. An absent Timeline is reported as fully within budget. readTimelinePerformanceDiagnostics() intentionally returns zeroes when no root is mounted (timelinePerformanceDiagnostics.ts:66-93), while getTimelineResourceBudgetStatus() ignores timelineRoots (:96-104). At this head, {timelineRoots: 0, ...zeroes} returns all three status flags true. A read taken before mount or after a render failure can therefore become a false-green strict run. Make root validity part of the status (normally exactly one Timeline), or expose a strict reader that rejects an absent/ambiguous root; pin the missing-root failure path.

Important

  • Fixture loads are not isolated from prior project state. useStudioTestHooks.ts:71-86 shallow-merges the fixture fields but preserves isPlaying, work-area points, clip manifest/parent map, pending requests, beat/edit state, and other project state that the store's own reset clears at playerStore.ts:517-546. The same fixture can therefore render/measure differently depending on what Studio had loaded first (at minimum, an already-playing project keeps advancing after the load). Install a reset-equivalent fixture state in the same single setState transaction and test from deliberately dirty state.

  • Run-quorum validation permits vacuous/non-executable contracts. timelineViewportBudgets.ts:99-123 accepts { measuredRuns: 0, requiredPassingRuns: 0 } and fractional run counts because it checks only finite/non-negative and required <= measured. Require integer counts, measuredRuns >= 1, requiredPassingRuns >= 1, and a non-negative integer warmup count; add the boundaries to timelineViewportBudgets.test.ts:40-47.

  • Runtime string validation trusts inherited object keys. timelinePerformanceFixture.ts:42-49 indexes a normal object and checks truthiness, so the browser-facing test hook accepts profile: "__proto__"/"constructor" and installs elements with duration: undefined and start: NaN. The sibling poster parser uses state in counts at timelinePerformanceDiagnostics.ts:37-40, which also accepts inherited keys. Use Object.hasOwn(...) at both contract boundaries and add prototype-key regressions.

  • The 50k “determinism” test does not cover the 50k output. useStudioTestHooks.test.tsx:47-63 compares the summary, elements 0–2, and the last element—49,996 middle elements and all profile-owned maps/sets are unchecked. A stable hash/full structural comparison would pin the reproducible-fixture claim without retaining another copy indefinitely.

CI

The exact head is not green: Preview parity hit its 20-minute job limit while installing ffmpeg, before Chrome or the parity check ran; preview-regression then failed because the dependency job was cancelled. This appears infrastructure/non-attributable—the PR does not touch that workflow—but the required gate still needs a successful rerun. Build, lint, format, typecheck, Studio/full tests, Windows, Fallow, file-size, CodeQL, and player-perf are green.

I am posting COMMENT rather than a formal changes-requested state because this PR is authored by the same shared miguel-heygen GitHub identity. Treat the two contract gaps above as merge blockers.

Verdict: COMMENT (With fixes)
Reasoning: The intended measurement foundation can currently report fabricated row metrics and pass an unmounted Timeline, so it is not yet a trustworthy gate for the later virtualization work.

— Magi

@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-performance-contract-v2 branch from e9360d1 to 09b469a Compare July 30, 2026 00:23
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Addressed at 09b469a31.

  • The real Timeline row now emits data-timeline-row; a component test pins the instrumentation.
  • Added an enforceable 64-row budget and row status.
  • Resource status now requires exactly one mounted Timeline, so zero diagnostics cannot false-green.
  • Fixture loading now atomically replaces project-scoped state from the shared reset owner; stale playback, manifests, pending requests, selections, caches, and beat state are cleared.
  • Warmup/measured/quorum counts must be integers, and measured/quorum must be nonzero.
  • Fixture profile and poster-state validation use own keys, so constructor / prototype names are rejected.
  • Every 50,000-element profile now gets full deep-equality determinism coverage.
  • All DOM ceilings use inclusive <=; orphan clips no longer form a fake aggregate row.

Targeted contract/store tests and the complete Studio suite/typecheck pass.

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Magi review resolution update: every item labeled as a follow-up is integrated into D1 itself at 09b469a31; none is deferred to a later PR.

  • Fixture loading now starts from the exported canonical timeline reset state, so playback, manifests, pending requests, selection, caches, beats, and related project state cannot leak between fixture runs.
  • Run configuration now requires non-zero integer measured/quorum values and rejects fractional or 0/0 quorums.
  • Profile/poster validation uses own-property checks, rejecting prototype keys such as constructor and __proto__.
  • The 50k determinism assertion now deep-compares every generated element for every profile.

The two blockers are also fixed in D1: real lane rows emit data-timeline-row, rows have an enforced budget, and a missing/duplicate timeline root fails instead of producing an all-zero green status.

@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-performance-contract-v2 branch 2 times, most recently from a186ca4 to f158e6c Compare July 30, 2026 03:47
@miguel-heygen
miguel-heygen force-pushed the codex/studio-timeline-d-performance-contract-v2 branch from f158e6c to c458578 Compare July 30, 2026 04:05
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.

3 participants