benchforge 0.1.8 → 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 +69 -42
- 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 +106 -48
- 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-HfimYuW_.mjs → src-B-DDaCa9.mjs} +1250 -991
- package/dist/src-B-DDaCa9.mjs.map +1 -0
- package/package.json +4 -3
- 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 +20 -6
- package/src/cli/FilterBenchmarks.ts +5 -5
- package/src/cli/RunBenchCLI.ts +533 -476
- 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 +36 -11
- package/src/test/TestUtils.ts +24 -24
- package/src/test/fixtures/fn-export-bench.ts +3 -0
- package/src/test/fixtures/suite-export-bench.ts +16 -0
- 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-HfimYuW_.mjs.map +0 -1
package/src/cli/RunBenchCLI.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
1
3
|
import pico from "picocolors";
|
|
2
4
|
import { hideBin } from "yargs/helpers";
|
|
3
5
|
import type {
|
|
@@ -12,10 +14,14 @@ import type {
|
|
|
12
14
|
ReportGroup,
|
|
13
15
|
ResultsMapper,
|
|
14
16
|
} from "../BenchmarkReport.ts";
|
|
15
|
-
import { reportResults } from "../BenchmarkReport.ts";
|
|
17
|
+
import { groupReports, reportResults } from "../BenchmarkReport.ts";
|
|
16
18
|
import type { BrowserProfileResult } from "../browser/BrowserHeapSampler.ts";
|
|
17
19
|
import { exportBenchmarkJson } from "../export/JsonExport.ts";
|
|
18
20
|
import { exportPerfettoTrace } from "../export/PerfettoExport.ts";
|
|
21
|
+
import {
|
|
22
|
+
exportAndLaunchSpeedscope,
|
|
23
|
+
exportSpeedscope,
|
|
24
|
+
} from "../export/SpeedscopeExport.ts";
|
|
19
25
|
import type { GitVersion } from "../GitUtils.ts";
|
|
20
26
|
import { prepareHtmlData } from "../HtmlDataPrep.ts";
|
|
21
27
|
import {
|
|
@@ -23,10 +29,11 @@ import {
|
|
|
23
29
|
filterSites,
|
|
24
30
|
flattenProfile,
|
|
25
31
|
formatHeapReport,
|
|
32
|
+
formatRawSamples,
|
|
26
33
|
type HeapReportOptions,
|
|
27
34
|
isBrowserUserCode,
|
|
28
|
-
totalProfileBytes,
|
|
29
35
|
} from "../heap-sample/HeapSampleReport.ts";
|
|
36
|
+
import { resolveProfile } from "../heap-sample/ResolvedProfile.ts";
|
|
30
37
|
import { generateHtmlReport } from "../html/index.ts";
|
|
31
38
|
import type { MeasuredResults } from "../MeasuredResults.ts";
|
|
32
39
|
import { loadCasesModule } from "../matrix/CaseLoader.ts";
|
|
@@ -61,27 +68,19 @@ import {
|
|
|
61
68
|
} from "./CliArgs.ts";
|
|
62
69
|
import { filterBenchmarks } from "./FilterBenchmarks.ts";
|
|
63
70
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
export interface ExportOptions {
|
|
72
|
+
results: ReportGroup[];
|
|
73
|
+
args: DefaultCliArgs;
|
|
74
|
+
sections?: any[];
|
|
75
|
+
suiteName?: string;
|
|
76
|
+
currentVersion?: GitVersion;
|
|
77
|
+
baselineVersion?: GitVersion;
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (args.cpu) ignored.push("--cpu");
|
|
78
|
-
if (args["trace-opt"]) ignored.push("--trace-opt");
|
|
79
|
-
if (args.collect) ignored.push("--collect");
|
|
80
|
-
if (args.adaptive) ignored.push("--adaptive");
|
|
81
|
-
if (args.batches > 1) ignored.push("--batches");
|
|
82
|
-
if (ignored.length) {
|
|
83
|
-
console.warn(yellow(`Ignored in browser mode: ${ignored.join(", ")}`));
|
|
84
|
-
}
|
|
80
|
+
export interface MatrixExportOptions {
|
|
81
|
+
sections?: any[];
|
|
82
|
+
currentVersion?: GitVersion;
|
|
83
|
+
baselineVersion?: GitVersion;
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
type RunParams = {
|
|
@@ -100,6 +99,11 @@ type SuiteParams = {
|
|
|
100
99
|
batches: number;
|
|
101
100
|
};
|
|
102
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
|
+
|
|
103
107
|
/** Parse CLI with custom configuration */
|
|
104
108
|
export function parseBenchArgs<T = DefaultCliArgs>(
|
|
105
109
|
configureArgs?: Configure<T>,
|
|
@@ -127,217 +131,6 @@ export async function runBenchmarks(
|
|
|
127
131
|
});
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
/** Execute all groups in suite */
|
|
131
|
-
async function runSuite(params: SuiteParams): Promise<ReportGroup[]> {
|
|
132
|
-
const { suite, runner, options, useWorker, batches } = params;
|
|
133
|
-
const results: ReportGroup[] = [];
|
|
134
|
-
for (const group of suite.groups) {
|
|
135
|
-
results.push(await runGroup(group, runner, options, useWorker, batches));
|
|
136
|
-
}
|
|
137
|
-
return results;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Execute group with shared setup, optionally batching to reduce ordering bias */
|
|
141
|
-
async function runGroup(
|
|
142
|
-
group: BenchGroup,
|
|
143
|
-
runner: KnownRunner,
|
|
144
|
-
options: RunnerOptions,
|
|
145
|
-
useWorker: boolean,
|
|
146
|
-
batches = 1,
|
|
147
|
-
): Promise<ReportGroup> {
|
|
148
|
-
const { name, benchmarks, baseline, setup, metadata } = group;
|
|
149
|
-
const setupParams = await setup?.();
|
|
150
|
-
validateBenchmarkParameters(group);
|
|
151
|
-
|
|
152
|
-
const runParams = {
|
|
153
|
-
runner,
|
|
154
|
-
options,
|
|
155
|
-
useWorker,
|
|
156
|
-
params: setupParams,
|
|
157
|
-
metadata,
|
|
158
|
-
};
|
|
159
|
-
if (batches === 1) {
|
|
160
|
-
return runSingleBatch(name, benchmarks, baseline, runParams);
|
|
161
|
-
}
|
|
162
|
-
return runMultipleBatches(name, benchmarks, baseline, runParams, batches);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Run benchmarks in a single batch */
|
|
166
|
-
async function runSingleBatch(
|
|
167
|
-
name: string,
|
|
168
|
-
benchmarks: BenchmarkSpec[],
|
|
169
|
-
baseline: BenchmarkSpec | undefined,
|
|
170
|
-
runParams: RunParams,
|
|
171
|
-
): Promise<ReportGroup> {
|
|
172
|
-
const baselineReport = baseline
|
|
173
|
-
? await runSingleBenchmark(baseline, runParams)
|
|
174
|
-
: undefined;
|
|
175
|
-
const reports = await serialMap(benchmarks, b =>
|
|
176
|
-
runSingleBenchmark(b, runParams),
|
|
177
|
-
);
|
|
178
|
-
return { name, reports, baseline: baselineReport };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** Run benchmarks in multiple batches, alternating order to reduce bias */
|
|
182
|
-
async function runMultipleBatches(
|
|
183
|
-
name: string,
|
|
184
|
-
benchmarks: BenchmarkSpec[],
|
|
185
|
-
baseline: BenchmarkSpec | undefined,
|
|
186
|
-
runParams: RunParams,
|
|
187
|
-
batches: number,
|
|
188
|
-
): Promise<ReportGroup> {
|
|
189
|
-
const timePerBatch = (runParams.options.maxTime || 5000) / batches;
|
|
190
|
-
const batchParams = {
|
|
191
|
-
...runParams,
|
|
192
|
-
options: { ...runParams.options, maxTime: timePerBatch },
|
|
193
|
-
};
|
|
194
|
-
const baselineBatches: MeasuredResults[] = [];
|
|
195
|
-
const benchmarkBatches = new Map<string, MeasuredResults[]>();
|
|
196
|
-
|
|
197
|
-
for (let i = 0; i < batches; i++) {
|
|
198
|
-
const reverseOrder = i % 2 === 1;
|
|
199
|
-
await runBatchIteration(
|
|
200
|
-
benchmarks,
|
|
201
|
-
baseline,
|
|
202
|
-
batchParams,
|
|
203
|
-
reverseOrder,
|
|
204
|
-
baselineBatches,
|
|
205
|
-
benchmarkBatches,
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const meta = runParams.metadata;
|
|
210
|
-
return mergeBatchResults(
|
|
211
|
-
name,
|
|
212
|
-
benchmarks,
|
|
213
|
-
baseline,
|
|
214
|
-
baselineBatches,
|
|
215
|
-
benchmarkBatches,
|
|
216
|
-
meta,
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Run one batch iteration in either order */
|
|
221
|
-
async function runBatchIteration(
|
|
222
|
-
benchmarks: BenchmarkSpec[],
|
|
223
|
-
baseline: BenchmarkSpec | undefined,
|
|
224
|
-
runParams: RunParams,
|
|
225
|
-
reverseOrder: boolean,
|
|
226
|
-
baselineBatches: MeasuredResults[],
|
|
227
|
-
benchmarkBatches: Map<string, MeasuredResults[]>,
|
|
228
|
-
): Promise<void> {
|
|
229
|
-
const runBaseline = async () => {
|
|
230
|
-
if (baseline) {
|
|
231
|
-
const r = await runSingleBenchmark(baseline, runParams);
|
|
232
|
-
baselineBatches.push(r.measuredResults);
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
const runBenches = async () => {
|
|
236
|
-
for (const b of benchmarks) {
|
|
237
|
-
const r = await runSingleBenchmark(b, runParams);
|
|
238
|
-
appendToMap(benchmarkBatches, b.name, r.measuredResults);
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
if (reverseOrder) {
|
|
243
|
-
await runBenches();
|
|
244
|
-
await runBaseline();
|
|
245
|
-
} else {
|
|
246
|
-
await runBaseline();
|
|
247
|
-
await runBenches();
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/** Merge batch results into final ReportGroup */
|
|
252
|
-
function mergeBatchResults(
|
|
253
|
-
name: string,
|
|
254
|
-
benchmarks: BenchmarkSpec[],
|
|
255
|
-
baseline: BenchmarkSpec | undefined,
|
|
256
|
-
baselineBatches: MeasuredResults[],
|
|
257
|
-
benchmarkBatches: Map<string, MeasuredResults[]>,
|
|
258
|
-
metadata?: Record<string, unknown>,
|
|
259
|
-
): ReportGroup {
|
|
260
|
-
const mergedBaseline = baseline
|
|
261
|
-
? {
|
|
262
|
-
name: baseline.name,
|
|
263
|
-
measuredResults: mergeResults(baselineBatches),
|
|
264
|
-
metadata,
|
|
265
|
-
}
|
|
266
|
-
: undefined;
|
|
267
|
-
const reports = benchmarks.map(b => ({
|
|
268
|
-
name: b.name,
|
|
269
|
-
measuredResults: mergeResults(benchmarkBatches.get(b.name) || []),
|
|
270
|
-
metadata,
|
|
271
|
-
}));
|
|
272
|
-
return { name, reports, baseline: mergedBaseline };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** Run single benchmark and create report */
|
|
276
|
-
async function runSingleBenchmark(
|
|
277
|
-
spec: BenchmarkSpec,
|
|
278
|
-
runParams: RunParams,
|
|
279
|
-
): Promise<BenchmarkReport> {
|
|
280
|
-
const { runner, options, useWorker, params, metadata } = runParams;
|
|
281
|
-
const benchmarkParams = { spec, runner, options, useWorker, params };
|
|
282
|
-
const [result] = await runBenchmark(benchmarkParams);
|
|
283
|
-
return { name: spec.name, measuredResults: result, metadata };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** Warn if parameterized benchmarks lack setup */
|
|
287
|
-
function validateBenchmarkParameters(group: BenchGroup): void {
|
|
288
|
-
const { name, setup, benchmarks, baseline } = group;
|
|
289
|
-
if (setup) return;
|
|
290
|
-
|
|
291
|
-
const allBenchmarks = baseline ? [...benchmarks, baseline] : benchmarks;
|
|
292
|
-
for (const benchmark of allBenchmarks) {
|
|
293
|
-
if (benchmark.fn.length > 0) {
|
|
294
|
-
console.warn(
|
|
295
|
-
`Benchmark "${benchmark.name}" in group "${name}" expects parameters but no setup() provided.`,
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/** Merge multiple batch results into a single MeasuredResults */
|
|
302
|
-
function mergeResults(results: MeasuredResults[]): MeasuredResults {
|
|
303
|
-
if (results.length === 0) {
|
|
304
|
-
throw new Error("Cannot merge empty results array");
|
|
305
|
-
}
|
|
306
|
-
if (results.length === 1) return results[0];
|
|
307
|
-
|
|
308
|
-
const allSamples = results.flatMap(r => r.samples);
|
|
309
|
-
const allWarmup = results.flatMap(r => r.warmupSamples || []);
|
|
310
|
-
const time = computeStats(allSamples);
|
|
311
|
-
|
|
312
|
-
let offset = 0;
|
|
313
|
-
const allPausePoints = results.flatMap(r => {
|
|
314
|
-
const pts = (r.pausePoints ?? []).map(p => ({
|
|
315
|
-
sampleIndex: p.sampleIndex + offset,
|
|
316
|
-
durationMs: p.durationMs,
|
|
317
|
-
}));
|
|
318
|
-
offset += r.samples.length;
|
|
319
|
-
return pts;
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
return {
|
|
323
|
-
name: results[0].name,
|
|
324
|
-
samples: allSamples,
|
|
325
|
-
warmupSamples: allWarmup.length ? allWarmup : undefined,
|
|
326
|
-
time,
|
|
327
|
-
totalTime: results.reduce((sum, r) => sum + (r.totalTime || 0), 0),
|
|
328
|
-
pausePoints: allPausePoints.length ? allPausePoints : undefined,
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function appendToMap(
|
|
333
|
-
map: Map<string, MeasuredResults[]>,
|
|
334
|
-
key: string,
|
|
335
|
-
value: MeasuredResults,
|
|
336
|
-
) {
|
|
337
|
-
if (!map.has(key)) map.set(key, []);
|
|
338
|
-
map.get(key)!.push(value);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
134
|
/** Generate table with standard sections */
|
|
342
135
|
export function defaultReport(
|
|
343
136
|
groups: ReportGroup[],
|
|
@@ -355,25 +148,6 @@ export function defaultReport(
|
|
|
355
148
|
return reportResults(groups, sections);
|
|
356
149
|
}
|
|
357
150
|
|
|
358
|
-
/** Build report sections based on CLI options */
|
|
359
|
-
function buildReportSections(
|
|
360
|
-
adaptive: boolean,
|
|
361
|
-
gcStats: boolean,
|
|
362
|
-
hasCpuData: boolean,
|
|
363
|
-
hasOptData: boolean,
|
|
364
|
-
) {
|
|
365
|
-
const sections = adaptive
|
|
366
|
-
? [adaptiveSection, totalTimeSection]
|
|
367
|
-
: [timeSection];
|
|
368
|
-
|
|
369
|
-
if (gcStats) sections.push(gcStatsSection);
|
|
370
|
-
if (hasCpuData) sections.push(cpuSection);
|
|
371
|
-
if (hasOptData) sections.push(optSection);
|
|
372
|
-
sections.push(runsSection);
|
|
373
|
-
|
|
374
|
-
return sections;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
151
|
/** Run benchmarks, display table, and optionally generate HTML report */
|
|
378
152
|
export async function benchExports(
|
|
379
153
|
suite: BenchSuite,
|
|
@@ -406,7 +180,7 @@ export async function browserBenchExports(args: DefaultCliArgs): Promise<void> {
|
|
|
406
180
|
const { iterations, time } = args;
|
|
407
181
|
const result = await profileBrowser({
|
|
408
182
|
url,
|
|
409
|
-
heapSample: args
|
|
183
|
+
heapSample: needsHeapSample(args),
|
|
410
184
|
heapOptions: {
|
|
411
185
|
samplingInterval: args["heap-interval"],
|
|
412
186
|
stackDepth: args["heap-depth"],
|
|
@@ -428,98 +202,32 @@ export async function browserBenchExports(args: DefaultCliArgs): Promise<void> {
|
|
|
428
202
|
await exportReports({ results, args });
|
|
429
203
|
}
|
|
430
204
|
|
|
431
|
-
/** Print browser benchmark tables and heap reports */
|
|
432
|
-
function printBrowserReport(
|
|
433
|
-
result: BrowserProfileResult,
|
|
434
|
-
results: ReportGroup[],
|
|
435
|
-
args: DefaultCliArgs,
|
|
436
|
-
): void {
|
|
437
|
-
const hasSamples = result.samples && result.samples.length > 0;
|
|
438
|
-
const sections: ResultsMapper<any>[] = [];
|
|
439
|
-
if (hasSamples || result.wallTimeMs != null) {
|
|
440
|
-
sections.push(timeSection);
|
|
441
|
-
}
|
|
442
|
-
if (result.gcStats) {
|
|
443
|
-
sections.push(browserGcStatsSection);
|
|
444
|
-
}
|
|
445
|
-
if (hasSamples || result.wallTimeMs != null) {
|
|
446
|
-
sections.push(runsSection);
|
|
447
|
-
}
|
|
448
|
-
if (sections.length > 0) {
|
|
449
|
-
console.log(reportResults(results, sections));
|
|
450
|
-
}
|
|
451
|
-
if (result.heapProfile) {
|
|
452
|
-
printHeapReports(results, {
|
|
453
|
-
...cliHeapReportOptions(args),
|
|
454
|
-
isUserCode: isBrowserUserCode,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/** Wrap browser profile result as ReportGroup[] for the standard pipeline */
|
|
460
|
-
function browserResultGroups(
|
|
461
|
-
name: string,
|
|
462
|
-
result: BrowserProfileResult,
|
|
463
|
-
): ReportGroup[] {
|
|
464
|
-
const { gcStats, heapProfile } = result;
|
|
465
|
-
let measured: MeasuredResults;
|
|
466
|
-
|
|
467
|
-
// Bench function mode: multiple timing samples with real statistics
|
|
468
|
-
if (result.samples && result.samples.length > 0) {
|
|
469
|
-
const { samples } = result;
|
|
470
|
-
const totalTime = result.wallTimeMs ? result.wallTimeMs / 1000 : undefined;
|
|
471
|
-
measured = {
|
|
472
|
-
name,
|
|
473
|
-
samples,
|
|
474
|
-
time: computeStats(samples),
|
|
475
|
-
totalTime,
|
|
476
|
-
gcStats,
|
|
477
|
-
heapProfile,
|
|
478
|
-
};
|
|
479
|
-
} else {
|
|
480
|
-
// Lap mode: 0 laps = single wall-clock, N laps handled above
|
|
481
|
-
const wallMs = result.wallTimeMs ?? 0;
|
|
482
|
-
const time = {
|
|
483
|
-
min: wallMs,
|
|
484
|
-
max: wallMs,
|
|
485
|
-
avg: wallMs,
|
|
486
|
-
p50: wallMs,
|
|
487
|
-
p75: wallMs,
|
|
488
|
-
p99: wallMs,
|
|
489
|
-
p999: wallMs,
|
|
490
|
-
};
|
|
491
|
-
measured = { name, samples: [wallMs], time, gcStats, heapProfile };
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return [{ name, reports: [{ name, measuredResults: measured }] }];
|
|
495
|
-
}
|
|
496
|
-
|
|
497
205
|
/** Print heap allocation reports for benchmarks with heap profiles */
|
|
498
206
|
export function printHeapReports(
|
|
499
207
|
groups: ReportGroup[],
|
|
500
208
|
options: HeapReportOptions,
|
|
501
209
|
): void {
|
|
502
210
|
for (const group of groups) {
|
|
503
|
-
const
|
|
504
|
-
? [...group.reports, group.baseline]
|
|
505
|
-
: group.reports;
|
|
506
|
-
|
|
507
|
-
for (const report of allReports) {
|
|
211
|
+
for (const report of groupReports(group)) {
|
|
508
212
|
const { heapProfile } = report.measuredResults;
|
|
509
213
|
if (!heapProfile) continue;
|
|
510
214
|
|
|
511
215
|
console.log(dim(`\n─── Heap profile: ${report.name} ───`));
|
|
512
|
-
const
|
|
513
|
-
const sites = flattenProfile(
|
|
216
|
+
const resolved = resolveProfile(heapProfile);
|
|
217
|
+
const sites = flattenProfile(resolved);
|
|
514
218
|
const userSites = filterSites(sites, options.isUserCode);
|
|
515
219
|
const totalUserCode = userSites.reduce((sum, s) => sum + s.bytes, 0);
|
|
516
220
|
const aggregated = aggregateSites(options.userOnly ? userSites : sites);
|
|
517
221
|
const extra = {
|
|
518
|
-
totalAll,
|
|
222
|
+
totalAll: resolved.totalBytes,
|
|
519
223
|
totalUserCode,
|
|
520
|
-
sampleCount:
|
|
224
|
+
sampleCount: resolved.sortedSamples?.length,
|
|
521
225
|
};
|
|
522
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
|
+
}
|
|
523
231
|
}
|
|
524
232
|
}
|
|
525
233
|
}
|
|
@@ -534,8 +242,12 @@ export async function runDefaultBench(
|
|
|
534
242
|
await browserBenchExports(args);
|
|
535
243
|
} else if (suite) {
|
|
536
244
|
await benchExports(suite, args);
|
|
245
|
+
} else if (args.file) {
|
|
246
|
+
await fileBenchExports(args.file, args);
|
|
537
247
|
} else {
|
|
538
|
-
throw new Error(
|
|
248
|
+
throw new Error(
|
|
249
|
+
"Provide a benchmark file, --url for browser mode, or pass a BenchSuite directly.",
|
|
250
|
+
);
|
|
539
251
|
}
|
|
540
252
|
}
|
|
541
253
|
|
|
@@ -553,51 +265,10 @@ export function cliToRunnerOptions(args: DefaultCliArgs): RunnerOptions {
|
|
|
553
265
|
};
|
|
554
266
|
}
|
|
555
267
|
|
|
556
|
-
/** Create options for adaptive mode */
|
|
557
|
-
function createAdaptiveOptions(args: DefaultCliArgs): RunnerOptions {
|
|
558
|
-
return {
|
|
559
|
-
minTime: (args["min-time"] ?? 1) * 1000,
|
|
560
|
-
maxTime: defaultAdaptiveMaxTime * 1000,
|
|
561
|
-
targetConfidence: args.convergence,
|
|
562
|
-
adaptive: true,
|
|
563
|
-
...cliCommonOptions(args),
|
|
564
|
-
} as any;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/** Runner/matrix options shared across all CLI modes */
|
|
568
|
-
function cliCommonOptions(args: DefaultCliArgs) {
|
|
569
|
-
const { collect, cpu, warmup } = args;
|
|
570
|
-
const { "trace-opt": traceOpt, "skip-settle": noSettle } = args;
|
|
571
|
-
const { "pause-first": pauseFirst, "pause-interval": pauseInterval } = args;
|
|
572
|
-
const { "pause-duration": pauseDuration, "gc-stats": gcStats } = args;
|
|
573
|
-
const { "heap-sample": heapSample, "heap-interval": heapInterval } = args;
|
|
574
|
-
const { "heap-depth": heapDepth } = args;
|
|
575
|
-
return {
|
|
576
|
-
collect,
|
|
577
|
-
cpuCounters: cpu,
|
|
578
|
-
warmup,
|
|
579
|
-
traceOpt,
|
|
580
|
-
noSettle,
|
|
581
|
-
pauseFirst,
|
|
582
|
-
pauseInterval,
|
|
583
|
-
pauseDuration,
|
|
584
|
-
gcStats,
|
|
585
|
-
heapSample,
|
|
586
|
-
heapInterval,
|
|
587
|
-
heapDepth,
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
|
|
592
|
-
const { yellow, dim } = isTest
|
|
593
|
-
? { yellow: (s: string) => s, dim: (s: string) => s }
|
|
594
|
-
: pico;
|
|
595
|
-
|
|
596
268
|
/** Log V8 optimization tier distribution and deoptimizations */
|
|
597
269
|
export function reportOptStatus(groups: ReportGroup[]): void {
|
|
598
|
-
const optData = groups.flatMap(
|
|
599
|
-
|
|
600
|
-
return all
|
|
270
|
+
const optData = groups.flatMap(group => {
|
|
271
|
+
return groupReports(group)
|
|
601
272
|
.filter(r => r.measuredResults.optStatus)
|
|
602
273
|
.map(r => ({
|
|
603
274
|
name: r.name,
|
|
@@ -634,34 +305,11 @@ export function hasField(
|
|
|
634
305
|
results: ReportGroup[],
|
|
635
306
|
field: keyof MeasuredResults,
|
|
636
307
|
): boolean {
|
|
637
|
-
return results.some(
|
|
638
|
-
|
|
639
|
-
return all.some(
|
|
308
|
+
return results.some(group =>
|
|
309
|
+
groupReports(group).some(
|
|
640
310
|
({ measuredResults }) => measuredResults[field] !== undefined,
|
|
641
|
-
)
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
export interface ExportOptions {
|
|
646
|
-
results: ReportGroup[];
|
|
647
|
-
args: DefaultCliArgs;
|
|
648
|
-
sections?: any[];
|
|
649
|
-
suiteName?: string;
|
|
650
|
-
currentVersion?: GitVersion;
|
|
651
|
-
baselineVersion?: GitVersion;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/** Print heap reports (if enabled) and export results */
|
|
655
|
-
async function finishReports(
|
|
656
|
-
results: ReportGroup[],
|
|
657
|
-
args: DefaultCliArgs,
|
|
658
|
-
suiteName?: string,
|
|
659
|
-
exportOptions?: MatrixExportOptions,
|
|
660
|
-
): Promise<void> {
|
|
661
|
-
if (args["heap-sample"]) {
|
|
662
|
-
printHeapReports(results, cliHeapReportOptions(args));
|
|
663
|
-
}
|
|
664
|
-
await exportReports({ results, args, suiteName, ...exportOptions });
|
|
311
|
+
),
|
|
312
|
+
);
|
|
665
313
|
}
|
|
666
314
|
|
|
667
315
|
/** Export reports (HTML, JSON, Perfetto) based on CLI args */
|
|
@@ -690,8 +338,16 @@ export async function exportReports(options: ExportOptions): Promise<void> {
|
|
|
690
338
|
await exportBenchmarkJson(results, args.json, args, suiteName);
|
|
691
339
|
}
|
|
692
340
|
|
|
693
|
-
if (args
|
|
694
|
-
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);
|
|
695
351
|
}
|
|
696
352
|
|
|
697
353
|
// Keep process running when HTML report is opened in browser
|
|
@@ -701,17 +357,6 @@ export async function exportReports(options: ExportOptions): Promise<void> {
|
|
|
701
357
|
}
|
|
702
358
|
}
|
|
703
359
|
|
|
704
|
-
/** Wait for Ctrl+C before exiting */
|
|
705
|
-
function waitForCtrlC(): Promise<void> {
|
|
706
|
-
return new Promise(resolve => {
|
|
707
|
-
console.log(dim("\nPress Ctrl+C to exit"));
|
|
708
|
-
process.on("SIGINT", () => {
|
|
709
|
-
console.log();
|
|
710
|
-
resolve();
|
|
711
|
-
});
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
|
|
715
360
|
/** Run matrix suite with CLI arguments.
|
|
716
361
|
* no options ==> defaultCases/defaultVariants, --filter ==> subset of defaults,
|
|
717
362
|
* --all --filter ==> subset of all, --all ==> all cases/variants */
|
|
@@ -778,37 +423,6 @@ export function defaultMatrixReport(
|
|
|
778
423
|
return results.map(r => reportMatrixResults(r, options)).join("\n\n");
|
|
779
424
|
}
|
|
780
425
|
|
|
781
|
-
/** @return HeapReportOptions from CLI args */
|
|
782
|
-
function cliHeapReportOptions(args: DefaultCliArgs): HeapReportOptions {
|
|
783
|
-
return {
|
|
784
|
-
topN: args["heap-rows"],
|
|
785
|
-
stackDepth: args["heap-stack"],
|
|
786
|
-
verbose: args["heap-verbose"],
|
|
787
|
-
userOnly: args["heap-user-only"],
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
/** Apply default sections and extra columns for matrix reports */
|
|
792
|
-
function mergeMatrixDefaults(
|
|
793
|
-
reportOptions: MatrixReportOptions | undefined,
|
|
794
|
-
args: DefaultCliArgs,
|
|
795
|
-
results: MatrixResults[],
|
|
796
|
-
): MatrixReportOptions {
|
|
797
|
-
const result: MatrixReportOptions = { ...reportOptions };
|
|
798
|
-
|
|
799
|
-
if (!result.sections?.length) {
|
|
800
|
-
const groups = matrixToReportGroups(results);
|
|
801
|
-
result.sections = buildReportSections(
|
|
802
|
-
args.adaptive,
|
|
803
|
-
args["gc-stats"],
|
|
804
|
-
hasField(groups, "cpu"),
|
|
805
|
-
args["trace-opt"] && hasField(groups, "optStatus"),
|
|
806
|
-
);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
return result;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
426
|
/** Run matrix suite with full CLI handling (parse, run, report, export) */
|
|
813
427
|
export async function runDefaultMatrixBench(
|
|
814
428
|
suite: MatrixSuite,
|
|
@@ -847,40 +461,6 @@ export function matrixToReportGroups(results: MatrixResults[]): ReportGroup[] {
|
|
|
847
461
|
);
|
|
848
462
|
}
|
|
849
463
|
|
|
850
|
-
export interface MatrixExportOptions {
|
|
851
|
-
sections?: any[];
|
|
852
|
-
currentVersion?: GitVersion;
|
|
853
|
-
baselineVersion?: GitVersion;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/** Strip surrounding quotes from a chrome arg token.
|
|
857
|
-
*
|
|
858
|
-
* (Needed because --chrome-args values pass through yargs and spawn() without
|
|
859
|
-
* shell processing, so literal quote characters reach Chrome/V8 unrecognized.)
|
|
860
|
-
*/
|
|
861
|
-
function stripQuotes(s: string): string {
|
|
862
|
-
/* (['"]): opening quote; (.*): content; \1: require same closing quote */
|
|
863
|
-
const unquote = s.replace(/^(['"])(.*)\1$/s, "$2");
|
|
864
|
-
|
|
865
|
-
/* value portion: --flag="--value" or --flag='--value'
|
|
866
|
-
(-[^=]+=): flag name and =; (['"])(.*)\2: quoted value */
|
|
867
|
-
const valueUnquote = unquote.replace(/^(-[^=]+=)(['"])(.*)\2$/s, "$1$3");
|
|
868
|
-
|
|
869
|
-
return valueUnquote;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
/** Sequential map - like Promise.all(arr.map(fn)) but runs one at a time */
|
|
873
|
-
async function serialMap<T, R>(
|
|
874
|
-
arr: T[],
|
|
875
|
-
fn: (item: T) => Promise<R>,
|
|
876
|
-
): Promise<R[]> {
|
|
877
|
-
const results: R[] = [];
|
|
878
|
-
for (const item of arr) {
|
|
879
|
-
results.push(await fn(item));
|
|
880
|
-
}
|
|
881
|
-
return results;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
464
|
/** Run matrix benchmarks, display table, and generate exports */
|
|
885
465
|
export async function matrixBenchExports(
|
|
886
466
|
suite: MatrixSuite,
|
|
@@ -895,3 +475,480 @@ export async function matrixBenchExports(
|
|
|
895
475
|
const reportGroups = matrixToReportGroups(results);
|
|
896
476
|
await finishReports(reportGroups, args, suite.name, exportOptions);
|
|
897
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
|
+
}
|