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.
Files changed (253) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +99 -294
  3. package/bin/benchforge +1 -2
  4. package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
  5. package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
  6. package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
  7. package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
  8. package/dist/{BenchRunner-BzyUfiyB.d.mts → BenchRunner-DglX1NOn.d.mts} +119 -66
  9. package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
  10. package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
  11. package/dist/Formatters-BWj3d4sv.mjs +95 -0
  12. package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
  13. package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
  14. package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
  15. package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
  16. package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
  17. package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
  18. package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
  19. package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
  20. package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
  21. package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
  22. package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
  23. package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
  24. package/dist/bin/benchforge.mjs +4 -5
  25. package/dist/bin/benchforge.mjs.map +1 -1
  26. package/dist/index.d.mts +711 -558
  27. package/dist/index.mjs +98 -3
  28. package/dist/index.mjs.map +1 -0
  29. package/dist/runners/WorkerScript.d.mts +12 -4
  30. package/dist/runners/WorkerScript.mjs +77 -105
  31. package/dist/runners/WorkerScript.mjs.map +1 -1
  32. package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
  33. package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
  34. package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
  35. package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
  36. package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
  37. package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
  38. package/dist/viewer/index.html +19 -0
  39. package/dist/viewer/speedscope/LICENSE +21 -0
  40. package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
  41. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
  42. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
  43. package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
  44. package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
  45. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
  46. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
  47. package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
  48. package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
  49. package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
  50. package/dist/viewer/speedscope/file-format-schema.json +274 -0
  51. package/dist/viewer/speedscope/index.html +19 -0
  52. package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
  53. package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
  54. package/dist/viewer/speedscope/release.txt +3 -0
  55. package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
  56. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
  57. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
  58. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
  59. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
  60. package/package.json +52 -27
  61. package/src/bin/benchforge.ts +2 -2
  62. package/src/cli/AnalyzeArchive.ts +232 -0
  63. package/src/cli/BrowserBench.ts +322 -0
  64. package/src/cli/CliArgs.ts +164 -51
  65. package/src/cli/CliExport.ts +179 -0
  66. package/src/cli/CliOptions.ts +147 -0
  67. package/src/cli/CliReport.ts +197 -0
  68. package/src/cli/FilterBenchmarks.ts +18 -30
  69. package/src/cli/RunBenchCLI.ts +132 -866
  70. package/src/cli/SuiteRunner.ts +160 -0
  71. package/src/cli/ViewerServer.ts +282 -0
  72. package/src/export/AllocExport.ts +121 -0
  73. package/src/export/ArchiveExport.ts +146 -0
  74. package/src/export/ArchiveFormat.ts +50 -0
  75. package/src/export/CoverageExport.ts +148 -0
  76. package/src/export/EditorUri.ts +10 -0
  77. package/src/export/PerfettoExport.ts +64 -99
  78. package/src/export/SpeedscopeTypes.ts +98 -0
  79. package/src/export/TimeExport.ts +115 -0
  80. package/src/index.ts +86 -67
  81. package/src/matrix/BenchMatrix.ts +230 -0
  82. package/src/matrix/CaseLoader.ts +8 -6
  83. package/src/matrix/MatrixDirRunner.ts +153 -0
  84. package/src/matrix/MatrixFilter.ts +49 -47
  85. package/src/matrix/MatrixInlineRunner.ts +50 -0
  86. package/src/matrix/MatrixReport.ts +90 -250
  87. package/src/matrix/VariantLoader.ts +5 -5
  88. package/src/profiling/browser/BenchLoop.ts +51 -0
  89. package/src/profiling/browser/BrowserCDP.ts +133 -0
  90. package/src/profiling/browser/BrowserGcStats.ts +33 -0
  91. package/src/profiling/browser/BrowserProfiler.ts +160 -0
  92. package/src/profiling/browser/CdpClient.ts +82 -0
  93. package/src/profiling/browser/CdpPage.ts +138 -0
  94. package/src/profiling/browser/ChromeLauncher.ts +158 -0
  95. package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
  96. package/src/profiling/browser/PageLoadMode.ts +61 -0
  97. package/src/profiling/node/CoverageSampler.ts +27 -0
  98. package/src/profiling/node/CoverageTypes.ts +23 -0
  99. package/src/profiling/node/HeapSampleReport.ts +261 -0
  100. package/src/{heap-sample → profiling/node}/HeapSampler.ts +1 -2
  101. package/src/{heap-sample → profiling/node}/ResolvedProfile.ts +18 -9
  102. package/src/profiling/node/TimeSampler.ts +57 -0
  103. package/src/report/BenchmarkReport.ts +146 -0
  104. package/src/report/Colors.ts +9 -0
  105. package/src/report/Formatters.ts +110 -0
  106. package/src/report/GcSections.ts +151 -0
  107. package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
  108. package/src/report/HtmlReport.ts +223 -0
  109. package/src/report/ParseStats.ts +73 -0
  110. package/src/report/StandardSections.ts +147 -0
  111. package/src/report/ViewerSections.ts +286 -0
  112. package/src/report/text/TableReport.ts +253 -0
  113. package/src/report/text/TextReport.ts +123 -0
  114. package/src/runners/AdaptiveWrapper.ts +116 -236
  115. package/src/runners/BenchRunner.ts +20 -15
  116. package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
  117. package/src/runners/CreateRunner.ts +5 -7
  118. package/src/runners/GcStats.ts +47 -50
  119. package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
  120. package/src/runners/MergeBatches.ts +123 -0
  121. package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
  122. package/src/runners/RunnerOrchestrator.ts +127 -243
  123. package/src/runners/RunnerUtils.ts +75 -1
  124. package/src/runners/SampleStats.ts +100 -0
  125. package/src/runners/TimingRunner.ts +244 -0
  126. package/src/runners/TimingUtils.ts +3 -2
  127. package/src/runners/WorkerScript.ts +135 -151
  128. package/src/stats/BootstrapDifference.ts +282 -0
  129. package/src/{PermutationTest.ts → stats/PermutationTest.ts} +8 -17
  130. package/src/stats/StatisticalUtils.ts +445 -0
  131. package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
  132. package/src/test/AdaptiveRunner.test.ts +39 -41
  133. package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
  134. package/src/test/AdaptiveStatistics.integration.ts +2 -2
  135. package/src/{tests → test}/BenchMatrix.test.ts +19 -16
  136. package/src/test/BenchmarkReport.test.ts +63 -13
  137. package/src/test/BrowserBench.e2e.test.ts +186 -17
  138. package/src/test/BrowserBench.test.ts +10 -5
  139. package/src/test/BuildTimeSection.test.ts +130 -0
  140. package/src/test/CapSamples.test.ts +82 -0
  141. package/src/test/CoverageExport.test.ts +115 -0
  142. package/src/test/CoverageSampler.test.ts +33 -0
  143. package/src/test/HeapAttribution.test.ts +14 -14
  144. package/src/{tests → test}/MatrixFilter.test.ts +1 -1
  145. package/src/{tests → test}/MatrixReport.test.ts +1 -1
  146. package/src/test/PermutationTest.test.ts +1 -1
  147. package/src/{tests → test}/RealDataValidation.test.ts +6 -6
  148. package/src/test/RunBenchCLI.test.ts +39 -38
  149. package/src/test/RunnerOrchestrator.test.ts +12 -12
  150. package/src/test/StatisticalUtils.test.ts +48 -12
  151. package/src/{table-util/test → test}/TableReport.test.ts +2 -2
  152. package/src/test/TestUtils.ts +12 -7
  153. package/src/test/TimeExport.test.ts +139 -0
  154. package/src/test/TimeSampler.test.ts +37 -0
  155. package/src/test/ViewerLive.e2e.test.ts +159 -0
  156. package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
  157. package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
  158. package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
  159. package/src/test/fixtures/cases/asyncCases.ts +9 -0
  160. package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
  161. package/src/test/fixtures/cases/variants/product.ts +2 -0
  162. package/src/test/fixtures/cases/variants/sum.ts +2 -0
  163. package/src/test/fixtures/discover/fast.ts +1 -0
  164. package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
  165. package/src/test/fixtures/invalid/bad.ts +1 -0
  166. package/src/test/fixtures/loader/fast.ts +1 -0
  167. package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
  168. package/src/test/fixtures/loader/stateful.ts +2 -0
  169. package/src/test/fixtures/stateful/stateful.ts +2 -0
  170. package/src/test/fixtures/variants/extra.ts +1 -0
  171. package/src/test/fixtures/variants/impl.ts +1 -0
  172. package/src/test/fixtures/worker/fast.ts +1 -0
  173. package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
  174. package/src/viewer/DateFormat.ts +30 -0
  175. package/src/viewer/Helpers.ts +23 -0
  176. package/src/viewer/LineData.ts +120 -0
  177. package/src/viewer/Providers.ts +191 -0
  178. package/src/viewer/ReportData.ts +123 -0
  179. package/src/viewer/State.ts +49 -0
  180. package/src/viewer/Theme.ts +15 -0
  181. package/src/viewer/components/App.tsx +73 -0
  182. package/src/viewer/components/DropZone.tsx +71 -0
  183. package/src/viewer/components/LazyPlot.ts +33 -0
  184. package/src/viewer/components/SamplesPanel.tsx +214 -0
  185. package/src/viewer/components/Shell.tsx +26 -0
  186. package/src/viewer/components/SourcePanel.tsx +216 -0
  187. package/src/viewer/components/SummaryPanel.tsx +332 -0
  188. package/src/viewer/components/TabBar.tsx +131 -0
  189. package/src/viewer/components/TabContent.tsx +46 -0
  190. package/src/viewer/components/ThemeToggle.tsx +50 -0
  191. package/src/viewer/index.html +20 -0
  192. package/src/viewer/main.tsx +4 -0
  193. package/src/viewer/plots/CIPlot.ts +313 -0
  194. package/src/{html/browser → viewer/plots}/HistogramKde.ts +33 -38
  195. package/src/viewer/plots/LegendUtils.ts +134 -0
  196. package/src/viewer/plots/PlotTypes.ts +85 -0
  197. package/src/viewer/plots/RenderPlots.ts +230 -0
  198. package/src/viewer/plots/SampleTimeSeries.ts +306 -0
  199. package/src/viewer/plots/SvgHelpers.ts +136 -0
  200. package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
  201. package/src/viewer/report.css +427 -0
  202. package/src/viewer/shell.css +357 -0
  203. package/src/viewer/tsconfig.json +11 -0
  204. package/dist/BrowserHeapSampler-B6asLKWQ.mjs +0 -202
  205. package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +0 -1
  206. package/dist/GcStats-wX7Xyblu.mjs +0 -77
  207. package/dist/GcStats-wX7Xyblu.mjs.map +0 -1
  208. package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
  209. package/dist/TimingUtils-DwOwkc8G.mjs +0 -597
  210. package/dist/TimingUtils-DwOwkc8G.mjs.map +0 -1
  211. package/dist/browser/index.js +0 -914
  212. package/dist/src-B-DDaCa9.mjs +0 -3108
  213. package/dist/src-B-DDaCa9.mjs.map +0 -1
  214. package/src/BenchMatrix.ts +0 -380
  215. package/src/BenchmarkReport.ts +0 -161
  216. package/src/HtmlDataPrep.ts +0 -148
  217. package/src/StandardSections.ts +0 -261
  218. package/src/StatisticalUtils.ts +0 -175
  219. package/src/TypeUtil.ts +0 -8
  220. package/src/browser/BrowserGcStats.ts +0 -44
  221. package/src/browser/BrowserHeapSampler.ts +0 -271
  222. package/src/export/JsonExport.ts +0 -103
  223. package/src/export/JsonFormat.ts +0 -91
  224. package/src/export/SpeedscopeExport.ts +0 -202
  225. package/src/heap-sample/HeapSampleReport.ts +0 -269
  226. package/src/html/HtmlReport.ts +0 -131
  227. package/src/html/HtmlTemplate.ts +0 -284
  228. package/src/html/Types.ts +0 -88
  229. package/src/html/browser/CIPlot.ts +0 -287
  230. package/src/html/browser/LegendUtils.ts +0 -163
  231. package/src/html/browser/RenderPlots.ts +0 -263
  232. package/src/html/browser/SampleTimeSeries.ts +0 -389
  233. package/src/html/browser/Types.ts +0 -96
  234. package/src/html/browser/index.ts +0 -1
  235. package/src/html/index.ts +0 -17
  236. package/src/runners/BasicRunner.ts +0 -364
  237. package/src/table-util/ConvergenceFormatters.ts +0 -19
  238. package/src/table-util/Formatters.ts +0 -157
  239. package/src/table-util/README.md +0 -70
  240. package/src/table-util/TableReport.ts +0 -293
  241. package/src/tests/fixtures/cases/asyncCases.ts +0 -7
  242. package/src/tests/fixtures/cases/variants/product.ts +0 -2
  243. package/src/tests/fixtures/cases/variants/sum.ts +0 -2
  244. package/src/tests/fixtures/discover/fast.ts +0 -1
  245. package/src/tests/fixtures/invalid/bad.ts +0 -1
  246. package/src/tests/fixtures/loader/fast.ts +0 -1
  247. package/src/tests/fixtures/loader/stateful.ts +0 -2
  248. package/src/tests/fixtures/stateful/stateful.ts +0 -2
  249. package/src/tests/fixtures/variants/extra.ts +0 -1
  250. package/src/tests/fixtures/variants/impl.ts +0 -1
  251. package/src/tests/fixtures/worker/fast.ts +0 -1
  252. /package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
  253. /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