History

Clean up perf run dirs and cover collector reset

- Persist and remove the seeded run parent directory after perf runs - Add regression coverage for seed cleanup, percentile clamping, and RAF reset

Julius Marminge committed 7 months ago

Commit 062c8091 parent b8fba1810 files · +260 −436Browse files

Unified · Split

10 files
  1. apps/server/integration/perf/seedPerfState.test.ts +7 −4
  2. apps/server/integration/perf/seedPerfState.ts +2 −0
  3. apps/server/scripts/seedPerfState.ts +1 −0
  4. apps/web/test/perf/appHarness.ts +166 −138
  5. apps/web/test/perf/supportHelpers.test.ts +52 −0
  6. artifacts/perf/virtualization-large_threads-1775077017735/virtualization-large_threads.json +0 −141
  7. artifacts/perf/websocket-application-burst_base-1775077022750/websocket-application-dense_assistant_stream.json +0 −139
  8. scripts/open-perf-app.ts +23 −10
  9. test/perf/support/artifact.ts +4 −1
  10. test/perf/support/browserMetrics.ts +5 −3
apps/server/integration/perf/seedPerfState.test.ts+7−4
@@ -7,20 +7,23 @@ import { PERF_CATALOG_IDS } from "@t3tools/shared/perf/scenarioCatalog"; · 6 unmodified lines
77 import { seedPerfState } from "./seedPerfState.ts";
88
99 describe("seedPerfState", () => {
10− const baseDirsToCleanup: string[] = [];
10+ const runParentDirsToCleanup: string[] = [];
1111
1212 afterEach(async () => {
1313 await Promise.all(
14− baseDirsToCleanup.splice(0).map((baseDir) => rm(baseDir, { recursive: true, force: true })),
14+ runParentDirsToCleanup
15+ .splice(0)
16+ .map((runParentDir) => rm(runParentDir, { recursive: true, force: true })),
1517 );
1618 });
1719
1820 it("seeds large thread fixtures through the real event store and projection pipeline", async () => {
1921 const seeded = await seedPerfState("large_threads");
20− baseDirsToCleanup.push(seeded.baseDir);
22+ runParentDirsToCleanup.push(seeded.runParentDir);
2123
2224 expect(seeded.snapshot.projects).toHaveLength(1);
2325 expect(seeded.snapshot.threads).toHaveLength(12);
26+ expect(seeded.baseDir).toBe(join(seeded.runParentDir, "base"));
2427
2528 const heavyThread = seeded.snapshot.threads.find(
2629 (thread) => thread.id === PERF_CATALOG_IDS.largeThreads.heavyAThreadId,
@@ -34,7 +37,7 @@ describe("seedPerfState", () => { · 7 unmodified lines
3437
3538 it("enables assistant streaming in the burst base seed for websocket perf runs", async () => {
3639 const seeded = await seedPerfState("burst_base");
37− baseDirsToCleanup.push(seeded.baseDir);
40+ runParentDirsToCleanup.push(seeded.runParentDir);
3841
3942 const rawSettings = await readFile(join(seeded.baseDir, "userdata/settings.json"), "utf8");
4043 expect(JSON.parse(rawSettings)).toMatchObject({
apps/server/integration/perf/seedPerfState.ts+2−0
@@ -34,6 +34,7 @@ import { ServerSettingsService, ServerSettingsLive } from "../../src/serverSetti · 33 unmodified lines
3434
3535 export interface PerfSeededState {
3636 readonly scenarioId: PerfSeedScenarioId;
37+ readonly runParentDir: string;
3738 readonly baseDir: string;
3839 readonly workspaceRoot: string;
3940 readonly snapshot: OrchestrationReadModel;
@@ -489,6 +490,7 @@ export async function seedPerfState(scenarioId: PerfSeedScenarioId): Promise<Per · 449 unmodified lines
489490
490491 return {
491492 scenarioId,
493+ runParentDir,
492494 baseDir,
493495 workspaceRoot,
494496 snapshot,
apps/server/scripts/seedPerfState.ts+1−0
@@ -13,6 +13,7 @@ const seeded = await seedPerfState(scenarioId); · 12 unmodified lines
1313 const payload = JSON.stringify(
1414 {
1515 scenarioId: seeded.scenarioId,
16+ runParentDir: seeded.runParentDir,
1617 baseDir: seeded.baseDir,
1718 workspaceRoot: seeded.workspaceRoot,
1819 projectTitle: seeded.snapshot.projects[0]?.title ?? null,
apps/web/test/perf/appHarness.ts+166−138
@@ -1,11 +1,11 @@
11 import { spawn, type ChildProcess } from "node:child_process";
2−import { access, mkdir, writeFile } from "node:fs/promises";
2+import { access, mkdir, rm, writeFile } from "node:fs/promises";
33 import { createServer } from "node:net";
44 import { join, resolve } from "node:path";
55 import { fileURLToPath } from "node:url";
66 import { once } from "node:events";
77
8−import { chromium, type Page } from "playwright";
8+import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
99
1010 import {
1111 type BrowserPerfMetrics,
@@ -50,6 +50,7 @@ interface PerfSeedThreadSummary { · 38 unmodified lines
5050
5151 export interface PerfSeededState {
5252 readonly scenarioId: PerfSeedScenarioId;
53+ readonly runParentDir: string;
5354 readonly baseDir: string;
5455 readonly workspaceRoot: string;
5556 readonly projectTitle: string | null;
@@ -181,6 +182,10 @@ async function ensureArtifactDir(suite: string, scenarioId: string): Promise<str · 125 unmodified lines
181182 return artifactDir;
182183 }
183184
185+async function cleanupPerfRunDir(runParentDir: string): Promise<void> {
186+ await rm(runParentDir, { recursive: true, force: true });
187+}
188+
184189 async function writeServerLogs(
185190 artifactDir: string,
186191 stdout: string,
@@ -295,151 +300,174 @@ export async function startPerfAppHarness( · 108 unmodified lines
295300 serverProcess.stderr?.on("data", (chunk) => {
296301 stderrBuffer += chunk.toString();
297302 });
303+ let browser: Browser | null = null;
304+ let context: BrowserContext | null = null;
305+ let page: Page | null = null;
298306
299− await waitForServerReady(url, serverProcess);
300−
301− const browser = await chromium.launch({
302− headless: process.env[PERF_HEADFUL_ENV] !== "1",
303− });
304− const context = await browser.newContext({
305− viewport: { width: 1440, height: 960 },
306− });
307− await context.addInitScript(installBrowserPerfCollector, "[data-timeline-row-kind]");
308− const page = await context.newPage();
309− await page.goto(url, { waitUntil: "domcontentloaded" });
310−
311− const firstProjectTitle =
312− seededState.projectTitle ?? getPerfSeedScenario(options.seedScenarioId).project.title;
313− if (!firstProjectTitle) {
314− throw new Error(`Seed scenario '${options.seedScenarioId}' produced no projects.`);
315− }
316− await page.getByText(firstProjectTitle, { exact: true }).first().waitFor({ timeout: 45_000 });
317−
318− const sampler = options.serverSampler ?? new NoopServerSampler();
319− if (serverProcess.pid) {
320− await sampler.start({ pid: serverProcess.pid });
321− }
322− const runStartedAt = new Date().toISOString();
307+ try {
308+ await waitForServerReady(url, serverProcess);
323309
324− let finishPromise: Promise<{
325− readonly artifactPath: string;
326− readonly artifact: PerfRunArtifact;
327− readonly browserMetrics: BrowserPerfMetrics;
328− readonly serverMetrics: ReadonlyArray<PerfServerMetricSample> | null;
329− }> | null = null;
310+ browser = await chromium.launch({
311+ headless: process.env[PERF_HEADFUL_ENV] !== "1",
312+ });
313+ context = await browser.newContext({
314+ viewport: { width: 1440, height: 960 },
315+ });
316+ await context.addInitScript(installBrowserPerfCollector, "[data-timeline-row-kind]");
317+ page = await context.newPage();
318+ await page.goto(url, { waitUntil: "domcontentloaded" });
319+
320+ const firstProjectTitle =
321+ seededState.projectTitle ?? getPerfSeedScenario(options.seedScenarioId).project.title;
322+ if (!firstProjectTitle) {
323+ throw new Error(`Seed scenario '${options.seedScenarioId}' produced no projects.`);
324+ }
325+ await page.getByText(firstProjectTitle, { exact: true }).first().waitFor({ timeout: 45_000 });
330326
331− const teardown = async () => {
332− await Promise.allSettled([context.close(), browser.close()]);
333− await stopChildProcess(serverProcess);
334− };
327+ const sampler = options.serverSampler ?? new NoopServerSampler();
328+ if (serverProcess.pid) {
329+ await sampler.start({ pid: serverProcess.pid });
330+ }
331+ const runStartedAt = new Date().toISOString();
332+ const readyPage = page;
333+ if (!readyPage) {
334+ throw new Error("Perf app harness did not initialize a browser page.");
335+ }
335336
336− return {
337− seededState,
338− page,
339− url,
340− artifactDir,
341− startAction: (name) =>
342− invokeBrowserCollector(
343− page,
344− (collectorName, actionName) => {
345− (window as Window & Record<string, any>)[collectorName]?.startAction(
346− actionName as string,
347− );
348− },
349− name,
350− ),
351− endAction: (name) =>
352− invokeBrowserCollector(
353− page,
354− (collectorName, actionName) => {
355− return (
356− (window as Window & Record<string, any>)[collectorName]?.endAction(
337+ let finishPromise:
338+ | Promise<{
339+ readonly artifactPath: string;
340+ readonly artifact: PerfRunArtifact;
341+ readonly browserMetrics: BrowserPerfMetrics;
342+ readonly serverMetrics: ReadonlyArray<PerfServerMetricSample> | null;
343+ }>
344+ | undefined;
345+
346+ const teardown = async () => {
347+ await Promise.allSettled([
348+ context ? context.close() : Promise.resolve(),
349+ browser ? browser.close() : Promise.resolve(),
350+ ]);
351+ await stopChildProcess(serverProcess);
352+ await cleanupPerfRunDir(seededState.runParentDir);
353+ };
354+
355+ return {
356+ seededState,
357+ page: readyPage,
358+ url,
359+ artifactDir,
360+ startAction: (name) =>
361+ invokeBrowserCollector(
362+ readyPage,
363+ (collectorName, actionName) => {
364+ (window as Window & Record<string, any>)[collectorName]?.startAction(
357365 actionName as string,
358− ) ?? null
359− );
360− },
361− name,
362− ),
363− resetBrowserMetrics: () =>
364− invokeBrowserCollector(page, (collectorName) => {
365− (window as Window & Record<string, any>)[collectorName]?.reset();
366− }),
367− sampleMountedRows: (label) =>
368− invokeBrowserCollector(
369− page,
370− (collectorName, sampleLabel) => {
371− return (
372− (window as Window & Record<string, any>)[collectorName]?.sampleMountedRows(
373− sampleLabel as string,
374− ) ?? 0
375− );
376− },
377− label,
378− ),
379− snapshotBrowserMetrics: () =>
380− invokeBrowserCollector(page, (collectorName) => {
381− return ((window as Window & Record<string, any>)[collectorName]?.snapshot() ?? {
382− actions: [],
383− longTasks: [],
384− rafGapsMs: [],
385− mountedRowSamples: [],
386− }) as BrowserPerfMetrics;
387− }),
388− finishRun: async (finishOptions) => {
389− if (finishPromise) {
390− return await finishPromise;
391− }
366+ );
367+ },
368+ name,
369+ ),
370+ endAction: (name) =>
371+ invokeBrowserCollector(
372+ readyPage,
373+ (collectorName, actionName) => {
374+ return (
375+ (window as Window & Record<string, any>)[collectorName]?.endAction(
376+ actionName as string,
377+ ) ?? null
378+ );
379+ },
380+ name,
381+ ),
382+ resetBrowserMetrics: () =>
383+ invokeBrowserCollector(readyPage, (collectorName) => {
384+ (window as Window & Record<string, any>)[collectorName]?.reset();
385+ }),
386+ sampleMountedRows: (label) =>
387+ invokeBrowserCollector(
388+ readyPage,
389+ (collectorName, sampleLabel) => {
390+ return (
391+ (window as Window & Record<string, any>)[collectorName]?.sampleMountedRows(
392+ sampleLabel as string,
393+ ) ?? 0
394+ );
395+ },
396+ label,
397+ ),
398+ snapshotBrowserMetrics: () =>
399+ invokeBrowserCollector(readyPage, (collectorName) => {
400+ return ((window as Window & Record<string, any>)[collectorName]?.snapshot() ?? {
401+ actions: [],
402+ longTasks: [],
403+ rafGapsMs: [],
404+ mountedRowSamples: [],
405+ }) as BrowserPerfMetrics;
406+ }),
407+ finishRun: async (finishOptions) => {
408+ if (finishPromise) {
409+ return await finishPromise;
410+ }
392411
393− finishPromise = (async () => {
394− const completedAt = new Date().toISOString();
395− const browserMetrics = await (async () => {
396− try {
397− return await invokeBrowserCollector(page, (collectorName) => {
398− return ((window as Window & Record<string, any>)[collectorName]?.snapshot() ?? {
412+ finishPromise = (async () => {
413+ const completedAt = new Date().toISOString();
414+ const browserMetrics: BrowserPerfMetrics = await (async () => {
415+ try {
416+ return await invokeBrowserCollector(readyPage, (collectorName) => {
417+ return ((window as Window & Record<string, any>)[collectorName]?.snapshot() ?? {
418+ actions: [],
419+ longTasks: [],
420+ rafGapsMs: [],
421+ mountedRowSamples: [],
422+ }) as BrowserPerfMetrics;
423+ });
424+ } catch {
425+ return {
399426 actions: [],
400427 longTasks: [],
401428 rafGapsMs: [],
402429 mountedRowSamples: [],
403− }) as BrowserPerfMetrics;
404− });
405− } catch {
406− return {
407− actions: [],
408− longTasks: [],
409− rafGapsMs: [],
410− mountedRowSamples: [],
411− } satisfies BrowserPerfMetrics;
412− }
430+ } satisfies BrowserPerfMetrics;
431+ }
432+ })();
433+ const serverMetrics = await sampler.stop();
434+ await teardown();
435+
436+ const basename =
437+ finishOptions.artifactBasename ?? `${finishOptions.suite}-${finishOptions.scenarioId}`;
438+ await writeServerLogs(artifactDir, stdoutBuffer, stderrBuffer, basename);
439+ const artifact: PerfRunArtifact = {
440+ suite: finishOptions.suite,
441+ scenarioId: finishOptions.scenarioId,
442+ startedAt: runStartedAt,
443+ completedAt,
444+ thresholds: finishOptions.thresholds,
445+ summary: summarizeBrowserPerfMetrics(browserMetrics, finishOptions.actionSummary),
446+ browserMetrics,
447+ serverMetrics,
448+ ...(finishOptions.metadata ? { metadata: finishOptions.metadata } : {}),
449+ };
450+ const artifactPath = join(artifactDir, `${basename}.json`);
451+ await writePerfArtifact(artifactPath, artifact);
452+
453+ return {
454+ artifactPath,
455+ artifact,
456+ browserMetrics,
457+ serverMetrics,
458+ };
413459 })();
414− const serverMetrics = await sampler.stop();
415− await teardown();
416−
417− const basename =
418− finishOptions.artifactBasename ?? `${finishOptions.suite}-${finishOptions.scenarioId}`;
419− await writeServerLogs(artifactDir, stdoutBuffer, stderrBuffer, basename);
420− const artifact: PerfRunArtifact = {
421− suite: finishOptions.suite,
422− scenarioId: finishOptions.scenarioId,
423− startedAt: runStartedAt,
424− completedAt,
425− thresholds: finishOptions.thresholds,
426− summary: summarizeBrowserPerfMetrics(browserMetrics, finishOptions.actionSummary),
427− browserMetrics,
428− serverMetrics,
429− ...(finishOptions.metadata ? { metadata: finishOptions.metadata } : {}),
430− };
431− const artifactPath = join(artifactDir, `${basename}.json`);
432− await writePerfArtifact(artifactPath, artifact);
433−
434− return {
435− artifactPath,
436− artifact,
437− browserMetrics,
438− serverMetrics,
439− };
440− })();
441−
442− return await finishPromise;
443− },
444− };
460+
461+ return await finishPromise;
462+ },
463+ };
464+ } catch (error) {
465+ await Promise.allSettled([
466+ context ? context.close() : Promise.resolve(),
467+ browser ? browser.close() : Promise.resolve(),
468+ ]);
469+ await stopChildProcess(serverProcess);
470+ await cleanupPerfRunDir(seededState.runParentDir);
471+ throw error;
472+ }
445473 }
apps/web/test/perf/supportHelpers.test.ts+52−0
@@ -0,0 +1,52 @@
1+import { afterEach, describe, expect, it, vi } from "vitest";
2+
3+import { percentile } from "../../../../test/perf/support/artifact";
4+import { installBrowserPerfCollector } from "../../../../test/perf/support/browserMetrics";
5+
6+describe("percentile", () => {
7+ it("returns the minimum value for the zero percentile", () => {
8+ expect(percentile([9, 3, 6], 0)).toBe(3);
9+ });
10+});
11+
12+describe("installBrowserPerfCollector", () => {
13+ afterEach(() => {
14+ vi.unstubAllGlobals();
15+ });
16+
17+ it("cancels the previous animation frame loop before reset starts a new one", () => {
18+ let nextHandle = 1;
19+ const callbacks = new Map<number, FrameRequestCallback>();
20+ const requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => {
21+ const handle = nextHandle++;
22+ callbacks.set(handle, callback);
23+ return handle;
24+ });
25+ const cancelAnimationFrame = vi.fn((handle: number) => {
26+ callbacks.delete(handle);
27+ });
28+
29+ vi.stubGlobal("window", {
30+ requestAnimationFrame,
31+ cancelAnimationFrame,
32+ } as unknown as Window & typeof globalThis);
33+ vi.stubGlobal("document", {
34+ querySelectorAll: vi.fn(() => []),
35+ } as unknown as Document);
36+ vi.stubGlobal("PerformanceObserver", undefined);
37+
38+ installBrowserPerfCollector();
39+
40+ const collector = window.__t3PerfCollector;
41+ expect(collector).toBeDefined();
42+ expect(requestAnimationFrame).toHaveBeenCalledTimes(1);
43+
44+ collector?.reset();
45+ expect(cancelAnimationFrame).toHaveBeenCalledWith(1);
46+ expect(requestAnimationFrame).toHaveBeenCalledTimes(2);
47+
48+ collector?.reset();
49+ expect(cancelAnimationFrame).toHaveBeenLastCalledWith(2);
50+ expect(requestAnimationFrame).toHaveBeenCalledTimes(3);
51+ });
52+});
artifacts/perf/virtualization-large_threads-1775077017735/virtualization-large_threads.json+0−141
@@ -1,141 +0,0 @@
1−{
2− "suite": "virtualization",
3− "scenarioId": "large_threads",
4− "startedAt": "2026-04-01T20:57:00.745Z",
5− "completedAt": "2026-04-01T20:57:01.524Z",
6− "thresholds": {
7− "maxMountedTimelineRows": 140,
8− "threadSwitchP50Ms": 250,
9− "threadSwitchP95Ms": 500,
10− "maxLongTaskMs": 120,
11− "maxRafGapMs": 120,
12− "burstCompletionMs": 5000,
13− "longTasksOver50MsMax": 2
14− },
15− "summary": {
16− "maxMountedTimelineRows": 23,
17− "threadSwitchP50Ms": 49.5,
18− "threadSwitchP95Ms": 117,
19− "maxLongTaskMs": 0,
20− "longTasksOver50Ms": 0,
21− "maxRafGapMs": 25,
22− "burstCompletionMs": null
23− },
24− "browserMetrics": {
25− "actions": [
26− {
27− "name": "thread-switch-warmup-a",
28− "durationMs": 117,
29− "startedAtMs": 591,
30− "endedAtMs": 708
31− },
32− {
33− "name": "thread-switch-1",
34− "durationMs": 46.5,
35− "startedAtMs": 709.7999999523163,
36− "endedAtMs": 756.2999999523163
37− },
38− {
39− "name": "thread-switch-2",
40− "durationMs": 49.5,
41− "startedAtMs": 787.6000000238419,
42− "endedAtMs": 837.1000000238419
43− },
44− {
45− "name": "thread-switch-3",
46− "durationMs": 70.69999992847443,
47− "startedAtMs": 840.7000000476837,
48− "endedAtMs": 911.3999999761581
49− },
50− {
51− "name": "thread-switch-4",
52− "durationMs": 45.89999997615814,
53− "startedAtMs": 916,
54− "endedAtMs": 961.8999999761581
55− },
56− {
57− "name": "thread-switch-5",
58− "durationMs": 46.699999928474426,
59− "startedAtMs": 964.6000000238419,
60− "endedAtMs": 1011.2999999523163
61− },
62− {
63− "name": "thread-switch-6",
64− "durationMs": 49.89999997615814,
65− "startedAtMs": 1047.8999999761581,
66− "endedAtMs": 1097.7999999523163
67− }
68− ],
69− "longTasks": [],
70− "rafGapsMs": [
71− 0, 8, 0, 17, 0, 7.300000000000068, 0, 8.799999999999955, 0, 8.200000000000045, 0,
72− 9.099999999999909, 0, 7.7000000000000455, 0, 8.399999999999977, 0, 8.800000000000068, 0, 16,
73− 0, 8.399999999999977, 0, 9, 0, 8.299999999999955, 0, 7.600000000000023, 0, 17.100000000000023,
74− 0, 8.600000000000023, 0, 8.299999999999955, 0, 16.200000000000045, 0, 7.899999999999977, 0,
75− 9.299999999999955, 0, 7.399999999999977, 0, 9.200000000000045, 0, 16.700000000000045, 0, 8, 0,
76− 8.299999999999955, 0, 7.899999999999977, 0, 8.700000000000045, 0, 17.100000000000023, 0,
77− 8.199999999999932, 0, 16, 0, 16.600000000000023, 0, 8.399999999999977, 0, 8.899999999999977,
78− 0, 7.800000000000068, 0, 17.399999999999977, 0, 8.299999999999955, 0, 8.300000000000068, 0,
79− 7.699999999999932, 0, 16.700000000000045, 0, 8.100000000000023, 0, 17.399999999999977, 0,
80− 7.7999999999999545, 0, 8.600000000000136, 0, 8.199999999999818, 0, 8.900000000000091, 0,
81− 8.099999999999909, 0, 16.200000000000045, 0, 9.100000000000136, 0, 7.399999999999864, 0, 8.5,
82− 0, 9, 0, 25, 0, 7.600000000000136, 0, 9.199999999999818, 0
83− ],
84− "mountedRowSamples": [
85− {
86− "label": "heavy-a-open",
87− "count": 17,
88− "capturedAtMs": 708.5
89− },
90− {
91− "label": "thread-switch-1-rows",
92− "count": 17,
93− "capturedAtMs": 782
94− },
95− {
96− "label": "thread-switch-2-rows",
97− "count": 17,
98− "capturedAtMs": 837.7000000476837
99− },
100− {
101− "label": "thread-switch-3-rows",
102− "count": 17,
103− "capturedAtMs": 914.2000000476837
104− },
105− {
106− "label": "thread-switch-4-rows",
107− "count": 17,
108− "capturedAtMs": 962.5
109− },
110− {
111− "label": "thread-switch-5-rows",
112− "count": 17,
113− "capturedAtMs": 1034.1000000238419
114− },
115− {
116− "label": "thread-switch-6-rows",
117− "count": 17,
118− "capturedAtMs": 1098.3999999761581
119− },
120− {
121− "label": "scroll-start",
122− "count": 17,
123− "capturedAtMs": 1113.3999999761581
124− },
125− {
126− "label": "scroll-top",
127− "count": 23,
128− "capturedAtMs": 1148.3999999761581
129− },
130− {
131− "label": "scroll-bottom",
132− "count": 17,
133− "capturedAtMs": 1164.2000000476837
134− }
135− ]
136− },
137− "serverMetrics": null,
138− "metadata": {
139− "heavyThreadMessageCount": 2000
140− }
141−}
artifacts/perf/websocket-application-burst_base-1775077022750/websocket-application-dense_assistant_stream.json+0−139
@@ -1,139 +0,0 @@
1−{
2− "suite": "websocket-application",
3− "scenarioId": "dense_assistant_stream",
4− "startedAt": "2026-04-01T20:57:03.910Z",
5− "completedAt": "2026-04-01T20:57:07.151Z",
6− "thresholds": {
7− "maxMountedTimelineRows": 140,
8− "threadSwitchP50Ms": 250,
9− "threadSwitchP95Ms": 500,
10− "maxLongTaskMs": 120,
11− "maxRafGapMs": 120,
12− "burstCompletionMs": 5000,
13− "longTasksOver50MsMax": 2
14− },
15− "summary": {
16− "maxMountedTimelineRows": 0,
17− "threadSwitchP50Ms": 51.199999928474426,
18− "threadSwitchP95Ms": 52.39999997615814,
19− "maxLongTaskMs": 0,
20− "longTasksOver50Ms": 0,
21− "maxRafGapMs": 16.200000000000045,
22− "burstCompletionMs": 3135
23− },
24− "browserMetrics": {
25− "actions": [
26− {
27− "name": "thread-switch-burst-nav",
28− "durationMs": 51.199999928474426,
29− "startedAtMs": 1348.3999999761581,
30− "endedAtMs": 1399.5999999046326
31− },
32− {
33− "name": "thread-switch-burst-return",
34− "durationMs": 52.39999997615814,
35− "startedAtMs": 1401.1999999284744,
36− "endedAtMs": 1453.5999999046326
37− },
38− {
39− "name": "burst-completion",
40− "durationMs": 3135,
41− "startedAtMs": 332.2999999523163,
42− "endedAtMs": 3467.2999999523163
43− }
44− ],
45− "longTasks": [],
46− "rafGapsMs": [
47− 0, 7.899999999999977, 0, 9, 0, 7.300000000000011, 0, 8.899999999999977, 0, 7.800000000000011,
48− 0, 9.300000000000011, 0, 8.100000000000023, 0, 7.699999999999989, 0, 9.199999999999989, 0,
49− 7.800000000000011, 0, 7.899999999999977, 0, 9.300000000000011, 0, 7.300000000000011, 0,
50− 9.099999999999966, 0, 7.7000000000000455, 0, 8.199999999999989, 0, 8.399999999999977, 0,
51− 8.300000000000011, 0, 8.399999999999977, 0, 8.5, 0, 9.100000000000023, 0, 7.7000000000000455,
52− 0, 8.199999999999932, 0, 8.600000000000023, 0, 8.799999999999955, 0, 7.7000000000000455, 0,
53− 8.200000000000045, 0, 8.399999999999977, 0, 9.100000000000023, 0, 7.5, 0, 8.199999999999932,
54− 0, 9.300000000000068, 0, 7.399999999999977, 0, 8.600000000000023, 0, 8.399999999999977, 0,
55− 8.899999999999977, 0, 8.299999999999955, 0, 7.800000000000068, 0, 8, 0, 8.399999999999977, 0,
56− 8.799999999999955, 0, 7.7000000000000455, 0, 9.299999999999955, 0, 7.400000000000091, 0,
57− 9.299999999999955, 0, 8.299999999999955, 0, 7.7000000000000455, 0, 8.100000000000023, 0,
58− 8.899999999999977, 0, 8.700000000000045, 0, 7.2999999999999545, 0, 9.399999999999977, 0,
59− 8.200000000000045, 0, 8.100000000000023, 0, 7.699999999999932, 0, 9.300000000000068, 0,
60− 8.199999999999932, 0, 8.300000000000068, 0, 7.699999999999932, 0, 8.700000000000045, 0,
61− 8.699999999999932, 0, 7.600000000000023, 0, 8.100000000000023, 0, 8.399999999999977, 0,
62− 8.700000000000045, 0, 8, 0, 8.699999999999932, 0, 8.200000000000045, 0, 8.799999999999955, 0,
63− 8.5, 0, 7.800000000000068, 0, 8.5, 0, 8.600000000000023, 0, 8.399999999999977, 0,
64− 7.899999999999977, 0, 8.100000000000023, 0, 8.100000000000023, 0, 8.399999999999977, 0,
65− 9.299999999999955, 0, 8.100000000000023, 0, 7.5, 0, 9.200000000000045, 0, 8.5, 0,
66− 7.899999999999864, 0, 8.800000000000182, 0, 8.199999999999818, 0, 7.800000000000182, 0, 8.5,
67− 0, 8.199999999999818, 0, 8.900000000000091, 0, 8.299999999999955, 0, 7.900000000000091, 0,
68− 7.899999999999864, 0, 9.300000000000182, 0, 7.599999999999909, 0, 8.400000000000091, 0, 8, 0,
69− 8.399999999999864, 0, 9.200000000000045, 0, 7.400000000000091, 0, 9.299999999999955, 0,
70− 8.200000000000045, 0, 7.5, 0, 9.299999999999955, 0, 8.099999999999909, 0, 7.600000000000136,
71− 0, 9.299999999999955, 0, 8.200000000000045, 0, 8.399999999999864, 0, 7.5, 0,
72− 8.400000000000091, 0, 8.599999999999909, 0, 7.7999999999999545, 0, 8.5, 0, 9.300000000000182,
73− 0, 7.2999999999999545, 0, 9.399999999999864, 0, 7.5, 0, 8.100000000000136, 0, 8.5, 0,
74− 8.099999999999909, 0, 9.100000000000136, 0, 8.599999999999909, 0, 7.5, 0, 9.099999999999909,
75− 0, 7.600000000000136, 0, 8.399999999999864, 0, 8.100000000000136, 0, 8.399999999999864, 0,
76− 8.200000000000045, 0, 9.299999999999955, 0, 8, 0, 16.200000000000045, 0, 9.200000000000045, 0,
77− 8.299999999999955, 0, 8.299999999999955, 0, 8.400000000000091, 0, 7.900000000000091, 0,
78− 8.599999999999909, 0, 8.5, 0, 8.099999999999909, 0, 7.7000000000000455, 0, 8.700000000000045,
79− 0, 8.5, 0, 8.5, 0, 7.599999999999909, 0, 8.900000000000091, 0, 8.599999999999909, 0,
80− 8.400000000000091, 0, 8.299999999999955, 0, 7.7999999999999545, 0, 8.600000000000136, 0,
81− 8.200000000000045, 0, 8.799999999999955, 0, 7.599999999999909, 0, 8.100000000000136, 0,
82− 8.599999999999909, 0, 8.299999999999955, 0, 8.100000000000136, 0, 8.599999999999909, 0,
83− 9.099999999999909, 0, 8.200000000000045, 0, 7.400000000000091, 0, 8.899999999999864, 0, 8.5,
84− 0, 8.5, 0, 7.900000000000091, 0, 8.799999999999955, 0, 7.400000000000091, 0,
85− 9.299999999999955, 0, 7.400000000000091, 0, 9.299999999999955, 0, 8.299999999999955, 0,
86− 8.299999999999955, 0, 7.400000000000091, 0, 8.899999999999864, 0, 8.800000000000182, 0,
87− 7.599999999999909, 0, 8.099999999999909, 0, 8.200000000000045, 0, 9.400000000000091, 0,
88− 7.7000000000000455, 0, 8.899999999999864, 0, 7.400000000000091, 0, 8.399999999999864, 0,
89− 8.900000000000091, 0, 8, 0, 8.5, 0, 8.799999999999955, 0, 7.7000000000000455, 0,
90− 8.200000000000045, 0, 8.700000000000045, 0, 7.899999999999864, 0, 8.799999999999955, 0,
91− 8.400000000000091, 0, 8.099999999999909, 0, 8.200000000000045, 0, 8.200000000000045, 0,
92− 8.200000000000045, 0, 8.799999999999955, 0, 7.7999999999999545, 0, 8.600000000000136, 0,
93− 8.199999999999818, 0, 9.200000000000045, 0, 7.500000000000227, 0, 8.199999999999818, 0,
94− 9.099999999999909, 0, 7.700000000000273, 0, 8.899999999999636, 0, 7.600000000000364, 0,
95− 8.399999999999636, 0, 9.300000000000182, 0, 7.400000000000091, 0, 8.299999999999727, 0,
96− 9.300000000000182, 0, 8.300000000000182, 0, 7.799999999999727, 0, 8.900000000000091, 0,
97− 7.699999999999818, 0, 8.800000000000182, 0, 8.300000000000182, 0, 7.599999999999909, 0,
98− 8.799999999999727, 0, 7.900000000000091, 0, 9.099999999999909, 0, 8.200000000000273, 0, 8, 0,
99− 8.799999999999727, 0, 7.600000000000364, 0, 9.299999999999727, 0, 8.200000000000273, 0,
100− 7.699999999999818, 0, 8, 0, 8.599999999999909, 0, 9.099999999999909, 0, 7.300000000000182, 0,
101− 8.900000000000091, 0, 8.799999999999727, 0, 8.100000000000364, 0, 7.899999999999636, 0, 8, 0,
102− 8.400000000000091, 0, 9.099999999999909, 0, 7.5, 0, 8.800000000000182, 0, 7.900000000000091,
103− 0, 9.299999999999727, 0, 7.5, 0, 9.100000000000364, 0, 8.099999999999909, 0,
104− 7.799999999999727, 0, 8.700000000000273, 0, 7.799999999999727, 0, 9.300000000000182, 0,
105− 8.300000000000182, 0, 8, 0, 8.699999999999818, 0, 7.599999999999909, 0, 9.099999999999909, 0,
106− 8.200000000000273, 0, 7.599999999999909, 0, 9.199999999999818, 0, 8.200000000000273, 0,
107− 8.400000000000091, 0, 8.299999999999727, 0, 8.200000000000273, 0, 8.5, 0, 8.299999999999727,
108− 0, 8.400000000000091, 0, 8.300000000000182, 0, 7.5, 0, 8.5, 0, 8.099999999999909, 0,
109− 9.299999999999727, 0, 7.400000000000091, 0, 9, 0, 8, 0, 8.900000000000091, 0,
110− 8.300000000000182, 0, 8.299999999999727, 0, 7.5, 0, 8.599999999999909, 0, 8, 0,
111− 9.300000000000182, 0, 7.400000000000091, 0, 9.299999999999727, 0, 8.300000000000182, 0,
112− 7.599999999999909, 0, 8.800000000000182, 0, 8.099999999999909, 0, 8, 0, 8.800000000000182, 0,
113− 8.5, 0, 8.5, 0, 8.099999999999909, 0, 8.5, 0, 8, 0, 8.199999999999818, 0, 7.900000000000091,
114− 0, 8.599999999999909, 0, 8.599999999999909, 0, 8, 0, 9.100000000000364, 0, 8.199999999999818,
115− 0, 8.5, 0, 8, 0, 7.800000000000182, 0, 8.899999999999636, 0, 7.800000000000182, 0,
116− 9.099999999999909, 0, 8.300000000000182, 0, 7.799999999999727, 0, 9, 0, 8.300000000000182, 0,
117− 7.5, 0, 9.199999999999818, 0, 7.400000000000091, 0, 9.300000000000182, 0, 8, 0,
118− 7.799999999999727, 0, 9.200000000000273, 0, 8, 0, 7.599999999999909, 0, 8.400000000000091, 0,
119− 9.299999999999727, 0, 7.300000000000182, 0, 8.5, 0, 9.199999999999818, 0, 8.200000000000273,
120− 0, 8.400000000000091, 0, 7.5, 0, 9.199999999999818, 0, 8.300000000000182, 0,
121− 7.399999999999636, 0, 8.400000000000091, 0, 9, 0, 8.599999999999909, 0, 8.200000000000273, 0,
122− 7.5, 0, 9.299999999999727, 0, 8.200000000000273, 0, 8.400000000000091, 0, 7.399999999999636,
123− 0, 8.300000000000182, 0, 9.400000000000091, 0, 8, 0, 8.5, 0, 8.099999999999909, 0, 8, 0, 8, 0,
124− 8.900000000000091, 0, 7.900000000000091, 0, 8.299999999999727, 0, 8.300000000000182, 0, 8.5,
125− 0, 8.699999999999818, 0, 8.099999999999909, 0, 9, 0, 8.300000000000182, 0, 7.400000000000091,
126− 0, 9.099999999999909, 0, 7.800000000000182, 0, 9.099999999999909, 0, 8.199999999999818, 0,
127− 8.400000000000091, 0, 8.300000000000182, 0, 7.899999999999636, 0, 8.700000000000273, 0, 8.5,
128− 0, 7.400000000000091, 0, 8.299999999999727, 0, 8.300000000000182, 0, 9.299999999999727, 0,
129− 8.300000000000182, 0, 8.199999999999818, 0
130− ],
131− "mountedRowSamples": []
132− },
133− "serverMetrics": null,
134− "metadata": {
135− "burstSeedThreadId": "perf-thread-burst",
136− "navigationThreadId": "perf-thread-light-01",
137− "sentinelText": "PERF_STREAM_SENTINEL:dense_assistant_stream:completed"
138− }
139−}
scripts/open-perf-app.ts+23−10
@@ -1,6 +1,6 @@
11 import { spawn, type ChildProcess } from "node:child_process";
22 import { once } from "node:events";
3−import { access } from "node:fs/promises";
3+import { access, rm } from "node:fs/promises";
44 import { createServer } from "node:net";
55 import { resolve } from "node:path";
66 import { fileURLToPath } from "node:url";
@@ -27,6 +27,7 @@ interface PerfSeedThreadSummary { · 20 unmodified lines
2727
2828 interface PerfSeededState {
2929 readonly scenarioId: PerfSeedScenarioId;
30+ readonly runParentDir: string;
3031 readonly baseDir: string;
3132 readonly workspaceRoot: string;
3233 readonly projectTitle: string | null;
@@ -252,6 +253,10 @@ async function stopChildProcess(process: ChildProcess): Promise<void> { · 219 unmodified lines
252253 }
253254 }
254255
256+async function cleanupPerfRunDir(runParentDir: string): Promise<void> {
257+ await rm(runParentDir, { recursive: true, force: true });
258+}
259+
255260 function openUrl(url: string): void {
256261 const command: [string, ...string[]] =
257262 process.platform === "darwin"
@@ -339,6 +344,7 @@ async function main(): Promise<void> { · 81 unmodified lines
339344 shuttingDown = true;
340345 process.stdout.write(`\nReceived ${signal}. Stopping perf app...\n`);
341346 await stopChildProcess(serverProcess);
347+ await cleanupPerfRunDir(seededState.runParentDir);
342348 process.exit(0);
343349 };
344350
@@ -349,17 +355,24 @@ async function main(): Promise<void> { · 4 unmodified lines
349355 void shutdown("SIGTERM");
350356 });
351357
352− const url = `http://${options.host}:${port.toString()}`;
353− await waitForServerReady(url, serverProcess);
354− printSeedSummary(seededState, url, options.providerScenarioId);
358+ try {
359+ const url = `http://${options.host}:${port.toString()}`;
360+ await waitForServerReady(url, serverProcess);
361+ printSeedSummary(seededState, url, options.providerScenarioId);
355362
356− if (options.openBrowser) {
357− openUrl(url);
358− }
363+ if (options.openBrowser) {
364+ openUrl(url);
365+ }
359366
360− const [exitCode] = (await once(serverProcess, "exit")) as [number | null];
361− if (!shuttingDown) {
362− process.exit(exitCode ?? 0);
367+ const [exitCode] = (await once(serverProcess, "exit")) as [number | null];
368+ if (!shuttingDown) {
369+ await cleanupPerfRunDir(seededState.runParentDir);
370+ process.exit(exitCode ?? 0);
371+ }
372+ } catch (error) {
373+ await stopChildProcess(serverProcess);
374+ await cleanupPerfRunDir(seededState.runParentDir);
375+ throw error;
363376 }
364377 }
365378
test/perf/support/artifact.ts+4−1
@@ -68,7 +68,10 @@ export function percentile(values: ReadonlyArray<number>, target: number): numbe · 67 unmodified lines
6868 }
6969 const sorted = values.toSorted((left, right) => left - right);
7070 const clampedTarget = Math.min(Math.max(target, 0), 1);
71− const index = Math.min(sorted.length - 1, Math.ceil(sorted.length * clampedTarget) - 1);
71+ const index = Math.min(
72+ sorted.length - 1,
73+ Math.max(0, Math.ceil(sorted.length * clampedTarget) - 1),
74+ );
7275 return sorted[index] ?? null;
7376 }
7477
test/perf/support/browserMetrics.ts+5−3
@@ -30,15 +30,16 @@ export function installBrowserPerfCollector( · 29 unmodified lines
3030 const rafGapsMs: number[] = [];
3131 const mountedRowSamples: Array<BrowserPerfMetrics["mountedRowSamples"][number]> = [];
3232 let previousAnimationFrameTs = 0;
33+ let rafHandle = 0;
3334
3435 const animationFrameLoop = (timestampMs: number) => {
3536 if (previousAnimationFrameTs > 0) {
3637 rafGapsMs.push(timestampMs - previousAnimationFrameTs);
3738 }
3839 previousAnimationFrameTs = timestampMs;
39− window.requestAnimationFrame(animationFrameLoop);
40+ rafHandle = window.requestAnimationFrame(animationFrameLoop);
4041 };
41− window.requestAnimationFrame(animationFrameLoop);
42+ rafHandle = window.requestAnimationFrame(animationFrameLoop);
4243
4344 if (typeof PerformanceObserver !== "undefined") {
4445 try {
@@ -103,7 +104,8 @@ export function installBrowserPerfCollector( · 58 unmodified lines
103104 rafGapsMs.length = 0;
104105 mountedRowSamples.length = 0;
105106 previousAnimationFrameTs = 0;
106− window.requestAnimationFrame(animationFrameLoop);
107+ window.cancelAnimationFrame(rafHandle);
108+ rafHandle = window.requestAnimationFrame(animationFrameLoop);
107109 },
108110 };
109111 }