benchforge 0.1.9 → 0.1.11
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/README.md +40 -6
- package/dist/{BenchRunner-CSKN9zPy.d.mts → BenchRunner-BzyUfiyB.d.mts} +32 -8
- package/dist/{BrowserHeapSampler-DCeL42RE.mjs → BrowserHeapSampler-B6asLKWQ.mjs} +57 -57
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +1 -0
- package/dist/{GcStats-ByEovUi1.mjs → GcStats-wX7Xyblu.mjs} +15 -15
- package/dist/GcStats-wX7Xyblu.mjs.map +1 -0
- package/dist/HeapSampler-B8dtKHn1.mjs.map +1 -1
- package/dist/{TimingUtils-ClclVQ7E.mjs → TimingUtils-DwOwkc8G.mjs} +225 -225
- package/dist/TimingUtils-DwOwkc8G.mjs.map +1 -0
- package/dist/bin/benchforge.mjs +1 -1
- package/dist/browser/index.js +210 -210
- package/dist/index.d.mts +102 -46
- package/dist/index.mjs +3 -3
- package/dist/runners/WorkerScript.d.mts +1 -1
- package/dist/runners/WorkerScript.mjs +66 -66
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/{src-Cf_LXwlp.mjs → src-B-DDaCa9.mjs} +1225 -990
- package/dist/src-B-DDaCa9.mjs.map +1 -0
- package/package.json +2 -1
- package/src/BenchMatrix.ts +125 -125
- package/src/BenchmarkReport.ts +50 -45
- package/src/HtmlDataPrep.ts +21 -21
- package/src/PermutationTest.ts +24 -24
- package/src/StandardSections.ts +45 -45
- package/src/StatisticalUtils.ts +60 -61
- package/src/browser/BrowserGcStats.ts +5 -5
- package/src/browser/BrowserHeapSampler.ts +63 -63
- package/src/cli/CliArgs.ts +6 -3
- package/src/cli/FilterBenchmarks.ts +5 -5
- package/src/cli/RunBenchCLI.ts +526 -498
- package/src/export/JsonExport.ts +10 -10
- package/src/export/PerfettoExport.ts +74 -74
- package/src/export/SpeedscopeExport.ts +202 -0
- package/src/heap-sample/HeapSampleReport.ts +143 -70
- package/src/heap-sample/HeapSampler.ts +55 -12
- package/src/heap-sample/ResolvedProfile.ts +89 -0
- package/src/html/HtmlReport.ts +33 -33
- package/src/html/HtmlTemplate.ts +67 -67
- package/src/html/browser/CIPlot.ts +50 -50
- package/src/html/browser/HistogramKde.ts +13 -13
- package/src/html/browser/LegendUtils.ts +48 -48
- package/src/html/browser/RenderPlots.ts +98 -98
- package/src/html/browser/SampleTimeSeries.ts +79 -79
- package/src/index.ts +6 -0
- package/src/matrix/MatrixFilter.ts +6 -6
- package/src/matrix/MatrixReport.ts +96 -96
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/runners/AdaptiveWrapper.ts +151 -151
- package/src/runners/BasicRunner.ts +175 -175
- package/src/runners/BenchRunner.ts +8 -8
- package/src/runners/GcStats.ts +22 -22
- package/src/runners/RunnerOrchestrator.ts +168 -168
- package/src/runners/WorkerScript.ts +96 -96
- package/src/table-util/Formatters.ts +41 -36
- package/src/table-util/TableReport.ts +122 -122
- package/src/table-util/test/TableValueExtractor.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +7 -39
- package/src/test/HeapAttribution.test.ts +51 -0
- package/src/test/RunBenchCLI.test.ts +18 -18
- package/src/test/TestUtils.ts +24 -24
- package/src/tests/BenchMatrix.test.ts +12 -12
- package/src/tests/MatrixFilter.test.ts +15 -15
- package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
- package/dist/GcStats-ByEovUi1.mjs.map +0 -1
- package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
- package/dist/src-Cf_LXwlp.mjs.map +0 -1
package/src/cli/RunBenchCLI.ts
CHANGED
|
@@ -14,10 +14,14 @@ import type {
|
|
|
14
14
|
ReportGroup,
|
|
15
15
|
ResultsMapper,
|
|
16
16
|
} from "../BenchmarkReport.ts";
|
|
17
|
-
import { reportResults } from "../BenchmarkReport.ts";
|
|
17
|
+
import { groupReports, reportResults } from "../BenchmarkReport.ts";
|
|
18
18
|
import type { BrowserProfileResult } from "../browser/BrowserHeapSampler.ts";
|
|
19
19
|
import { exportBenchmarkJson } from "../export/JsonExport.ts";
|
|
20
20
|
import { exportPerfettoTrace } from "../export/PerfettoExport.ts";
|
|
21
|
+
import {
|
|
22
|
+
exportAndLaunchSpeedscope,
|
|
23
|
+
exportSpeedscope,
|
|
24
|
+
} from "../export/SpeedscopeExport.ts";
|
|
21
25
|
import type { GitVersion } from "../GitUtils.ts";
|
|
22
26
|
import { prepareHtmlData } from "../HtmlDataPrep.ts";
|
|
23
27
|
import {
|
|
@@ -25,10 +29,11 @@ import {
|
|
|
25
29
|
filterSites,
|
|
26
30
|
flattenProfile,
|
|
27
31
|
formatHeapReport,
|
|
32
|
+
formatRawSamples,
|
|
28
33
|
type HeapReportOptions,
|
|
29
34
|
isBrowserUserCode,
|
|
30
|
-
totalProfileBytes,
|
|
31
35
|
} from "../heap-sample/HeapSampleReport.ts";
|
|
36
|
+
import { resolveProfile } from "../heap-sample/ResolvedProfile.ts";
|
|
32
37
|
import { generateHtmlReport } from "../html/index.ts";
|
|
33
38
|
import type { MeasuredResults } from "../MeasuredResults.ts";
|
|
34
39
|
import { loadCasesModule } from "../matrix/CaseLoader.ts";
|
|
@@ -63,27 +68,19 @@ import {
|
|
|
63
68
|
} from "./CliArgs.ts";
|
|
64
69
|
import { filterBenchmarks } from "./FilterBenchmarks.ts";
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
export interface ExportOptions {
|
|
72
|
+
results: ReportGroup[];
|
|
73
|
+
args: DefaultCliArgs;
|
|
74
|
+
sections?: any[];
|
|
75
|
+
suiteName?: string;
|
|
76
|
+
currentVersion?: GitVersion;
|
|
77
|
+
baselineVersion?: GitVersion;
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (args.cpu) ignored.push("--cpu");
|
|
80
|
-
if (args["trace-opt"]) ignored.push("--trace-opt");
|
|
81
|
-
if (args.collect) ignored.push("--collect");
|
|
82
|
-
if (args.adaptive) ignored.push("--adaptive");
|
|
83
|
-
if (args.batches > 1) ignored.push("--batches");
|
|
84
|
-
if (ignored.length) {
|
|
85
|
-
console.warn(yellow(`Ignored in browser mode: ${ignored.join(", ")}`));
|
|
86
|
-
}
|
|
80
|
+
export interface MatrixExportOptions {
|
|
81
|
+
sections?: any[];
|
|
82
|
+
currentVersion?: GitVersion;
|
|
83
|
+
baselineVersion?: GitVersion;
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
type RunParams = {
|
|
@@ -102,6 +99,11 @@ type SuiteParams = {
|
|
|
102
99
|
batches: number;
|
|
103
100
|
};
|
|
104
101
|
|
|
102
|
+
const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
|
|
103
|
+
const { yellow, dim } = isTest
|
|
104
|
+
? { yellow: (s: string) => s, dim: (s: string) => s }
|
|
105
|
+
: pico;
|
|
106
|
+
|
|
105
107
|
/** Parse CLI with custom configuration */
|
|
106
108
|
export function parseBenchArgs<T = DefaultCliArgs>(
|
|
107
109
|
configureArgs?: Configure<T>,
|
|
@@ -129,217 +131,6 @@ export async function runBenchmarks(
|
|
|
129
131
|
});
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
/** Execute all groups in suite */
|
|
133
|
-
async function runSuite(params: SuiteParams): Promise<ReportGroup[]> {
|
|
134
|
-
const { suite, runner, options, useWorker, batches } = params;
|
|
135
|
-
const results: ReportGroup[] = [];
|
|
136
|
-
for (const group of suite.groups) {
|
|
137
|
-
results.push(await runGroup(group, runner, options, useWorker, batches));
|
|
138
|
-
}
|
|
139
|
-
return results;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Execute group with shared setup, optionally batching to reduce ordering bias */
|
|
143
|
-
async function runGroup(
|
|
144
|
-
group: BenchGroup,
|
|
145
|
-
runner: KnownRunner,
|
|
146
|
-
options: RunnerOptions,
|
|
147
|
-
useWorker: boolean,
|
|
148
|
-
batches = 1,
|
|
149
|
-
): Promise<ReportGroup> {
|
|
150
|
-
const { name, benchmarks, baseline, setup, metadata } = group;
|
|
151
|
-
const setupParams = await setup?.();
|
|
152
|
-
validateBenchmarkParameters(group);
|
|
153
|
-
|
|
154
|
-
const runParams = {
|
|
155
|
-
runner,
|
|
156
|
-
options,
|
|
157
|
-
useWorker,
|
|
158
|
-
params: setupParams,
|
|
159
|
-
metadata,
|
|
160
|
-
};
|
|
161
|
-
if (batches === 1) {
|
|
162
|
-
return runSingleBatch(name, benchmarks, baseline, runParams);
|
|
163
|
-
}
|
|
164
|
-
return runMultipleBatches(name, benchmarks, baseline, runParams, batches);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Run benchmarks in a single batch */
|
|
168
|
-
async function runSingleBatch(
|
|
169
|
-
name: string,
|
|
170
|
-
benchmarks: BenchmarkSpec[],
|
|
171
|
-
baseline: BenchmarkSpec | undefined,
|
|
172
|
-
runParams: RunParams,
|
|
173
|
-
): Promise<ReportGroup> {
|
|
174
|
-
const baselineReport = baseline
|
|
175
|
-
? await runSingleBenchmark(baseline, runParams)
|
|
176
|
-
: undefined;
|
|
177
|
-
const reports = await serialMap(benchmarks, b =>
|
|
178
|
-
runSingleBenchmark(b, runParams),
|
|
179
|
-
);
|
|
180
|
-
return { name, reports, baseline: baselineReport };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Run benchmarks in multiple batches, alternating order to reduce bias */
|
|
184
|
-
async function runMultipleBatches(
|
|
185
|
-
name: string,
|
|
186
|
-
benchmarks: BenchmarkSpec[],
|
|
187
|
-
baseline: BenchmarkSpec | undefined,
|
|
188
|
-
runParams: RunParams,
|
|
189
|
-
batches: number,
|
|
190
|
-
): Promise<ReportGroup> {
|
|
191
|
-
const timePerBatch = (runParams.options.maxTime || 5000) / batches;
|
|
192
|
-
const batchParams = {
|
|
193
|
-
...runParams,
|
|
194
|
-
options: { ...runParams.options, maxTime: timePerBatch },
|
|
195
|
-
};
|
|
196
|
-
const baselineBatches: MeasuredResults[] = [];
|
|
197
|
-
const benchmarkBatches = new Map<string, MeasuredResults[]>();
|
|
198
|
-
|
|
199
|
-
for (let i = 0; i < batches; i++) {
|
|
200
|
-
const reverseOrder = i % 2 === 1;
|
|
201
|
-
await runBatchIteration(
|
|
202
|
-
benchmarks,
|
|
203
|
-
baseline,
|
|
204
|
-
batchParams,
|
|
205
|
-
reverseOrder,
|
|
206
|
-
baselineBatches,
|
|
207
|
-
benchmarkBatches,
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const meta = runParams.metadata;
|
|
212
|
-
return mergeBatchResults(
|
|
213
|
-
name,
|
|
214
|
-
benchmarks,
|
|
215
|
-
baseline,
|
|
216
|
-
baselineBatches,
|
|
217
|
-
benchmarkBatches,
|
|
218
|
-
meta,
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** Run one batch iteration in either order */
|
|
223
|
-
async function runBatchIteration(
|
|
224
|
-
benchmarks: BenchmarkSpec[],
|
|
225
|
-
baseline: BenchmarkSpec | undefined,
|
|
226
|
-
runParams: RunParams,
|
|
227
|
-
reverseOrder: boolean,
|
|
228
|
-
baselineBatches: MeasuredResults[],
|
|
229
|
-
benchmarkBatches: Map<string, MeasuredResults[]>,
|
|
230
|
-
): Promise<void> {
|
|
231
|
-
const runBaseline = async () => {
|
|
232
|
-
if (baseline) {
|
|
233
|
-
const r = await runSingleBenchmark(baseline, runParams);
|
|
234
|
-
baselineBatches.push(r.measuredResults);
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
const runBenches = async () => {
|
|
238
|
-
for (const b of benchmarks) {
|
|
239
|
-
const r = await runSingleBenchmark(b, runParams);
|
|
240
|
-
appendToMap(benchmarkBatches, b.name, r.measuredResults);
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
if (reverseOrder) {
|
|
245
|
-
await runBenches();
|
|
246
|
-
await runBaseline();
|
|
247
|
-
} else {
|
|
248
|
-
await runBaseline();
|
|
249
|
-
await runBenches();
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/** Merge batch results into final ReportGroup */
|
|
254
|
-
function mergeBatchResults(
|
|
255
|
-
name: string,
|
|
256
|
-
benchmarks: BenchmarkSpec[],
|
|
257
|
-
baseline: BenchmarkSpec | undefined,
|
|
258
|
-
baselineBatches: MeasuredResults[],
|
|
259
|
-
benchmarkBatches: Map<string, MeasuredResults[]>,
|
|
260
|
-
metadata?: Record<string, unknown>,
|
|
261
|
-
): ReportGroup {
|
|
262
|
-
const mergedBaseline = baseline
|
|
263
|
-
? {
|
|
264
|
-
name: baseline.name,
|
|
265
|
-
measuredResults: mergeResults(baselineBatches),
|
|
266
|
-
metadata,
|
|
267
|
-
}
|
|
268
|
-
: undefined;
|
|
269
|
-
const reports = benchmarks.map(b => ({
|
|
270
|
-
name: b.name,
|
|
271
|
-
measuredResults: mergeResults(benchmarkBatches.get(b.name) || []),
|
|
272
|
-
metadata,
|
|
273
|
-
}));
|
|
274
|
-
return { name, reports, baseline: mergedBaseline };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** Run single benchmark and create report */
|
|
278
|
-
async function runSingleBenchmark(
|
|
279
|
-
spec: BenchmarkSpec,
|
|
280
|
-
runParams: RunParams,
|
|
281
|
-
): Promise<BenchmarkReport> {
|
|
282
|
-
const { runner, options, useWorker, params, metadata } = runParams;
|
|
283
|
-
const benchmarkParams = { spec, runner, options, useWorker, params };
|
|
284
|
-
const [result] = await runBenchmark(benchmarkParams);
|
|
285
|
-
return { name: spec.name, measuredResults: result, metadata };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** Warn if parameterized benchmarks lack setup */
|
|
289
|
-
function validateBenchmarkParameters(group: BenchGroup): void {
|
|
290
|
-
const { name, setup, benchmarks, baseline } = group;
|
|
291
|
-
if (setup) return;
|
|
292
|
-
|
|
293
|
-
const allBenchmarks = baseline ? [...benchmarks, baseline] : benchmarks;
|
|
294
|
-
for (const benchmark of allBenchmarks) {
|
|
295
|
-
if (benchmark.fn.length > 0) {
|
|
296
|
-
console.warn(
|
|
297
|
-
`Benchmark "${benchmark.name}" in group "${name}" expects parameters but no setup() provided.`,
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** Merge multiple batch results into a single MeasuredResults */
|
|
304
|
-
function mergeResults(results: MeasuredResults[]): MeasuredResults {
|
|
305
|
-
if (results.length === 0) {
|
|
306
|
-
throw new Error("Cannot merge empty results array");
|
|
307
|
-
}
|
|
308
|
-
if (results.length === 1) return results[0];
|
|
309
|
-
|
|
310
|
-
const allSamples = results.flatMap(r => r.samples);
|
|
311
|
-
const allWarmup = results.flatMap(r => r.warmupSamples || []);
|
|
312
|
-
const time = computeStats(allSamples);
|
|
313
|
-
|
|
314
|
-
let offset = 0;
|
|
315
|
-
const allPausePoints = results.flatMap(r => {
|
|
316
|
-
const pts = (r.pausePoints ?? []).map(p => ({
|
|
317
|
-
sampleIndex: p.sampleIndex + offset,
|
|
318
|
-
durationMs: p.durationMs,
|
|
319
|
-
}));
|
|
320
|
-
offset += r.samples.length;
|
|
321
|
-
return pts;
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
return {
|
|
325
|
-
name: results[0].name,
|
|
326
|
-
samples: allSamples,
|
|
327
|
-
warmupSamples: allWarmup.length ? allWarmup : undefined,
|
|
328
|
-
time,
|
|
329
|
-
totalTime: results.reduce((sum, r) => sum + (r.totalTime || 0), 0),
|
|
330
|
-
pausePoints: allPausePoints.length ? allPausePoints : undefined,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function appendToMap(
|
|
335
|
-
map: Map<string, MeasuredResults[]>,
|
|
336
|
-
key: string,
|
|
337
|
-
value: MeasuredResults,
|
|
338
|
-
) {
|
|
339
|
-
if (!map.has(key)) map.set(key, []);
|
|
340
|
-
map.get(key)!.push(value);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
134
|
/** Generate table with standard sections */
|
|
344
135
|
export function defaultReport(
|
|
345
136
|
groups: ReportGroup[],
|
|
@@ -357,25 +148,6 @@ export function defaultReport(
|
|
|
357
148
|
return reportResults(groups, sections);
|
|
358
149
|
}
|
|
359
150
|
|
|
360
|
-
/** Build report sections based on CLI options */
|
|
361
|
-
function buildReportSections(
|
|
362
|
-
adaptive: boolean,
|
|
363
|
-
gcStats: boolean,
|
|
364
|
-
hasCpuData: boolean,
|
|
365
|
-
hasOptData: boolean,
|
|
366
|
-
) {
|
|
367
|
-
const sections = adaptive
|
|
368
|
-
? [adaptiveSection, totalTimeSection]
|
|
369
|
-
: [timeSection];
|
|
370
|
-
|
|
371
|
-
if (gcStats) sections.push(gcStatsSection);
|
|
372
|
-
if (hasCpuData) sections.push(cpuSection);
|
|
373
|
-
if (hasOptData) sections.push(optSection);
|
|
374
|
-
sections.push(runsSection);
|
|
375
|
-
|
|
376
|
-
return sections;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
151
|
/** Run benchmarks, display table, and optionally generate HTML report */
|
|
380
152
|
export async function benchExports(
|
|
381
153
|
suite: BenchSuite,
|
|
@@ -408,7 +180,7 @@ export async function browserBenchExports(args: DefaultCliArgs): Promise<void> {
|
|
|
408
180
|
const { iterations, time } = args;
|
|
409
181
|
const result = await profileBrowser({
|
|
410
182
|
url,
|
|
411
|
-
heapSample: args
|
|
183
|
+
heapSample: needsHeapSample(args),
|
|
412
184
|
heapOptions: {
|
|
413
185
|
samplingInterval: args["heap-interval"],
|
|
414
186
|
stackDepth: args["heap-depth"],
|
|
@@ -430,98 +202,32 @@ export async function browserBenchExports(args: DefaultCliArgs): Promise<void> {
|
|
|
430
202
|
await exportReports({ results, args });
|
|
431
203
|
}
|
|
432
204
|
|
|
433
|
-
/** Print browser benchmark tables and heap reports */
|
|
434
|
-
function printBrowserReport(
|
|
435
|
-
result: BrowserProfileResult,
|
|
436
|
-
results: ReportGroup[],
|
|
437
|
-
args: DefaultCliArgs,
|
|
438
|
-
): void {
|
|
439
|
-
const hasSamples = result.samples && result.samples.length > 0;
|
|
440
|
-
const sections: ResultsMapper<any>[] = [];
|
|
441
|
-
if (hasSamples || result.wallTimeMs != null) {
|
|
442
|
-
sections.push(timeSection);
|
|
443
|
-
}
|
|
444
|
-
if (result.gcStats) {
|
|
445
|
-
sections.push(browserGcStatsSection);
|
|
446
|
-
}
|
|
447
|
-
if (hasSamples || result.wallTimeMs != null) {
|
|
448
|
-
sections.push(runsSection);
|
|
449
|
-
}
|
|
450
|
-
if (sections.length > 0) {
|
|
451
|
-
console.log(reportResults(results, sections));
|
|
452
|
-
}
|
|
453
|
-
if (result.heapProfile) {
|
|
454
|
-
printHeapReports(results, {
|
|
455
|
-
...cliHeapReportOptions(args),
|
|
456
|
-
isUserCode: isBrowserUserCode,
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/** Wrap browser profile result as ReportGroup[] for the standard pipeline */
|
|
462
|
-
function browserResultGroups(
|
|
463
|
-
name: string,
|
|
464
|
-
result: BrowserProfileResult,
|
|
465
|
-
): ReportGroup[] {
|
|
466
|
-
const { gcStats, heapProfile } = result;
|
|
467
|
-
let measured: MeasuredResults;
|
|
468
|
-
|
|
469
|
-
// Bench function mode: multiple timing samples with real statistics
|
|
470
|
-
if (result.samples && result.samples.length > 0) {
|
|
471
|
-
const { samples } = result;
|
|
472
|
-
const totalTime = result.wallTimeMs ? result.wallTimeMs / 1000 : undefined;
|
|
473
|
-
measured = {
|
|
474
|
-
name,
|
|
475
|
-
samples,
|
|
476
|
-
time: computeStats(samples),
|
|
477
|
-
totalTime,
|
|
478
|
-
gcStats,
|
|
479
|
-
heapProfile,
|
|
480
|
-
};
|
|
481
|
-
} else {
|
|
482
|
-
// Lap mode: 0 laps = single wall-clock, N laps handled above
|
|
483
|
-
const wallMs = result.wallTimeMs ?? 0;
|
|
484
|
-
const time = {
|
|
485
|
-
min: wallMs,
|
|
486
|
-
max: wallMs,
|
|
487
|
-
avg: wallMs,
|
|
488
|
-
p50: wallMs,
|
|
489
|
-
p75: wallMs,
|
|
490
|
-
p99: wallMs,
|
|
491
|
-
p999: wallMs,
|
|
492
|
-
};
|
|
493
|
-
measured = { name, samples: [wallMs], time, gcStats, heapProfile };
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return [{ name, reports: [{ name, measuredResults: measured }] }];
|
|
497
|
-
}
|
|
498
|
-
|
|
499
205
|
/** Print heap allocation reports for benchmarks with heap profiles */
|
|
500
206
|
export function printHeapReports(
|
|
501
207
|
groups: ReportGroup[],
|
|
502
208
|
options: HeapReportOptions,
|
|
503
209
|
): void {
|
|
504
210
|
for (const group of groups) {
|
|
505
|
-
const
|
|
506
|
-
? [...group.reports, group.baseline]
|
|
507
|
-
: group.reports;
|
|
508
|
-
|
|
509
|
-
for (const report of allReports) {
|
|
211
|
+
for (const report of groupReports(group)) {
|
|
510
212
|
const { heapProfile } = report.measuredResults;
|
|
511
213
|
if (!heapProfile) continue;
|
|
512
214
|
|
|
513
215
|
console.log(dim(`\n─── Heap profile: ${report.name} ───`));
|
|
514
|
-
const
|
|
515
|
-
const sites = flattenProfile(
|
|
216
|
+
const resolved = resolveProfile(heapProfile);
|
|
217
|
+
const sites = flattenProfile(resolved);
|
|
516
218
|
const userSites = filterSites(sites, options.isUserCode);
|
|
517
219
|
const totalUserCode = userSites.reduce((sum, s) => sum + s.bytes, 0);
|
|
518
220
|
const aggregated = aggregateSites(options.userOnly ? userSites : sites);
|
|
519
221
|
const extra = {
|
|
520
|
-
totalAll,
|
|
222
|
+
totalAll: resolved.totalBytes,
|
|
521
223
|
totalUserCode,
|
|
522
|
-
sampleCount:
|
|
224
|
+
sampleCount: resolved.sortedSamples?.length,
|
|
523
225
|
};
|
|
524
226
|
console.log(formatHeapReport(aggregated, { ...options, ...extra }));
|
|
227
|
+
if (options.raw) {
|
|
228
|
+
console.log(dim(`\n─── Raw samples: ${report.name} ───`));
|
|
229
|
+
console.log(formatRawSamples(resolved));
|
|
230
|
+
}
|
|
525
231
|
}
|
|
526
232
|
}
|
|
527
233
|
}
|
|
@@ -545,29 +251,6 @@ export async function runDefaultBench(
|
|
|
545
251
|
}
|
|
546
252
|
}
|
|
547
253
|
|
|
548
|
-
/** Import a file and run it as a benchmark based on what it exports */
|
|
549
|
-
async function fileBenchExports(
|
|
550
|
-
filePath: string,
|
|
551
|
-
args: DefaultCliArgs,
|
|
552
|
-
): Promise<void> {
|
|
553
|
-
const fileUrl = pathToFileURL(resolve(filePath)).href;
|
|
554
|
-
const mod = await import(fileUrl);
|
|
555
|
-
const candidate = mod.default;
|
|
556
|
-
|
|
557
|
-
if (candidate && Array.isArray(candidate.groups)) {
|
|
558
|
-
// BenchSuite export
|
|
559
|
-
await benchExports(candidate as BenchSuite, args);
|
|
560
|
-
} else if (typeof candidate === "function") {
|
|
561
|
-
// Default function export: wrap as a single benchmark
|
|
562
|
-
const name = basename(filePath).replace(/\.[^.]+$/, "");
|
|
563
|
-
await benchExports(
|
|
564
|
-
{ name, groups: [{ name, benchmarks: [{ name, fn: candidate }] }] },
|
|
565
|
-
args,
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
// else: self-executing file already ran on import
|
|
569
|
-
}
|
|
570
|
-
|
|
571
254
|
/** Convert CLI args to runner options */
|
|
572
255
|
export function cliToRunnerOptions(args: DefaultCliArgs): RunnerOptions {
|
|
573
256
|
const { profile, collect, iterations } = args;
|
|
@@ -582,51 +265,10 @@ export function cliToRunnerOptions(args: DefaultCliArgs): RunnerOptions {
|
|
|
582
265
|
};
|
|
583
266
|
}
|
|
584
267
|
|
|
585
|
-
/** Create options for adaptive mode */
|
|
586
|
-
function createAdaptiveOptions(args: DefaultCliArgs): RunnerOptions {
|
|
587
|
-
return {
|
|
588
|
-
minTime: (args["min-time"] ?? 1) * 1000,
|
|
589
|
-
maxTime: defaultAdaptiveMaxTime * 1000,
|
|
590
|
-
targetConfidence: args.convergence,
|
|
591
|
-
adaptive: true,
|
|
592
|
-
...cliCommonOptions(args),
|
|
593
|
-
} as any;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
/** Runner/matrix options shared across all CLI modes */
|
|
597
|
-
function cliCommonOptions(args: DefaultCliArgs) {
|
|
598
|
-
const { collect, cpu, warmup } = args;
|
|
599
|
-
const { "trace-opt": traceOpt, "skip-settle": noSettle } = args;
|
|
600
|
-
const { "pause-first": pauseFirst, "pause-interval": pauseInterval } = args;
|
|
601
|
-
const { "pause-duration": pauseDuration, "gc-stats": gcStats } = args;
|
|
602
|
-
const { "heap-sample": heapSample, "heap-interval": heapInterval } = args;
|
|
603
|
-
const { "heap-depth": heapDepth } = args;
|
|
604
|
-
return {
|
|
605
|
-
collect,
|
|
606
|
-
cpuCounters: cpu,
|
|
607
|
-
warmup,
|
|
608
|
-
traceOpt,
|
|
609
|
-
noSettle,
|
|
610
|
-
pauseFirst,
|
|
611
|
-
pauseInterval,
|
|
612
|
-
pauseDuration,
|
|
613
|
-
gcStats,
|
|
614
|
-
heapSample,
|
|
615
|
-
heapInterval,
|
|
616
|
-
heapDepth,
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
|
|
621
|
-
const { yellow, dim } = isTest
|
|
622
|
-
? { yellow: (s: string) => s, dim: (s: string) => s }
|
|
623
|
-
: pico;
|
|
624
|
-
|
|
625
268
|
/** Log V8 optimization tier distribution and deoptimizations */
|
|
626
269
|
export function reportOptStatus(groups: ReportGroup[]): void {
|
|
627
|
-
const optData = groups.flatMap(
|
|
628
|
-
|
|
629
|
-
return all
|
|
270
|
+
const optData = groups.flatMap(group => {
|
|
271
|
+
return groupReports(group)
|
|
630
272
|
.filter(r => r.measuredResults.optStatus)
|
|
631
273
|
.map(r => ({
|
|
632
274
|
name: r.name,
|
|
@@ -663,34 +305,11 @@ export function hasField(
|
|
|
663
305
|
results: ReportGroup[],
|
|
664
306
|
field: keyof MeasuredResults,
|
|
665
307
|
): boolean {
|
|
666
|
-
return results.some(
|
|
667
|
-
|
|
668
|
-
return all.some(
|
|
308
|
+
return results.some(group =>
|
|
309
|
+
groupReports(group).some(
|
|
669
310
|
({ measuredResults }) => measuredResults[field] !== undefined,
|
|
670
|
-
)
|
|
671
|
-
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
export interface ExportOptions {
|
|
675
|
-
results: ReportGroup[];
|
|
676
|
-
args: DefaultCliArgs;
|
|
677
|
-
sections?: any[];
|
|
678
|
-
suiteName?: string;
|
|
679
|
-
currentVersion?: GitVersion;
|
|
680
|
-
baselineVersion?: GitVersion;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
/** Print heap reports (if enabled) and export results */
|
|
684
|
-
async function finishReports(
|
|
685
|
-
results: ReportGroup[],
|
|
686
|
-
args: DefaultCliArgs,
|
|
687
|
-
suiteName?: string,
|
|
688
|
-
exportOptions?: MatrixExportOptions,
|
|
689
|
-
): Promise<void> {
|
|
690
|
-
if (args["heap-sample"]) {
|
|
691
|
-
printHeapReports(results, cliHeapReportOptions(args));
|
|
692
|
-
}
|
|
693
|
-
await exportReports({ results, args, suiteName, ...exportOptions });
|
|
311
|
+
),
|
|
312
|
+
);
|
|
694
313
|
}
|
|
695
314
|
|
|
696
315
|
/** Export reports (HTML, JSON, Perfetto) based on CLI args */
|
|
@@ -719,8 +338,16 @@ export async function exportReports(options: ExportOptions): Promise<void> {
|
|
|
719
338
|
await exportBenchmarkJson(results, args.json, args, suiteName);
|
|
720
339
|
}
|
|
721
340
|
|
|
722
|
-
if (args
|
|
723
|
-
exportPerfettoTrace(results, args
|
|
341
|
+
if (args["export-perfetto"]) {
|
|
342
|
+
exportPerfettoTrace(results, args["export-perfetto"], args);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (args["export-speedscope"]) {
|
|
346
|
+
exportSpeedscope(results, args["export-speedscope"]);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (args.speedscope) {
|
|
350
|
+
exportAndLaunchSpeedscope(results);
|
|
724
351
|
}
|
|
725
352
|
|
|
726
353
|
// Keep process running when HTML report is opened in browser
|
|
@@ -730,17 +357,6 @@ export async function exportReports(options: ExportOptions): Promise<void> {
|
|
|
730
357
|
}
|
|
731
358
|
}
|
|
732
359
|
|
|
733
|
-
/** Wait for Ctrl+C before exiting */
|
|
734
|
-
function waitForCtrlC(): Promise<void> {
|
|
735
|
-
return new Promise(resolve => {
|
|
736
|
-
console.log(dim("\nPress Ctrl+C to exit"));
|
|
737
|
-
process.on("SIGINT", () => {
|
|
738
|
-
console.log();
|
|
739
|
-
resolve();
|
|
740
|
-
});
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
|
|
744
360
|
/** Run matrix suite with CLI arguments.
|
|
745
361
|
* no options ==> defaultCases/defaultVariants, --filter ==> subset of defaults,
|
|
746
362
|
* --all --filter ==> subset of all, --all ==> all cases/variants */
|
|
@@ -807,37 +423,6 @@ export function defaultMatrixReport(
|
|
|
807
423
|
return results.map(r => reportMatrixResults(r, options)).join("\n\n");
|
|
808
424
|
}
|
|
809
425
|
|
|
810
|
-
/** @return HeapReportOptions from CLI args */
|
|
811
|
-
function cliHeapReportOptions(args: DefaultCliArgs): HeapReportOptions {
|
|
812
|
-
return {
|
|
813
|
-
topN: args["heap-rows"],
|
|
814
|
-
stackDepth: args["heap-stack"],
|
|
815
|
-
verbose: args["heap-verbose"],
|
|
816
|
-
userOnly: args["heap-user-only"],
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
/** Apply default sections and extra columns for matrix reports */
|
|
821
|
-
function mergeMatrixDefaults(
|
|
822
|
-
reportOptions: MatrixReportOptions | undefined,
|
|
823
|
-
args: DefaultCliArgs,
|
|
824
|
-
results: MatrixResults[],
|
|
825
|
-
): MatrixReportOptions {
|
|
826
|
-
const result: MatrixReportOptions = { ...reportOptions };
|
|
827
|
-
|
|
828
|
-
if (!result.sections?.length) {
|
|
829
|
-
const groups = matrixToReportGroups(results);
|
|
830
|
-
result.sections = buildReportSections(
|
|
831
|
-
args.adaptive,
|
|
832
|
-
args["gc-stats"],
|
|
833
|
-
hasField(groups, "cpu"),
|
|
834
|
-
args["trace-opt"] && hasField(groups, "optStatus"),
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
return result;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
426
|
/** Run matrix suite with full CLI handling (parse, run, report, export) */
|
|
842
427
|
export async function runDefaultMatrixBench(
|
|
843
428
|
suite: MatrixSuite,
|
|
@@ -876,40 +461,6 @@ export function matrixToReportGroups(results: MatrixResults[]): ReportGroup[] {
|
|
|
876
461
|
);
|
|
877
462
|
}
|
|
878
463
|
|
|
879
|
-
export interface MatrixExportOptions {
|
|
880
|
-
sections?: any[];
|
|
881
|
-
currentVersion?: GitVersion;
|
|
882
|
-
baselineVersion?: GitVersion;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
/** Strip surrounding quotes from a chrome arg token.
|
|
886
|
-
*
|
|
887
|
-
* (Needed because --chrome-args values pass through yargs and spawn() without
|
|
888
|
-
* shell processing, so literal quote characters reach Chrome/V8 unrecognized.)
|
|
889
|
-
*/
|
|
890
|
-
function stripQuotes(s: string): string {
|
|
891
|
-
/* (['"]): opening quote; (.*): content; \1: require same closing quote */
|
|
892
|
-
const unquote = s.replace(/^(['"])(.*)\1$/s, "$2");
|
|
893
|
-
|
|
894
|
-
/* value portion: --flag="--value" or --flag='--value'
|
|
895
|
-
(-[^=]+=): flag name and =; (['"])(.*)\2: quoted value */
|
|
896
|
-
const valueUnquote = unquote.replace(/^(-[^=]+=)(['"])(.*)\2$/s, "$1$3");
|
|
897
|
-
|
|
898
|
-
return valueUnquote;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
/** Sequential map - like Promise.all(arr.map(fn)) but runs one at a time */
|
|
902
|
-
async function serialMap<T, R>(
|
|
903
|
-
arr: T[],
|
|
904
|
-
fn: (item: T) => Promise<R>,
|
|
905
|
-
): Promise<R[]> {
|
|
906
|
-
const results: R[] = [];
|
|
907
|
-
for (const item of arr) {
|
|
908
|
-
results.push(await fn(item));
|
|
909
|
-
}
|
|
910
|
-
return results;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
464
|
/** Run matrix benchmarks, display table, and generate exports */
|
|
914
465
|
export async function matrixBenchExports(
|
|
915
466
|
suite: MatrixSuite,
|
|
@@ -924,3 +475,480 @@ export async function matrixBenchExports(
|
|
|
924
475
|
const reportGroups = matrixToReportGroups(results);
|
|
925
476
|
await finishReports(reportGroups, args, suite.name, exportOptions);
|
|
926
477
|
}
|
|
478
|
+
|
|
479
|
+
/** Validate CLI argument combinations */
|
|
480
|
+
function validateArgs(args: DefaultCliArgs): void {
|
|
481
|
+
if (args["gc-stats"] && !args.worker && !args.url) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
"--gc-stats requires worker mode (the default). Remove --no-worker flag.",
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Execute all groups in suite */
|
|
489
|
+
async function runSuite(params: SuiteParams): Promise<ReportGroup[]> {
|
|
490
|
+
const { suite, runner, options, useWorker, batches } = params;
|
|
491
|
+
const results: ReportGroup[] = [];
|
|
492
|
+
for (const group of suite.groups) {
|
|
493
|
+
results.push(await runGroup(group, runner, options, useWorker, batches));
|
|
494
|
+
}
|
|
495
|
+
return results;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Build report sections based on CLI options */
|
|
499
|
+
function buildReportSections(
|
|
500
|
+
adaptive: boolean,
|
|
501
|
+
gcStats: boolean,
|
|
502
|
+
hasCpuData: boolean,
|
|
503
|
+
hasOptData: boolean,
|
|
504
|
+
) {
|
|
505
|
+
const sections = adaptive
|
|
506
|
+
? [adaptiveSection, totalTimeSection]
|
|
507
|
+
: [timeSection];
|
|
508
|
+
|
|
509
|
+
if (gcStats) sections.push(gcStatsSection);
|
|
510
|
+
if (hasCpuData) sections.push(cpuSection);
|
|
511
|
+
if (hasOptData) sections.push(optSection);
|
|
512
|
+
sections.push(runsSection);
|
|
513
|
+
|
|
514
|
+
return sections;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Print heap reports (if enabled) and export results */
|
|
518
|
+
async function finishReports(
|
|
519
|
+
results: ReportGroup[],
|
|
520
|
+
args: DefaultCliArgs,
|
|
521
|
+
suiteName?: string,
|
|
522
|
+
exportOptions?: MatrixExportOptions,
|
|
523
|
+
): Promise<void> {
|
|
524
|
+
if (needsHeapSample(args)) {
|
|
525
|
+
printHeapReports(results, cliHeapReportOptions(args));
|
|
526
|
+
}
|
|
527
|
+
await exportReports({ results, args, suiteName, ...exportOptions });
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Warn about Node-only flags that are ignored in browser mode. */
|
|
531
|
+
function warnBrowserFlags(args: DefaultCliArgs): void {
|
|
532
|
+
const ignored: string[] = [];
|
|
533
|
+
if (!args.worker) ignored.push("--no-worker");
|
|
534
|
+
if (args.cpu) ignored.push("--cpu");
|
|
535
|
+
if (args["trace-opt"]) ignored.push("--trace-opt");
|
|
536
|
+
if (args.collect) ignored.push("--collect");
|
|
537
|
+
if (args.adaptive) ignored.push("--adaptive");
|
|
538
|
+
if (args.batches > 1) ignored.push("--batches");
|
|
539
|
+
if (ignored.length) {
|
|
540
|
+
console.warn(yellow(`Ignored in browser mode: ${ignored.join(", ")}`));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** @return true if any heap-related flag implies heap sampling */
|
|
545
|
+
function needsHeapSample(args: DefaultCliArgs): boolean {
|
|
546
|
+
return (
|
|
547
|
+
args["heap-sample"] ||
|
|
548
|
+
args.speedscope ||
|
|
549
|
+
!!args["export-speedscope"] ||
|
|
550
|
+
args["heap-raw"] ||
|
|
551
|
+
args["heap-verbose"] ||
|
|
552
|
+
args["heap-user-only"]
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Strip surrounding quotes from a chrome arg token.
|
|
557
|
+
*
|
|
558
|
+
* (Needed because --chrome-args values pass through yargs and spawn() without
|
|
559
|
+
* shell processing, so literal quote characters reach Chrome/V8 unrecognized.)
|
|
560
|
+
*/
|
|
561
|
+
function stripQuotes(s: string): string {
|
|
562
|
+
/* (['"]): opening quote; (.*): content; \1: require same closing quote */
|
|
563
|
+
const unquote = s.replace(/^(['"])(.*)\1$/s, "$2");
|
|
564
|
+
|
|
565
|
+
/* value portion: --flag="--value" or --flag='--value'
|
|
566
|
+
(-[^=]+=): flag name and =; (['"])(.*)\2: quoted value */
|
|
567
|
+
const valueUnquote = unquote.replace(/^(-[^=]+=)(['"])(.*)\2$/s, "$1$3");
|
|
568
|
+
|
|
569
|
+
return valueUnquote;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Wrap browser profile result as ReportGroup[] for the standard pipeline */
|
|
573
|
+
function browserResultGroups(
|
|
574
|
+
name: string,
|
|
575
|
+
result: BrowserProfileResult,
|
|
576
|
+
): ReportGroup[] {
|
|
577
|
+
const { gcStats, heapProfile } = result;
|
|
578
|
+
let measured: MeasuredResults;
|
|
579
|
+
|
|
580
|
+
// Bench function mode: multiple timing samples with real statistics
|
|
581
|
+
if (result.samples && result.samples.length > 0) {
|
|
582
|
+
const { samples } = result;
|
|
583
|
+
const totalTime = result.wallTimeMs ? result.wallTimeMs / 1000 : undefined;
|
|
584
|
+
measured = {
|
|
585
|
+
name,
|
|
586
|
+
samples,
|
|
587
|
+
time: computeStats(samples),
|
|
588
|
+
totalTime,
|
|
589
|
+
gcStats,
|
|
590
|
+
heapProfile,
|
|
591
|
+
};
|
|
592
|
+
} else {
|
|
593
|
+
// Lap mode: 0 laps = single wall-clock, N laps handled above
|
|
594
|
+
const wallMs = result.wallTimeMs ?? 0;
|
|
595
|
+
const time = {
|
|
596
|
+
min: wallMs,
|
|
597
|
+
max: wallMs,
|
|
598
|
+
avg: wallMs,
|
|
599
|
+
p50: wallMs,
|
|
600
|
+
p75: wallMs,
|
|
601
|
+
p99: wallMs,
|
|
602
|
+
p999: wallMs,
|
|
603
|
+
};
|
|
604
|
+
measured = { name, samples: [wallMs], time, gcStats, heapProfile };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return [{ name, reports: [{ name, measuredResults: measured }] }];
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Print browser benchmark tables and heap reports */
|
|
611
|
+
function printBrowserReport(
|
|
612
|
+
result: BrowserProfileResult,
|
|
613
|
+
results: ReportGroup[],
|
|
614
|
+
args: DefaultCliArgs,
|
|
615
|
+
): void {
|
|
616
|
+
const hasSamples = result.samples && result.samples.length > 0;
|
|
617
|
+
const sections: ResultsMapper<any>[] = [];
|
|
618
|
+
if (hasSamples || result.wallTimeMs != null) {
|
|
619
|
+
sections.push(timeSection);
|
|
620
|
+
}
|
|
621
|
+
if (result.gcStats) {
|
|
622
|
+
sections.push(browserGcStatsSection);
|
|
623
|
+
}
|
|
624
|
+
if (hasSamples || result.wallTimeMs != null) {
|
|
625
|
+
sections.push(runsSection);
|
|
626
|
+
}
|
|
627
|
+
if (sections.length > 0) {
|
|
628
|
+
console.log(reportResults(results, sections));
|
|
629
|
+
}
|
|
630
|
+
if (result.heapProfile) {
|
|
631
|
+
printHeapReports(results, {
|
|
632
|
+
...cliHeapReportOptions(args),
|
|
633
|
+
isUserCode: isBrowserUserCode,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** Import a file and run it as a benchmark based on what it exports */
|
|
639
|
+
async function fileBenchExports(
|
|
640
|
+
filePath: string,
|
|
641
|
+
args: DefaultCliArgs,
|
|
642
|
+
): Promise<void> {
|
|
643
|
+
const fileUrl = pathToFileURL(resolve(filePath)).href;
|
|
644
|
+
const mod = await import(fileUrl);
|
|
645
|
+
const candidate = mod.default;
|
|
646
|
+
|
|
647
|
+
if (candidate && Array.isArray(candidate.matrices)) {
|
|
648
|
+
// MatrixSuite export
|
|
649
|
+
await matrixBenchExports(candidate as MatrixSuite, args);
|
|
650
|
+
} else if (candidate && Array.isArray(candidate.groups)) {
|
|
651
|
+
// BenchSuite export
|
|
652
|
+
await benchExports(candidate as BenchSuite, args);
|
|
653
|
+
} else if (typeof candidate === "function") {
|
|
654
|
+
// Default function export: wrap as a single benchmark
|
|
655
|
+
const name = basename(filePath).replace(/\.[^.]+$/, "");
|
|
656
|
+
await benchExports(
|
|
657
|
+
{ name, groups: [{ name, benchmarks: [{ name, fn: candidate }] }] },
|
|
658
|
+
args,
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
// else: self-executing file already ran on import
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Create options for adaptive mode */
|
|
665
|
+
function createAdaptiveOptions(args: DefaultCliArgs): RunnerOptions {
|
|
666
|
+
return {
|
|
667
|
+
minTime: (args["min-time"] ?? 1) * 1000,
|
|
668
|
+
maxTime: defaultAdaptiveMaxTime * 1000,
|
|
669
|
+
targetConfidence: args.convergence,
|
|
670
|
+
adaptive: true,
|
|
671
|
+
...cliCommonOptions(args),
|
|
672
|
+
} as any;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** Runner/matrix options shared across all CLI modes */
|
|
676
|
+
function cliCommonOptions(args: DefaultCliArgs) {
|
|
677
|
+
const { collect, cpu, warmup } = args;
|
|
678
|
+
const { "trace-opt": traceOpt, "skip-settle": noSettle } = args;
|
|
679
|
+
const { "pause-first": pauseFirst, "pause-interval": pauseInterval } = args;
|
|
680
|
+
const { "pause-duration": pauseDuration, "gc-stats": gcStats } = args;
|
|
681
|
+
const heapSample = needsHeapSample(args);
|
|
682
|
+
const { "heap-interval": heapInterval } = args;
|
|
683
|
+
const { "heap-depth": heapDepth } = args;
|
|
684
|
+
return {
|
|
685
|
+
collect,
|
|
686
|
+
cpuCounters: cpu,
|
|
687
|
+
warmup,
|
|
688
|
+
traceOpt,
|
|
689
|
+
noSettle,
|
|
690
|
+
pauseFirst,
|
|
691
|
+
pauseInterval,
|
|
692
|
+
pauseDuration,
|
|
693
|
+
gcStats,
|
|
694
|
+
heapSample,
|
|
695
|
+
heapInterval,
|
|
696
|
+
heapDepth,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** Wait for Ctrl+C before exiting */
|
|
701
|
+
function waitForCtrlC(): Promise<void> {
|
|
702
|
+
return new Promise(resolve => {
|
|
703
|
+
console.log(dim("\nPress Ctrl+C to exit"));
|
|
704
|
+
process.on("SIGINT", () => {
|
|
705
|
+
console.log();
|
|
706
|
+
resolve();
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** Apply default sections and extra columns for matrix reports */
|
|
712
|
+
function mergeMatrixDefaults(
|
|
713
|
+
reportOptions: MatrixReportOptions | undefined,
|
|
714
|
+
args: DefaultCliArgs,
|
|
715
|
+
results: MatrixResults[],
|
|
716
|
+
): MatrixReportOptions {
|
|
717
|
+
const result: MatrixReportOptions = { ...reportOptions };
|
|
718
|
+
|
|
719
|
+
if (!result.sections?.length) {
|
|
720
|
+
const groups = matrixToReportGroups(results);
|
|
721
|
+
result.sections = buildReportSections(
|
|
722
|
+
args.adaptive,
|
|
723
|
+
args["gc-stats"],
|
|
724
|
+
hasField(groups, "cpu"),
|
|
725
|
+
args["trace-opt"] && hasField(groups, "optStatus"),
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return result;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** Execute group with shared setup, optionally batching to reduce ordering bias */
|
|
733
|
+
async function runGroup(
|
|
734
|
+
group: BenchGroup,
|
|
735
|
+
runner: KnownRunner,
|
|
736
|
+
options: RunnerOptions,
|
|
737
|
+
useWorker: boolean,
|
|
738
|
+
batches = 1,
|
|
739
|
+
): Promise<ReportGroup> {
|
|
740
|
+
const { name, benchmarks, baseline, setup, metadata } = group;
|
|
741
|
+
const setupParams = await setup?.();
|
|
742
|
+
validateBenchmarkParameters(group);
|
|
743
|
+
|
|
744
|
+
const runParams = {
|
|
745
|
+
runner,
|
|
746
|
+
options,
|
|
747
|
+
useWorker,
|
|
748
|
+
params: setupParams,
|
|
749
|
+
metadata,
|
|
750
|
+
};
|
|
751
|
+
if (batches === 1) {
|
|
752
|
+
return runSingleBatch(name, benchmarks, baseline, runParams);
|
|
753
|
+
}
|
|
754
|
+
return runMultipleBatches(name, benchmarks, baseline, runParams, batches);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** @return HeapReportOptions from CLI args */
|
|
758
|
+
function cliHeapReportOptions(args: DefaultCliArgs): HeapReportOptions {
|
|
759
|
+
return {
|
|
760
|
+
topN: args["heap-rows"],
|
|
761
|
+
stackDepth: args["heap-stack"],
|
|
762
|
+
verbose: args["heap-verbose"],
|
|
763
|
+
raw: args["heap-raw"],
|
|
764
|
+
userOnly: args["heap-user-only"],
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** Warn if parameterized benchmarks lack setup */
|
|
769
|
+
function validateBenchmarkParameters(group: BenchGroup): void {
|
|
770
|
+
const { name, setup, benchmarks, baseline } = group;
|
|
771
|
+
if (setup) return;
|
|
772
|
+
|
|
773
|
+
const allBenchmarks = baseline ? [...benchmarks, baseline] : benchmarks;
|
|
774
|
+
for (const benchmark of allBenchmarks) {
|
|
775
|
+
if (benchmark.fn.length > 0) {
|
|
776
|
+
console.warn(
|
|
777
|
+
`Benchmark "${benchmark.name}" in group "${name}" expects parameters but no setup() provided.`,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Run benchmarks in a single batch */
|
|
784
|
+
async function runSingleBatch(
|
|
785
|
+
name: string,
|
|
786
|
+
benchmarks: BenchmarkSpec[],
|
|
787
|
+
baseline: BenchmarkSpec | undefined,
|
|
788
|
+
runParams: RunParams,
|
|
789
|
+
): Promise<ReportGroup> {
|
|
790
|
+
const baselineReport = baseline
|
|
791
|
+
? await runSingleBenchmark(baseline, runParams)
|
|
792
|
+
: undefined;
|
|
793
|
+
const reports = await serialMap(benchmarks, b =>
|
|
794
|
+
runSingleBenchmark(b, runParams),
|
|
795
|
+
);
|
|
796
|
+
return { name, reports, baseline: baselineReport };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** Run benchmarks in multiple batches, alternating order to reduce bias */
|
|
800
|
+
async function runMultipleBatches(
|
|
801
|
+
name: string,
|
|
802
|
+
benchmarks: BenchmarkSpec[],
|
|
803
|
+
baseline: BenchmarkSpec | undefined,
|
|
804
|
+
runParams: RunParams,
|
|
805
|
+
batches: number,
|
|
806
|
+
): Promise<ReportGroup> {
|
|
807
|
+
const timePerBatch = (runParams.options.maxTime || 5000) / batches;
|
|
808
|
+
const batchParams = {
|
|
809
|
+
...runParams,
|
|
810
|
+
options: { ...runParams.options, maxTime: timePerBatch },
|
|
811
|
+
};
|
|
812
|
+
const baselineBatches: MeasuredResults[] = [];
|
|
813
|
+
const benchmarkBatches = new Map<string, MeasuredResults[]>();
|
|
814
|
+
|
|
815
|
+
for (let i = 0; i < batches; i++) {
|
|
816
|
+
const reverseOrder = i % 2 === 1;
|
|
817
|
+
await runBatchIteration(
|
|
818
|
+
benchmarks,
|
|
819
|
+
baseline,
|
|
820
|
+
batchParams,
|
|
821
|
+
reverseOrder,
|
|
822
|
+
baselineBatches,
|
|
823
|
+
benchmarkBatches,
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const meta = runParams.metadata;
|
|
828
|
+
return mergeBatchResults(
|
|
829
|
+
name,
|
|
830
|
+
benchmarks,
|
|
831
|
+
baseline,
|
|
832
|
+
baselineBatches,
|
|
833
|
+
benchmarkBatches,
|
|
834
|
+
meta,
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/** Run single benchmark and create report */
|
|
839
|
+
async function runSingleBenchmark(
|
|
840
|
+
spec: BenchmarkSpec,
|
|
841
|
+
runParams: RunParams,
|
|
842
|
+
): Promise<BenchmarkReport> {
|
|
843
|
+
const { runner, options, useWorker, params, metadata } = runParams;
|
|
844
|
+
const benchmarkParams = { spec, runner, options, useWorker, params };
|
|
845
|
+
const [result] = await runBenchmark(benchmarkParams);
|
|
846
|
+
return { name: spec.name, measuredResults: result, metadata };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** Sequential map - like Promise.all(arr.map(fn)) but runs one at a time */
|
|
850
|
+
async function serialMap<T, R>(
|
|
851
|
+
arr: T[],
|
|
852
|
+
fn: (item: T) => Promise<R>,
|
|
853
|
+
): Promise<R[]> {
|
|
854
|
+
const results: R[] = [];
|
|
855
|
+
for (const item of arr) {
|
|
856
|
+
results.push(await fn(item));
|
|
857
|
+
}
|
|
858
|
+
return results;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** Run one batch iteration in either order */
|
|
862
|
+
async function runBatchIteration(
|
|
863
|
+
benchmarks: BenchmarkSpec[],
|
|
864
|
+
baseline: BenchmarkSpec | undefined,
|
|
865
|
+
runParams: RunParams,
|
|
866
|
+
reverseOrder: boolean,
|
|
867
|
+
baselineBatches: MeasuredResults[],
|
|
868
|
+
benchmarkBatches: Map<string, MeasuredResults[]>,
|
|
869
|
+
): Promise<void> {
|
|
870
|
+
const runBaseline = async () => {
|
|
871
|
+
if (baseline) {
|
|
872
|
+
const r = await runSingleBenchmark(baseline, runParams);
|
|
873
|
+
baselineBatches.push(r.measuredResults);
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
const runBenches = async () => {
|
|
877
|
+
for (const b of benchmarks) {
|
|
878
|
+
const r = await runSingleBenchmark(b, runParams);
|
|
879
|
+
appendToMap(benchmarkBatches, b.name, r.measuredResults);
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
if (reverseOrder) {
|
|
884
|
+
await runBenches();
|
|
885
|
+
await runBaseline();
|
|
886
|
+
} else {
|
|
887
|
+
await runBaseline();
|
|
888
|
+
await runBenches();
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/** Merge batch results into final ReportGroup */
|
|
893
|
+
function mergeBatchResults(
|
|
894
|
+
name: string,
|
|
895
|
+
benchmarks: BenchmarkSpec[],
|
|
896
|
+
baseline: BenchmarkSpec | undefined,
|
|
897
|
+
baselineBatches: MeasuredResults[],
|
|
898
|
+
benchmarkBatches: Map<string, MeasuredResults[]>,
|
|
899
|
+
metadata?: Record<string, unknown>,
|
|
900
|
+
): ReportGroup {
|
|
901
|
+
const mergedBaseline = baseline
|
|
902
|
+
? {
|
|
903
|
+
name: baseline.name,
|
|
904
|
+
measuredResults: mergeResults(baselineBatches),
|
|
905
|
+
metadata,
|
|
906
|
+
}
|
|
907
|
+
: undefined;
|
|
908
|
+
const reports = benchmarks.map(b => ({
|
|
909
|
+
name: b.name,
|
|
910
|
+
measuredResults: mergeResults(benchmarkBatches.get(b.name) || []),
|
|
911
|
+
metadata,
|
|
912
|
+
}));
|
|
913
|
+
return { name, reports, baseline: mergedBaseline };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function appendToMap(
|
|
917
|
+
map: Map<string, MeasuredResults[]>,
|
|
918
|
+
key: string,
|
|
919
|
+
value: MeasuredResults,
|
|
920
|
+
) {
|
|
921
|
+
if (!map.has(key)) map.set(key, []);
|
|
922
|
+
map.get(key)!.push(value);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/** Merge multiple batch results into a single MeasuredResults */
|
|
926
|
+
function mergeResults(results: MeasuredResults[]): MeasuredResults {
|
|
927
|
+
if (results.length === 0) {
|
|
928
|
+
throw new Error("Cannot merge empty results array");
|
|
929
|
+
}
|
|
930
|
+
if (results.length === 1) return results[0];
|
|
931
|
+
|
|
932
|
+
const allSamples = results.flatMap(r => r.samples);
|
|
933
|
+
const allWarmup = results.flatMap(r => r.warmupSamples || []);
|
|
934
|
+
const time = computeStats(allSamples);
|
|
935
|
+
|
|
936
|
+
let offset = 0;
|
|
937
|
+
const allPausePoints = results.flatMap(r => {
|
|
938
|
+
const pts = (r.pausePoints ?? []).map(p => ({
|
|
939
|
+
sampleIndex: p.sampleIndex + offset,
|
|
940
|
+
durationMs: p.durationMs,
|
|
941
|
+
}));
|
|
942
|
+
offset += r.samples.length;
|
|
943
|
+
return pts;
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
return {
|
|
947
|
+
name: results[0].name,
|
|
948
|
+
samples: allSamples,
|
|
949
|
+
warmupSamples: allWarmup.length ? allWarmup : undefined,
|
|
950
|
+
time,
|
|
951
|
+
totalTime: results.reduce((sum, r) => sum + (r.totalTime || 0), 0),
|
|
952
|
+
pausePoints: allPausePoints.length ? allPausePoints : undefined,
|
|
953
|
+
};
|
|
954
|
+
}
|