Skip to content

ci(studio): run the timeline viewport gate on studio changes - #2884

Open
miguel-heygen wants to merge 4 commits into
fix/studio-timeline-scroll-gatefrom
ci/studio-timeline-viewport-gate
Open

ci(studio): run the timeline viewport gate on studio changes#2884
miguel-heygen wants to merge 4 commits into
fix/studio-timeline-scroll-gatefrom
ci/studio-timeline-viewport-gate

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

Adds a studio-timeline-viewport CI job that runs both arms of the timeline viewport gate: the default build at 1,000 elements and the row-virtualized build at 50,000.

Supporting changes:

  • A studio paths filter, so the job runs on packages/studio, packages/core and packages/studio-server rather than on the broad code filter.
  • A ci tier in the gate script. It applies the constrained budgets with no emulation.
  • The job copies the tracked fixture composition from packages/studio/tests/e2e/fixtures/timeline-virtualization into packages/studio/data/projects, because that is where Studio resolves projects from and it is gitignored.

Why

The gate has existed since the row virtualization stack landed, but nothing under .github/ referenced it, so it only ran when someone ran it by hand. That is how a scroll regression in the default build reached eight ready-to-merge PRs with no signal: the gate takes STUDIO_URL from the environment and never sets the flag itself, so every run anyone did happened to measure the enabled build.

A gate nobody schedules is documentation, not a gate.

How

Two dev servers, one per flag state. Row virtualization is read from import.meta.env at module load, so a single process cannot serve both builds. The boot-and-wait shape is copied from the existing studio-load-smoke job, including the workspace build steps that let vite.config.ts resolve the workspace packages through their node export condition.

The ci tier exists because neither existing non-primary tier fits a hosted runner. low-resource throttles the CPU a further 4x, which on an already slower and noisier machine measures the throttle rather than the build, and high-dpr changes the scale factor. ci takes the constrained budgets (75ms interaction p95, 50ms frame interval p95) and leaves emulation alone.

Chrome resolution needs no new setup: the gate already probes /usr/bin/google-chrome, which the hosted image provides. The Chrome major is only pinned when TIMELINE_CHROME_MAJOR is set, and this job does not set it.

Gate evidence is uploaded as an artifact on success and failure, since a red run is exactly when someone needs to read it.

Test plan

Both arms were run locally against the ci tier, on the same dev servers CI will boot, using the same commands the job runs:

arm interaction p95 passing runs exit
default, 1,000 elements 33.2ms against a 75ms budget 5 of 5 0
virtualized, 50,000 elements within budget 4 of 5 0

Each arm takes about 7 seconds, so the job cost is almost entirely the dependency install and workspace build it shares with studio-load-smoke.

The workflow file was parsed to confirm the job and the new filter output are wired up.

One thing to watch, stated plainly rather than discovered later: the virtualized arm passes 4 of 5 runs, which is exactly the required threshold. The failing run is a 53-58ms long task against a 50ms limit, reproducibly on the fourth measured run. That is the residual window-swap tail, it predates this PR, and the constrained budgets do not relax longTaskLimitMs. On a slower runner more runs could cross it and this arm would go red. This PR's own CI run is the real measurement; if the virtualized arm proves flaky there, the honest fix is to address the tail or hold that arm back, not to widen the budget.

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable)

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Exact pushed tip ea6f93043 was exercised locally in Chrome 150 against both CI configurations:

  • Row virtualization off, 1,000 elements: 5/5 measured runs passed; interaction p95 33.1ms, frame-interval p95 16.7ms, one timeline root and 1,000 real mounted rows in every run.
  • Row virtualization on, 50,000 elements: clean rerun passed 5/5; interaction p95 33.7ms, frame-interval p95 17.6ms, 10 mounted rows, 240 mounted clip roots, 25 max clips in one row, and 995 timeline descendants. Every DOM budget passed and memory returned.

One immediately preceding virtualized invocation passed every DOM budget but reached only 3/5 responsiveness quorum because two samples recorded isolated 52ms and 61ms long tasks. The clean rerun recorded no long tasks. This is included for transparency; the gate correctly rejected the noisy invocation rather than hiding it.

The GitHub Preview parity rerun is now green as well, including ffmpeg installation and the Chrome parity job that previously timed out.

@miguel-heygen
miguel-heygen force-pushed the ci/studio-timeline-viewport-gate branch from ea6f930 to 3e47aa7 Compare July 30, 2026 01:31
@miguel-heygen
miguel-heygen force-pushed the fix/studio-timeline-scroll-gate branch 2 times, most recently from 546751c to d82a1d1 Compare July 30, 2026 02:19
@miguel-heygen
miguel-heygen force-pushed the ci/studio-timeline-viewport-gate branch from 3e47aa7 to 1acce7a Compare July 30, 2026 02:19

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

