benchforge 0.1.9 → 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 -260
  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-DglX1NOn.d.mts +302 -0
  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 +731 -522
  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 +92 -120
  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 -26
  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 -48
  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 +138 -844
  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 +91 -126
  78. package/src/export/SpeedscopeTypes.ts +98 -0
  79. package/src/export/TimeExport.ts +115 -0
  80. package/src/index.ts +87 -62
  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 +55 -53
  85. package/src/matrix/MatrixInlineRunner.ts +50 -0
  86. package/src/matrix/MatrixReport.ts +94 -254
  87. package/src/matrix/VariantLoader.ts +9 -9
  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 +55 -13
  101. package/src/profiling/node/ResolvedProfile.ts +98 -0
  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 +167 -287
  115. package/src/runners/BenchRunner.ts +27 -22
  116. package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
  117. package/src/runners/CreateRunner.ts +5 -7
  118. package/src/runners/GcStats.ts +58 -61
  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 +180 -296
  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 +162 -178
  128. package/src/stats/BootstrapDifference.ts +282 -0
  129. package/src/{PermutationTest.ts → stats/PermutationTest.ts} +31 -40
  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 +9 -41
  135. package/src/{tests → test}/BenchMatrix.test.ts +31 -28
  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 +51 -0
  144. package/src/{tests → test}/MatrixFilter.test.ts +16 -16
  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 +57 -56
  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 +35 -30
  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 +42 -47
  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/BenchRunner-CSKN9zPy.d.mts +0 -225
  205. package/dist/BrowserHeapSampler-DCeL42RE.mjs +0 -202
  206. package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
  207. package/dist/GcStats-ByEovUi1.mjs +0 -77
  208. package/dist/GcStats-ByEovUi1.mjs.map +0 -1
  209. package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
  210. package/dist/TimingUtils-ClclVQ7E.mjs +0 -597
  211. package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
  212. package/dist/browser/index.js +0 -914
  213. package/dist/src-Cf_LXwlp.mjs +0 -2873
  214. package/dist/src-Cf_LXwlp.mjs.map +0 -1
  215. package/src/BenchMatrix.ts +0 -380
  216. package/src/BenchmarkReport.ts +0 -156
  217. package/src/HtmlDataPrep.ts +0 -148
  218. package/src/StandardSections.ts +0 -261
  219. package/src/StatisticalUtils.ts +0 -176
  220. package/src/TypeUtil.ts +0 -8
  221. package/src/browser/BrowserGcStats.ts +0 -44
  222. package/src/browser/BrowserHeapSampler.ts +0 -271
  223. package/src/export/JsonExport.ts +0 -103
  224. package/src/export/JsonFormat.ts +0 -91
  225. package/src/heap-sample/HeapSampleReport.ts +0 -196
  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 -152
  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 +9 -9
@@ -1,16 +1,19 @@
1
1
  import { type ChildProcess, fork } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import path from "node:path";
4
- import type { BenchmarkSpec } from "../Benchmark.ts";
5
- import type { HeapProfile } from "../heap-sample/HeapSampler.ts";
6
- import type { MeasuredResults } from "../MeasuredResults.ts";
7
- import {
8
- type AdaptiveOptions,
9
- createAdaptiveWrapper,
10
- } from "./AdaptiveWrapper.ts";
4
+ import type { CoverageData } from "../profiling/node/CoverageTypes.ts";
5
+ import type { HeapProfile } from "../profiling/node/HeapSampler.ts";
6
+ import type { TimeProfile } from "../profiling/node/TimeSampler.ts";
7
+ import type { BenchmarkFunction, BenchmarkSpec } from "./BenchmarkSpec.ts";
11
8
  import type { RunnerOptions } from "./BenchRunner.ts";
12
- import { createRunner, type KnownRunner } from "./CreateRunner.ts";
9
+ import type { KnownRunner } from "./CreateRunner.ts";
13
10
  import { aggregateGcStats, type GcEvent, parseGcLine } from "./GcStats.ts";
