diff --git a/packages/studio/package.json b/packages/studio/package.json
index c4744110c8..b5971ff4ec 100644
--- a/packages/studio/package.json
+++ b/packages/studio/package.json
@@ -49,6 +49,7 @@
"build": "vite build && tsup",
"typecheck": "tsc --noEmit",
"test": "vitest run",
+ "test:timeline-virtualization": "node tests/e2e/timeline-virtualization.mjs",
"test:watch": "vitest",
"report:sdk-cutover": "bun src/utils/sdkCutoverPolicy.report.ts"
},
diff --git a/packages/studio/tests/e2e/fixtures/timeline-virtualization/hyperframes.json b/packages/studio/tests/e2e/fixtures/timeline-virtualization/hyperframes.json
new file mode 100644
index 0000000000..c4816b5eaa
--- /dev/null
+++ b/packages/studio/tests/e2e/fixtures/timeline-virtualization/hyperframes.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://hyperframes.heygen.com/schema/hyperframes.json",
+ "paths": {
+ "blocks": "compositions",
+ "components": "compositions/components",
+ "assets": "assets"
+ }
+}
diff --git a/packages/studio/tests/e2e/fixtures/timeline-virtualization/index.html b/packages/studio/tests/e2e/fixtures/timeline-virtualization/index.html
new file mode 100644
index 0000000000..2846170bf5
--- /dev/null
+++ b/packages/studio/tests/e2e/fixtures/timeline-virtualization/index.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+ Timeline virtualization QA fixture
+
+
+
+
+
+
+
diff --git a/packages/studio/tests/e2e/timeline-virtualization.mjs b/packages/studio/tests/e2e/timeline-virtualization.mjs
new file mode 100644
index 0000000000..de44a88a95
--- /dev/null
+++ b/packages/studio/tests/e2e/timeline-virtualization.mjs
@@ -0,0 +1,298 @@
+#!/usr/bin/env node
+/**
+ * Reproducible timeline viewport gate against a running Studio preview of the
+ * adjacent fixture. The script prints machine-readable evidence; it never
+ * substitutes synthetic timings for a browser run.
+ *
+ * STUDIO_URL=http://127.0.0.1:5190/#project/timeline-virtualization \
+ * node packages/studio/tests/e2e/timeline-virtualization.mjs
+ */
+import { existsSync, readdirSync } from "node:fs";
+import { homedir, platform, arch } from "node:os";
+import { join } from "node:path";
+import puppeteer from "puppeteer-core";
+
+const STUDIO_URL = process.env.STUDIO_URL;
+const PROFILE = process.env.TIMELINE_PROFILE || "dense-short";
+const ELEMENT_COUNT = Number(process.env.TIMELINE_ELEMENT_COUNT || 50_000);
+const TIER = process.env.TIMELINE_TIER || "primary";
+const EXPECTED_CHROME_MAJOR = process.env.TIMELINE_CHROME_MAJOR
+ ? Number(process.env.TIMELINE_CHROME_MAJOR)
+ : null;
+
+if (!STUDIO_URL) {
+ console.error("STUDIO_URL is required and must point at the timeline-virtualization fixture");
+ process.exit(2);
+}
+if (
+ ![1_000, 50_000].includes(ELEMENT_COUNT) ||
+ !["primary", "low-resource", "high-dpr"].includes(TIER)
+) {
+ console.error(
+ "TIMELINE_ELEMENT_COUNT must be 1000 or 50000; " +
+ "TIMELINE_TIER must be primary, low-resource, or high-dpr",
+ );
+ process.exit(2);
+}
+
+function resolveChromeExecutable() {
+ const chromeRoot = join(homedir(), ".cache", "puppeteer", "chrome");
+ const builds = existsSync(chromeRoot) ? readdirSync(chromeRoot).sort().reverse() : [];
+ const installedCandidates = builds.flatMap((build) =>
+ [
+ "chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
+ "chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
+ "chrome-linux64/chrome",
+ ].map((relative) => join(chromeRoot, build, relative)),
+ );
+ return [
+ process.env.PUPPETEER_EXECUTABLE_PATH,
+ process.env.CHROME_PATH,
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
+ "/usr/bin/google-chrome",
+ "/usr/bin/chromium",
+ ...installedCandidates,
+ ].find((candidate) => candidate && existsSync(candidate));
+}
+
+function percentile(values, ratio) {
+ if (values.length === 0) return 0;
+ const sorted = [...values].sort((a, b) => a - b);
+ return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * ratio) - 1)];
+}
+
+async function collectHeapBytes(client) {
+ const usage = await client.send("Runtime.getHeapUsage");
+ return usage.usedSize;
+}
+
+async function collectRun(page) {
+ return page.evaluate(async () => {
+ const longTasks = [];
+ const scroller = findTimelineScroller();
+ const observer = observeLongTasks(longTasks);
+ const { interactions, frameIntervals } = await measureScrollInteractions(scroller);
+ observer?.disconnect();
+ return {
+ interactionP95Ms: percentileInPage(interactions, 0.95),
+ frameIntervalP95Ms: percentileInPage(frameIntervals, 0.95),
+ longestTaskMs: Math.max(0, ...longTasks),
+ scrollWidth: scroller.scrollWidth,
+ scrollHeight: scroller.scrollHeight,
+ diagnostics: window.__studioTest.readTimelinePerformanceDiagnostics(),
+ };
+
+ function percentileInPage(values, ratio) {
+ if (values.length === 0) return 0;
+ const sorted = [...values].sort((a, b) => a - b);
+ return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * ratio) - 1)];
+ }
+
+ function findTimelineScroller() {
+ const root = document.querySelector('[aria-label="Timeline"]');
+ if (!(root instanceof HTMLElement)) throw new Error("Timeline root not mounted");
+ const scroller = Array.from(root.querySelectorAll("div")).find(
+ (node) => node.scrollWidth > node.clientWidth || node.scrollHeight > node.clientHeight,
+ );
+ if (!(scroller instanceof HTMLElement)) throw new Error("Timeline scroller not mounted");
+ return scroller;
+ }
+
+ function observeLongTasks(longTaskDurations) {
+ if (
+ typeof PerformanceObserver !== "function" ||
+ !PerformanceObserver.supportedEntryTypes.includes("longtask")
+ ) {
+ return null;
+ }
+ const performanceObserver = new PerformanceObserver((list) => {
+ for (const entry of list.getEntries()) longTaskDurations.push(entry.duration);
+ });
+ performanceObserver.observe({ entryTypes: ["longtask"] });
+ return performanceObserver;
+ }
+
+ async function measureScrollInteractions(timelineScroller) {
+ const interactions = [];
+ const frameIntervals = [];
+ const nextFrame = () => new Promise((resolve) => requestAnimationFrame(resolve));
+ for (const ratio of [0, 0.25, 0.5, 0.75, 1, 0.5, 0]) {
+ const started = performance.now();
+ timelineScroller.scrollLeft = Math.round(
+ (timelineScroller.scrollWidth - timelineScroller.clientWidth) * ratio,
+ );
+ timelineScroller.scrollTop = Math.round(
+ (timelineScroller.scrollHeight - timelineScroller.clientHeight) * ratio,
+ );
+ const firstFrame = await nextFrame();
+ const secondFrame = await nextFrame();
+ interactions.push(secondFrame - started);
+ frameIntervals.push(secondFrame - firstFrame);
+ }
+ return { interactions, frameIntervals };
+ }
+ });
+}
+
+async function measureMaximumReliableScrollWidth(page) {
+ return page.evaluate(() => {
+ const viewportWidth = 320;
+ const container = document.createElement("div");
+ const content = document.createElement("div");
+ container.style.cssText = `position:fixed;left:-10000px;top:0;width:${viewportWidth}px;height:1px;overflow:auto`;
+ content.style.height = "1px";
+ container.append(content);
+ document.body.append(container);
+ const reliable = (width) => {
+ content.style.width = `${width}px`;
+ container.scrollLeft = width;
+ const expected = width - viewportWidth;
+ return container.scrollWidth >= width - 1 && container.scrollLeft >= expected - 1;
+ };
+ let low = 0;
+ let high = 64_000_000;
+ while (low + 1 < high) {
+ const middle = Math.floor((low + high) / 2);
+ if (reliable(middle)) low = middle;
+ else high = middle;
+ }
+ container.remove();
+ return low;
+ });
+}
+
+const executablePath = resolveChromeExecutable();
+if (!executablePath) {
+ console.error("No Chrome executable found; set PUPPETEER_EXECUTABLE_PATH");
+ process.exit(2);
+}
+
+const browser = await puppeteer.launch({
+ executablePath,
+ headless: true,
+ args: ["--no-sandbox", "--disable-dev-shm-usage"],
+});
+let exitCode = 1;
+try {
+ const version = await browser.version();
+ const chromeMajor = Number(/(?:Chrome|Chromium)\/(\d+)/.exec(version)?.[1]);
+ if (EXPECTED_CHROME_MAJOR !== null && chromeMajor !== EXPECTED_CHROME_MAJOR) {
+ throw new Error(
+ `Pinned Chrome ${EXPECTED_CHROME_MAJOR} required, received ${version}. ` +
+ "Override TIMELINE_CHROME_MAJOR only when intentionally recording a new baseline.",
+ );
+ }
+ const page = await browser.newPage();
+ await page.setViewport({
+ width: 1440,
+ height: 900,
+ deviceScaleFactor: TIER === "high-dpr" ? 2 : 1,
+ });
+ const client = await page.createCDPSession();
+ if (TIER === "low-resource") {
+ await client.send("Emulation.setCPUThrottlingRate", { rate: 4 });
+ }
+ await page.goto(STUDIO_URL, { waitUntil: "domcontentloaded", timeout: 60_000 });
+ await page.waitForFunction(
+ () => typeof window.__studioTest?.loadTimelinePerformanceFixture === "function",
+ { timeout: 30_000 },
+ );
+
+ await page.evaluate((profile) => {
+ window.__studioTest.loadTimelinePerformanceFixture({ elementCount: 1_000, profile });
+ }, PROFILE);
+ await client.send("HeapProfiler.collectGarbage");
+ const baselineHeapBytes = await collectHeapBytes(client);
+
+ const summary = await page.evaluate(
+ ({ elementCount, profile }) =>
+ window.__studioTest.loadTimelinePerformanceFixture({ elementCount, profile }),
+ { elementCount: ELEMENT_COUNT, profile: PROFILE },
+ );
+ await page.waitForFunction(() => document.querySelector('[aria-label="Timeline"]'));
+ const budgets = await page.evaluate(() => window.__studioTest.timelineViewportBudgets);
+ const measuredMaxReliableScrollWidth = await measureMaximumReliableScrollWidth(page);
+
+ const runs = [];
+ const interactionLimitMs =
+ TIER === "primary" ? budgets.interactionP95Ms : budgets.constrainedInteractionP95Ms;
+ const frameIntervalLimitMs =
+ TIER === "primary" ? budgets.frameIntervalP95Ms : budgets.constrainedFrameIntervalP95Ms;
+ for (let index = 0; index < budgets.warmupRuns + budgets.measuredRuns; index += 1) {
+ const run = await collectRun(page);
+ if (index >= budgets.warmupRuns) runs.push(run);
+ }
+ for (const run of runs) {
+ run.passed =
+ run.interactionP95Ms <= interactionLimitMs &&
+ run.frameIntervalP95Ms <= frameIntervalLimitMs &&
+ run.longestTaskMs <= budgets.longTaskLimitMs &&
+ run.diagnostics.mountedClipRoots <= budgets.maxMountedClipRoots &&
+ run.diagnostics.maxMountedClipRootsInOneRow <= budgets.maxMountedClipRootsPerRow &&
+ run.diagnostics.mountedTimelineDescendants < budgets.maxMountedTimelineDescendants;
+ }
+
+ await page.evaluate((profile) => {
+ window.__studioTest.loadTimelinePerformanceFixture({ elementCount: 1_000, profile });
+ }, PROFILE);
+ await client.send("HeapProfiler.collectGarbage");
+ const returnedHeapBytes = await collectHeapBytes(client);
+ const memoryReturned =
+ returnedHeapBytes <= baselineHeapBytes * (1 + budgets.memoryReturnToleranceRatio);
+ const passingRuns = runs.filter((run) => run.passed).length;
+ const maxTimelineContentWidthPx = Math.max(0, ...runs.map((run) => run.scrollWidth));
+ const directScrollGate = {
+ safetyEnvelopePx: budgets.directScrollSafetyPx,
+ maxTimelineContentWidthPx,
+ measuredMaxReliableScrollWidth,
+ decision:
+ maxTimelineContentWidthPx <= budgets.directScrollSafetyPx &&
+ measuredMaxReliableScrollWidth >= maxTimelineContentWidthPx
+ ? "approved"
+ : "rejected",
+ };
+ const evidence = {
+ environment: {
+ browser: version,
+ executablePath,
+ os: platform(),
+ architecture: arch(),
+ viewport: { width: 1440, height: 900 },
+ deviceScaleFactor: TIER === "high-dpr" ? 2 : 1,
+ cpuThrottleRate: TIER === "low-resource" ? 4 : 1,
+ tier: TIER,
+ fixture: summary,
+ runProtocol: {
+ warmups: budgets.warmupRuns,
+ measured: budgets.measuredRuns,
+ requiredPassing: budgets.requiredPassingRuns,
+ },
+ },
+ directScrollGate,
+ runs,
+ aggregate: {
+ interactionP95Ms: percentile(
+ runs.map((run) => run.interactionP95Ms),
+ 0.95,
+ ),
+ frameIntervalP95Ms: percentile(
+ runs.map((run) => run.frameIntervalP95Ms),
+ 0.95,
+ ),
+ passingRuns,
+ baselineHeapBytes,
+ returnedHeapBytes,
+ memoryReturned,
+ },
+ };
+ console.log(JSON.stringify(evidence, null, 2));
+ exitCode =
+ directScrollGate.decision === "approved" &&
+ passingRuns >= budgets.requiredPassingRuns &&
+ memoryReturned
+ ? 0
+ : 1;
+} finally {
+ await browser.close();
+}
+process.exit(exitCode);