🟠 Mechanism looks sound at exact head 1acce7a60a0344880a967e3126c680eeb7615d60, with one required landing check and one evidence-quality follow-up.

Reviewed only d82a1d12..1acce7a6 (the workflow + gate-script delta), not the preceding performance implementation.

Landing gate

The new Studio: timeline viewport gate has not run on this exact head. This PR is stacked on #2883 rather than main, while the workflow only triggers for pull_request.branches: [main]; the exact-head check list contains preview/regression checks but no execution of the job introduced here. The local Chrome evidence is useful, but it is not proof that the GitHub Actions wiring, dependency build, dual-server startup, hosted-runner budgets, and artifact upload work together.

Before stamping/merging, restack this PR onto main and require Studio: timeline viewport gate to pass on the final exact SHA. The added edited trigger is specifically intended to make that happen when Graphite restores the base.

P2 — the “both arms” step stops after the first failing arm

The job uses set -euo pipefail, then runs the default arm before the virtualized arm. If the default arm fails, the shell exits immediately: the virtualized build is never tested and the always-upload step contains only /tmp/timeline-gate-default.json (or no complete JSON if failure occurs before final output). That is weaker than the step name and PR claim that both arms run and evidence is retained on failure.

This does not create a false green, so I am not treating it as a correctness blocker. For useful red-run evidence, capture each arm's status, run both regardless, then exit nonzero if either failed.

The rest of the wiring checks out: Studio/core/server changes activate the gate, the tracked fixture is copied into the expected project location, the two module-load configurations use separate Vite processes, CI gets a dedicated budget tier, the gate asserts the observed flag state, and the latest push-only file-size range correction is valid.

git diff --check passes.

— Magi

@miguel-heygen
miguel-heygen force-pushed the ci/studio-timeline-viewport-gate branch from 1acce7a to 923e96b Compare July 30, 2026 02:58
@miguel-heygen
miguel-heygen force-pushed the fix/studio-timeline-scroll-gate branch from d82a1d1 to b97d8c7 Compare July 30, 2026 02:58
@miguel-heygen
miguel-heygen changed the base branch from fix/studio-timeline-scroll-gate to main July 30, 2026 02:59
@miguel-heygen
miguel-heygen force-pushed the ci/studio-timeline-viewport-gate branch from 923e96b to b2908dd Compare July 30, 2026 03:02
@miguel-heygen miguel-heygen reopened this Jul 30, 2026
@miguel-heygen
miguel-heygen force-pushed the ci/studio-timeline-viewport-gate branch from b2908dd to f366401 Compare July 30, 2026 03:05
@miguel-heygen
miguel-heygen changed the base branch from main to graphite-base/2884 July 30, 2026 03:14
@miguel-heygen
miguel-heygen force-pushed the ci/studio-timeline-viewport-gate branch from fd2138d to 392cd8a Compare July 30, 2026 03:15
@miguel-heygen
miguel-heygen changed the base branch from graphite-base/2884 to main July 30, 2026 03:15
@miguel-heygen
miguel-heygen changed the base branch from main to fix/studio-timeline-scroll-gate July 30, 2026 03:15
@miguel-heygen
miguel-heygen changed the base branch from fix/studio-timeline-scroll-gate to main July 30, 2026 03:21
@miguel-heygen
miguel-heygen changed the base branch from main to graphite-base/2884 July 30, 2026 03:47
@miguel-heygen
miguel-heygen force-pushed the ci/studio-timeline-viewport-gate branch from fe5b251 to a1cc721 Compare July 30, 2026 03:47
@miguel-heygen
miguel-heygen changed the base branch from graphite-base/2884 to fix/studio-timeline-scroll-gate July 30, 2026 03:48
@miguel-heygen
miguel-heygen changed the base branch from fix/studio-timeline-scroll-gate to main July 30, 2026 03:48
The gate has existed since the row virtualization stack landed but nothing
under `.github/` referenced it, so it only ever ran when someone ran it by
hand. That is how the flag-off scroll regression reached eight merged-ready
PRs without anything noticing.

Adds a `studio-timeline-viewport` job that boots two Studio dev servers, one
per flag state, and runs both arms of the gate against them. Two servers are
needed because row virtualization is read from `import.meta.env` at module
load, so one process cannot serve both builds.

Scoped to a new `studio` paths filter rather than the broad `code` one: the
gate only says anything about `packages/studio`, `packages/core` and
`packages/studio-server`.

