benchforge 0.1.11 → 0.2.4
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/LICENSE +20 -0
- package/README.md +99 -294
- package/bin/benchforge +1 -2
- package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
- package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
- package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
- package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
- package/dist/{BenchRunner-BzyUfiyB.d.mts → BenchRunner-DglX1NOn.d.mts} +119 -66
- package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
- package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
- package/dist/Formatters-BWj3d4sv.mjs +95 -0
- package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
- package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
- package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
- package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
- package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
- package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
- package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
- package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
- package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
- package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
- package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
- package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
- package/dist/bin/benchforge.mjs +4 -5
- package/dist/bin/benchforge.mjs.map +1 -1
- package/dist/index.d.mts +711 -558
- package/dist/index.mjs +98 -3
- package/dist/index.mjs.map +1 -0
- package/dist/runners/WorkerScript.d.mts +12 -4
- package/dist/runners/WorkerScript.mjs +77 -105
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
- package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
- package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
- package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
- package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
- package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
- package/dist/viewer/index.html +19 -0
- package/dist/viewer/speedscope/LICENSE +21 -0
- package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
- package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
- package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
- package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
- package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
- package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
- package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
- package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
- package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
- package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
- package/dist/viewer/speedscope/file-format-schema.json +274 -0
- package/dist/viewer/speedscope/index.html +19 -0
- package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
- package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
- package/dist/viewer/speedscope/release.txt +3 -0
- package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
- package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
- package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
- package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
- package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
- package/package.json +52 -27
- package/src/bin/benchforge.ts +2 -2
- package/src/cli/AnalyzeArchive.ts +232 -0
- package/src/cli/BrowserBench.ts +322 -0
- package/src/cli/CliArgs.ts +164 -51
- package/src/cli/CliExport.ts +179 -0
- package/src/cli/CliOptions.ts +147 -0
- package/src/cli/CliReport.ts +197 -0
- package/src/cli/FilterBenchmarks.ts +18 -30
- package/src/cli/RunBenchCLI.ts +132 -866
- package/src/cli/SuiteRunner.ts +160 -0
- package/src/cli/ViewerServer.ts +282 -0
- package/src/export/AllocExport.ts +121 -0
- package/src/export/ArchiveExport.ts +146 -0
- package/src/export/ArchiveFormat.ts +50 -0
- package/src/export/CoverageExport.ts +148 -0
- package/src/export/EditorUri.ts +10 -0
- package/src/export/PerfettoExport.ts +64 -99
- package/src/export/SpeedscopeTypes.ts +98 -0
- package/src/export/TimeExport.ts +115 -0
- package/src/index.ts +86 -67
- package/src/matrix/BenchMatrix.ts +230 -0
- package/src/matrix/CaseLoader.ts +8 -6
- package/src/matrix/MatrixDirRunner.ts +153 -0
- package/src/matrix/MatrixFilter.ts +49 -47
- package/src/matrix/MatrixInlineRunner.ts +50 -0
- package/src/matrix/MatrixReport.ts +90 -250
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/profiling/browser/BenchLoop.ts +51 -0
- package/src/profiling/browser/BrowserCDP.ts +133 -0
- package/src/profiling/browser/BrowserGcStats.ts +33 -0
- package/src/profiling/browser/BrowserProfiler.ts +160 -0
- package/src/profiling/browser/CdpClient.ts +82 -0
- package/src/profiling/browser/CdpPage.ts +138 -0
- package/src/profiling/browser/ChromeLauncher.ts +158 -0
- package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
- package/src/profiling/browser/PageLoadMode.ts +61 -0
- package/src/profiling/node/CoverageSampler.ts +27 -0
- package/src/profiling/node/CoverageTypes.ts +23 -0
- package/src/profiling/node/HeapSampleReport.ts +261 -0
- package/src/{heap-sample → profiling/node}/HeapSampler.ts +1 -2
- package/src/{heap-sample → profiling/node}/ResolvedProfile.ts +18 -9
- package/src/profiling/node/TimeSampler.ts +57 -0
- package/src/report/BenchmarkReport.ts +146 -0
- package/src/report/Colors.ts +9 -0
- package/src/report/Formatters.ts +110 -0
- package/src/report/GcSections.ts +151 -0
- package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
- package/src/report/HtmlReport.ts +223 -0
- package/src/report/ParseStats.ts +73 -0
- package/src/report/StandardSections.ts +147 -0
- package/src/report/ViewerSections.ts +286 -0
- package/src/report/text/TableReport.ts +253 -0
- package/src/report/text/TextReport.ts +123 -0
- package/src/runners/AdaptiveWrapper.ts +116 -236
- package/src/runners/BenchRunner.ts +20 -15
- package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
- package/src/runners/CreateRunner.ts +5 -7
- package/src/runners/GcStats.ts +47 -50
- package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
- package/src/runners/MergeBatches.ts +123 -0
- package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
- package/src/runners/RunnerOrchestrator.ts +127 -243
- package/src/runners/RunnerUtils.ts +75 -1
- package/src/runners/SampleStats.ts +100 -0
- package/src/runners/TimingRunner.ts +244 -0
- package/src/runners/TimingUtils.ts +3 -2
- package/src/runners/WorkerScript.ts +135 -151
- package/src/stats/BootstrapDifference.ts +282 -0
- package/src/{PermutationTest.ts → stats/PermutationTest.ts} +8 -17
- package/src/stats/StatisticalUtils.ts +445 -0
- package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
- package/src/test/AdaptiveRunner.test.ts +39 -41
- package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +2 -2
- package/src/{tests → test}/BenchMatrix.test.ts +19 -16
- package/src/test/BenchmarkReport.test.ts +63 -13
- package/src/test/BrowserBench.e2e.test.ts +186 -17
- package/src/test/BrowserBench.test.ts +10 -5
- package/src/test/BuildTimeSection.test.ts +130 -0
- package/src/test/CapSamples.test.ts +82 -0
- package/src/test/CoverageExport.test.ts +115 -0
- package/src/test/CoverageSampler.test.ts +33 -0
- package/src/test/HeapAttribution.test.ts +14 -14
- package/src/{tests → test}/MatrixFilter.test.ts +1 -1
- package/src/{tests → test}/MatrixReport.test.ts +1 -1
- package/src/test/PermutationTest.test.ts +1 -1
- package/src/{tests → test}/RealDataValidation.test.ts +6 -6
- package/src/test/RunBenchCLI.test.ts +39 -38
- package/src/test/RunnerOrchestrator.test.ts +12 -12
- package/src/test/StatisticalUtils.test.ts +48 -12
- package/src/{table-util/test → test}/TableReport.test.ts +2 -2
- package/src/test/TestUtils.ts +12 -7
- package/src/test/TimeExport.test.ts +139 -0
- package/src/test/TimeSampler.test.ts +37 -0
- package/src/test/ViewerLive.e2e.test.ts +159 -0
- package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
- package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
- package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
- package/src/test/fixtures/cases/asyncCases.ts +9 -0
- package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
- package/src/test/fixtures/cases/variants/product.ts +2 -0
- package/src/test/fixtures/cases/variants/sum.ts +2 -0
- package/src/test/fixtures/discover/fast.ts +1 -0
- package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
- package/src/test/fixtures/invalid/bad.ts +1 -0
- package/src/test/fixtures/loader/fast.ts +1 -0
- package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
- package/src/test/fixtures/loader/stateful.ts +2 -0
- package/src/test/fixtures/stateful/stateful.ts +2 -0
- package/src/test/fixtures/variants/extra.ts +1 -0
- package/src/test/fixtures/variants/impl.ts +1 -0
- package/src/test/fixtures/worker/fast.ts +1 -0
- package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
- package/src/viewer/DateFormat.ts +30 -0
- package/src/viewer/Helpers.ts +23 -0
- package/src/viewer/LineData.ts +120 -0
- package/src/viewer/Providers.ts +191 -0
- package/src/viewer/ReportData.ts +123 -0
- package/src/viewer/State.ts +49 -0
- package/src/viewer/Theme.ts +15 -0
- package/src/viewer/components/App.tsx +73 -0
- package/src/viewer/components/DropZone.tsx +71 -0
- package/src/viewer/components/LazyPlot.ts +33 -0
- package/src/viewer/components/SamplesPanel.tsx +214 -0
- package/src/viewer/components/Shell.tsx +26 -0
- package/src/viewer/components/SourcePanel.tsx +216 -0
- package/src/viewer/components/SummaryPanel.tsx +332 -0
- package/src/viewer/components/TabBar.tsx +131 -0
- package/src/viewer/components/TabContent.tsx +46 -0
- package/src/viewer/components/ThemeToggle.tsx +50 -0
- package/src/viewer/index.html +20 -0
- package/src/viewer/main.tsx +4 -0
- package/src/viewer/plots/CIPlot.ts +313 -0
- package/src/{html/browser → viewer/plots}/HistogramKde.ts +33 -38
- package/src/viewer/plots/LegendUtils.ts +134 -0
- package/src/viewer/plots/PlotTypes.ts +85 -0
- package/src/viewer/plots/RenderPlots.ts +230 -0
- package/src/viewer/plots/SampleTimeSeries.ts +306 -0
- package/src/viewer/plots/SvgHelpers.ts +136 -0
- package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
- package/src/viewer/report.css +427 -0
- package/src/viewer/shell.css +357 -0
- package/src/viewer/tsconfig.json +11 -0
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs +0 -202
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +0 -1
- package/dist/GcStats-wX7Xyblu.mjs +0 -77
- package/dist/GcStats-wX7Xyblu.mjs.map +0 -1
- package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
- package/dist/TimingUtils-DwOwkc8G.mjs +0 -597
- package/dist/TimingUtils-DwOwkc8G.mjs.map +0 -1
- package/dist/browser/index.js +0 -914
- package/dist/src-B-DDaCa9.mjs +0 -3108
- package/dist/src-B-DDaCa9.mjs.map +0 -1
- package/src/BenchMatrix.ts +0 -380
- package/src/BenchmarkReport.ts +0 -161
- package/src/HtmlDataPrep.ts +0 -148
- package/src/StandardSections.ts +0 -261
- package/src/StatisticalUtils.ts +0 -175
- package/src/TypeUtil.ts +0 -8
- package/src/browser/BrowserGcStats.ts +0 -44
- package/src/browser/BrowserHeapSampler.ts +0 -271
- package/src/export/JsonExport.ts +0 -103
- package/src/export/JsonFormat.ts +0 -91
- package/src/export/SpeedscopeExport.ts +0 -202
- package/src/heap-sample/HeapSampleReport.ts +0 -269
- package/src/html/HtmlReport.ts +0 -131
- package/src/html/HtmlTemplate.ts +0 -284
- package/src/html/Types.ts +0 -88
- package/src/html/browser/CIPlot.ts +0 -287
- package/src/html/browser/LegendUtils.ts +0 -163
- package/src/html/browser/RenderPlots.ts +0 -263
- package/src/html/browser/SampleTimeSeries.ts +0 -389
- package/src/html/browser/Types.ts +0 -96
- package/src/html/browser/index.ts +0 -1
- package/src/html/index.ts +0 -17
- package/src/runners/BasicRunner.ts +0 -364
- package/src/table-util/ConvergenceFormatters.ts +0 -19
- package/src/table-util/Formatters.ts +0 -157
- package/src/table-util/README.md +0 -70
- package/src/table-util/TableReport.ts +0 -293
- package/src/tests/fixtures/cases/asyncCases.ts +0 -7
- package/src/tests/fixtures/cases/variants/product.ts +0 -2
- package/src/tests/fixtures/cases/variants/sum.ts +0 -2
- package/src/tests/fixtures/discover/fast.ts +0 -1
- package/src/tests/fixtures/invalid/bad.ts +0 -1
- package/src/tests/fixtures/loader/fast.ts +0 -1
- package/src/tests/fixtures/loader/stateful.ts +0 -2
- package/src/tests/fixtures/stateful/stateful.ts +0 -2
- package/src/tests/fixtures/variants/extra.ts +0 -1
- package/src/tests/fixtures/variants/impl.ts +0 -1
- package/src/tests/fixtures/worker/fast.ts +0 -1
- /package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
- /package/src/{table-util/test → test}/TableValueExtractor.ts +0 -0
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
import { g as percentile, i as coefficientOfVariation, m as medianAbsoluteDeviation, p as median, t as average } from "./StatisticalUtils-BD92crgM.mjs";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { fork } from "node:child_process";
|
|
7
|
+
import { getHeapStatistics } from "node:v8";
|
|
8
|
+
//#region src/matrix/CaseLoader.ts
|
|
9
|
+
/** Import and validate a cases module, which must export a `cases` array */
|
|
10
|
+
async function loadCasesModule(moduleUrl) {
|
|
11
|
+
const module = await import(moduleUrl);
|
|
12
|
+
if (!Array.isArray(module.cases)) throw new Error(`Cases module at ${moduleUrl} must export 'cases' array`);
|
|
13
|
+
return {
|
|
14
|
+
cases: module.cases,
|
|
15
|
+
defaultCases: module.defaultCases,
|
|
16
|
+
defaultVariants: module.defaultVariants,
|
|
17
|
+
loadCase: module.loadCase
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Load case data from a CasesModule, or use the caseId as data if no module */
|
|
21
|
+
async function loadCaseData(casesModule, caseId) {
|
|
22
|
+
if (casesModule?.loadCase) return casesModule.loadCase(caseId);
|
|
23
|
+
return { data: caseId };
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/runners/MeasuredResults.ts
|
|
27
|
+
/**
|
|
28
|
+
* V8 GetOptimizationStatus() return codes ==> human-readable tier names.
|
|
29
|
+
* Bit 0 (1): is_function
|
|
30
|
+
* Bit 4 (16): is_optimized (TurboFan)
|
|
31
|
+
* Bit 5 (32): is_optimized (Maglev)
|
|
32
|
+
* Bit 7 (128): is_baseline (Sparkplug)
|
|
33
|
+
* Bit 3 (8): maybe_deoptimized
|
|
34
|
+
*/
|
|
35
|
+
const optStatusNames = {
|
|
36
|
+
1: "interpreted",
|
|
37
|
+
129: "sparkplug",
|
|
38
|
+
17: "turbofan",
|
|
39
|
+
33: "maglev",
|
|
40
|
+
49: "turbofan+maglev",
|
|
41
|
+
32769: "optimized"
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/runners/SampleStats.ts
|
|
45
|
+
/** Compute percentiles, CV, MAD, and outlier rate from timing samples. */
|
|
46
|
+
function computeStats(samples) {
|
|
47
|
+
let min = Number.POSITIVE_INFINITY;
|
|
48
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
49
|
+
let sum = 0;
|
|
50
|
+
for (const s of samples) {
|
|
51
|
+
if (s < min) min = s;
|
|
52
|
+
if (s > max) max = s;
|
|
53
|
+
sum += s;
|
|
54
|
+
}
|
|
55
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
56
|
+
const pct = (p) => sorted[Math.max(0, Math.ceil(sorted.length * p) - 1)];
|
|
57
|
+
return {
|
|
58
|
+
min,
|
|
59
|
+
max,
|
|
60
|
+
avg: sum / samples.length,
|
|
61
|
+
p25: pct(.25),
|
|
62
|
+
p50: pct(.5),
|
|
63
|
+
p75: pct(.75),
|
|
64
|
+
p95: pct(.95),
|
|
65
|
+
p99: pct(.99),
|
|
66
|
+
p999: pct(.999),
|
|
67
|
+
cv: coefficientOfVariation(samples),
|
|
68
|
+
mad: medianAbsoluteDeviation(samples),
|
|
69
|
+
outlierRate: outlierImpactRatio(samples)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Measure outlier impact as proportion of excess time above 1.5*IQR threshold. */
|
|
73
|
+
function outlierImpactRatio(samples) {
|
|
74
|
+
if (samples.length === 0) return 0;
|
|
75
|
+
const med = median(samples);
|
|
76
|
+
const threshold = med + 1.5 * (percentile(samples, .75) - med);
|
|
77
|
+
let excessTime = 0;
|
|
78
|
+
for (const sample of samples) if (sample > threshold) excessTime += sample - med;
|
|
79
|
+
const total = samples.reduce((a, b) => a + b, 0);
|
|
80
|
+
return total > 0 ? excessTime / total : 0;
|
|
81
|
+
}
|
|
82
|
+
/** Group samples by V8 optimization tier and count deopts. */
|
|
83
|
+
function analyzeOptStatus(samples, statuses) {
|
|
84
|
+
if (statuses.length === 0 || statuses[0] === void 0) return void 0;
|
|
85
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
86
|
+
let deoptCount = 0;
|
|
87
|
+
for (let i = 0; i < samples.length; i++) {
|
|
88
|
+
const status = statuses[i];
|
|
89
|
+
if (status === void 0) continue;
|
|
90
|
+
if (status & 8) deoptCount++;
|
|
91
|
+
const group = byStatus.get(status);
|
|
92
|
+
if (group) group.push(samples[i]);
|
|
93
|
+
else byStatus.set(status, [samples[i]]);
|
|
94
|
+
}
|
|
95
|
+
const entries = [...byStatus].map(([status, times]) => {
|
|
96
|
+
return [optStatusNames[status] || `status=${status}`, {
|
|
97
|
+
count: times.length,
|
|
98
|
+
medianMs: median(times)
|
|
99
|
+
}];
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
byTier: Object.fromEntries(entries),
|
|
103
|
+
deoptCount
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** @return runtime gc() function, or a no-op if --expose-gc wasn't passed. */
|
|
107
|
+
function gcFunction() {
|
|
108
|
+
const gc = globalThis.gc ?? globalThis.__gc;
|
|
109
|
+
if (gc) return gc;
|
|
110
|
+
console.warn("gc() not available, run node/bun with --expose-gc");
|
|
111
|
+
return () => {};
|
|
112
|
+
}
|
|
113
|
+
/** @return function that reads V8 optimization status via %GetOptimizationStatus. */
|
|
114
|
+
function createOptStatusGetter() {
|
|
115
|
+
try {
|
|
116
|
+
const fn = new Function("f", "return %GetOptimizationStatus(f)");
|
|
117
|
+
fn(() => {});
|
|
118
|
+
return fn;
|
|
119
|
+
} catch {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/runners/MergeBatches.ts
|
|
125
|
+
/** Merge multiple batch results, concatenating samples and tracking batch boundaries. */
|
|
126
|
+
function mergeBatchResults(results) {
|
|
127
|
+
if (results.length === 0) throw new Error("Cannot merge empty results array");
|
|
128
|
+
if (results.length === 1) return {
|
|
129
|
+
...results[0],
|
|
130
|
+
batchOffsets: [0]
|
|
131
|
+
};
|
|
132
|
+
const allSamples = results.flatMap((r) => r.samples);
|
|
133
|
+
const time = computeStats(allSamples);
|
|
134
|
+
const batchOffsets = [];
|
|
135
|
+
const offsetPauses = [];
|
|
136
|
+
let offset = 0;
|
|
137
|
+
for (const r of results) {
|
|
138
|
+
batchOffsets.push(offset);
|
|
139
|
+
for (const p of r.pausePoints ?? []) {
|
|
140
|
+
const sampleIndex = p.sampleIndex + offset;
|
|
141
|
+
offsetPauses.push({
|
|
142
|
+
sampleIndex,
|
|
143
|
+
durationMs: p.durationMs
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
offset += r.samples.length;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
...results[results.length - 1],
|
|
150
|
+
name: results[0].name,
|
|
151
|
+
samples: allSamples,
|
|
152
|
+
warmupSamples: concatOptional(results, (r) => r.warmupSamples),
|
|
153
|
+
allocationSamples: concatOptional(results, (r) => r.allocationSamples),
|
|
154
|
+
heapSamples: concatOptional(results, (r) => r.heapSamples),
|
|
155
|
+
optSamples: concatOptional(results, (r) => r.optSamples),
|
|
156
|
+
time,
|
|
157
|
+
startTime: results[0].startTime,
|
|
158
|
+
totalTime: results.reduce((sum, r) => sum + (r.totalTime || 0), 0),
|
|
159
|
+
pausePoints: offsetPauses.length ? offsetPauses : void 0,
|
|
160
|
+
batchOffsets,
|
|
161
|
+
gcStats: mergeGcStats(results)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Sum GcStats across batches, or undefined if none collected. */
|
|
165
|
+
function mergeGcStats(results) {
|
|
166
|
+
const stats = results.map((r) => r.gcStats).filter(Boolean);
|
|
167
|
+
if (!stats.length) return void 0;
|
|
168
|
+
const sum = (fn) => stats.reduce((acc, s) => acc + (fn(s) ?? 0), 0);
|
|
169
|
+
return {
|
|
170
|
+
scavenges: sum((s) => s.scavenges),
|
|
171
|
+
markCompacts: sum((s) => s.markCompacts),
|
|
172
|
+
totalCollected: sum((s) => s.totalCollected),
|
|
173
|
+
gcPauseTime: sum((s) => s.gcPauseTime),
|
|
174
|
+
totalAllocated: sum((s) => s.totalAllocated) || void 0,
|
|
175
|
+
totalPromoted: sum((s) => s.totalPromoted) || void 0,
|
|
176
|
+
totalSurvived: sum((s) => s.totalSurvived) || void 0
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** Run N benchmarks + optional baseline in batched alternation, merge results. */
|
|
180
|
+
async function runBatched(runners, baseline, batches, warmupBatch = false, onProgress) {
|
|
181
|
+
const runnerBatches = runners.map(() => []);
|
|
182
|
+
const baselineBatches = [];
|
|
183
|
+
const start = performance.now();
|
|
184
|
+
const report = (batch, label) => onProgress?.({
|
|
185
|
+
batch,
|
|
186
|
+
batches,
|
|
187
|
+
label,
|
|
188
|
+
elapsed: performance.now() - start
|
|
189
|
+
});
|
|
190
|
+
for (let i = 0; i < batches; i++) {
|
|
191
|
+
const reverse = i % 2 === 1;
|
|
192
|
+
if (!reverse && baseline) {
|
|
193
|
+
baselineBatches.push(await baseline());
|
|
194
|
+
report(i, "baseline");
|
|
195
|
+
}
|
|
196
|
+
for (let j = 0; j < runners.length; j++) {
|
|
197
|
+
runnerBatches[j].push(await runners[j]());
|
|
198
|
+
report(i, "current");
|
|
199
|
+
}
|
|
200
|
+
if (reverse && baseline) {
|
|
201
|
+
baselineBatches.push(await baseline());
|
|
202
|
+
report(i, "baseline");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!warmupBatch && batches > 1) {
|
|
206
|
+
for (const b of runnerBatches) b.shift();
|
|
207
|
+
baselineBatches.shift();
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
results: runnerBatches.map((b) => mergeBatchResults(b)),
|
|
211
|
+
baseline: baselineBatches.length ? mergeBatchResults(baselineBatches) : void 0
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/** Concat optional number arrays across batches. */
|
|
215
|
+
function concatOptional(results, fn) {
|
|
216
|
+
const all = results.flatMap((r) => fn(r) || []);
|
|
217
|
+
return all.length ? all : void 0;
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/runners/GcStats.ts
|
|
221
|
+
/** Parse a single --trace-gc-nvp stderr line into a GcEvent. */
|
|
222
|
+
function parseGcLine(line) {
|
|
223
|
+
if (!line.includes("pause=")) return void 0;
|
|
224
|
+
const fields = parseNvpFields(line);
|
|
225
|
+
if (!fields.gc) return void 0;
|
|
226
|
+
const intField = (name) => Number.parseInt(fields[name] || "0", 10);
|
|
227
|
+
const type = parseGcType(fields.gc);
|
|
228
|
+
const pauseMs = Number.parseFloat(fields.pause || "0");
|
|
229
|
+
if (Number.isNaN(pauseMs)) return void 0;
|
|
230
|
+
const allocated = intField("allocated");
|
|
231
|
+
const promoted = intField("promoted");
|
|
232
|
+
const survived = intField("new_space_survived") || intField("survived");
|
|
233
|
+
const start = intField("start_object_size");
|
|
234
|
+
const end = intField("end_object_size");
|
|
235
|
+
return {
|
|
236
|
+
type,
|
|
237
|
+
pauseMs,
|
|
238
|
+
allocated,
|
|
239
|
+
collected: start > end ? start - end : 0,
|
|
240
|
+
promoted,
|
|
241
|
+
survived
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** Aggregate a list of GC events into summary statistics. */
|
|
245
|
+
function aggregateGcStats(events) {
|
|
246
|
+
let scavenges = 0;
|
|
247
|
+
let markCompacts = 0;
|
|
248
|
+
let gcPauseTime = 0;
|
|
249
|
+
let totalCollected = 0;
|
|
250
|
+
let hasNode = false;
|
|
251
|
+
let totalAllocated = 0;
|
|
252
|
+
let totalPromoted = 0;
|
|
253
|
+
let totalSurvived = 0;
|
|
254
|
+
for (const event of events) {
|
|
255
|
+
if (event.type === "scavenge" || event.type === "minor-ms") scavenges++;
|
|
256
|
+
else if (event.type === "mark-compact") markCompacts++;
|
|
257
|
+
gcPauseTime += event.pauseMs;
|
|
258
|
+
totalCollected += event.collected;
|
|
259
|
+
if (event.allocated != null) {
|
|
260
|
+
hasNode = true;
|
|
261
|
+
totalAllocated += event.allocated;
|
|
262
|
+
totalPromoted += event.promoted ?? 0;
|
|
263
|
+
totalSurvived += event.survived ?? 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
scavenges,
|
|
268
|
+
markCompacts,
|
|
269
|
+
totalCollected,
|
|
270
|
+
gcPauseTime,
|
|
271
|
+
...hasNode && {
|
|
272
|
+
totalAllocated,
|
|
273
|
+
totalPromoted,
|
|
274
|
+
totalSurvived
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/** Parse name=value pairs from a trace-gc-nvp line. */
|
|
279
|
+
function parseNvpFields(line) {
|
|
280
|
+
const pairs = [...line.matchAll(/(\w+)=([^\s,]+)/g)];
|
|
281
|
+
return Object.fromEntries(pairs.map(([, key, value]) => [key, value]));
|
|
282
|
+
}
|
|
283
|
+
/** Map V8 gc type codes to normalized event types. */
|
|
284
|
+
function parseGcType(gcField) {
|
|
285
|
+
if (gcField === "s" || gcField === "scavenge") return "scavenge";
|
|
286
|
+
if (gcField === "mc" || gcField === "ms" || gcField === "mark-compact") return "mark-compact";
|
|
287
|
+
if (gcField === "mmc" || gcField === "minor-mc" || gcField === "minor-ms") return "minor-ms";
|
|
288
|
+
return "unknown";
|
|
289
|
+
}
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region src/matrix/VariantLoader.ts
|
|
292
|
+
/** List variant IDs by scanning .ts files in a directory */
|
|
293
|
+
async function discoverVariants(dirUrl) {
|
|
294
|
+
const dirPath = fileURLToPath(dirUrl);
|
|
295
|
+
return (await fs.readdir(dirPath, { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".ts")).map((e) => e.name.slice(0, -3)).sort();
|
|
296
|
+
}
|
|
297
|
+
/** Import a variant module and return its run/setup exports as a Variant */
|
|
298
|
+
async function loadVariant(dirUrl, variantId) {
|
|
299
|
+
const moduleUrl = variantModuleUrl(dirUrl, variantId);
|
|
300
|
+
return extractVariant(await import(moduleUrl), variantId, moduleUrl);
|
|
301
|
+
}
|
|
302
|
+
/** Resolve the import URL for a variant file */
|
|
303
|
+
function variantModuleUrl(dirUrl, variantId) {
|
|
304
|
+
return new URL(`${variantId}.ts`, dirUrl).href;
|
|
305
|
+
}
|
|
306
|
+
/** Validate and extract a Variant from a module's exports */
|
|
307
|
+
function extractVariant(module, variantId, moduleUrl) {
|
|
308
|
+
const { setup, run } = module;
|
|
309
|
+
const loc = `Variant '${variantId}' at ${moduleUrl}`;
|
|
310
|
+
if (typeof run !== "function") throw new Error(`${loc} must export 'run'`);
|
|
311
|
+
if (setup === void 0) return run;
|
|
312
|
+
if (typeof setup !== "function") throw new Error(`${loc}: 'setup' must be a function`);
|
|
313
|
+
return {
|
|
314
|
+
setup,
|
|
315
|
+
run
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/runners/AdaptiveWrapper.ts
|
|
320
|
+
const minTime = 1e3;
|
|
321
|
+
const maxTime = 1e4;
|
|
322
|
+
const targetConfidence = 95;
|
|
323
|
+
const fallbackThreshold = 80;
|
|
324
|
+
const windowSize = 50;
|
|
325
|
+
const stability = .05;
|
|
326
|
+
const initialBatch = 100;
|
|
327
|
+
const continueBatch = 100;
|
|
328
|
+
const continueIterations = 10;
|
|
329
|
+
/** Wrap a runner with adaptive sampling (convergence detection or timeout). */
|
|
330
|
+
function createAdaptiveWrapper(baseRunner, options) {
|
|
331
|
+
return { async runBench(bench, opts, params) {
|
|
332
|
+
return runAdaptiveBench(baseRunner, bench, opts, options, params);
|
|
333
|
+
} };
|
|
334
|
+
}
|
|
335
|
+
/** Check convergence by comparing sliding windows of samples for stability. */
|
|
336
|
+
function checkConvergence(samples) {
|
|
337
|
+
const windowSize = getWindowSize(samples);
|
|
338
|
+
const minSamples = windowSize * 2;
|
|
339
|
+
if (samples.length < minSamples) return {
|
|
340
|
+
converged: false,
|
|
341
|
+
confidence: samples.length / minSamples * 100,
|
|
342
|
+
reason: `Collecting samples: ${samples.length}/${minSamples}`
|
|
343
|
+
};
|
|
344
|
+
return buildConvergence(getStability(samples, windowSize));
|
|
345
|
+
}
|
|
346
|
+
/** Run benchmark with adaptive sampling until convergence or timeout. */
|
|
347
|
+
async function runAdaptiveBench(runner, bench, opts, adaptive, params) {
|
|
348
|
+
const overrides = opts;
|
|
349
|
+
const min = overrides.minTime ?? adaptive.minTime ?? minTime;
|
|
350
|
+
const max = overrides.maxTime ?? adaptive.maxTime ?? maxTime;
|
|
351
|
+
const target = overrides.convergence ?? adaptive.convergence ?? targetConfidence;
|
|
352
|
+
const allSamples = [];
|
|
353
|
+
const { warmup, startTime: hrtimeStart } = await collectInitial(runner, bench, opts, params, allSamples);
|
|
354
|
+
const startTime = performance.now();
|
|
355
|
+
await collectAdaptive(runner, bench, opts, params, allSamples, {
|
|
356
|
+
minTime: min,
|
|
357
|
+
maxTime: max,
|
|
358
|
+
targetConfidence: target,
|
|
359
|
+
startTime
|
|
360
|
+
});
|
|
361
|
+
return buildResults(allSamples, startTime, checkConvergence(allSamples.map((s) => s * msToNs)), bench.name, warmup, hrtimeStart);
|
|
362
|
+
}
|
|
363
|
+
/** Scale window size inversely with execution time -- fast ops need more samples. */
|
|
364
|
+
function getWindowSize(samples) {
|
|
365
|
+
if (samples.length < 20) return windowSize;
|
|
366
|
+
const recentMedian = median(samples.slice(-20).map((s) => s / msToNs));
|
|
367
|
+
if (recentMedian < .01) return 200;
|
|
368
|
+
if (recentMedian < .1) return 100;
|
|
369
|
+
if (recentMedian < 1) return 50;
|
|
370
|
+
if (recentMedian < 10) return 30;
|
|
371
|
+
return 20;
|
|
372
|
+
}
|
|
373
|
+
/** Convert stability metrics to a convergence result with confidence score. */
|
|
374
|
+
function buildConvergence(metrics) {
|
|
375
|
+
const { medianDrift, impactDrift, medianStable, impactStable } = metrics;
|
|
376
|
+
if (medianStable && impactStable) return {
|
|
377
|
+
converged: true,
|
|
378
|
+
confidence: 100,
|
|
379
|
+
reason: "Stable performance pattern"
|
|
380
|
+
};
|
|
381
|
+
const raw = (1 - medianDrift / stability) * 50 + (1 - impactDrift / stability) * 50;
|
|
382
|
+
return {
|
|
383
|
+
converged: false,
|
|
384
|
+
confidence: Math.max(0, Math.min(100, raw)),
|
|
385
|
+
reason: medianDrift > impactDrift ? `Median drifting: ${(medianDrift * 100).toFixed(1)}%` : `Outlier impact changing: ${(impactDrift * 100).toFixed(1)}%`
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/** Compare median and outlier-impact drift between recent and previous windows. */
|
|
389
|
+
function getStability(samples, windowSize) {
|
|
390
|
+
const toMs = (s) => s / msToNs;
|
|
391
|
+
const recentMs = samples.slice(-windowSize).map(toMs);
|
|
392
|
+
const previousMs = samples.slice(-windowSize * 2, -windowSize).map(toMs);
|
|
393
|
+
const medianRecent = median(recentMs);
|
|
394
|
+
const medianPrevious = median(previousMs);
|
|
395
|
+
const medianDrift = Math.abs(medianRecent - medianPrevious) / medianPrevious;
|
|
396
|
+
const impactRecent = outlierImpactRatio(recentMs);
|
|
397
|
+
const impactPrevious = outlierImpactRatio(previousMs);
|
|
398
|
+
const impactDrift = Math.abs(impactRecent - impactPrevious);
|
|
399
|
+
return {
|
|
400
|
+
medianDrift,
|
|
401
|
+
impactDrift,
|
|
402
|
+
medianStable: medianDrift < stability,
|
|
403
|
+
impactStable: impactDrift < stability
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
/** Collect the initial batch (warmup + settle), returning warmup samples. */
|
|
407
|
+
async function collectInitial(runner, bench, opts, params, allSamples) {
|
|
408
|
+
const batchOpts = {
|
|
409
|
+
...opts,
|
|
410
|
+
maxTime: initialBatch,
|
|
411
|
+
maxIterations: void 0
|
|
412
|
+
};
|
|
413
|
+
const results = await runner.runBench(bench, batchOpts, params);
|
|
414
|
+
appendSamples(results[0], allSamples);
|
|
415
|
+
return {
|
|
416
|
+
warmup: results[0].warmupSamples,
|
|
417
|
+
startTime: results[0].startTime
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/** Collect batches until convergence or timeout, with progress logging. */
|
|
421
|
+
async function collectAdaptive(runner, bench, opts, params, allSamples, limits) {
|
|
422
|
+
const { minTime, maxTime, targetConfidence, startTime } = limits;
|
|
423
|
+
let lastLog = 0;
|
|
424
|
+
while (performance.now() - startTime < maxTime) {
|
|
425
|
+
const convergence = checkConvergence(allSamples.map((s) => s * msToNs));
|
|
426
|
+
const elapsed = performance.now() - startTime;
|
|
427
|
+
lastLog = logProgress(bench.name, convergence, elapsed, lastLog);
|
|
428
|
+
if (shouldStop(convergence, targetConfidence, elapsed, minTime)) break;
|
|
429
|
+
const batch = {
|
|
430
|
+
...opts,
|
|
431
|
+
maxTime: continueBatch,
|
|
432
|
+
maxIterations: continueIterations,
|
|
433
|
+
skipWarmup: true
|
|
434
|
+
};
|
|
435
|
+
appendSamples((await runner.runBench(bench, batch, params))[0], allSamples);
|
|
436
|
+
}
|
|
437
|
+
process.stderr.write("\r" + " ".repeat(60) + "\r");
|
|
438
|
+
}
|
|
439
|
+
/** Build final MeasuredResults from collected samples and convergence state. */
|
|
440
|
+
function buildResults(samples, elapsedStart, convergence, name, warmupSamples, startTime) {
|
|
441
|
+
const totalTime = (performance.now() - elapsedStart) / 1e3;
|
|
442
|
+
return [{
|
|
443
|
+
name,
|
|
444
|
+
samples,
|
|
445
|
+
warmupSamples,
|
|
446
|
+
time: computeStats(samples),
|
|
447
|
+
totalTime,
|
|
448
|
+
startTime,
|
|
449
|
+
convergence
|
|
450
|
+
}];
|
|
451
|
+
}
|
|
452
|
+
/** Append samples one-by-one to avoid stack overflow from spread on large arrays. */
|
|
453
|
+
function appendSamples(result, samples) {
|
|
454
|
+
if (!result.samples?.length) return;
|
|
455
|
+
for (const sample of result.samples) samples.push(sample);
|
|
456
|
+
}
|
|
457
|
+
/** Log adaptive sampling progress at ~1s intervals. */
|
|
458
|
+
function logProgress(name, convergence, elapsed, lastLog) {
|
|
459
|
+
if (elapsed - lastLog <= 1e3) return lastLog;
|
|
460
|
+
const sec = (elapsed / 1e3).toFixed(1);
|
|
461
|
+
const conf = convergence.confidence.toFixed(0);
|
|
462
|
+
process.stderr.write(`\r◊ ${name}: ${conf}% confident (${sec}s) `);
|
|
463
|
+
return elapsed;
|
|
464
|
+
}
|
|
465
|
+
/** @return true if convergence target met, or minTime elapsed with fallback confidence. */
|
|
466
|
+
function shouldStop(convergence, target, elapsed, minElapsed) {
|
|
467
|
+
if (convergence.converged && convergence.confidence >= target) return true;
|
|
468
|
+
return elapsed >= minElapsed && convergence.confidence >= Math.max(target, fallbackThreshold);
|
|
469
|
+
}
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/runners/BenchRunner.ts
|
|
472
|
+
/** Invoke the benchmark function, forwarding setup params. */
|
|
473
|
+
function executeBenchmark(benchmark, params) {
|
|
474
|
+
benchmark.fn(params);
|
|
475
|
+
}
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/runners/TimingRunner.ts
|
|
478
|
+
const defaultCollectOptions = {
|
|
479
|
+
maxTime: 5e3,
|
|
480
|
+
maxIterations: 1e6,
|
|
481
|
+
warmup: 0,
|
|
482
|
+
traceOpt: false,
|
|
483
|
+
pauseWarmup: 0
|
|
484
|
+
};
|
|
485
|
+
/**
|
|
486
|
+
* Timing-based runner that collects samples within time/iteration limits.
|
|
487
|
+
* Handles warmup, heap tracking, V8 optimization tracing, and periodic pauses.
|
|
488
|
+
*/
|
|
489
|
+
var TimingRunner = class {
|
|
490
|
+
async runBench(benchmark, options, params) {
|
|
491
|
+
const collected = await collectSamples({
|
|
492
|
+
benchmark,
|
|
493
|
+
params,
|
|
494
|
+
...defaultCollectOptions,
|
|
495
|
+
...options
|
|
496
|
+
});
|
|
497
|
+
return [buildMeasuredResults(benchmark.name, collected)];
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
/** Collect timing samples with warmup, heap tracking, and optional V8 opt tracing. */
|
|
501
|
+
async function collectSamples(config) {
|
|
502
|
+
if (!config.maxIterations && !config.maxTime) throw new Error(`At least one of maxIterations or maxTime must be set`);
|
|
503
|
+
const warmupSamples = config.skipWarmup ? [] : await runWarmup(config);
|
|
504
|
+
const heapBefore = process.memoryUsage().heapUsed;
|
|
505
|
+
const { samples, heapSamples, optStatuses, pausePoints, startTime } = await runSampleLoop(config);
|
|
506
|
+
if (samples.length === 0) throw new Error(`No samples collected for benchmark: ${config.benchmark.name}`);
|
|
507
|
+
const heapAfter = process.memoryUsage().heapUsed;
|
|
508
|
+
return {
|
|
509
|
+
samples,
|
|
510
|
+
warmupSamples,
|
|
511
|
+
heapGrowth: Math.max(0, heapAfter - heapBefore) / 1024 / samples.length,
|
|
512
|
+
heapSamples,
|
|
513
|
+
startTime,
|
|
514
|
+
optStatus: config.traceOpt ? analyzeOptStatus(samples, optStatuses) : void 0,
|
|
515
|
+
optSamples: config.traceOpt && optStatuses.length > 0 ? optStatuses : void 0,
|
|
516
|
+
pausePoints
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
/** Assemble CollectResult into a MeasuredResults record. */
|
|
520
|
+
function buildMeasuredResults(name, collected) {
|
|
521
|
+
const { samples, warmupSamples, heapSamples } = collected;
|
|
522
|
+
const { optStatus, optSamples, pausePoints, heapGrowth, startTime } = collected;
|
|
523
|
+
return {
|
|
524
|
+
name,
|
|
525
|
+
samples,
|
|
526
|
+
warmupSamples,
|
|
527
|
+
heapSamples,
|
|
528
|
+
time: computeStats(samples),
|
|
529
|
+
heapSize: {
|
|
530
|
+
avg: heapGrowth,
|
|
531
|
+
min: heapGrowth,
|
|
532
|
+
max: heapGrowth
|
|
533
|
+
},
|
|
534
|
+
startTime,
|
|
535
|
+
optStatus,
|
|
536
|
+
optSamples,
|
|
537
|
+
pausePoints
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Run warmup iterations with gc + settle time for V8 optimization. Returns warmup timings.
|
|
542
|
+
*
|
|
543
|
+
* V8 has 4 compilation tiers: Ignition (interpreter) ==> Sparkplug (baseline) ==>
|
|
544
|
+
* Maglev (mid-tier optimizer) ==> TurboFan (full optimizer). Tiering thresholds:
|
|
545
|
+
* - Ignition ==> Sparkplug: 8 invocations
|
|
546
|
+
* - Sparkplug ==> Maglev: 500 invocations
|
|
547
|
+
* - Maglev ==> TurboFan: 6000 invocations
|
|
548
|
+
*
|
|
549
|
+
* Optimization compilation happens on background threads and requires idle time
|
|
550
|
+
* on the main thread to complete. Without sufficient warmup + settle time,
|
|
551
|
+
* benchmarks exhibit bimodal timing: slow Sparkplug samples (~30% slower) mixed
|
|
552
|
+
* with fast optimized samples.
|
|
553
|
+
*
|
|
554
|
+
* The warmup iterations trigger the optimization decision, then settle time
|
|
555
|
+
* provides idle time for background compilation to finish before measurement.
|
|
556
|
+
*
|
|
557
|
+
* @see https://v8.dev/blog/sparkplug
|
|
558
|
+
* @see https://v8.dev/blog/maglev
|
|
559
|
+
* @see https://v8.dev/blog/background-compilation
|
|
560
|
+
*/
|
|
561
|
+
async function runWarmup(config) {
|
|
562
|
+
const gc = gcFunction();
|
|
563
|
+
const samples = new Array(config.warmup);
|
|
564
|
+
for (let i = 0; i < config.warmup; i++) {
|
|
565
|
+
const start = performance.now();
|
|
566
|
+
executeBenchmark(config.benchmark, config.params);
|
|
567
|
+
samples[i] = performance.now() - start;
|
|
568
|
+
}
|
|
569
|
+
gc();
|
|
570
|
+
if (config.pauseWarmup) {
|
|
571
|
+
await new Promise((r) => setTimeout(r, config.pauseWarmup));
|
|
572
|
+
gc();
|
|
573
|
+
}
|
|
574
|
+
return samples;
|
|
575
|
+
}
|
|
576
|
+
/** Collect timing samples with optional periodic pauses for V8 background compilation to complete. */
|
|
577
|
+
async function runSampleLoop(config) {
|
|
578
|
+
const { maxTime, maxIterations, pauseFirst } = config;
|
|
579
|
+
const { pauseInterval = 0, pauseDuration = 100 } = config;
|
|
580
|
+
const getOptStatus = config.traceOpt ? createOptStatusGetter() : void 0;
|
|
581
|
+
const trackOpt = !!getOptStatus;
|
|
582
|
+
const arrays = createSampleArrays(maxIterations || Math.ceil(maxTime / .1), trackOpt);
|
|
583
|
+
let count = 0;
|
|
584
|
+
let elapsed = 0;
|
|
585
|
+
let totalPauseTime = 0;
|
|
586
|
+
const startTime = Number(process.hrtime.bigint() / 1000n);
|
|
587
|
+
const loopStart = performance.now();
|
|
588
|
+
while ((!maxIterations || count < maxIterations) && (!maxTime || elapsed < maxTime)) {
|
|
589
|
+
const start = performance.now();
|
|
590
|
+
executeBenchmark(config.benchmark, config.params);
|
|
591
|
+
const end = performance.now();
|
|
592
|
+
arrays.samples[count] = end - start;
|
|
593
|
+
arrays.heapSamples[count] = getHeapStatistics().used_heap_size;
|
|
594
|
+
if (getOptStatus) arrays.optStatuses[count] = getOptStatus(config.benchmark.fn);
|
|
595
|
+
count++;
|
|
596
|
+
if (shouldPause(count, pauseFirst, pauseInterval)) {
|
|
597
|
+
const sampleIndex = count - 1;
|
|
598
|
+
arrays.pausePoints.push({
|
|
599
|
+
sampleIndex,
|
|
600
|
+
durationMs: pauseDuration
|
|
601
|
+
});
|
|
602
|
+
const pauseStart = performance.now();
|
|
603
|
+
await new Promise((r) => setTimeout(r, pauseDuration));
|
|
604
|
+
totalPauseTime += performance.now() - pauseStart;
|
|
605
|
+
}
|
|
606
|
+
elapsed = performance.now() - loopStart - totalPauseTime;
|
|
607
|
+
}
|
|
608
|
+
trimArrays(arrays, count, trackOpt);
|
|
609
|
+
return {
|
|
610
|
+
...arrays,
|
|
611
|
+
startTime
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
/** Pre-allocate sample arrays to reduce GC pressure during measurement. */
|
|
615
|
+
function createSampleArrays(n, trackOpt) {
|
|
616
|
+
const arr = () => new Array(n);
|
|
617
|
+
return {
|
|
618
|
+
samples: arr(),
|
|
619
|
+
heapSamples: arr(),
|
|
620
|
+
optStatuses: trackOpt ? arr() : [],
|
|
621
|
+
pausePoints: []
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
/** @return true if this iteration should pause for V8 background compilation. */
|
|
625
|
+
function shouldPause(iter, first, interval) {
|
|
626
|
+
if (first !== void 0 && iter === first) return true;
|
|
627
|
+
if (interval <= 0) return false;
|
|
628
|
+
if (first === void 0) return iter % interval === 0;
|
|
629
|
+
return (iter - first) % interval === 0;
|
|
630
|
+
}
|
|
631
|
+
/** Trim pre-allocated arrays to the actual sample count. */
|
|
632
|
+
function trimArrays(arrays, count, trackOpt) {
|
|
633
|
+
arrays.samples.length = arrays.heapSamples.length = count;
|
|
634
|
+
if (trackOpt) arrays.optStatuses.length = count;
|
|
635
|
+
}
|
|
636
|
+
//#endregion
|
|
637
|
+
//#region src/runners/CreateRunner.ts
|
|
638
|
+
/** Create a benchmark runner by name. */
|
|
639
|
+
async function createRunner(_name) {
|
|
640
|
+
return new TimingRunner();
|
|
641
|
+
}
|
|
642
|
+
//#endregion
|
|
643
|
+
//#region src/runners/RunnerUtils.ts
|
|
644
|
+
const msToNs = 1e6;
|
|
645
|
+
/** Get named or default export from module, throw if not a function */
|
|
646
|
+
function getModuleExport(module, exportName, modulePath) {
|
|
647
|
+
const fn = exportName ? module[exportName] : module.default || module;
|
|
648
|
+
if (typeof fn !== "function") throw new Error(`Export '${exportName || "default"}' from ${modulePath} is not a function`);
|
|
649
|
+
return fn;
|
|
650
|
+
}
|
|
651
|
+
/** Import a benchmark function from a module, optionally running a setup export */
|
|
652
|
+
async function importBenchFn(modulePath, exportName, setupExportName, params) {
|
|
653
|
+
const module = await import(modulePath);
|
|
654
|
+
const fn = getModuleExport(module, exportName, modulePath);
|
|
655
|
+
if (!setupExportName) return {
|
|
656
|
+
fn,
|
|
657
|
+
params
|
|
658
|
+
};
|
|
659
|
+
return {
|
|
660
|
+
fn,
|
|
661
|
+
params: await getModuleExport(module, setupExportName, modulePath)(params)
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
/** Resolve a matrix variant to a benchmark function (shared by orchestrator and worker). */
|
|
665
|
+
async function resolveVariantFn(params) {
|
|
666
|
+
let { caseData } = params;
|
|
667
|
+
if (params.casesModule && params.caseId) caseData = (await loadCaseData(await loadCasesModule(params.casesModule), params.caseId)).data;
|
|
668
|
+
return {
|
|
669
|
+
fn: await prepareBenchFn(await loadVariant(params.variantDir, params.variantId), caseData),
|
|
670
|
+
params: void 0
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
/** Create runner, wrapping with adaptive sampling if options.adaptive is set */
|
|
674
|
+
async function createBenchRunner(runnerName, options) {
|
|
675
|
+
const base = await createRunner(runnerName);
|
|
676
|
+
if ("adaptive" in options && options.adaptive) return createAdaptiveWrapper(base, options);
|
|
677
|
+
return base;
|
|
678
|
+
}
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region src/runners/TimingUtils.ts
|
|
681
|
+
/** Current time in ms, or 0 when debug timing is off (zero-cost no-op) */
|
|
682
|
+
function getPerfNow() {
|
|
683
|
+
return 0;
|
|
684
|
+
}
|
|
685
|
+
/** Elapsed ms between marks, or 0 when debug timing is off */
|
|
686
|
+
function getElapsed(startMark, endMark) {
|
|
687
|
+
return 0;
|
|
688
|
+
}
|
|
689
|
+
//#endregion
|
|
690
|
+
//#region src/runners/RunnerOrchestrator.ts
|
|
691
|
+
const logTiming = () => {};
|
|
692
|
+
/** Run a benchmark spec, optionally in an isolated worker process for profiling support. */
|
|
693
|
+
async function runBenchmark({ spec, runner, options, useWorker = false, params }) {
|
|
694
|
+
if (!useWorker) {
|
|
695
|
+
const resolved = spec.modulePath ? await resolveModuleSpec(spec, params) : {
|
|
696
|
+
spec,
|
|
697
|
+
params
|
|
698
|
+
};
|
|
699
|
+
return (await createBenchRunner(runner, options)).runBench(resolved.spec, options, resolved.params);
|
|
700
|
+
}
|
|
701
|
+
const msg = createRunMessage(spec, runner, options, params);
|
|
702
|
+
return runWorkerWithMessage(spec.name, options, msg);
|
|
703
|
+
}
|
|
704
|
+
/** Run a matrix variant benchmark, directly or in a worker. */
|
|
705
|
+
async function runMatrixVariant(params) {
|
|
706
|
+
const { variantId, caseId, runner, options, useWorker = true } = params;
|
|
707
|
+
const name = `${variantId}/${caseId}`;
|
|
708
|
+
if (!useWorker) return runMatrixVariantDirect(params, name);
|
|
709
|
+
const { variantDir, caseData, casesModule } = params;
|
|
710
|
+
return runWorkerWithMessage(name, options, {
|
|
711
|
+
type: "run",
|
|
712
|
+
spec: { name },
|
|
713
|
+
runnerName: runner,
|
|
714
|
+
options,
|
|
715
|
+
variantDir,
|
|
716
|
+
variantId,
|
|
717
|
+
caseId,
|
|
718
|
+
caseData,
|
|
719
|
+
casesModule
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
/** Resolve modulePath/exportName to a real function for non-worker mode */
|
|
723
|
+
async function resolveModuleSpec(spec, params) {
|
|
724
|
+
const { modulePath, exportName, setupExportName } = spec;
|
|
725
|
+
const imported = await importBenchFn(modulePath, exportName, setupExportName, params);
|
|
726
|
+
const fn = imported.fn;
|
|
727
|
+
return {
|
|
728
|
+
spec: {
|
|
729
|
+
...spec,
|
|
730
|
+
fn
|
|
731
|
+
},
|
|
732
|
+
params: imported.params
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
/** Serialize a BenchmarkSpec into a worker-safe message (modulePath or fnCode) */
|
|
736
|
+
function createRunMessage(spec, runnerName, options, params) {
|
|
737
|
+
const { fn, ...rest } = spec;
|
|
738
|
+
const message = {
|
|
739
|
+
type: "run",
|
|
740
|
+
spec: rest,
|
|
741
|
+
runnerName,
|
|
742
|
+
options,
|
|
743
|
+
params
|
|
744
|
+
};
|
|
745
|
+
if (spec.modulePath) {
|
|
746
|
+
message.modulePath = spec.modulePath;
|
|
747
|
+
message.exportName = spec.exportName;
|
|
748
|
+
if (spec.setupExportName) message.setupExportName = spec.setupExportName;
|
|
749
|
+
} else message.fnCode = fn.toString();
|
|
750
|
+
return message;
|
|
751
|
+
}
|
|
752
|
+
/** Run a benchmark in an isolated worker process with timeout and GC capture. */
|
|
753
|
+
function runWorkerWithMessage(name, options, message) {
|
|
754
|
+
const startTime = getPerfNow();
|
|
755
|
+
const collectGcStats = options.gcStats ?? false;
|
|
756
|
+
logTiming(`Starting worker for ${name}`);
|
|
757
|
+
return new Promise((resolve, reject) => {
|
|
758
|
+
const gcEvents = [];
|
|
759
|
+
const worker = spawnWorkerProcess(collectGcStats);
|
|
760
|
+
if (collectGcStats && worker.stdout) setupGcCapture(worker, gcEvents);
|
|
761
|
+
const timeoutId = setTimeout(() => {
|
|
762
|
+
killWorker();
|
|
763
|
+
reject(/* @__PURE__ */ new Error(`Benchmark "${name}" timed out after 60 seconds`));
|
|
764
|
+
}, 6e4);
|
|
765
|
+
function killWorker() {
|
|
766
|
+
clearTimeout(timeoutId);
|
|
767
|
+
if (!worker.killed) worker.kill("SIGTERM");
|
|
768
|
+
}
|
|
769
|
+
worker.on("message", (msg) => {
|
|
770
|
+
killWorker();
|
|
771
|
+
if (msg.type === "error") {
|
|
772
|
+
const error = /* @__PURE__ */ new Error(`Benchmark "${name}" failed: ${msg.error}`);
|
|
773
|
+
if (msg.stack) error.stack = msg.stack;
|
|
774
|
+
return reject(error);
|
|
775
|
+
}
|
|
776
|
+
logTiming(`Total worker time for ${name}: ${getElapsed(startTime).toFixed(1)}ms`);
|
|
777
|
+
const { results, heapProfile, timeProfile, coverage } = msg;
|
|
778
|
+
attachProfilingData(results, gcEvents, heapProfile, timeProfile, coverage);
|
|
779
|
+
resolve(results);
|
|
780
|
+
});
|
|
781
|
+
worker.on("error", (error) => {
|
|
782
|
+
killWorker();
|
|
783
|
+
const msg = `Worker process failed for "${name}": ${error.message}`;
|
|
784
|
+
reject(new Error(msg));
|
|
785
|
+
});
|
|
786
|
+
worker.on("exit", (code) => {
|
|
787
|
+
if (code !== 0 && code !== null) {
|
|
788
|
+
killWorker();
|
|
789
|
+
reject(/* @__PURE__ */ new Error(`Worker exited with code ${code} for "${name}"`));
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
worker.send(message);
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
/** Run matrix variant in-process (no worker isolation) */
|
|
796
|
+
async function runMatrixVariantDirect(params, name) {
|
|
797
|
+
const { runner, options } = params;
|
|
798
|
+
const { fn } = await resolveVariantFn(params);
|
|
799
|
+
return (await createBenchRunner(runner, options)).runBench({
|
|
800
|
+
name,
|
|
801
|
+
fn
|
|
802
|
+
}, options);
|
|
803
|
+
}
|
|
804
|
+
/** Spawn worker process with V8 flags */
|
|
805
|
+
function spawnWorkerProcess(gcStats) {
|
|
806
|
+
const workerPath = resolveWorkerPath();
|
|
807
|
+
const execArgv = ["--expose-gc", "--allow-natives-syntax"];
|
|
808
|
+
if (gcStats) execArgv.push("--trace-gc-nvp");
|
|
809
|
+
return fork(workerPath, [], {
|
|
810
|
+
execArgv,
|
|
811
|
+
silent: gcStats,
|
|
812
|
+
env: {
|
|
813
|
+
...process.env,
|
|
814
|
+
NODE_OPTIONS: ""
|
|
815
|
+
},
|
|
816
|
+
serialization: "advanced"
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
/** Capture and parse GC lines from worker stdout (--trace-gc-nvp). */
|
|
820
|
+
function setupGcCapture(worker, gcEvents) {
|
|
821
|
+
let buffer = "";
|
|
822
|
+
worker.stdout.on("data", (data) => {
|
|
823
|
+
buffer += data.toString();
|
|
824
|
+
const lines = buffer.split("\n");
|
|
825
|
+
buffer = lines.pop() || "";
|
|
826
|
+
for (const line of lines) {
|
|
827
|
+
const event = parseGcLine(line);
|
|
828
|
+
if (event) gcEvents.push(event);
|
|
829
|
+
else if (line.trim()) process.stdout.write(line + "\n");
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
/** Attach profiling data collected by the worker to each result. */
|
|
834
|
+
function attachProfilingData(results, gcEvents, heapProfile, timeProfile, coverage) {
|
|
835
|
+
const gcStats = gcEvents?.length ? aggregateGcStats(gcEvents) : void 0;
|
|
836
|
+
const attach = (key, value) => {
|
|
837
|
+
if (value) for (const r of results) r[key] = value;
|
|
838
|
+
};
|
|
839
|
+
attach("gcStats", gcStats);
|
|
840
|
+
attach("heapProfile", heapProfile);
|
|
841
|
+
attach("timeProfile", timeProfile);
|
|
842
|
+
attach("coverage", coverage);
|
|
843
|
+
}
|
|
844
|
+
/** Resolve WorkerScript path for dev (.ts) or dist (.mjs) */
|
|
845
|
+
function resolveWorkerPath() {
|
|
846
|
+
const dir = import.meta.dirname;
|
|
847
|
+
const tsPath = path.join(dir, "WorkerScript.ts");
|
|
848
|
+
if (existsSync(tsPath)) return tsPath;
|
|
849
|
+
return path.join(dir, "runners", "WorkerScript.mjs");
|
|
850
|
+
}
|
|
851
|
+
//#endregion
|
|
852
|
+
//#region src/matrix/MatrixDirRunner.ts
|
|
853
|
+
/** Run matrix using variant files from a directory, each in a worker process */
|
|
854
|
+
async function runMatrixWithDir(matrix, options) {
|
|
855
|
+
const allVariantIds = await discoverVariants(matrix.variantDir);
|
|
856
|
+
if (allVariantIds.length === 0) throw new Error(`No variants found in ${matrix.variantDir}`);
|
|
857
|
+
const variants = await runDirVariants(options.filteredVariants ?? allVariantIds, await createDirContext(matrix, options));
|
|
858
|
+
return {
|
|
859
|
+
name: matrix.name,
|
|
860
|
+
variants
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
/** Create context for directory-based matrix execution */
|
|
864
|
+
async function createDirContext(matrix, options) {
|
|
865
|
+
const baselineIds = matrix.baselineDir ? await discoverVariants(matrix.baselineDir) : [];
|
|
866
|
+
const { casesModule, caseIds } = await resolveCases(matrix, options);
|
|
867
|
+
const runnerOpts = buildRunnerOptions(options);
|
|
868
|
+
const { batches = 1, warmupBatch = false, useWorker = true } = options;
|
|
869
|
+
return {
|
|
870
|
+
matrix,
|
|
871
|
+
casesModule,
|
|
872
|
+
baselineIds,
|
|
873
|
+
caseIds,
|
|
874
|
+
runnerOpts,
|
|
875
|
+
batches,
|
|
876
|
+
warmupBatch,
|
|
877
|
+
useWorker
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
/** Run all variants sequentially, collecting per-case results */
|
|
881
|
+
async function runDirVariants(variantIds, ctx) {
|
|
882
|
+
const variants = [];
|
|
883
|
+
for (const id of variantIds) {
|
|
884
|
+
const cases = await runDirVariantCases(id, ctx);
|
|
885
|
+
variants.push({
|
|
886
|
+
id,
|
|
887
|
+
cases
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
return variants;
|
|
891
|
+
}
|
|
892
|
+
/** Run all cases for a single variant */
|
|
893
|
+
async function runDirVariantCases(variantId, ctx) {
|
|
894
|
+
const { matrix, casesModule, caseIds, runnerOpts, batches } = ctx;
|
|
895
|
+
const cases = [];
|
|
896
|
+
for (const caseId of caseIds) {
|
|
897
|
+
const caseData = matrix.cases && !matrix.casesModule ? caseId : void 0;
|
|
898
|
+
const variantArgs = {
|
|
899
|
+
variantDir: matrix.variantDir,
|
|
900
|
+
variantId,
|
|
901
|
+
caseId,
|
|
902
|
+
caseData,
|
|
903
|
+
casesModule: matrix.casesModule,
|
|
904
|
+
runner: "timing",
|
|
905
|
+
options: runnerOpts,
|
|
906
|
+
useWorker: ctx.useWorker
|
|
907
|
+
};
|
|
908
|
+
const baselineArgs = matrix.baselineDir && ctx.baselineIds.includes(variantId) ? {
|
|
909
|
+
...variantArgs,
|
|
910
|
+
variantDir: matrix.baselineDir
|
|
911
|
+
} : void 0;
|
|
912
|
+
const { metadata } = await loadCaseData(casesModule, caseId);
|
|
913
|
+
const { measured, baseline } = batches > 1 ? await runCaseBatched(variantArgs, baselineArgs, ctx) : await runCaseSingle(variantArgs, baselineArgs);
|
|
914
|
+
const deltaPercent = baseline ? computeDeltaPercent(baseline, measured) : void 0;
|
|
915
|
+
cases.push({
|
|
916
|
+
caseId,
|
|
917
|
+
measured,
|
|
918
|
+
metadata,
|
|
919
|
+
baseline,
|
|
920
|
+
deltaPercent
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
return cases;
|
|
924
|
+
}
|
|
925
|
+
/** Run a batched measurement for a case, alternating current/baseline order. */
|
|
926
|
+
async function runCaseBatched(variantArgs, baselineArgs, ctx) {
|
|
927
|
+
const runCurrent = async () => (await runMatrixVariant(variantArgs))[0];
|
|
928
|
+
const { results, baseline } = await runBatched([runCurrent], baselineArgs ? async () => (await runMatrixVariant(baselineArgs))[0] : void 0, ctx.batches, ctx.warmupBatch);
|
|
929
|
+
return {
|
|
930
|
+
measured: results[0],
|
|
931
|
+
baseline
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
/** Run a single unbatched measurement for a case. */
|
|
935
|
+
async function runCaseSingle(variantArgs, baselineArgs) {
|
|
936
|
+
const [measured] = await runMatrixVariant(variantArgs);
|
|
937
|
+
return {
|
|
938
|
+
measured,
|
|
939
|
+
baseline: baselineArgs ? (await runMatrixVariant(baselineArgs))[0] : void 0
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
//#endregion
|
|
943
|
+
//#region src/matrix/MatrixInlineRunner.ts
|
|
944
|
+
/** Run matrix with in-memory variant functions (no worker isolation) */
|
|
945
|
+
async function runMatrixInline(matrix, options) {
|
|
946
|
+
if (matrix.baselineDir) throw new Error("BenchMatrix with inline 'variants' cannot use 'baselineDir'. Use 'variantDir' instead.");
|
|
947
|
+
const { casesModule, caseIds } = await resolveCases(matrix, options);
|
|
948
|
+
const runner = new TimingRunner();
|
|
949
|
+
const runnerOpts = buildRunnerOptions(options);
|
|
950
|
+
const allEntries = Object.entries(matrix.variants);
|
|
951
|
+
const { filteredVariants } = options;
|
|
952
|
+
const variantEntries = filteredVariants ? allEntries.filter(([id]) => filteredVariants.includes(id)) : allEntries;
|
|
953
|
+
const variants = [];
|
|
954
|
+
for (const [variantId, variant] of variantEntries) {
|
|
955
|
+
const cases = [];
|
|
956
|
+
for (const caseId of caseIds) {
|
|
957
|
+
const loaded = await loadCaseData(casesModule, caseId);
|
|
958
|
+
const spec = {
|
|
959
|
+
name: variantId,
|
|
960
|
+
fn: await prepareBenchFn(variant, casesModule || matrix.cases ? loaded.data : void 0)
|
|
961
|
+
};
|
|
962
|
+
const [measured] = await runner.runBench(spec, runnerOpts);
|
|
963
|
+
cases.push({
|
|
964
|
+
caseId,
|
|
965
|
+
measured,
|
|
966
|
+
metadata: loaded.metadata
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
variants.push({
|
|
970
|
+
id: variantId,
|
|
971
|
+
cases
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
name: matrix.name,
|
|
976
|
+
variants
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
//#endregion
|
|
980
|
+
//#region src/matrix/BenchMatrix.ts
|
|
981
|
+
/** Run a BenchMatrix with inline variants or variantDir */
|
|
982
|
+
async function runMatrix(matrix, options = {}) {
|
|
983
|
+
if (matrix.baselineDir && matrix.baselineVariant) throw new Error("BenchMatrix cannot have both 'baselineDir' and 'baselineVariant'");
|
|
984
|
+
if (!matrix.variantDir && !matrix.variants) throw new Error("BenchMatrix requires either 'variants' or 'variantDir'");
|
|
985
|
+
const effectiveOptions = {
|
|
986
|
+
...matrix.defaults,
|
|
987
|
+
...options
|
|
988
|
+
};
|
|
989
|
+
const result = matrix.variantDir ? await runMatrixWithDir(matrix, effectiveOptions) : await runMatrixInline(matrix, effectiveOptions);
|
|
990
|
+
if (matrix.baselineVariant) applyBaselineVariant(result.variants, matrix.baselineVariant);
|
|
991
|
+
return result;
|
|
992
|
+
}
|
|
993
|
+
/** Prepare a benchmark function from a variant, calling setup if stateful. */
|
|
994
|
+
async function prepareBenchFn(variant, data) {
|
|
995
|
+
if (isStatefulVariant(variant)) {
|
|
996
|
+
const state = await variant.setup(data);
|
|
997
|
+
return () => variant.run(state);
|
|
998
|
+
}
|
|
999
|
+
return () => variant(data);
|
|
1000
|
+
}
|
|
1001
|
+
/** Type guard for StatefulVariant */
|
|
1002
|
+
function isStatefulVariant(v) {
|
|
1003
|
+
return typeof v === "object" && "setup" in v && "run" in v;
|
|
1004
|
+
}
|
|
1005
|
+
/** Apply baselineVariant comparison - one variant is the reference for all others */
|
|
1006
|
+
function applyBaselineVariant(variants, baselineVariantId) {
|
|
1007
|
+
const baselineVariant = variants.find((v) => v.id === baselineVariantId);
|
|
1008
|
+
if (!baselineVariant) return;
|
|
1009
|
+
const baselineByCase = new Map(baselineVariant.cases.map((c) => [c.caseId, c.measured]));
|
|
1010
|
+
for (const variant of variants) {
|
|
1011
|
+
if (variant.id === baselineVariantId) continue;
|
|
1012
|
+
for (const cr of variant.cases) {
|
|
1013
|
+
const base = baselineByCase.get(cr.caseId);
|
|
1014
|
+
if (base) {
|
|
1015
|
+
cr.baseline = base;
|
|
1016
|
+
cr.deltaPercent = computeDeltaPercent(base, cr.measured);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/** Load cases module and resolve filtered case IDs */
|
|
1022
|
+
async function resolveCases(matrix, options) {
|
|
1023
|
+
const casesModule = matrix.casesModule ? await loadCasesModule(matrix.casesModule) : void 0;
|
|
1024
|
+
const allCaseIds = casesModule?.cases ?? matrix.cases ?? ["default"];
|
|
1025
|
+
return {
|
|
1026
|
+
casesModule,
|
|
1027
|
+
caseIds: options.filteredCases ?? allCaseIds
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
/** Map matrix options to runner options, applying defaults for maxTime and warmup */
|
|
1031
|
+
function buildRunnerOptions(opts) {
|
|
1032
|
+
const { filteredCases, filteredVariants, useWorker, batches, warmupBatch, ...base } = opts;
|
|
1033
|
+
const { iterations, maxTime, warmup, ...rest } = base;
|
|
1034
|
+
return {
|
|
1035
|
+
maxIterations: iterations,
|
|
1036
|
+
maxTime: maxTime ?? (iterations ? void 0 : 1e3),
|
|
1037
|
+
warmup: warmup ?? 0,
|
|
1038
|
+
...rest
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
/** Compute percentage change of current vs baseline mean */
|
|
1042
|
+
function computeDeltaPercent(base, cur) {
|
|
1043
|
+
const avg = average(base.samples);
|
|
1044
|
+
if (avg === 0) return 0;
|
|
1045
|
+
return (average(cur.samples) - avg) / avg * 100;
|
|
1046
|
+
}
|
|
1047
|
+
//#endregion
|
|
1048
|
+
export { getPerfNow as a, resolveVariantFn as c, mergeGcStats as d, runBatched as f, loadCasesModule as h, getElapsed as i, discoverVariants as l, loadCaseData as m, runMatrix as n, createBenchRunner as o, computeStats as p, runBenchmark as r, importBenchFn as s, isStatefulVariant as t, aggregateGcStats as u };
|
|
1049
|
+
|
|
1050
|
+
//# sourceMappingURL=BenchMatrix-BZVrBB_h.mjs.map
|