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,244 @@
1
+ import { getHeapStatistics } from "node:v8";
2
+ import type { BenchmarkSpec } from "./BenchmarkSpec.ts";
3
+ import type { BenchRunner, RunnerOptions } from "./BenchRunner.ts";
4
+ import { executeBenchmark } from "./BenchRunner.ts";
5
+ import type {
6
+ MeasuredResults,
7
+ OptStatusInfo,
8
+ PausePoint,
9
+ } from "./MeasuredResults.ts";
10
+ import {
11
+ analyzeOptStatus,
12
+ computeStats,
13
+ createOptStatusGetter,
14
+ gcFunction,
15
+ } from "./SampleStats.ts";
16
+
17
+ type CollectParams<T = unknown> = {
18
+ benchmark: BenchmarkSpec<T>;
19
+ maxTime: number;
20
+ maxIterations: number;
21
+ warmup: number;
22
+ params?: T;
23
+ skipWarmup?: boolean;
24
+ traceOpt?: boolean;
25
+ pauseWarmup?: number;
26
+ pauseFirst?: number;
27
+ pauseInterval?: number;
28
+ pauseDuration?: number;
29
+ };
30
+
31
+ type CollectResult = {
32
+ samples: number[];
33
+ warmupSamples: number[];
34
+ heapGrowth: number;
35
+ heapSamples: number[];
36
+ startTime: number;
37
+ optStatus?: OptStatusInfo;
38
+ optSamples?: number[];
39
+ pausePoints: PausePoint[];
40
+ };
41
+
42
+ type SampleArrays = {
43
+ samples: number[];
44
+ heapSamples: number[];
45
+ optStatuses: number[];
46
+ pausePoints: PausePoint[];
47
+ };
48
+
49
+ const defaultCollectOptions = {
50
+ maxTime: 5000,
51
+ maxIterations: 1000000,
52
+ warmup: 0,
53
+ traceOpt: false,
54
+ pauseWarmup: 0,
55
+ };
56
+
57
+ /**
58
+ * Timing-based runner that collects samples within time/iteration limits.
59
+ * Handles warmup, heap tracking, V8 optimization tracing, and periodic pauses.
60
+ */
61
+ export class TimingRunner implements BenchRunner {
62
+ async runBench<T = unknown>(
63
+ benchmark: BenchmarkSpec<T>,
64
+ options: RunnerOptions,
65
+ params?: T,
66
+ ): Promise<MeasuredResults[]> {
67
+ const opts = { ...defaultCollectOptions, ...(options as any) };
68
+ const collected = await collectSamples({ benchmark, params, ...opts });
69
+ return [buildMeasuredResults(benchmark.name, collected)];
70
+ }
71
+ }
72
+
73
+ /** Collect timing samples with warmup, heap tracking, and optional V8 opt tracing. */
74
+ async function collectSamples<T>(
75
+ config: CollectParams<T>,
76
+ ): Promise<CollectResult> {
77
+ if (!config.maxIterations && !config.maxTime) {
78
+ throw new Error(`At least one of maxIterations or maxTime must be set`);
79
+ }
80
+ const warmupSamples = config.skipWarmup ? [] : await runWarmup(config);
81
+ const heapBefore = process.memoryUsage().heapUsed;
82
+ const { samples, heapSamples, optStatuses, pausePoints, startTime } =
83
+ await runSampleLoop(config);
84
+ if (samples.length === 0)
85
+ throw new Error(
86
+ `No samples collected for benchmark: ${config.benchmark.name}`,
87
+ );
88
+ const heapAfter = process.memoryUsage().heapUsed;
89
+ const heapGrowth =
90
+ Math.max(0, heapAfter - heapBefore) / 1024 / samples.length;
91
+ const optStatus = config.traceOpt
92
+ ? analyzeOptStatus(samples, optStatuses)
93
+ : undefined;
94
+ const optSamples =
95
+ config.traceOpt && optStatuses.length > 0 ? optStatuses : undefined;
96
+ return {
97
+ samples,
98
+ warmupSamples,
99
+ heapGrowth,
100
+ heapSamples,
101
+ startTime,
102
+ optStatus,
103
+ optSamples,
104
+ pausePoints,
105
+ };
106
+ }
107
+
108
+ /** Assemble CollectResult into a MeasuredResults record. */
109
+ function buildMeasuredResults(
110
+ name: string,
111
+ collected: CollectResult,
112
+ ): MeasuredResults {
113
+ const { samples, warmupSamples, heapSamples } = collected;
114
+ const { optStatus, optSamples, pausePoints, heapGrowth, startTime } =
115
+ collected;
116
+ const time = computeStats(samples);
117
+ const heapSize = { avg: heapGrowth, min: heapGrowth, max: heapGrowth };
118
+ return {
119
+ name,
120
+ samples,
121
+ warmupSamples,
122
+ heapSamples,
123
+ time,
124
+ heapSize,
125
+ startTime,
126
+ optStatus,
127
+ optSamples,
128
+ pausePoints,
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Run warmup iterations with gc + settle time for V8 optimization. Returns warmup timings.
134
+ *
135
+ * V8 has 4 compilation tiers: Ignition (interpreter) ==> Sparkplug (baseline) ==>
136
+ * Maglev (mid-tier optimizer) ==> TurboFan (full optimizer). Tiering thresholds:
137
+ * - Ignition ==> Sparkplug: 8 invocations
138
+ * - Sparkplug ==> Maglev: 500 invocations
139
+ * - Maglev ==> TurboFan: 6000 invocations
140
+ *
141
+ * Optimization compilation happens on background threads and requires idle time
142
+ * on the main thread to complete. Without sufficient warmup + settle time,
143
+ * benchmarks exhibit bimodal timing: slow Sparkplug samples (~30% slower) mixed
144
+ * with fast optimized samples.
145
+ *
146
+ * The warmup iterations trigger the optimization decision, then settle time
147
+ * provides idle time for background compilation to finish before measurement.
148
+ *
149
+ * @see https://v8.dev/blog/sparkplug
150
+ * @see https://v8.dev/blog/maglev
151
+ * @see https://v8.dev/blog/background-compilation
152
+ */
153
+ async function runWarmup<T>(config: CollectParams<T>): Promise<number[]> {
154
+ const gc = gcFunction();
155
+ const samples = new Array<number>(config.warmup);
156
+ for (let i = 0; i < config.warmup; i++) {
157
+ const start = performance.now();
158
+ executeBenchmark(config.benchmark, config.params);
159
+ samples[i] = performance.now() - start;
160
+ }
161
+ gc();
162
+ if (config.pauseWarmup) {
163
+ await new Promise(r => setTimeout(r, config.pauseWarmup));
164
+ gc();
165
+ }
166
+ return samples;
167
+ }
168
+
169
+ /** Collect timing samples with optional periodic pauses for V8 background compilation to complete. */
170
+ async function runSampleLoop<T>(
171
+ config: CollectParams<T>,
172
+ ): Promise<SampleArrays & { startTime: number }> {
173
+ const { maxTime, maxIterations, pauseFirst } = config;
174
+ const { pauseInterval = 0, pauseDuration = 100 } = config;
175
+ const getOptStatus = config.traceOpt ? createOptStatusGetter() : undefined;
176
+ const trackOpt = !!getOptStatus;
177
+ const estimated = maxIterations || Math.ceil(maxTime / 0.1);
178
+ const arrays = createSampleArrays(estimated, trackOpt);
179
+
180
+ let count = 0;
181
+ let elapsed = 0;
182
+ let totalPauseTime = 0;
183
+ const startTime = Number(process.hrtime.bigint() / 1000n);
184
+ const loopStart = performance.now();
185
+
186
+ while (
187
+ (!maxIterations || count < maxIterations) &&
188
+ (!maxTime || elapsed < maxTime)
189
+ ) {
190
+ const start = performance.now();
191
+ executeBenchmark(config.benchmark, config.params);
192
+ const end = performance.now();
193
+ arrays.samples[count] = end - start;
194
+ arrays.heapSamples[count] = getHeapStatistics().used_heap_size;
195
+ if (getOptStatus)
196
+ arrays.optStatuses[count] = getOptStatus(config.benchmark.fn);
197
+ count++;
198
+
199
+ if (shouldPause(count, pauseFirst, pauseInterval)) {
200
+ const sampleIndex = count - 1;
201
+ arrays.pausePoints.push({ sampleIndex, durationMs: pauseDuration });
202
+ const pauseStart = performance.now();
203
+ await new Promise(r => setTimeout(r, pauseDuration));
204
+ totalPauseTime += performance.now() - pauseStart;
205
+ }
206
+ elapsed = performance.now() - loopStart - totalPauseTime;
207
+ }
208
+
209
+ trimArrays(arrays, count, trackOpt);
210
+ return { ...arrays, startTime };
211
+ }
212
+
213
+ /** Pre-allocate sample arrays to reduce GC pressure during measurement. */
214
+ function createSampleArrays(n: number, trackOpt: boolean): SampleArrays {
215
+ const arr = () => new Array<number>(n);
216
+ return {
217
+ samples: arr(),
218
+ heapSamples: arr(),
219
+ optStatuses: trackOpt ? arr() : [],
220
+ pausePoints: [],
221
+ };
222
+ }
223
+
224
+ /** @return true if this iteration should pause for V8 background compilation. */
225
+ function shouldPause(
226
+ iter: number,
227
+ first: number | undefined,
228
+ interval: number,
229
+ ): boolean {
230
+ if (first !== undefined && iter === first) return true;
231
+ if (interval <= 0) return false;
232
+ if (first === undefined) return iter % interval === 0;
233
+ return (iter - first) % interval === 0;
234
+ }
235
+
236
+ /** Trim pre-allocated arrays to the actual sample count. */
237
+ function trimArrays(
238
+ arrays: SampleArrays,
239
+ count: number,
240
+ trackOpt: boolean,
241
+ ): void {
242
+ arrays.samples.length = arrays.heapSamples.length = count;
243
+ if (trackOpt) arrays.optStatuses.length = count;
244
+ }
@@ -1,11 +1,12 @@
1
+ /** Toggle for worker process timing logs (manual, not exposed as CLI flag) */
1
2
  export const debugWorkerTiming = false;
2
3
 
3
- /** Get current time or 0 if debugging disabled */
4
+ /** Current time in ms, or 0 when debug timing is off (zero-cost no-op) */
4
5
  export function getPerfNow(): number {
5
6
  return debugWorkerTiming ? performance.now() : 0;
6
7
  }
7
8
 
8
- /** Calculate elapsed milliseconds between marks */
9
+ /** Elapsed ms between marks, or 0 when debug timing is off */
9
10
  export function getElapsed(startMark: number, endMark?: number): number {
10
11
  if (!debugWorkerTiming) return 0;
11
12
  const end = endMark ?? performance.now();
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import type { BenchmarkFunction, BenchmarkSpec } from "../Benchmark.ts";
3
- import type { HeapProfile } from "../heap-sample/HeapSampler.ts";
4
- import type { MeasuredResults } from "../MeasuredResults.ts";
5
- import { variantModuleUrl } from "../matrix/VariantLoader.ts";
2
+ import type { Session } from "node:inspector/promises";
3
+ import type { CoverageData } from "../profiling/node/CoverageTypes.ts";
4
+ import type { HeapProfile } from "../profiling/node/HeapSampler.ts";
5
+ import type { TimeProfile } from "../profiling/node/TimeSampler.ts";
6
+ import type { BenchmarkFunction, BenchmarkSpec } from "./BenchmarkSpec.ts";
7
+ import type { BenchRunner, RunnerOptions } from "./BenchRunner.ts";
8
+ import type { KnownRunner } from "./CreateRunner.ts";
9
+ import type { MeasuredResults } from "./MeasuredResults.ts";
6
10
  import {
7
- type AdaptiveOptions,
8
- createAdaptiveWrapper,
9
- } from "./AdaptiveWrapper.ts";
10
- import type { RunnerOptions } from "./BenchRunner.ts";
11
- import { createRunner, type KnownRunner } from "./CreateRunner.ts";
12
- import { debugWorkerTiming, getElapsed, getPerfNow } from "./TimingUtils.ts"; // 5 minutes
11
+ createBenchRunner,
12
+ importBenchFn,
13
+ resolveVariantFn,
14
+ } from "./RunnerUtils.ts";
15
+ import { debugWorkerTiming, getElapsed, getPerfNow } from "./TimingUtils.ts";
13
16
 
14
17
  /** Message sent to worker process to start a benchmark run. */
15
18
  export interface RunMessage {
@@ -17,27 +20,35 @@ export interface RunMessage {
17
20
  spec: BenchmarkSpec;
18
21
  runnerName: KnownRunner;
19
22
  options: RunnerOptions;
20
- fnCode?: string; // Made optional - either fnCode or modulePath is required
21
- modulePath?: string; // Path to module for dynamic import
22
- exportName?: string; // Export name from module
23
- setupExportName?: string; // Setup function export name - called once, result passed to fn
23
+ /** Serialized function body (mutually exclusive with modulePath) */
24
+ fnCode?: string;
25
+ modulePath?: string;
26
+ /** Defaults to default export */
27
+ exportName?: string;
28
+ /** Called once before benchmarking; result passed as params to fn */
29
+ setupExportName?: string;
24
30
  params?: unknown;
25
- // Variant directory mode (BenchMatrix)
26
- variantDir?: string; // Directory URL containing variant .ts files
27
- variantId?: string; // Variant filename (without .ts)
28
- caseData?: unknown; // Data to pass to variant
29
- caseId?: string; // Case identifier
30
- casesModule?: string; // URL to cases module (exports cases[] and loadCase())
31
+
32
+ /** Directory URL containing variant .ts files (BenchMatrix mode) */
33
+ variantDir?: string;
34
+ /** Variant filename without .ts extension */
35
+ variantId?: string;
36
+ caseData?: unknown;
37
+ caseId?: string;
38
+ /** Module URL exporting cases[] and loadCase() */
39
+ casesModule?: string;
31
40
  }
32
41
 
33
- /** Message returned from worker process with benchmark results. */
42
+ /** Benchmark results returned from worker process. */
34
43
  export interface ResultMessage {
35
44
  type: "result";
36
45
  results: MeasuredResults[];
37
46
  heapProfile?: HeapProfile;
47
+ timeProfile?: TimeProfile;
48
+ coverage?: CoverageData;
38
49
  }
39
50
 
40
- /** Message returned from worker process when benchmark fails. */
51
+ /** Error returned from worker process when benchmark fails. */
41
52
  export interface ErrorMessage {
42
53
  type: "error";
43
54
  error: string;
@@ -51,20 +62,25 @@ interface BenchmarkImportResult {
51
62
  params: unknown;
52
63
  }
53
64
 
65
+ /** Profiling state accumulated during worker benchmark execution */
66
+ interface ProfilingState {
67
+ heapProfile?: HeapProfile;
68
+ timeProfile?: TimeProfile;
69
+ coverage?: CoverageData;
70
+ /** Shared session so TimeSampler doesn't reset coverage counters */
71
+ profilerSession?: Session;
72
+ }
73
+
54
74
  const workerStartTime = getPerfNow();
55
75
  const maxLifetime = 5 * 60 * 1000;
56
76
 
57
- /** Log timing with consistent format */
58
77
  const logTiming = debugWorkerTiming ? _logTiming : () => {};
59
78
  function _logTiming(operation: string, duration?: number) {
60
- if (duration === undefined) {
61
- console.log(`[Worker] ${operation}`);
62
- } else {
63
- console.log(`[Worker] ${operation} ${duration.toFixed(1)}ms`);
64
- }
79
+ const suffix = duration !== undefined ? ` ${duration.toFixed(1)}ms` : "";
80
+ console.log(`[Worker] ${operation}${suffix}`);
65
81
  }
66
82
 
67
- /** Send message and exit with duration log */
83
+ /** Send IPC message to parent then exit the worker process */
68
84
  function sendAndExit(msg: ResultMessage | ErrorMessage, exitCode: number) {
69
85
  process.send!(msg, undefined, undefined, (err: Error | null): void => {
70
86
  if (err) {
@@ -85,7 +101,12 @@ async function resolveBenchmarkFn(
85
101
  return importVariantModule(message);
86
102
  }
87
103
  if (message.modulePath) {
88
- return importBenchmarkWithSetup(message);
104
+ const { modulePath, exportName, setupExportName, params } = message;
105
+ logTiming(
106
+ `Importing from ${modulePath}${exportName ? ` (${exportName})` : ""}`,
107
+ );
108
+ if (setupExportName) logTiming(`Calling setup: ${setupExportName}`);
109
+ return importBenchFn(modulePath, exportName, setupExportName, params);
89
110
  }
90
111
  return { fn: reconstructFunction(message.fnCode!), params: message.params };
91
112
  }
@@ -94,56 +115,16 @@ async function resolveBenchmarkFn(
94
115
  async function importVariantModule(
95
116
  message: RunMessage,
96
117
  ): Promise<BenchmarkImportResult> {
97
- const { variantDir, variantId, caseId, casesModule } = message;
98
- let { caseData } = message;
99
- const moduleUrl = variantModuleUrl(variantDir!, variantId!);
118
+ const { variantDir, variantId } = message;
100
119
  logTiming(`Importing variant ${variantId} from ${variantDir}`);
101
-
102
- if (casesModule && caseId) {
103
- caseData = (await loadCaseFromModule(casesModule, caseId)).data;
104
- }
105
-
106
- const module = await import(moduleUrl);
107
- const { setup, run } = module;
108
-
109
- if (typeof run !== "function") {
110
- throw new Error(`Variant '${variantId}' must export 'run' function`);
111
- }
112
-
113
- // Stateful variant: setup returns state, run receives state
114
- if (typeof setup === "function") {
115
- logTiming(`Calling setup for ${variantId}`);
116
- const state = await setup(caseData);
117
- return { fn: () => run(state), params: undefined };
118
- }
119
-
120
- // Stateless variant: run receives caseData directly
121
- return { fn: () => run(caseData), params: undefined };
122
- }
123
-
124
- /** Import benchmark function and optionally run setup */
125
- async function importBenchmarkWithSetup(
126
- message: RunMessage,
127
- ): Promise<BenchmarkImportResult> {
128
- const { modulePath, exportName, setupExportName, params } = message;
129
- logTiming(
130
- `Importing from ${modulePath}${exportName ? ` (${exportName})` : ""}`,
131
- );
132
- const module = await import(modulePath!);
133
-
134
- const fn = getModuleExport(module, exportName, modulePath!);
135
-
136
- if (setupExportName) {
137
- logTiming(`Calling setup: ${setupExportName}`);
138
- const setupFn = getModuleExport(module, setupExportName, modulePath!);
139
- const setupResult = await setupFn(params);
140
- return { fn, params: setupResult };
141
- }
142
-
143
- return { fn, params };
120
+ return resolveVariantFn({
121
+ ...message,
122
+ variantDir: variantDir!,
123
+ variantId: variantId!,
124
+ });
144
125
  }
145
126
 
146
- /** Reconstruct function from string code */
127
+ /** Eval serialized function body back into a callable */
147
128
  function reconstructFunction(fnCode: string): BenchmarkFunction {
148
129
  // biome-ignore lint/security/noGlobalEval: Necessary for worker process isolation, code is from trusted source
149
130
  const fn = eval(`(${fnCode})`); // eslint-disable-line no-eval
@@ -153,46 +134,75 @@ function reconstructFunction(fnCode: string): BenchmarkFunction {
153
134
  return fn;
154
135
  }
155
136
 
156
- /** Load case data from a cases module */
157
- async function loadCaseFromModule(
158
- casesModuleUrl: string,
159
- caseId: string,
160
- ): Promise<{ data: unknown; metadata?: Record<string, unknown> }> {
161
- logTiming(`Loading case '${caseId}' from ${casesModuleUrl}`);
162
- const module = await import(casesModuleUrl);
163
- if (typeof module.loadCase === "function") {
164
- return module.loadCase(caseId);
137
+ /** Run benchmark with optional heap, time, and coverage profiling */
138
+ async function runWithProfiling(
139
+ message: RunMessage,
140
+ runner: BenchRunner,
141
+ ): Promise<ResultMessage> {
142
+ const state: ProfilingState = {};
143
+ const runBench = buildProfilingChain(message, runner, state);
144
+
145
+ if (!message.options.callCounts) {
146
+ const results = await runBench();
147
+ return { type: "result", results, ...state };
165
148
  }
166
- return { data: caseId };
167
- }
168
149
 
169
- /** Get named or default export from module */
170
- function getModuleExport(
171
- module: any,
172
- exportName: string | undefined,
173
- modulePath: string,
174
- ): BenchmarkFunction {
175
- const fn = exportName ? module[exportName] : module.default || module;
176
- if (typeof fn !== "function") {
177
- const name = exportName || "default";
178
- throw new Error(`Export '${name}' from ${modulePath} is not a function`);
179
- }
180
- return fn;
150
+ const { withCoverageProfiling } = await import(
151
+ "../profiling/node/CoverageSampler.ts"
152
+ );
153
+ const r = await withCoverageProfiling(async session => {
154
+ state.profilerSession = session;
155
+ return runBench();
156
+ });
157
+ state.coverage = r.coverage;
158
+ return { type: "result", results: r.result, ...state };
181
159
  }
182
160
 
183
- /** Create error message from exception */
184
- function createErrorMessage(error: unknown): ErrorMessage {
185
- return {
186
- type: "error",
187
- error: error instanceof Error ? error.message : String(error),
188
- stack: error instanceof Error ? error.stack : undefined,
161
+ /** Build nested profiling wrappers: outer heap, inner time */
162
+ function buildProfilingChain(
163
+ message: RunMessage,
164
+ runner: BenchRunner,
165
+ state: ProfilingState,
166
+ ): () => Promise<MeasuredResults[]> {
167
+ const { alloc, profile, profileInterval, allocInterval, allocDepth } =
168
+ message.options;
169
+
170
+ const run = async () => {
171
+ const { fn, params } = await resolveBenchmarkFn(message);
172
+ return runner.runBench({ ...message.spec, fn }, message.options, params);
189
173
  };
174
+
175
+ const runMaybeWithTime = profile
176
+ ? async () => {
177
+ const { withTimeProfiling } = await import(
178
+ "../profiling/node/TimeSampler.ts"
179
+ );
180
+ const opts = {
181
+ interval: profileInterval,
182
+ session: state.profilerSession,
183
+ };
184
+ const r = await withTimeProfiling(opts, run);
185
+ state.timeProfile = r.profile;
186
+ return r.result;
187
+ }
188
+ : run;
189
+
190
+ return alloc
191
+ ? async () => {
192
+ const { withHeapSampling } = await import(
193
+ "../profiling/node/HeapSampler.ts"
194
+ );
195
+ const heapOpts = {
196
+ samplingInterval: allocInterval,
197
+ stackDepth: allocDepth,
198
+ };
199
+ const r = await withHeapSampling(heapOpts, runMaybeWithTime);
200
+ state.heapProfile = r.profile;
201
+ return r.result;
202
+ }
203
+ : runMaybeWithTime;
190
204
  }
191
205
 
192
- /**
193
- * Worker process for isolated benchmark execution.
194
- * Uses eval() safely in isolated child process with trusted code.
195
- */
196
206
  process.on("message", async (message: RunMessage) => {
197
207
  if (message.type !== "run") return;
198
208
 
@@ -200,57 +210,31 @@ process.on("message", async (message: RunMessage) => {
200
210
 
201
211
  try {
202
212
  const start = getPerfNow();
203
- const baseRunner = await createRunner(message.runnerName);
204
-
205
- const runner = (message.options as any).adaptive
206
- ? createAdaptiveWrapper(baseRunner, message.options as AdaptiveOptions)
207
- : baseRunner;
208
-
213
+ const runner = await createBenchRunner(message.runnerName, message.options);
209
214
  logTiming("Runner created in", getElapsed(start));
210
215
 
211
216
  const benchStart = getPerfNow();
212
-
213
- // Run with heap sampling if enabled (covers module import + execution)
214
- if (message.options.heapSample) {
215
- const { withHeapSampling } = await import(
216
- "../heap-sample/HeapSampler.ts"
217
- );
218
- const heapOpts = {
219
- samplingInterval: message.options.heapInterval,
220
- stackDepth: message.options.heapDepth,
221
- };
222
- const { result: results, profile: heapProfile } = await withHeapSampling(
223
- heapOpts,
224
- async () => {
225
- const { fn, params } = await resolveBenchmarkFn(message);
226
- return runner.runBench(
227
- { ...message.spec, fn },
228
- message.options,
229
- params,
230
- );
231
- },
232
- );
233
- logTiming("Benchmark execution took", getElapsed(benchStart));
234
- sendAndExit({ type: "result", results, heapProfile }, 0);
235
- } else {
236
- const { fn, params } = await resolveBenchmarkFn(message);
237
- const results = await runner.runBench(
238
- { ...message.spec, fn },
239
- message.options,
240
- params,
241
- );
242
- logTiming("Benchmark execution took", getElapsed(benchStart));
243
- sendAndExit({ type: "result", results }, 0);
244
- }
217
+ const result = await runWithProfiling(message, runner);
218
+ logTiming("Benchmark execution took", getElapsed(benchStart));
219
+ sendAndExit(result, 0);
245
220
  } catch (error) {
246
- sendAndExit(createErrorMessage(error), 1);
221
+ const err = error instanceof Error ? error : undefined;
222
+ sendAndExit(
223
+ {
224
+ type: "error",
225
+ error: err?.message ?? String(error),
226
+ stack: err?.stack,
227
+ },
228
+ 1,
229
+ );
247
230
  }
248
231
  });
249
232
 
250
- // Exit after 5 minutes to prevent zombie processes
233
+ // Prevent zombie processes
251
234
  setTimeout(() => {
252
235
  console.error("WorkerScript: Maximum lifetime exceeded, exiting");
253
236
  process.exit(1);
254
237
  }, maxLifetime);
255
238
 
239
+ // Prevent stdin from keeping the worker process alive
256
240
  process.stdin.pause();