Adds a `ci` tier. It applies the constrained budgets without any emulation,
because a hosted runner is already slower and noisier than the machine the
strict numbers were recorded on, while the existing `low-resource` tier would
throttle it a further 4x and measure the throttle rather than the build.

The fixture composition is tracked under `tests/e2e/fixtures` but Studio
resolves projects from the gitignored `data/projects`, so the job copies it
into place instead of a project directory being committed.

Both arms run in about 7 seconds each locally, so the job cost is almost
entirely dependency install and the workspace build it shares with
`studio-load-smoke`.
@miguel-heygen
miguel-heygen changed the base branch from main to graphite-base/2884 July 30, 2026 04:05
@miguel-heygen
miguel-heygen force-pushed the ci/studio-timeline-viewport-gate branch from a1cc721 to 25503ec Compare July 30, 2026 04:05
@miguel-heygen
miguel-heygen changed the base branch from graphite-base/2884 to fix/studio-timeline-scroll-gate July 30, 2026 04:06
@miguel-heygen
miguel-heygen changed the base branch from fix/studio-timeline-scroll-gate to main July 30, 2026 04:06
@github-actions

Copy link
Copy Markdown

Fallow audit report

Found 22 findings.

Duplication (21)
Severity Rule Location Description
minor fallow/code-duplication packages/studio/src/player/components/Timeline.virtualization.test.tsx:94 Code clone group 1 (12 lines, 3 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.virtualization.test.tsx:128 Code clone group 2 (8 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.virtualization.test.tsx:145 Code clone group 4 (8 lines, 3 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.virtualization.test.tsx:145 Code clone group 3 (8 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.virtualization.test.tsx:182 Code clone group 1 (12 lines, 3 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.virtualization.test.tsx:322 Code clone group 1 (12 lines, 3 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.virtualization.test.tsx:329 Code clone group 2 (8 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.virtualization.test.tsx:363 Code clone group 5 (6 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.virtualization.test.tsx:363 Code clone group 3 (8 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.virtualization.test.tsx:363 Code clone group 4 (8 lines, 3 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.virtualization.test.tsx:380 Code clone group 5 (6 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.virtualization.test.tsx:401 Code clone group 4 (8 lines, 3 instances)
minor fallow/code-duplication packages/studio/src/player/components/useTimelineScrollViewport.test.tsx:95 Code clone group 6 (9 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/useTimelineScrollViewport.test.tsx:102 Code clone group 7 (8 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/useTimelineScrollViewport.test.tsx:121 Code clone group 7 (8 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/useTimelineScrollViewport.test.tsx:160 Code clone group 6 (9 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/useTimelineTrackLayout.test.ts:26 Code clone group 8 (25 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/useTimelineTrackLayout.test.ts:73 Code clone group 8 (25 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/hooks/useTimelinePlayer.seek.test.ts:85 Code clone group 9 (11 lines, 3 instances)
minor fallow/code-duplication packages/studio/src/player/hooks/useTimelinePlayer.seek.test.ts:227 Code clone group 9 (11 lines, 3 instances)
minor fallow/code-duplication packages/studio/src/player/hooks/useTimelinePlayer.seek.test.ts:433 Code clone group 9 (11 lines, 3 instances)
Health (1)
Severity Rule Location Description
minor fallow/high-crap-score packages/studio/tests/e2e/timeline-virtualization.mjs:447 '<arrow>' has CRAP score 30.0 (threshold: 30.0, cyclomatic 5)

Generated by fallow.

@miguel-heygen
miguel-heygen changed the base branch from main to fix/studio-timeline-scroll-gate July 30, 2026 04:09
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Exact stacked-head evidence is now green for 25503ec04: https://github.com/heygen-com/hyperframes/actions/runs/30512973405/job/90776821797

  • The workflow captures each arm status independently, always attempts both arms, and uploads both JSON evidence files with if: always() / if-no-files-found: error.
  • Default arm: 5/5 passing runs.
  • Virtualized 50k arm: required 4/5 passing quorum; every run held at 160 mounted clip roots and 707 timeline descendants. The four passing runs measured interaction p95 46.4–70.0ms and frame p95 49.9–66.7ms.
  • Fixture cleanup/unmount and the heap-return assertion passed in both arms.
  • Strict product targets remain 50ms interaction / 33.3ms frame / 50ms long task. The explicitly named constrained hosted-runner tier uses 75/75/300 so CI runner noise cannot silently redefine the product target. The Long Tasks observer probe is evaluated against the selected tier and drains queued records before disconnect.

I temporarily based this PR on main to run the complete stack on this exact head, then restored its Graphite parent to fix/studio-timeline-scroll-gate after the job passed.

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