11
+ import type { MeasuredResults } from "./MeasuredResults.ts";
12
+ import {
13
+ createBenchRunner,
14
+ importBenchFn,
15
+ resolveVariantFn,
16
+ } from "./RunnerUtils.ts";
14
17
  import { debugWorkerTiming, getElapsed, getPerfNow } from "./TimingUtils.ts";
15
18
  import type {
16
19
  ErrorMessage,
@@ -18,21 +21,17 @@ import type {
18
21
  RunMessage,
19
22
  } from "./WorkerScript.ts";
20
23
 
21
- const logTiming = debugWorkerTiming
22
- ? (message: string) => console.log(`[RunnerOrchestrator] ${message}`)
23
- : () => {};
24
-
25
- type WorkerParams<T = unknown> = {
26
- spec: BenchmarkSpec<T>;
24
+ /** Parameters for running a matrix variant */
25
+ export interface RunMatrixVariantParams {
26
+ variantDir: string;
27
+ variantId: string;
28
+ caseId: string;
29
+ caseData?: unknown;
30
+ casesModule?: string;
27
31
  runner: KnownRunner;
28
32
  options: RunnerOptions;
29
- params?: T;
30
- };
31
-
32
- type WorkerHandlers = {
33
- resolve: (results: MeasuredResults[], heapProfile?: HeapProfile) => void;
34
- reject: (error: Error) => void;
35
- };
33
+ useWorker?: boolean;
34
+ }
36
35
 
37
36
  interface RunBenchmarkParams<T = unknown> {
38
37
  spec: BenchmarkSpec<T>;
@@ -42,7 +41,11 @@ interface RunBenchmarkParams<T = unknown> {
42
41
  params?: T;
43
42
  }
44
43
 
45
- /** Execute benchmarks directly or in worker process */
44
+ const logTiming = debugWorkerTiming
45
+ ? (message: string) => console.log(`[RunnerOrchestrator] ${message}`)
46
+ : () => {};
47
+
48
+ /** Run a benchmark spec, optionally in an isolated worker process for profiling support. */
46
49
  export async function runBenchmark<T = unknown>({
47
50
  spec,
48
51
  runner,
@@ -51,22 +54,39 @@ export async function runBenchmark<T = unknown>({
51
54
  params,
52
55
  }: RunBenchmarkParams<T>): Promise<MeasuredResults[]> {
53
56
  if (!useWorker) {
54
- const resolvedSpec = spec.modulePath
57
+ const resolved = spec.modulePath
55
58
  ? await resolveModuleSpec(spec, params)
56
59
  : { spec, params };
57
-
58
- const base = await createRunner(runner);
59
- const benchRunner = (options as any).adaptive
60
- ? createAdaptiveWrapper(base, options as AdaptiveOptions)
61
- : base;
62
- return benchRunner.runBench(
63
- resolvedSpec.spec,
64
- options,
65
- resolvedSpec.params,
66
- );
60
+ const benchRunner = await createBenchRunner(runner, options);
61
+ return benchRunner.runBench(resolved.spec, options, resolved.params);
67
62
  }
68
63
 
69
- return runInWorker({ spec, runner, options, params });
64
+ const msg = createRunMessage(spec, runner, options, params);
65
+ return runWorkerWithMessage(spec.name, options, msg);
66
+ }
67
+
68
+ /** Run a matrix variant benchmark, directly or in a worker. */
69
+ export async function runMatrixVariant(
70
+ params: RunMatrixVariantParams,
71
+ ): Promise<MeasuredResults[]> {
72
+ const { variantId, caseId, runner, options, useWorker = true } = params;
73
+ const name = `${variantId}/${caseId}`;
74
+
75
+ if (!useWorker) return runMatrixVariantDirect(params, name);
76
+
77
+ const { variantDir, caseData, casesModule } = params;
78
+ const message: RunMessage = {
79
+ type: "run",
80
+ spec: { name } as BenchmarkSpec,
81
+ runnerName: runner,
82
+ options,
83
+ variantDir,
84
+ variantId,
85
+ caseId,
86
+ caseData,
87
+ casesModule,
88
+ };
89
+ return runWorkerWithMessage(name, options, message);
70
90
  }
71
91
 
72
92
  /** Resolve modulePath/exportName to a real function for non-worker mode */
@@ -74,74 +94,43 @@ async function resolveModuleSpec<T>(
74
94
  spec: BenchmarkSpec<T>,
75
95
  params: T | undefined,
76
96
  ): Promise<{ spec: BenchmarkSpec<T>; params: T | undefined }> {
77
- const module = await import(spec.modulePath!);
78
-
79
- const fn = spec.exportName
80
- ? module[spec.exportName]
81
- : module.default || module;
82
-
83
- if (typeof fn !== "function") {
84
- const name = spec.exportName || "default";
85
- throw new Error(
86
- `Export '${name}' from ${spec.modulePath} is not a function`,
87
- );
88
- }
89
-
90
- let resolvedParams = params;
91
- if (spec.setupExportName) {
92
- const setupFn = module[spec.setupExportName];
93
- if (typeof setupFn !== "function") {
94
- const msg = `Setup export '${spec.setupExportName}' from ${spec.modulePath} is not a function`;
95
- throw new Error(msg);
96
- }
97
- resolvedParams = await setupFn(params);
98
- }
99
-
100
- return { spec: { ...spec, fn }, params: resolvedParams };
101
- }
102
-
103
- /** Run benchmark in isolated worker process */
104
- async function runInWorker<T>(
105
- workerParams: WorkerParams<T>,
106
- ): Promise<MeasuredResults[]> {
107
- const { spec, runner, options, params } = workerParams;
108
- const msg = createRunMessage(spec, runner, options, params);
109
- return runWorkerWithMessage(spec.name, options, msg);
110
- }
111
-
112
- /** Create worker process with timing logs */
113
- function createWorkerWithTiming(gcStats: boolean) {
114
- const workerStart = getPerfNow();
115
- const gcEvents: GcEvent[] = [];
116
- const worker = createWorkerProcess(gcStats);
117
- const createTime = getPerfNow();
118
- if (gcStats && worker.stdout) setupGcCapture(worker, gcEvents);
119
- logTiming(
120
- `Worker process created in ${getElapsed(workerStart, createTime).toFixed(1)}ms`,
97
+ const { modulePath, exportName, setupExportName } = spec;
98
+ const imported = await importBenchFn(
99
+ modulePath!,
100
+ exportName,
101
+ setupExportName,
102
+ params,
121
103
  );
122
- return { worker, createTime, gcEvents };
104
+ const fn = imported.fn as BenchmarkFunction<T>;
105
+ return { spec: { ...spec, fn }, params: imported.params as T | undefined };
123
106
  }
124
107
 
125
- /** Capture and parse GC lines from stdout (V8's --trace-gc-nvp outputs to stdout) */
126
- function setupGcCapture(worker: ChildProcess, gcEvents: GcEvent[]): void {
127
- let buffer = "";
128
- worker.stdout!.on("data", (data: Buffer) => {
129
- buffer += data.toString();
130
- const lines = buffer.split("\n");
131
- buffer = lines.pop() || ""; // Keep incomplete line in buffer
132
- for (const line of lines) {
133
- const event = parseGcLine(line);
134
- if (event) {
135
- gcEvents.push(event);
136
- } else if (line.trim()) {
137
- // Forward non-GC stdout to console (worker status messages)
138
- process.stdout.write(line + "\n");
139
- }
140
- }
141
- });
108
+ /** Serialize a BenchmarkSpec into a worker-safe message (modulePath or fnCode) */
109
+ function createRunMessage<T>(
110
+ spec: BenchmarkSpec<T>,
111
+ runnerName: KnownRunner,
112
+ options: RunnerOptions,
113
+ params?: T,
114
+ ): RunMessage {
115
+ const { fn, ...rest } = spec;
116
+ const message: RunMessage = {
117
+ type: "run",
118
+ spec: rest as BenchmarkSpec,
119
+ runnerName,
120
+ options,
121
+ params,
122
+ };
123
+ if (spec.modulePath) {
124
+ message.modulePath = spec.modulePath;
125
+ message.exportName = spec.exportName;
126
+ if (spec.setupExportName) message.setupExportName = spec.setupExportName;
127
+ } else {
128
+ message.fnCode = fn.toString();
129
+ }
130
+ return message;
142
131
  }
143
132
 
144
- /** Spawn worker, wire handlers, send message, return results */
133
+ /** Run a benchmark in an isolated worker process with timeout and GC capture. */
145
134
  function runWorkerWithMessage(
146
135
  name: string,
147
136
  options: RunnerOptions,
@@ -152,227 +141,122 @@ function runWorkerWithMessage(
152
141
  logTiming(`Starting worker for ${name}`);
153
142
 
154
143
  return new Promise((resolve, reject) => {
155
- const { worker, createTime, gcEvents } =
156
- createWorkerWithTiming(collectGcStats);
157
- const handlers = createWorkerHandlers(
158
- name,
159
- startTime,
160
- gcEvents,
161
- resolve,
162
- reject,
163
- );
164
- setupWorkerHandlers(worker, name, handlers);
165
- sendWorkerMessage(worker, message, createTime);
166
- });
167
- }
168
-
169
- /** Send message to worker with timing log */
170
- function sendWorkerMessage(
171
- worker: ReturnType<typeof createWorkerProcess>,
172
- message: RunMessage,
173
- createTime: number,
174
- ): void {
175
- const messageTime = getPerfNow();
176
- worker.send(message);
177
- logTiming(
178
- `Message sent to worker in ${getElapsed(createTime, messageTime).toFixed(1)}ms`,
179
- );
180
- }
144
+ const gcEvents: GcEvent[] = [];
145
+ const worker = spawnWorkerProcess(collectGcStats);
146
+ if (collectGcStats && worker.stdout) setupGcCapture(worker, gcEvents);
181
147
 
182
- /** Setup worker event handlers with cleanup */
183
- function setupWorkerHandlers(
184
- worker: ReturnType<typeof createWorkerProcess>,
185
- specName: string,
186
- handlers: WorkerHandlers,
187
- ) {
188
- const { resolve, reject } = handlers;
189
- const cleanup = createCleanup(worker, specName, reject);
190
- worker.on(
191
- "message",
192
- createMessageHandler(specName, cleanup, resolve, reject),
193
- );
194
- worker.on("error", createErrorHandler(specName, cleanup, reject));
195
- worker.on("exit", createExitHandler(specName, cleanup, reject));
196
- }
148
+ const timeoutId = setTimeout(() => {
149
+ killWorker();
150
+ reject(new Error(`Benchmark "${name}" timed out after 60 seconds`));
151
+ }, 60000);
197
152
 
198
- /** Handle worker messages (results or errors) */
199
- function createMessageHandler(
200
- specName: string,
201
- cleanup: () => void,
202
- resolve: (results: MeasuredResults[], heapProfile?: HeapProfile) => void,
203
- reject: (error: Error) => void,
204
- ) {
205
- return (msg: ResultMessage | ErrorMessage) => {
206
- cleanup();
207
- if (msg.type === "result") {
208
- resolve(msg.results, msg.heapProfile);
209
- } else if (msg.type === "error") {
210
- const error = new Error(`Benchmark "${specName}" failed: ${msg.error}`);
211
- if (msg.stack) error.stack = msg.stack;
212
- reject(error);
153
+ function killWorker() {
154
+ clearTimeout(timeoutId);
155
+ if (!worker.killed) worker.kill("SIGTERM");
213
156
  }
214
- };
215
- }
216
157
 
217
- /** Handle worker process errors */
218
- function createErrorHandler(
219
- specName: string,
220
- cleanup: () => void,
221
- reject: (error: Error) => void,
222
- ) {
223
- return (error: Error) => {
224
- cleanup();
225
- reject(
226
- new Error(
227
- `Worker process failed for benchmark "${specName}": ${error.message}`,
228
- ),
229
- );
230
- };
231
- }
232
-
233
- /** Handle worker process exit */
234
- function createExitHandler(
235
- specName: string,
236
- cleanup: () => void,
237
- reject: (error: Error) => void,
238
- ) {
239
- return (code: number | null, _signal: NodeJS.Signals | null) => {
240
- if (code !== 0 && code !== null) {
241
- cleanup();
242
- const msg = `Worker exited with code ${code} for benchmark "${specName}"`;
158
+ worker.on("message", (msg: ResultMessage | ErrorMessage) => {
159
+ killWorker();
160
+ if (msg.type === "error") {
161
+ const error = new Error(`Benchmark "${name}" failed: ${msg.error}`);
162
+ if (msg.stack) error.stack = msg.stack;
163
+ return reject(error);
164
+ }
165
+ const elapsed = getElapsed(startTime).toFixed(1);
166
+ logTiming(`Total worker time for ${name}: ${elapsed}ms`);
167
+ const { results, heapProfile, timeProfile, coverage } = msg;
168
+ attachProfilingData(
169
+ results,
170
+ gcEvents,
171
+ heapProfile,
172
+ timeProfile,
173
+ coverage,
174
+ );
175
+ resolve(results);
176
+ });
177
+ worker.on("error", (error: Error) => {
178
+ killWorker();
179
+ const msg = `Worker process failed for "${name}": ${error.message}`;
243
180
  reject(new Error(msg));
244
- }
245
- };
181
+ });
182
+ worker.on("exit", (code: number | null) => {
183
+ if (code !== 0 && code !== null) {
184
+ killWorker();
185
+ reject(new Error(`Worker exited with code ${code} for "${name}"`));
186
+ }
187
+ });
188
+
189
+ worker.send(message);
190
+ });
246
191
  }
247
192
 
248
- /** Create cleanup for timeout and termination */
249
- function createCleanup(
250
- worker: ReturnType<typeof createWorkerProcess>,
251
- specName: string,
252
- reject: (error: Error) => void,
253
- ) {
254
- const timeoutId = setTimeout(() => {
255
- cleanup();
256
- reject(new Error(`Benchmark "${specName}" timed out after 60 seconds`));
257
- }, 60000);
258
- const cleanup = () => {
259
- clearTimeout(timeoutId);
260
- if (!worker.killed) worker.kill("SIGTERM");
261
- };
262
- return cleanup;
193
+ /** Run matrix variant in-process (no worker isolation) */
194
+ async function runMatrixVariantDirect(
195
+ params: RunMatrixVariantParams,
196
+ name: string,
197
+ ): Promise<MeasuredResults[]> {
198
+ const { runner, options } = params;
199
+ const { fn } = await resolveVariantFn(params);
200
+ const benchRunner = await createBenchRunner(runner, options);
201
+ return benchRunner.runBench({ name, fn }, options);
263
202
  }
264
203
 
265
- /** Create worker process with configuration */
266
- function createWorkerProcess(gcStats: boolean) {
204
+ /** Spawn worker process with V8 flags */
205
+ function spawnWorkerProcess(gcStats: boolean) {
267
206
  const workerPath = resolveWorkerPath();
268
207
  const execArgv = ["--expose-gc", "--allow-natives-syntax"];
269
208
  if (gcStats) execArgv.push("--trace-gc-nvp");
270
209
 
210
+ const env = { ...process.env, NODE_OPTIONS: "" };
211
+ // silent mode captures stdout so we can parse --trace-gc-nvp output
271
212
  return fork(workerPath, [], {
272
213
  execArgv,
273
- silent: gcStats, // Capture stdout/stderr when collecting GC stats
274
- env: {
275
- ...process.env,
276
- NODE_OPTIONS: "",
277
- },
214
+ silent: gcStats,
215
+ env,
216
+ serialization: "advanced",
278
217
  });
279
218
  }
280
219
 
281
- /** Resolve WorkerScript path for dev (.ts) or dist (.mjs) */
282
- function resolveWorkerPath(): string {
283
- const dir = import.meta.dirname!;
284
- const tsPath = path.join(dir, "WorkerScript.ts");
285
- if (existsSync(tsPath)) return tsPath;
286
- return path.join(dir, "runners", "WorkerScript.mjs");
220
+ /** Capture and parse GC lines from worker stdout (--trace-gc-nvp). */
221
+ function setupGcCapture(worker: ChildProcess, gcEvents: GcEvent[]): void {
222
+ let buffer = "";
223
+ worker.stdout!.on("data", (data: Buffer) => {
224
+ buffer += data.toString();
225
+ const lines = buffer.split("\n");
226
+ buffer = lines.pop() || "";
227
+ for (const line of lines) {
228
+ const event = parseGcLine(line);
229
+ if (event) gcEvents.push(event);
230
+ else if (line.trim()) process.stdout.write(line + "\n");
231
+ }
232
+ });
287
233
  }
288
234
 
289
- // Consider: --no-compilation-cache, --max-old-space-size=512, --no-lazy
290
- // for consistency (less realistic)
291
-
292
- /** @return handlers that attach GC stats and heap profile to results */
293
- function createWorkerHandlers(
294
- specName: string,
295
- startTime: number,
235
+ /** Attach profiling data collected by the worker to each result. */
236
+ function attachProfilingData(
237
+ results: MeasuredResults[],
296
238
  gcEvents: GcEvent[] | undefined,
297
- resolve: (results: MeasuredResults[]) => void,
298
- reject: (error: Error) => void,
299
- ): WorkerHandlers {
300
- return {
301
- resolve: (results: MeasuredResults[], heapProfile?: HeapProfile) => {
302
- logTiming(
303
- `Total worker time for ${specName}: ${getElapsed(startTime).toFixed(1)}ms`,
304
- );
305
- if (gcEvents?.length) {
306
- const gcStats = aggregateGcStats(gcEvents);
307
- for (const r of results) r.gcStats = gcStats;
308
- }
309
- if (heapProfile) for (const r of results) r.heapProfile = heapProfile;
310
- resolve(results);
311
- },
312
- reject,
313
- };
314
- }
315
-
316
- /** Create message for worker execution */
317
- function createRunMessage<T>(
318
- spec: BenchmarkSpec<T>,
319
- runnerName: KnownRunner,
320
- options: RunnerOptions,
321
- params?: T,
322
- ): RunMessage {
323
- const { fn, ...rest } = spec;
324
- const message: RunMessage = {
325
- type: "run",
326
- spec: rest as BenchmarkSpec,
327
- runnerName,
328
- options,
329
- params,
239
+ heapProfile?: HeapProfile,
240
+ timeProfile?: TimeProfile,
241
+ coverage?: CoverageData,
242
+ ): void {
243
+ const gcStats = gcEvents?.length ? aggregateGcStats(gcEvents) : undefined;
244
+ const attach = <K extends keyof MeasuredResults>(
245
+ key: K,
246
+ value: MeasuredResults[K] | undefined,
247
+ ) => {
248
+ if (value) for (const r of results) r[key] = value;
330
249
  };
331
- if (spec.modulePath) {
332
- message.modulePath = spec.modulePath;
333
- message.exportName = spec.exportName;
334
- if (spec.setupExportName) message.setupExportName = spec.setupExportName;
335
- } else {
336
- message.fnCode = fn.toString();
337
- }
338
- return message;
250
+ attach("gcStats", gcStats);
251
+ attach("heapProfile", heapProfile);
252
+ attach("timeProfile", timeProfile);
253
+ attach("coverage", coverage);
339
254
  }
340
255
 
341
- /** Parameters for running a matrix variant in worker */
342
- export interface RunMatrixVariantParams {
343
- variantDir: string;
344
- variantId: string;
345
- caseId: string;
346
- caseData?: unknown;
347
- casesModule?: string;
348
- runner: KnownRunner;
349
- options: RunnerOptions;
350
- }
351
-
352
- /** Run a matrix variant benchmark in isolated worker process */
353
- export async function runMatrixVariant(
354
- params: RunMatrixVariantParams,
355
- ): Promise<MeasuredResults[]> {
356
- const {
357
- variantDir,
358
- variantId,
359
- caseId,
360
- caseData,
361
- casesModule,
362
- runner,
363
- options,
364
- } = params;
365
- const name = `${variantId}/${caseId}`;
366
- const message: RunMessage = {
367
- type: "run",
368
- spec: { name, fn: () => {} },
369
- runnerName: runner,
370
- options,
371
- variantDir,
372
- variantId,
373
- caseId,
374
- caseData,
375
- casesModule,
376
- };
377
- return runWorkerWithMessage(name, options, message);
256
+ /** Resolve WorkerScript path for dev (.ts) or dist (.mjs) */
257
+ function resolveWorkerPath(): string {
258
+ const dir = import.meta.dirname!;
259
+ const tsPath = path.join(dir, "WorkerScript.ts");
260
+ if (existsSync(tsPath)) return tsPath;
261
+ return path.join(dir, "runners", "WorkerScript.mjs");
378
262
  }
@@ -1,2 +1,76 @@
1
+ import { prepareBenchFn } from "../matrix/BenchMatrix.ts";
2
+ import { loadCaseData, loadCasesModule } from "../matrix/CaseLoader.ts";
3
+ import { loadVariant } from "../matrix/VariantLoader.ts";
4
+ import {
5
+ type AdaptiveOptions,
6
+ createAdaptiveWrapper,
7
+ } from "./AdaptiveWrapper.ts";
8
+ import type { BenchmarkFunction } from "./BenchmarkSpec.ts";
9
+ import type { BenchRunner, RunnerOptions } from "./BenchRunner.ts";
10
+ import { createRunner, type KnownRunner } from "./CreateRunner.ts";
11
+
1
12
  export const msToNs = 1e6;
2
- export const nsToMs = 1e-6;
13
+
14
+ /** Get named or default export from module, throw if not a function */
15
+ // biome-ignore lint/complexity/noBannedTypes: generic function constraint
16
+ export function getModuleExport<T extends Function = Function>(
17
+ module: any,
18
+ exportName: string | undefined,
19
+ modulePath: string,
20
+ ): T {
21
+ const fn = exportName ? module[exportName] : module.default || module;
22
+ if (typeof fn !== "function") {
23
+ const name = exportName || "default";
24
+ throw new Error(`Export '${name}' from ${modulePath} is not a function`);
25
+ }
26
+ return fn as T;
27
+ }
28
+
29
+ /** Import a benchmark function from a module, optionally running a setup export */
30
+ export async function importBenchFn(
31
+ modulePath: string,
32
+ exportName: string | undefined,
33
+ setupExportName: string | undefined,
34
+ params: unknown,
35
+ ): Promise<{ fn: BenchmarkFunction; params: unknown }> {
36
+ const module = await import(modulePath);
37
+ const fn = getModuleExport<BenchmarkFunction>(module, exportName, modulePath);
38
+ if (!setupExportName) return { fn, params };
39
+
40
+ const setup = getModuleExport<BenchmarkFunction>(
41
+ module,
42
+ setupExportName,
43
+ modulePath,
44
+ );
45
+ return { fn, params: await setup(params) };
46
+ }
47
+
48
+ /** Resolve a matrix variant to a benchmark function (shared by orchestrator and worker). */
49
+ export async function resolveVariantFn(params: {
50
+ variantDir: string;
51
+ variantId: string;
52
+ caseId?: string;
53
+ caseData?: unknown;
54
+ casesModule?: string;
55
+ }): Promise<{ fn: BenchmarkFunction; params: undefined }> {
56
+ let { caseData } = params;
57
+ if (params.casesModule && params.caseId) {
58
+ const cases = await loadCasesModule(params.casesModule);
59
+ caseData = (await loadCaseData(cases, params.caseId)).data;
60
+ }
61
+ const variant = await loadVariant(params.variantDir, params.variantId);
62
+ const fn = await prepareBenchFn(variant, caseData);
63
+ return { fn, params: undefined };
64
+ }
65
+
66
+ /** Create runner, wrapping with adaptive sampling if options.adaptive is set */
67
+ export async function createBenchRunner(
68
+ runnerName: KnownRunner,
69
+ options: RunnerOptions,
70
+ ): Promise<BenchRunner> {
71
+ const base = await createRunner(runnerName);
72
+ if ("adaptive" in options && options.adaptive) {
73
+ return createAdaptiveWrapper(base, options as AdaptiveOptions);
74
+ }
75
+ return base;
76
+ }