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
7import { seedPerfState } from "./seedPerfState.ts";7import { seedPerfState } from "./seedPerfState.ts";
8 8
9describe("seedPerfState", () => {9describe("seedPerfState", () => {
10 const baseDirsToCleanup: string[] = [];10 const runParentDirsToCleanup: string[] = [];
11 11
12 afterEach(async () => {12 afterEach(async () => {
13 await Promise.all(13 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 })),
15 );17 );
16 });18 });
17 19
18 it("seeds large thread fixtures through the real event store and projection pipeline", async () => {20 it("seeds large thread fixtures through the real event store and projection pipeline", async () => {
19 const seeded = await seedPerfState("large_threads");21 const seeded = await seedPerfState("large_threads");
20 baseDirsToCleanup.push(seeded.baseDir);22 runParentDirsToCleanup.push(seeded.runParentDir);
21 23
22 expect(seeded.snapshot.projects).toHaveLength(1);24 expect(seeded.snapshot.projects).toHaveLength(1);
23 expect(seeded.snapshot.threads).toHaveLength(12);25 expect(seeded.snapshot.threads).toHaveLength(12);
26 expect(seeded.baseDir).toBe(join(seeded.runParentDir, "base"));
24 27
25 const heavyThread = seeded.snapshot.threads.find(28 const heavyThread = seeded.snapshot.threads.find(
26 (thread) => thread.id === PERF_CATALOG_IDS.largeThreads.heavyAThreadId,29 (thread) => thread.id === PERF_CATALOG_IDS.largeThreads.heavyAThreadId,
@@ -34,7 +37,7 @@ describe("seedPerfState", () => { · 7 unmodified lines
34 37
35 it("enables assistant streaming in the burst base seed for websocket perf runs", async () => {38 it("enables assistant streaming in the burst base seed for websocket perf runs", async () => {
36 const seeded = await seedPerfState("burst_base");39 const seeded = await seedPerfState("burst_base");
37 baseDirsToCleanup.push(seeded.baseDir);40 runParentDirsToCleanup.push(seeded.runParentDir);
38 41
39 const rawSettings = await readFile(join(seeded.baseDir, "userdata/settings.json"), "utf8");42 const rawSettings = await readFile(join(seeded.baseDir, "userdata/settings.json"), "utf8");
40 expect(JSON.parse(rawSettings)).toMatchObject({43 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
34 34
35export interface PerfSeededState {35export interface PerfSeededState {
36 readonly scenarioId: PerfSeedScenarioId;36 readonly scenarioId: PerfSeedScenarioId;
37 readonly runParentDir: string;
37 readonly baseDir: string;38 readonly baseDir: string;
38 readonly workspaceRoot: string;39 readonly workspaceRoot: string;
39 readonly snapshot: OrchestrationReadModel;40 readonly snapshot: OrchestrationReadModel;
@@ -489,6 +490,7 @@ export async function seedPerfState(scenarioId: PerfSeedScenarioId): Promise<Per · 449 unmodified lines
489 490
490 return {491 return {
491 scenarioId,492 scenarioId,
493 runParentDir,
492 baseDir,494 baseDir,
493 workspaceRoot,495 workspaceRoot,
494 snapshot,496 snapshot,
apps/server/scripts/seedPerfState.ts+1−0
@@ -13,6 +13,7 @@ const seeded = await seedPerfState(scenarioId); · 12 unmodified lines
13const payload = JSON.stringify(13const payload = JSON.stringify(
14 {14 {
15 scenarioId: seeded.scenarioId,15 scenarioId: seeded.scenarioId,
16 runParentDir: seeded.runParentDir,
16 baseDir: seeded.baseDir,17 baseDir: seeded.baseDir,
17 workspaceRoot: seeded.workspaceRoot,18 workspaceRoot: seeded.workspaceRoot,
18 projectTitle: seeded.snapshot.projects[0]?.title ?? null,19 projectTitle: seeded.snapshot.projects[0]?.title ?? null,
apps/web/test/perf/appHarness.ts+166−138
@@ -1,11 +1,11 @@
1import { spawn, type ChildProcess } from "node:child_process";1import { spawn, type ChildProcess } from "node:child_process";
2import { access, mkdir, writeFile } from "node:fs/promises";2import { access, mkdir, rm, writeFile } from "node:fs/promises";
3import { createServer } from "node:net";3import { createServer } from "node:net";
4import { join, resolve } from "node:path";4import { join, resolve } from "node:path";
5import { fileURLToPath } from "node:url";5import { fileURLToPath } from "node:url";
6import { once } from "node:events";6import { once } from "node:events";
7 7
8import { chromium, type Page } from "playwright";8import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
9 9
10import {10import {
11 type BrowserPerfMetrics,11 type BrowserPerfMetrics,
@@ -50,6 +50,7 @@ interface PerfSeedThreadSummary { · 38 unmodified lines
50 50
51export interface PerfSeededState {51export interface PerfSeededState {
52 readonly scenarioId: PerfSeedScenarioId;52 readonly scenarioId: PerfSeedScenarioId;
53 readonly runParentDir: string;
53 readonly baseDir: string;54 readonly baseDir: string;
54 readonly workspaceRoot: string;55 readonly workspaceRoot: string;
55 readonly projectTitle: string | null;56 readonly projectTitle: string | null;
@@ -181,6 +182,10 @@ async function ensureArtifactDir(suite: string, scenarioId: string): Promise<str · 125 unmodified lines
181 return artifactDir;182 return artifactDir;
182}183}
183 184
185async function cleanupPerfRunDir(runParentDir: string): Promise<void> {
186 await rm(runParentDir, { recursive: true, force: true });
187}
188
184async function writeServerLogs(189async function writeServerLogs(
185 artifactDir: string,190 artifactDir: string,
186 stdout: string,191 stdout: string,
@@ -295,151 +300,174 @@ export async function startPerfAppHarness( · 108 unmodified lines
295 serverProcess.stderr?.on("data", (chunk) => {300 serverProcess.stderr?.on("data", (chunk) => {
296 stderrBuffer += chunk.toString();301 stderrBuffer += chunk.toString();
297 });302 });
303 let browser: Browser | null = null;
304 let context: BrowserContext | null = null;
305 let page: Page | null = null;
298 306
299 await waitForServerReady(url, serverProcess);307 try {
300 308 await waitForServerReady(url, serverProcess);
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();
323 309
324 let finishPromise: Promise<{310 browser = await chromium.launch({
325 readonly artifactPath: string;311 headless: process.env[PERF_HEADFUL_ENV] !== "1",
326 readonly artifact: PerfRunArtifact;312 });
327 readonly browserMetrics: BrowserPerfMetrics;313 context = await browser.newContext({
328 readonly serverMetrics: ReadonlyArray<PerfServerMetricSample> | null;314 viewport: { width: 1440, height: 960 },
329 }> | null = null;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 });
330 326
331 const teardown = async () => {327 const sampler = options.serverSampler ?? new NoopServerSampler();
332 await Promise.allSettled([context.close(), browser.close()]);328 if (serverProcess.pid) {
333 await stopChildProcess(serverProcess);329 await sampler.start({ pid: serverProcess.pid });
334 };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 }
335 336
336 return {337 let finishPromise:
337 seededState,338 | Promise<{
338 page,339 readonly artifactPath: string;
339 url,340 readonly artifact: PerfRunArtifact;
340 artifactDir,341 readonly browserMetrics: BrowserPerfMetrics;
341 startAction: (name) =>342 readonly serverMetrics: ReadonlyArray<PerfServerMetricSample> | null;
342 invokeBrowserCollector(343 }>
343 page,344 | undefined;
344 (collectorName, actionName) => {345
345 (window as Window & Record<string, any>)[collectorName]?.startAction(346 const teardown = async () => {
346 actionName as string,347 await Promise.allSettled([
347 );348 context ? context.close() : Promise.resolve(),
348 },349 browser ? browser.close() : Promise.resolve(),
349 name,350 ]);
350 ),351 await stopChildProcess(serverProcess);
351 endAction: (name) =>352 await cleanupPerfRunDir(seededState.runParentDir);
352 invokeBrowserCollector(353 };
353 page,354
354 (collectorName, actionName) => {355 return {
355 return (356 seededState,
356 (window as Window & Record<string, any>)[collectorName]?.endAction(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(
357 actionName as string,365 actionName as string,
358 ) ?? null366 );
359 );367 },
360 },368 name,
361 name,369 ),
362 ),370 endAction: (name) =>
363 resetBrowserMetrics: () =>371 invokeBrowserCollector(
364 invokeBrowserCollector(page, (collectorName) => {372 readyPage,
365 (window as Window & Record<string, any>)[collectorName]?.reset();373 (collectorName, actionName) => {
366 }),374 return (
367 sampleMountedRows: (label) =>375 (window as Window & Record<string, any>)[collectorName]?.endAction(
368 invokeBrowserCollector(376 actionName as string,
369 page,377 ) ?? null
370 (collectorName, sampleLabel) => {378 );
371 return (379 },
372 (window as Window & Record<string, any>)[collectorName]?.sampleMountedRows(380 name,
373 sampleLabel as string,381 ),
374 ) ?? 0382 resetBrowserMetrics: () =>
375 );383 invokeBrowserCollector(readyPage, (collectorName) => {
376 },384 (window as Window & Record<string, any>)[collectorName]?.reset();
377 label,385 }),
378 ),386 sampleMountedRows: (label) =>
379 snapshotBrowserMetrics: () =>387 invokeBrowserCollector(
380 invokeBrowserCollector(page, (collectorName) => {388 readyPage,
381 return ((window as Window & Record<string, any>)[collectorName]?.snapshot() ?? {389 (collectorName, sampleLabel) => {
382 actions: [],390 return (
383 longTasks: [],391 (window as Window & Record<string, any>)[collectorName]?.sampleMountedRows(
384 rafGapsMs: [],392 sampleLabel as string,
385 mountedRowSamples: [],393 ) ?? 0
386 }) as BrowserPerfMetrics;394 );
387 }),395 },
388 finishRun: async (finishOptions) => {396 label,
389 if (finishPromise) {397 ),
390 return await finishPromise;398 snapshotBrowserMetrics: () =>
391 }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 }
392 411
393 finishPromise = (async () => {412 finishPromise = (async () => {
394 const completedAt = new Date().toISOString();413 const completedAt = new Date().toISOString();
395 const browserMetrics = await (async () => {414 const browserMetrics: BrowserPerfMetrics = await (async () => {
396 try {415 try {
397 return await invokeBrowserCollector(page, (collectorName) => {416 return await invokeBrowserCollector(readyPage, (collectorName) => {
398 return ((window as Window & Record<string, any>)[collectorName]?.snapshot() ?? {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 {
399 actions: [],426 actions: [],
400 longTasks: [],427 longTasks: [],
401 rafGapsMs: [],428 rafGapsMs: [],
402 mountedRowSamples: [],429 mountedRowSamples: [],
403 }) as BrowserPerfMetrics;430 } satisfies BrowserPerfMetrics;
404 });431 }
405 } catch {432 })();
406 return {433 const serverMetrics = await sampler.stop();
407 actions: [],434 await teardown();
408 longTasks: [],435
409 rafGapsMs: [],436 const basename =
410 mountedRowSamples: [],437 finishOptions.artifactBasename ?? `${finishOptions.suite}-${finishOptions.scenarioId}`;
411 } satisfies BrowserPerfMetrics;438 await writeServerLogs(artifactDir, stdoutBuffer, stderrBuffer, basename);
412 }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 };
413 })();459 })();
414 const serverMetrics = await sampler.stop();460
415 await teardown();461 return await finishPromise;
416 462 },
417 const basename =463 };
418 finishOptions.artifactBasename ?? `${finishOptions.suite}-${finishOptions.scenarioId}`;464 } catch (error) {
419 await writeServerLogs(artifactDir, stdoutBuffer, stderrBuffer, basename);465 await Promise.allSettled([
420 const artifact: PerfRunArtifact = {466 context ? context.close() : Promise.resolve(),
421 suite: finishOptions.suite,467 browser ? browser.close() : Promise.resolve(),
422 scenarioId: finishOptions.scenarioId,468 ]);
423 startedAt: runStartedAt,469 await stopChildProcess(serverProcess);
424 completedAt,470 await cleanupPerfRunDir(seededState.runParentDir);
425 thresholds: finishOptions.thresholds,471 throw error;
426 summary: summarizeBrowserPerfMetrics(browserMetrics, finishOptions.actionSummary),472 }
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 };
445}473}
apps/web/test/perf/supportHelpers.test.ts+52−0
@@ -0,0 +1,52 @@
1import { afterEach, describe, expect, it, vi } from "vitest";
2
3import { percentile } from "../../../../test/perf/support/artifact";
4import { installBrowserPerfCollector } from "../../../../test/perf/support/browserMetrics";
5
6describe("percentile", () => {
7 it("returns the minimum value for the zero percentile", () => {
8 expect(percentile([9, 3, 6], 0)).toBe(3);
9 });
10});
11
12describe("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 @@
1import { spawn, type ChildProcess } from "node:child_process";1import { spawn, type ChildProcess } from "node:child_process";
2import { once } from "node:events";2import { once } from "node:events";
3import { access } from "node:fs/promises";3import { access, rm } from "node:fs/promises";
4import { createServer } from "node:net";4import { createServer } from "node:net";
5import { resolve } from "node:path";5import { resolve } from "node:path";
6import { fileURLToPath } from "node:url";6import { fileURLToPath } from "node:url";
@@ -27,6 +27,7 @@ interface PerfSeedThreadSummary { · 20 unmodified lines
27 27
28interface PerfSeededState {28interface PerfSeededState {
29 readonly scenarioId: PerfSeedScenarioId;29 readonly scenarioId: PerfSeedScenarioId;
30 readonly runParentDir: string;
30 readonly baseDir: string;31 readonly baseDir: string;
31 readonly workspaceRoot: string;32 readonly workspaceRoot: string;
32 readonly projectTitle: string | null;33 readonly projectTitle: string | null;
@@ -252,6 +253,10 @@ async function stopChildProcess(process: ChildProcess): Promise<void> { · 219 unmodified lines
252 }253 }
253}254}
254 255
256async function cleanupPerfRunDir(runParentDir: string): Promise<void> {
257 await rm(runParentDir, { recursive: true, force: true });
258}
259
255function openUrl(url: string): void {260function openUrl(url: string): void {
256 const command: [string, ...string[]] =261 const command: [string, ...string[]] =
257 process.platform === "darwin"262 process.platform === "darwin"
@@ -339,6 +344,7 @@ async function main(): Promise<void> { · 81 unmodified lines
339 shuttingDown = true;344 shuttingDown = true;
340 process.stdout.write(`\nReceived ${signal}. Stopping perf app...\n`);345 process.stdout.write(`\nReceived ${signal}. Stopping perf app...\n`);
341 await stopChildProcess(serverProcess);346 await stopChildProcess(serverProcess);
347 await cleanupPerfRunDir(seededState.runParentDir);
342 process.exit(0);348 process.exit(0);
343 };349 };
344 350
@@ -349,17 +355,24 @@ async function main(): Promise<void> { · 4 unmodified lines
349 void shutdown("SIGTERM");355 void shutdown("SIGTERM");
350 });356 });
351 357
352 const url = `http://${options.host}:${port.toString()}`;358 try {
353 await waitForServerReady(url, serverProcess);359 const url = `http://${options.host}:${port.toString()}`;
354 printSeedSummary(seededState, url, options.providerScenarioId);360 await waitForServerReady(url, serverProcess);
361 printSeedSummary(seededState, url, options.providerScenarioId);
355 362
356 if (options.openBrowser) {363 if (options.openBrowser) {
357 openUrl(url);364 openUrl(url);
358 }365 }
359 366
360 const [exitCode] = (await once(serverProcess, "exit")) as [number | null];367 const [exitCode] = (await once(serverProcess, "exit")) as [number | null];
361 if (!shuttingDown) {368 if (!shuttingDown) {
362 process.exit(exitCode ?? 0);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;
363 }376 }
364}377}
365 378
test/perf/support/artifact.ts+4−1
@@ -68,7 +68,10 @@ export function percentile(values: ReadonlyArray<number>, target: number): numbe · 67 unmodified lines
68 }68 }
69 const sorted = values.toSorted((left, right) => left - right);69 const sorted = values.toSorted((left, right) => left - right);
70 const clampedTarget = Math.min(Math.max(target, 0), 1);70 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 );
72 return sorted[index] ?? null;75 return sorted[index] ?? null;
73}76}
74 77
test/perf/support/browserMetrics.ts+5−3
@@ -30,15 +30,16 @@ export function installBrowserPerfCollector( · 29 unmodified lines
30 const rafGapsMs: number[] = [];30 const rafGapsMs: number[] = [];
31 const mountedRowSamples: Array<BrowserPerfMetrics["mountedRowSamples"][number]> = [];31 const mountedRowSamples: Array<BrowserPerfMetrics["mountedRowSamples"][number]> = [];
32 let previousAnimationFrameTs = 0;32 let previousAnimationFrameTs = 0;
33 let rafHandle = 0;
33 34
34 const animationFrameLoop = (timestampMs: number) => {35 const animationFrameLoop = (timestampMs: number) => {
35 if (previousAnimationFrameTs > 0) {36 if (previousAnimationFrameTs > 0) {
36 rafGapsMs.push(timestampMs - previousAnimationFrameTs);37 rafGapsMs.push(timestampMs - previousAnimationFrameTs);
37 }38 }
38 previousAnimationFrameTs = timestampMs;39 previousAnimationFrameTs = timestampMs;
39 window.requestAnimationFrame(animationFrameLoop);40 rafHandle = window.requestAnimationFrame(animationFrameLoop);
40 };41 };
41 window.requestAnimationFrame(animationFrameLoop);42 rafHandle = window.requestAnimationFrame(animationFrameLoop);
42 43
43 if (typeof PerformanceObserver !== "undefined") {44 if (typeof PerformanceObserver !== "undefined") {
44 try {45 try {
@@ -103,7 +104,8 @@ export function installBrowserPerfCollector( · 58 unmodified lines
103 rafGapsMs.length = 0;104 rafGapsMs.length = 0;
104 mountedRowSamples.length = 0;105 mountedRowSamples.length = 0;
105 previousAnimationFrameTs = 0;106 previousAnimationFrameTs = 0;
106 window.requestAnimationFrame(animationFrameLoop);107 window.cancelAnimationFrame(rafHandle);
108 rafHandle = window.requestAnimationFrame(animationFrameLoop);
107 },109 },
108 };110 };
109}111}