Skip to content

Commit a18831d

Browse files
committed
fix(ci): measure timeline performance in production React
1 parent 6b98c8d commit a18831d

6 files changed

Lines changed: 81 additions & 10 deletions

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -553,8 +553,11 @@ jobs:
553553
trap stop_server EXIT
554554
555555
# Run one server at a time so the measured browser never competes with
556-
# a second Vite module graph on the shared runner.
557-
bun run --cwd packages/studio dev -- --port 5313 --strictPort &
556+
# a second Vite module graph on the shared runner. The development
557+
# server supplies the fixture API; production React matches shipped
558+
# rendering behavior, and the gate asserts that runtime before timing.
559+
NODE_ENV=production \
560+
bun run --cwd packages/studio dev -- --port 5313 --strictPort &
558561
SERVER_PID=$!
559562
DEFAULT_STATUS=0
560563
if wait_for_server 5313; then
@@ -570,7 +573,8 @@ jobs:
570573
fi
571574
stop_server
572575
573-
VITE_STUDIO_TIMELINE_ROW_VIRTUALIZATION_ENABLED=0 \
576+
NODE_ENV=production \
577+
VITE_STUDIO_TIMELINE_ROW_VIRTUALIZATION_ENABLED=0 \
574578
bun run --cwd packages/studio dev -- --port 5314 --strictPort &
575579
SERVER_PID=$!
576580
DISABLED_STATUS=0
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
3+
afterEach(() => {
4+
vi.unstubAllEnvs();
5+
vi.resetModules();
6+
});
7+
8+
describe("Studio test mode", () => {
9+
it("exposes hooks in the normal development runtime", async () => {
10+
const { STUDIO_RUNTIME_MODE, STUDIO_TEST_HOOKS_ENABLED } = await import("./studioTestMode");
11+
12+
expect(STUDIO_RUNTIME_MODE).toBe("development");
13+
expect(STUDIO_TEST_HOOKS_ENABLED).toBe(true);
14+
});
15+
16+
it("keeps hooks on the development server while it uses production React", async () => {
17+
vi.stubEnv("DEV", false);
18+
vi.stubEnv("MODE", "development");
19+
20+
const { STUDIO_RUNTIME_MODE, STUDIO_TEST_HOOKS_ENABLED } = await import("./studioTestMode");
21+
22+
expect(STUDIO_RUNTIME_MODE).toBe("production");
23+
expect(STUDIO_TEST_HOOKS_ENABLED).toBe(true);
24+
});
25+
26+
it("keeps test hooks out of an ordinary production build", async () => {
27+
vi.stubEnv("DEV", false);
28+
vi.stubEnv("MODE", "production");
29+
30+
const { STUDIO_RUNTIME_MODE, STUDIO_TEST_HOOKS_ENABLED } = await import("./studioTestMode");
31+
32+
expect(STUDIO_RUNTIME_MODE).toBe("production");
33+
expect(STUDIO_TEST_HOOKS_ENABLED).toBe(false);
34+
});
35+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export type StudioRuntimeMode = "development" | "production";
2+
3+
function readStudioImportMetaEnv(): ImportMetaEnv | undefined {
4+
try {
5+
return import.meta.env;
6+
} catch {
7+
return undefined;
8+
}
9+
}
10+
11+
const studioImportMetaEnv = readStudioImportMetaEnv();
12+
13+
/**
14+
* Test hooks belong to Vite's development server, even when that server uses
15+
* production React for performance measurement. A production build has neither
16+
* DEV nor the development server mode, so the API stays out of shipped assets.
17+
*/
18+
export const STUDIO_RUNTIME_MODE: StudioRuntimeMode = studioImportMetaEnv?.DEV
19+
? "development"
20+
: "production";
21+
22+
export const STUDIO_TEST_HOOKS_ENABLED =
23+
studioImportMetaEnv?.DEV === true || studioImportMetaEnv?.MODE === "development";

packages/studio/src/hooks/useStudioTestHooks.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ describe("timeline performance fixture", () => {
8585
const api = window.__studioTest;
8686
expect(api).toBeDefined();
8787
if (!api) throw new Error("Expected dev Studio test API");
88+
expect(api.runtimeMode).toBe("development");
8889
let notifications = 0;
8990
usePlayerStore.setState({
9091
isPlaying: true,

packages/studio/src/hooks/useStudioTestHooks.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type TimelinePerformanceFixtureSummary,
1313
} from "../player/lib/timelinePerformanceFixture";
1414
import { TIMELINE_VIEWPORT_BUDGETS } from "../player/lib/timelineViewportBudgets";
15+
import { STUDIO_RUNTIME_MODE, STUDIO_TEST_HOOKS_ENABLED } from "./studioTestMode";
1516

1617
interface StudioTestHookDeps {
1718
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
@@ -23,6 +24,7 @@ interface StudioTestHookDeps {
2324
}
2425

2526
interface StudioTestApi {
27+
runtimeMode: typeof STUDIO_RUNTIME_MODE;
2628
selectByDomId: (id: string) => Promise<boolean>;
2729
loadTimelinePerformanceFixture: (
2830
spec: TimelinePerformanceFixtureSpec,
@@ -54,14 +56,9 @@ export function useStudioTestHooks({
5456
}: StudioTestHookDeps): void {
5557
// eslint-disable-next-line no-restricted-syntax
5658
useEffect(() => {
57-
let isDev = false;
58-
try {
59-
isDev = import.meta.env.DEV === true;
60-
} catch {
61-
isDev = false;
62-
}
63-
if (!isDev || typeof window === "undefined") return;
59+
if (!STUDIO_TEST_HOOKS_ENABLED || typeof window === "undefined") return;
6460
const api: StudioTestApi = {
61+
runtimeMode: STUDIO_RUNTIME_MODE,
6562
selectByDomId: async (id: string): Promise<boolean> => {
6663
const element = previewIframeRef.current?.contentDocument?.getElementById(id) ?? null;
6764
if (!element) return false;

packages/studio/tests/e2e/timeline-virtualization.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
* shared runner: no emulation, but the constrained budgets, because a hosted
1414
* runner is already slower and noisier than the machine the strict numbers were
1515
* recorded on. Throttling it further would measure the throttle, not the build.
16+
* CI also requires production React and reports the observed runtime so Vite's
17+
* development-only checks can never contaminate the shipped-code measurement.
1618
*
1719
* TIMELINE_ROW_VIRTUALIZATION selects which build is under test and defaults to
1820
* "on", the product default. The script asserts the configuration it observes
@@ -284,6 +286,14 @@ try {
284286
);
285287
await waitForStudioTestHookSettle(page);
286288

289+
const runtimeMode = await page.evaluate(() => window.__studioTest.runtimeMode);
290+
if (TIER === "ci" && runtimeMode !== "production") {
291+
throw new Error(
292+
`Timeline CI must measure the production React runtime, received ${runtimeMode}. ` +
293+
"Start the Studio development server with NODE_ENV=production.",
294+
);
295+
}
296+
287297
await loadFixtureAndWait(page, 1_000, PROFILE);
288298
await client.send("HeapProfiler.collectGarbage");
289299
const baselineHeapBytes = await collectHeapBytes(client);
@@ -374,6 +384,7 @@ try {
374384
deviceScaleFactor: TIER === "high-dpr" ? 2 : 1,
375385
cpuThrottleRate: TIER === "low-resource" ? 4 : 1,
376386
tier: TIER,
387+
runtimeMode,
377388
longTaskObserverProbe,
378389
rowVirtualization: ROW_VIRTUALIZATION,
379390
observedClipRootsAtLoad: observedClipRoots,

0 commit comments

Comments
 (0)