@mui/internal-benchmark 0.0.3-canary.2 → 0.0.3-canary.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/reporter.mjs CHANGED
@@ -1,46 +1,33 @@
1
1
  import * as path from 'node:path';
2
2
  import * as fs from 'node:fs/promises';
3
3
  import { benchmarkUploadSchema, getCiMetadata } from "./ciReport.mjs";
4
- import { calculateMean, calculateStdDev, quantile, isOutlier } from "./stats.mjs";
4
+ import { calculateMean, aggregateSamples } from "./stats.mjs";
5
5
  import { dim, red, green, yellow, cyan, printTable, fileUrl } from "./format.mjs";
6
6
  import { uploadCiReport } from "./upload.mjs";
7
7
  import { syncPrComment } from "./syncPrComment.mjs";
8
8
  // Import for TaskMeta augmentation side effect
9
9
  import "./taskMetaAugmentation.mjs";
10
- const byNumeric = (a, b) => a - b;
11
10
  function getEventKey(event) {
12
11
  return `${event.id}:${event.phase}`;
13
12
  }
14
- function aggregateMetrics(iterations) {
15
- // Collect all metric names across iterations
16
- const metricValues = new Map();
17
- for (const iteration of iterations) {
18
- for (const metric of iteration.metrics) {
19
- let values = metricValues.get(metric.name);
20
- if (!values) {
21
- values = [];
22
- metricValues.set(metric.name, values);
23
- }
24
- values.push(metric.value);
25
- }
13
+
14
+ /** Order-insensitive deep equality, treating a missing key and an `undefined` value as equal. */
15
+ function deepEqual(first, second) {
16
+ if (first === second) {
17
+ return true;
26
18
  }
27
- const result = {};
28
- for (const [name, values] of metricValues) {
29
- // Apply IQR filtering
30
- const sorted = [...values].sort(byNumeric);
31
- const q1 = quantile(sorted, 0.25);
32
- const q3 = quantile(sorted, 0.75);
33
- const filtered = values.filter(d => !isOutlier(d, q1, q3));
34
- const used = filtered.length > 0 ? filtered : values;
35
- const mean = calculateMean(used);
36
- const stdDev = calculateStdDev(used, mean);
37
- result[name] = {
38
- mean,
39
- stdDev,
40
- outliers: values.length - used.length
41
- };
19
+ if (typeof first !== 'object' || first === null || typeof second !== 'object' || second === null) {
20
+ return false;
42
21
  }
43
- return result;
22
+ const firstRecord = first;
23
+ const secondRecord = second;
24
+ const keys = new Set([...Object.keys(firstRecord), ...Object.keys(secondRecord)]);
25
+ for (const key of keys) {
26
+ if (!deepEqual(firstRecord[key], secondRecord[key])) {
27
+ return false;
28
+ }
29
+ }
30
+ return true;
44
31
  }
45
32
  function generateReportFromIterations(iterations) {
46
33
  if (iterations.length === 0) {
@@ -69,13 +56,11 @@ function generateReportFromIterations(iterations) {
69
56
  const renderStats = [];
70
57
  for (let index = 0; index < expectedLength; index += 1) {
71
58
  const durations = iterations.map(iteration => iteration.renders[index].actualDuration);
72
- const sorted = [...durations].sort(byNumeric);
73
- const q1 = quantile(sorted, 0.25);
74
- const q3 = quantile(sorted, 0.75);
75
- const filtered = durations.filter(d => !isOutlier(d, q1, q3));
76
- const used = filtered.length > 0 ? filtered : durations;
77
- const iqrMean = calculateMean(used);
78
- const iqrStdDev = calculateStdDev(used, iqrMean);
59
+ const {
60
+ mean: iqrMean,
61
+ stdDev: iqrStdDev,
62
+ outliers
63
+ } = aggregateSamples(durations);
79
64
  const coefficientOfVariation = iqrMean > 0 ? iqrStdDev / iqrMean : 0;
80
65
  if (iqrMean > 1 && coefficientOfVariation > 0.1) {
81
66
  const event = firstIteration.renders[index];
@@ -85,7 +70,7 @@ function generateReportFromIterations(iterations) {
85
70
  event: firstIteration.renders[index],
86
71
  iqrMean,
87
72
  iqrStdDev,
88
- outliers: durations.length - used.length
73
+ outliers
89
74
  });
90
75
  }
91
76
 
@@ -119,8 +104,8 @@ function generateReportFromIterations(iterations) {
119
104
  totalDuration += iqrMean;
120
105
  }
121
106
 
122
- // Aggregate metrics
123
- const metrics = aggregateMetrics(iterations);
107
+ // Custom + paint metrics are merged separately from `task.meta.benchmarkMetrics`.
108
+ const metrics = {};
124
109
  return {
125
110
  iterations: iterationCount,
126
111
  totalDuration,
@@ -167,21 +152,71 @@ function printDurationMatrix(name, report, footer) {
167
152
  width: 4
168
153
  }], rows, footer, name);
169
154
  }
170
- function printMetricsTable(name, metrics, iterationCount) {
155
+
156
+ /** Strips a `#sub-series` suffix to recover the metric name used to look up its definition. */
157
+ function baseMetricName(key) {
158
+ const hashIndex = key.indexOf('#');
159
+ return hashIndex === -1 ? key : key.slice(0, hashIndex);
160
+ }
161
+ function formatMetricValue(value, definition) {
162
+ if (definition?.format) {
163
+ return new Intl.NumberFormat(undefined, definition.format).format(value);
164
+ }
165
+ return value.toFixed(2);
166
+ }
167
+
168
+ /**
169
+ * Merges aggregated custom metrics (already stats, not raw samples) into a report entry, keyed
170
+ * `name` or `name#id` for sub-series, and collects each metric's config into the shared
171
+ * top-level definitions. For a metric-only test, derives the iteration count from the samples.
172
+ */
173
+ function mergeCustomMetrics(report, customMetrics, definitions) {
174
+ let maxCount = 0;
175
+ for (const [metricName, metric] of Object.entries(customMetrics)) {
176
+ for (const [seriesId, stats] of Object.entries(metric.series)) {
177
+ const key = seriesId === '' ? metricName : `${metricName}#${seriesId}`;
178
+ report.metrics[key] = {
179
+ mean: stats.mean,
180
+ stdDev: stats.stdDev,
181
+ outliers: stats.outliers
182
+ };
183
+ maxCount = Math.max(maxCount, stats.count);
184
+ }
185
+ const definition = {
186
+ kind: metric.kind,
187
+ format: metric.config.format,
188
+ alarm: metric.config.alarm
189
+ };
190
+ // A metric name maps to one definition. Reusing it across benchmarks is fine when the config
191
+ // matches (e.g. the harness `bench:paint`), but conflicting config would silently apply
192
+ // last-write-wins to every entry — reject it instead.
193
+ const existing = definitions[metricName];
194
+ if (existing && !deepEqual(existing, definition)) {
195
+ throw new Error(`Benchmark metric "${metricName}" is defined with conflicting configuration across ` + `benchmarks. A metric name must map to a single kind, format, and alarm.`);
196
+ }
197
+ definitions[metricName] = definition;
198
+ }
199
+ if (report.iterations === 0) {
200
+ report.iterations = maxCount;
201
+ }
202
+ }
203
+ function printMetricsTable(name, metrics, iterationCount, definitions) {
171
204
  const entries = Object.entries(metrics);
172
205
  if (entries.length === 0) {
173
206
  return;
174
207
  }
175
208
  const rows = entries.map(([metricName, stats]) => {
176
- const iqrStr = `${stats.mean.toFixed(2)}±${stats.stdDev.toFixed(2)}`;
209
+ const definition = definitions[baseMetricName(metricName)];
210
+ const iqrStr = `${formatMetricValue(stats.mean, definition)}±${formatMetricValue(stats.stdDev, definition)}`;
177
211
  const cv = stats.mean > 0 ? stats.stdDev / stats.mean * 100 : 0;
178
- return [metricName.slice(0, LABEL_WIDTH).padStart(LABEL_WIDTH), cyan(iqrStr.padStart(STAT_WIDTH)), colorCV(cv), stats.outliers > 0 ? yellow(String(stats.outliers).padStart(4)) : dim('0'.padStart(4))];
212
+ const label = definition?.alarm ? `${metricName} ⚠` : metricName;
213
+ return [label.slice(0, LABEL_WIDTH).padStart(LABEL_WIDTH), cyan(iqrStr.padStart(STAT_WIDTH)), colorCV(cv), stats.outliers > 0 ? yellow(String(stats.outliers).padStart(4)) : dim('0'.padStart(4))];
179
214
  });
180
215
  printTable([{
181
216
  header: 'Metric',
182
217
  width: LABEL_WIDTH
183
218
  }, {
184
- header: 'Mean±σ (ms)',
219
+ header: 'Mean±σ',
185
220
  width: STAT_WIDTH
186
221
  }, {
187
222
  header: 'Var%',
@@ -204,28 +239,47 @@ async function loadBaselineReport(baselinePath) {
204
239
  }
205
240
  class BenchmarkReporter {
206
241
  benchmarks = {};
242
+ metricDefinitions = {};
207
243
  hasFailures = false;
208
244
  constructor(options) {
209
245
  this.outputPath = options?.outputPath ?? process.env.BENCHMARK_OUTPUT_PATH ?? path.resolve(process.cwd(), 'benchmarks', 'results.json');
210
246
  this.upload = options?.upload ?? process.env.BENCHMARK_UPLOAD === 'true';
211
247
  this.baselinePath = options?.baselinePath ?? process.env.BENCHMARK_BASELINE_PATH;
212
248
  }
249
+
250
+ // Reset accumulated state at the start of every run so watch-mode re-runs start clean (the
251
+ // reporter instance is reused across runs). Otherwise stale benchmarks/definitions linger — and
252
+ // an edited metric config would conflict with its own previous-run definition.
253
+ onTestRunStart() {
254
+ this.benchmarks = {};
255
+ this.metricDefinitions = {};
256
+ this.hasFailures = false;
257
+ }
213
258
  onTestCaseResult(testCase) {
214
259
  if (testCase.result().state === 'failed') {
215
260
  this.hasFailures = true;
216
261
  }
217
262
  const meta = testCase.meta();
218
263
  const iterations = meta.benchmarkIterations;
219
- if (!iterations) {
264
+ const customMetrics = meta.benchmarkMetrics;
265
+ if (!iterations && !customMetrics) {
220
266
  console.warn(yellow(` No iterations recorded for: ${testCase.fullName}`));
221
267
  return;
222
268
  }
223
269
  const name = meta.benchmarkName ?? testCase.fullName;
224
- const report = generateReportFromIterations(iterations);
270
+ const report = iterations ? generateReportFromIterations(iterations) : {
271
+ iterations: 0,
272
+ totalDuration: 0,
273
+ renders: [],
274
+ metrics: {}
275
+ };
276
+ if (customMetrics) {
277
+ mergeCustomMetrics(report, customMetrics, this.metricDefinitions);
278
+ }
225
279
  this.benchmarks[name] = report;
226
280
  const summary = dim('Total: ') + green(`${report.totalDuration.toFixed(2)}ms`) + dim(` (${report.renders.length} renders, ${report.iterations} iterations)`);
227
281
  printDurationMatrix(`${name} — React`, report, summary);
228
- printMetricsTable(name, report.metrics, report.iterations);
282
+ printMetricsTable(name, report.metrics, report.iterations, this.metricDefinitions);
229
283
  }
230
284
  async onTestRunEnd() {
231
285
  const count = Object.keys(this.benchmarks).length;
@@ -237,11 +291,15 @@ class BenchmarkReporter {
237
291
  console.log(` ${name}: ${result.totalDuration.toFixed(2)}ms ${dim(`(${result.renders.length} renders, ${result.iterations} iterations)`)}`);
238
292
  }
239
293
  const baseline = this.baselinePath ? await loadBaselineReport(this.baselinePath) : undefined;
294
+ const hasMetricDefinitions = Object.keys(this.metricDefinitions).length > 0;
240
295
  const results = {
241
296
  version: 1,
242
297
  reportType: 'benchmark',
243
298
  ...(await getCiMetadata()),
244
299
  report: this.benchmarks,
300
+ ...(hasMetricDefinitions ? {
301
+ metricDefinitions: this.metricDefinitions
302
+ } : {}),
245
303
  ...(baseline ? {
246
304
  base: baseline
247
305
  } : {})
package/stats.d.mts CHANGED
@@ -7,4 +7,14 @@ export declare function quantile(sorted: number[], q: number): number;
7
7
  *
8
8
  * See https://en.wikipedia.org/wiki/Interquartile_range#Outliers
9
9
  */
10
- export declare function isOutlier(value: number, q1: number, q3: number): boolean;
10
+ export declare function isOutlier(value: number, q1: number, q3: number): boolean;
11
+ /**
12
+ * Aggregates a series of samples into a mean, standard deviation, and outlier count using
13
+ * IQR-based outlier removal. Falls back to the raw values when filtering would remove
14
+ * everything. This is the shared aggregation core for custom metrics.
15
+ */
16
+ export declare function aggregateSamples(values: number[]): {
17
+ mean: number;
18
+ stdDev: number;
19
+ outliers: number;
20
+ };
package/stats.mjs CHANGED
@@ -24,4 +24,24 @@ export function quantile(sorted, q) {
24
24
  export function isOutlier(value, q1, q3) {
25
25
  const iqr = q3 - q1;
26
26
  return value < q1 - 1.5 * iqr || value > q3 + 1.5 * iqr;
27
+ }
28
+
29
+ /**
30
+ * Aggregates a series of samples into a mean, standard deviation, and outlier count using
31
+ * IQR-based outlier removal. Falls back to the raw values when filtering would remove
32
+ * everything. This is the shared aggregation core for custom metrics.
33
+ */
34
+ export function aggregateSamples(values) {
35
+ const sorted = values.toSorted((first, second) => first - second);
36
+ const q1 = quantile(sorted, 0.25);
37
+ const q3 = quantile(sorted, 0.75);
38
+ const filtered = values.filter(value => !isOutlier(value, q1, q3));
39
+ const used = filtered.length > 0 ? filtered : values;
40
+ const mean = calculateMean(used);
41
+ const stdDev = calculateStdDev(used, mean);
42
+ return {
43
+ mean,
44
+ stdDev,
45
+ outliers: values.length - used.length
46
+ };
27
47
  }
@@ -1,7 +1,9 @@
1
- import type { IterationData } from "./types.mjs";
1
+ import type { IterationData, MetricReport } from "./types.mjs";
2
2
  declare module 'vitest' {
3
3
  interface TaskMeta {
4
4
  benchmarkName?: string;
5
5
  benchmarkIterations?: IterationData[];
6
+ /** Custom metrics recorded via `ScalarMetric`/`DiscreteMetric`, keyed by metric name. */
7
+ benchmarkMetrics?: Record<string, MetricReport>;
6
8
  }
7
9
  }
package/types.d.mts CHANGED
@@ -13,15 +13,8 @@ export interface RenderEvent {
13
13
  /** Start time in milliseconds (from performance.now()) */
14
14
  startTime: number;
15
15
  }
16
- export interface BenchmarkMetric {
17
- /** Metric name, e.g. "paint:bench", "paint:grid-header" */
18
- name: string;
19
- /** Measured value in ms */
20
- value: number;
21
- }
22
16
  export interface IterationData {
23
17
  renders: RenderEvent[];
24
- metrics: BenchmarkMetric[];
25
18
  }
26
19
  export interface InteractionContext {
27
20
  /**
@@ -30,4 +23,79 @@ export interface InteractionContext {
30
23
  * @param timeout - Timeout in ms. Default: 5000. Pass 0 or Infinity to rely on the test timeout.
31
24
  */
32
25
  waitForElementTiming: (identifier: string, timeout?: number) => Promise<void>;
26
+ /**
27
+ * Pause recording of the harness's React render/paint measurements. Custom metrics keep
28
+ * recording. Throws if recording is already paused.
29
+ */
30
+ pauseReactRecording: () => void;
31
+ /**
32
+ * Resume recording of the harness's React render/paint measurements. Throws if recording is
33
+ * already active.
34
+ */
35
+ resumeReactRecording: () => void;
36
+ }
37
+ export interface BenchmarkCaseRuntime {
38
+ mount: () => void;
39
+ /**
40
+ * Runs the case's interaction with the harness context. Absent when the case has no interaction —
41
+ * the profiling panel uses its presence to decide whether to show the "Run interaction" button.
42
+ */
43
+ interact?: () => Promise<void>;
44
+ unmount: () => void;
45
+ isMounted: () => boolean;
46
+ }
47
+ /**
48
+ * Whether a custom metric measures a continuous value or a discrete count.
49
+ * - `scalar` — continuous measurements (timings, sizes); compared with a relative noise band.
50
+ * - `discrete` — counts/events; compared as exact integers.
51
+ */
52
+ export type MetricKind = 'scalar' | 'discrete';
53
+ /** Which direction of change counts as a regression for a metric in alarm mode. */
54
+ export type MetricDirection = 'lowerIsBetter' | 'higherIsBetter';
55
+ export interface MetricAlarm {
56
+ /** Defaults to `lowerIsBetter`. */
57
+ direction?: MetricDirection;
58
+ /**
59
+ * Softer band: a regression past `warn` (but within `error`) is flagged as a warning.
60
+ * Scalar metrics: a relative fraction (`0.1` = 10%). Discrete metrics: an absolute count delta.
61
+ */
62
+ warn?: number;
63
+ /**
64
+ * Harder band: a regression past `error` is flagged as an error (the alarm). When **both**
65
+ * `warn` and `error` are omitted, `error` defaults to the dashboard's global noise band; with
66
+ * only `warn` set there is no error band (warning-only).
67
+ * Scalar metrics: a relative fraction (`0.25` = 25%). Discrete metrics: an absolute count delta.
68
+ */
69
+ error?: number;
70
+ }
71
+ export interface MetricConfig {
72
+ name: string;
73
+ /** Display formatting applied by the reporter and dashboard via `Intl.NumberFormat`. */
74
+ format?: Intl.NumberFormatOptions;
75
+ /**
76
+ * Regression judgment. Its presence opts the metric into alarming; when omitted the metric
77
+ * is informational (the diff is shown but never flagged).
78
+ */
79
+ alarm?: MetricAlarm;
80
+ }
81
+ /** Aggregated stats for a single metric sub-series, as they cross the browser→runner boundary. */
82
+ export interface MetricSampleStats {
83
+ mean: number;
84
+ stdDev: number;
85
+ outliers: number;
86
+ /** Number of recorded samples (used to derive iteration counts; stripped from the report). */
87
+ count: number;
88
+ }
89
+ /** A custom metric's aggregated data attached to `task.meta`, keyed by metric name. */
90
+ export interface MetricReport {
91
+ kind: MetricKind;
92
+ config: MetricConfig;
93
+ /** Aggregated stats keyed by sub-series id (`''` is the base series). */
94
+ series: Record<string, MetricSampleStats>;
95
+ }
96
+ /** Per-metric configuration hoisted to the top level of the report (keyed by metric name). */
97
+ export interface MetricDefinition {
98
+ kind: MetricKind;
99
+ format?: Intl.NumberFormatOptions;
100
+ alarm?: MetricAlarm;
33
101
  }
package/vitest.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { ViteUserConfig } from 'vitest/config';
1
+ import type { ViteUserConfig } from 'vitest/config';
2
2
  export interface CreateBenchmarkVitestConfigOptions {
3
3
  /**
4
4
  * Path to save benchmark results JSON file. If not provided, results will not be saved to disk.
@@ -15,5 +15,22 @@ export interface CreateBenchmarkVitestConfigOptions {
15
15
  * Additional Chromium launch arguments.
16
16
  */
17
17
  launchArgs?: string[];
18
+ /**
19
+ * Run each `benchmark()` case as an interactive profiling session in a headed
20
+ * browser instead of the automated measurement loop. Each case renders a
21
+ * control panel with Render / Unmount / Finish buttons so you can start the
22
+ * DevTools profiler before the component mounts. Profiling auto-opens DevTools
23
+ * and runs headed (pass `viewport` to size the window). Also settable via
24
+ * `BENCHMARK_PROFILE=true`.
25
+ */
26
+ profile?: boolean;
27
+ /**
28
+ * Browser viewport — and, in profile mode, the matching window size. Defaults to 1920x1080 for
29
+ * both measurement and profiling. Also settable via `BENCHMARK_VIEWPORT` (e.g. `2560x1440`).
30
+ */
31
+ viewport?: {
32
+ width: number;
33
+ height: number;
34
+ };
18
35
  }
19
36
  export declare function createBenchmarkVitestConfig(options?: CreateBenchmarkVitestConfigOptions): ViteUserConfig;
package/vitest.mjs CHANGED
@@ -1,15 +1,56 @@
1
1
  import react from '@vitejs/plugin-react';
2
2
  import { playwright } from '@vitest/browser-playwright';
3
+ // Default viewport for all benchmark runs (measurement and profiling alike) — a desktop size is
4
+ // more representative for component benchmarks than Vitest's phone-sized 414x896 browser default.
5
+ const DEFAULT_VIEWPORT = {
6
+ width: 1920,
7
+ height: 1080
8
+ };
9
+
10
+ // Explicit viewport from the `viewport` option or the `BENCHMARK_VIEWPORT` env var
11
+ // (`<width>x<height>`); undefined when neither is set, so the caller can apply the default.
12
+ function resolveViewport(option) {
13
+ if (option) {
14
+ return option;
15
+ }
16
+ const env = process.env.BENCHMARK_VIEWPORT;
17
+ const match = env ? env.split('x') : null;
18
+ if (match && match.length === 2) {
19
+ return {
20
+ width: Number(match[0]),
21
+ height: Number(match[1])
22
+ };
23
+ }
24
+ return undefined;
25
+ }
26
+
27
+ // Chromium/V8 launch args shared by measurement and profiling, kept intentionally minimal.
28
+ // `--expose-gc` is required: the harness forces GC between iterations for clean, comparable
29
+ // timings. The backgrounding flags stop Chrome from throttling the (headless or occluded)
30
+ // benchmark tab, which would otherwise add large variance. Heavier "determinism" flags
31
+ // (`--no-opt`, `--predictable`, `--hash-seed`/`--random-seed`, `--disable-gpu`,
32
+ // `--enable-benchmarking`) were measured to slow renders ~40% and distort paint timing without
33
+ // reducing variance, so they are omitted — add them per project via `launchArgs` if a specific
34
+ // workload needs them.
35
+ const LAUNCH_ARGS = ['--js-flags=--expose-gc', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding'];
3
36
  export function createBenchmarkVitestConfig(options) {
4
37
  const {
5
38
  outputPath,
6
39
  baselinePath,
7
40
  launchArgs = []
8
41
  } = options ?? {};
42
+ const profile = options?.profile ?? process.env.BENCHMARK_PROFILE === 'true';
43
+ const viewport = resolveViewport(options?.viewport) ?? DEFAULT_VIEWPORT;
44
+
45
+ // Profiling adds DevTools on top of the shared args, plus a window sized to match the viewport —
46
+ // Vitest's `viewport` only sizes the iframe, so otherwise it's cropped/scrolled in the headed
47
+ // window instead of filling it. (Measurement is headless, so it has no window to size.)
48
+ const profileArgs = [...LAUNCH_ARGS, '--auto-open-devtools-for-tabs', `--window-size=${viewport.width},${viewport.height}`];
9
49
  return {
10
50
  plugins: [react()],
11
51
  define: {
12
- 'process.env.NODE_ENV': '"production"'
52
+ 'process.env.NODE_ENV': '"production"',
53
+ 'process.env.BENCHMARK_PROFILE': JSON.stringify(profile ? 'true' : '')
13
54
  },
14
55
  resolve: {
15
56
  dedupe: ['react', 'react-dom'],
@@ -21,29 +62,28 @@ export function createBenchmarkVitestConfig(options) {
21
62
  test: {
22
63
  browser: {
23
64
  enabled: true,
24
- headless: true,
65
+ headless: !profile,
66
+ // Profiling renders into a clean page: hide Vitest's browser runner UI
67
+ // so the orchestrator chrome doesn't clutter what you're profiling.
68
+ ui: profile ? false : undefined,
69
+ // Same viewport for both modes (DEFAULT_VIEWPORT unless overridden).
70
+ viewport,
25
71
  screenshotFailures: false,
26
72
  instances: [{
27
73
  browser: 'chromium',
28
- testTimeout: 120_000
74
+ // Profiling sessions are driven by hand, so give them effectively
75
+ // unlimited time instead of the measurement timeout.
76
+ testTimeout: profile ? 0 : 120_000
29
77
  }],
30
78
  provider: playwright({
31
79
  launchOptions: {
32
- args: [
33
- // V8 flags for deterministic JS execution
34
- '--js-flags=--expose-gc,--predictable,--no-opt,--predictable-gc-schedule,--no-concurrent-sweeping,--hash-seed=1,--random-seed=1,--max-old-space-size=4096',
35
- // Chromium flags to reduce renderer/compositor noise
36
- '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-background-networking',
37
- // Reduces environmental noise by disabling field trials,
38
- // for more consistent profiling results.
39
- '--enable-benchmarking',
40
- // Forces software rendering instead of GPU, which is more deterministic.
41
- '--disable-gpu', ...launchArgs]
80
+ args: [...(profile ? profileArgs : LAUNCH_ARGS), ...launchArgs]
42
81
  }
43
82
  })
44
83
  },
45
84
  fileParallelism: false,
46
- reporters: ['default', ['@mui/internal-benchmark/reporter', {
85
+ // Profiling sessions don't measure anything, so skip the results reporter.
86
+ reporters: profile ? ['default'] : ['default', ['@mui/internal-benchmark/reporter', {
47
87
  outputPath,
48
88
  baselinePath
49
89
  }]],