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
@@ -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,7 +21,7 @@ import type {
18
21
  RunMessage,
19
22
  } from "./WorkerScript.ts";
20
23
 
21
- /** Parameters for running a matrix variant in worker */
24
+ /** Parameters for running a matrix variant */
22
25
  export interface RunMatrixVariantParams {
23
26
  variantDir: string;
24
27
  variantId: string;
@@ -27,20 +30,9 @@ export interface RunMatrixVariantParams {
27
30
  casesModule?: string;
28
31
  runner: KnownRunner;
29
32
  options: RunnerOptions;
33
+ useWorker?: boolean;
30
34
  }
31
35
 
32
- type WorkerParams<T = unknown> = {
33
- spec: BenchmarkSpec<T>;
34
- runner: KnownRunner;
35
- options: RunnerOptions;
36
- params?: T;
37
- };
38
-
39
- type WorkerHandlers = {
40
- resolve: (results: MeasuredResults[], heapProfile?: HeapProfile) => void;
41
- reject: (error: Error) => void;
42
- };
43
-
44
36
  interface RunBenchmarkParams<T = unknown> {
45
37
  spec: BenchmarkSpec<T>;
46
38
  runner: KnownRunner;
@@ -53,7 +45,7 @@ const logTiming = debugWorkerTiming
53
45
  ? (message: string) => console.log(`[RunnerOrchestrator] ${message}`)
54
46
  : () => {};
55
47
 
56
- /** Execute benchmarks directly or in worker process */
48
+ /** Run a benchmark spec, optionally in an isolated worker process for profiling support. */
57
49
  export async function runBenchmark<T = unknown>({
58
50
  spec,
59
51
  runner,
@@ -62,41 +54,30 @@ export async function runBenchmark<T = unknown>({
62
54
  params,
63
55
  }: RunBenchmarkParams<T>): Promise<MeasuredResults[]> {
64
56
  if (!useWorker) {
65
- const resolvedSpec = spec.modulePath
57
+ const resolved = spec.modulePath
66
58
  ? await resolveModuleSpec(spec, params)
67
59
  : { spec, params };
68
-
69
- const base = await createRunner(runner);
70
- const benchRunner = (options as any).adaptive
71
- ? createAdaptiveWrapper(base, options as AdaptiveOptions)
72
- : base;
73
- return benchRunner.runBench(
74
- resolvedSpec.spec,
75
- options,
76
- resolvedSpec.params,
77
- );
60
+ const benchRunner = await createBenchRunner(runner, options);
61
+ return benchRunner.runBench(resolved.spec, options, resolved.params);
78
62
  }
79
63
 
80
- return runInWorker({ spec, runner, options, params });
64
+ const msg = createRunMessage(spec, runner, options, params);
65
+ return runWorkerWithMessage(spec.name, options, msg);
81
66
  }
82
67
 
83
- /** Run a matrix variant benchmark in isolated worker process */
68
+ /** Run a matrix variant benchmark, directly or in a worker. */
84
69
  export async function runMatrixVariant(
85
70
  params: RunMatrixVariantParams,
86
71
  ): Promise<MeasuredResults[]> {
87
- const {
88
- variantDir,
89
- variantId,
90
- caseId,
91
- caseData,
92
- casesModule,
93
- runner,
94
- options,
95
- } = params;
72
+ const { variantId, caseId, runner, options, useWorker = true } = params;
96
73
  const name = `${variantId}/${caseId}`;
74
+
75
+ if (!useWorker) return runMatrixVariantDirect(params, name);
76
+
77
+ const { variantDir, caseData, casesModule } = params;
97
78
  const message: RunMessage = {
98
79
  type: "run",
99
- spec: { name, fn: () => {} },
80
+ spec: { name } as BenchmarkSpec,
100
81
  runnerName: runner,
101
82
  options,
102
83
  variantDir,
@@ -113,67 +94,18 @@ async function resolveModuleSpec<T>(
113
94
  spec: BenchmarkSpec<T>,
114
95
  params: T | undefined,
115
96
  ): Promise<{ spec: BenchmarkSpec<T>; params: T | undefined }> {
116
- const module = await import(spec.modulePath!);
117
-
118
- const fn = spec.exportName
119
- ? module[spec.exportName]
120
- : module.default || module;
121
-
122
- if (typeof fn !== "function") {
123
- const name = spec.exportName || "default";
124
- throw new Error(
125
- `Export '${name}' from ${spec.modulePath} is not a function`,
126
- );
127
- }
128
-
129
- let resolvedParams = params;
130
- if (spec.setupExportName) {
131
- const setupFn = module[spec.setupExportName];
132
- if (typeof setupFn !== "function") {
133
- const msg = `Setup export '${spec.setupExportName}' from ${spec.modulePath} is not a function`;
134
- throw new Error(msg);
135
- }
136
- resolvedParams = await setupFn(params);
137
- }
138
-
139
- return { spec: { ...spec, fn }, params: resolvedParams };
140
- }
141
-
142
- /** Run benchmark in isolated worker process */
143
- async function runInWorker<T>(
144
- workerParams: WorkerParams<T>,
145
- ): Promise<MeasuredResults[]> {
146
- const { spec, runner, options, params } = workerParams;
147
- const msg = createRunMessage(spec, runner, options, params);
148
- return runWorkerWithMessage(spec.name, options, msg);
149
- }
150
-
151
- /** Spawn worker, wire handlers, send message, return results */
152
- function runWorkerWithMessage(
153
- name: string,
154
- options: RunnerOptions,
155
- message: RunMessage,
156
- ): Promise<MeasuredResults[]> {
157
- const startTime = getPerfNow();
158
- const collectGcStats = options.gcStats ?? false;
159
- logTiming(`Starting worker for ${name}`);
160
-
161
- return new Promise((resolve, reject) => {
162
- const { worker, createTime, gcEvents } =
163
- createWorkerWithTiming(collectGcStats);
164
- const handlers = createWorkerHandlers(
165
- name,
166
- startTime,
167
- gcEvents,
168
- resolve,
169
- reject,
170
- );
171
- setupWorkerHandlers(worker, name, handlers);
172
- sendWorkerMessage(worker, message, createTime);
173
- });
97
+ const { modulePath, exportName, setupExportName } = spec;
98
+ const imported = await importBenchFn(
99
+ modulePath!,
100
+ exportName,
101
+ setupExportName,
102
+ params,
103
+ );
104
+ const fn = imported.fn as BenchmarkFunction<T>;
105
+ return { spec: { ...spec, fn }, params: imported.params as T | undefined };
174
106
  }
175
107
 
176
- /** Create message for worker execution */
108
+ /** Serialize a BenchmarkSpec into a worker-safe message (modulePath or fnCode) */
177
109
  function createRunMessage<T>(
178
110
  spec: BenchmarkSpec<T>,
179
111
  runnerName: KnownRunner,
@@ -198,175 +130,127 @@ function createRunMessage<T>(
198
130
  return message;
199
131
  }
200
132
 
201
- /** Create worker process with timing logs */
202
- function createWorkerWithTiming(gcStats: boolean) {
203
- const workerStart = getPerfNow();
204
- const gcEvents: GcEvent[] = [];
205
- const worker = createWorkerProcess(gcStats);
206
- const createTime = getPerfNow();
207
- if (gcStats && worker.stdout) setupGcCapture(worker, gcEvents);
208
- logTiming(
209
- `Worker process created in ${getElapsed(workerStart, createTime).toFixed(1)}ms`,
210
- );
211
- return { worker, createTime, gcEvents };
212
- }
133
+ /** Run a benchmark in an isolated worker process with timeout and GC capture. */
134
+ function runWorkerWithMessage(
135
+ name: string,
136
+ options: RunnerOptions,
137
+ message: RunMessage,
138
+ ): Promise<MeasuredResults[]> {
139
+ const startTime = getPerfNow();
140
+ const collectGcStats = options.gcStats ?? false;
141
+ logTiming(`Starting worker for ${name}`);
213
142
 
214
- // Consider: --no-compilation-cache, --max-old-space-size=512, --no-lazy
215
- // for consistency (less realistic)
143
+ return new Promise((resolve, reject) => {
144
+ const gcEvents: GcEvent[] = [];
145
+ const worker = spawnWorkerProcess(collectGcStats);
146
+ if (collectGcStats && worker.stdout) setupGcCapture(worker, gcEvents);
216
147
 
217
- /** @return handlers that attach GC stats and heap profile to results */
218
- function createWorkerHandlers(
219
- specName: string,
220
- startTime: number,
221
- gcEvents: GcEvent[] | undefined,
222
- resolve: (results: MeasuredResults[]) => void,
223
- reject: (error: Error) => void,
224
- ): WorkerHandlers {
225
- return {
226
- resolve: (results: MeasuredResults[], heapProfile?: HeapProfile) => {
227
- logTiming(
228
- `Total worker time for ${specName}: ${getElapsed(startTime).toFixed(1)}ms`,
229
- );
230
- if (gcEvents?.length) {
231
- const gcStats = aggregateGcStats(gcEvents);
232
- for (const r of results) r.gcStats = gcStats;
148
+ const timeoutId = setTimeout(() => {
149
+ killWorker();
150
+ reject(new Error(`Benchmark "${name}" timed out after 60 seconds`));
151
+ }, 60000);
152
+
153
+ function killWorker() {
154
+ clearTimeout(timeoutId);
155
+ if (!worker.killed) worker.kill("SIGTERM");
156
+ }
157
+
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);
233
164
  }
234
- if (heapProfile) for (const r of results) r.heapProfile = heapProfile;
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
+ );
235
175
  resolve(results);
236
- },
237
- reject,
238
- };
239
- }
176
+ });
177
+ worker.on("error", (error: Error) => {
178
+ killWorker();
179
+ const msg = `Worker process failed for "${name}": ${error.message}`;
180
+ reject(new Error(msg));
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
+ });
240
188
 
241
- /** Setup worker event handlers with cleanup */
242
- function setupWorkerHandlers(
243
- worker: ReturnType<typeof createWorkerProcess>,
244
- specName: string,
245
- handlers: WorkerHandlers,
246
- ) {
247
- const { resolve, reject } = handlers;
248
- const cleanup = createCleanup(worker, specName, reject);
249
- worker.on(
250
- "message",
251
- createMessageHandler(specName, cleanup, resolve, reject),
252
- );
253
- worker.on("error", createErrorHandler(specName, cleanup, reject));
254
- worker.on("exit", createExitHandler(specName, cleanup, reject));
189
+ worker.send(message);
190
+ });
255
191
  }
256
192
 
257
- /** Send message to worker with timing log */
258
- function sendWorkerMessage(
259
- worker: ReturnType<typeof createWorkerProcess>,
260
- message: RunMessage,
261
- createTime: number,
262
- ): void {
263
- const messageTime = getPerfNow();
264
- worker.send(message);
265
- logTiming(
266
- `Message sent to worker in ${getElapsed(createTime, messageTime).toFixed(1)}ms`,
267
- );
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);
268
202
  }
269
203
 
270
- /** Create worker process with configuration */
271
- function createWorkerProcess(gcStats: boolean) {
204
+ /** Spawn worker process with V8 flags */
205
+ function spawnWorkerProcess(gcStats: boolean) {
272
206
  const workerPath = resolveWorkerPath();
273
207
  const execArgv = ["--expose-gc", "--allow-natives-syntax"];
274
208
  if (gcStats) execArgv.push("--trace-gc-nvp");
275
209
 
210
+ const env = { ...process.env, NODE_OPTIONS: "" };
211
+ // silent mode captures stdout so we can parse --trace-gc-nvp output
276
212
  return fork(workerPath, [], {
277
213
  execArgv,
278
- silent: gcStats, // Capture stdout/stderr when collecting GC stats
279
- env: {
280
- ...process.env,
281
- NODE_OPTIONS: "",
282
- },
214
+ silent: gcStats,
215
+ env,
216
+ serialization: "advanced",
283
217
  });
284
218
  }
285
219
 
286
- /** Capture and parse GC lines from stdout (V8's --trace-gc-nvp outputs to stdout) */
220
+ /** Capture and parse GC lines from worker stdout (--trace-gc-nvp). */
287
221
  function setupGcCapture(worker: ChildProcess, gcEvents: GcEvent[]): void {
288
222
  let buffer = "";
289
223
  worker.stdout!.on("data", (data: Buffer) => {
290
224
  buffer += data.toString();
291
225
  const lines = buffer.split("\n");
292
- buffer = lines.pop() || ""; // Keep incomplete line in buffer
226
+ buffer = lines.pop() || "";
293
227
  for (const line of lines) {
294
228
  const event = parseGcLine(line);
295
- if (event) {
296
- gcEvents.push(event);
297
- } else if (line.trim()) {
298
- // Forward non-GC stdout to console (worker status messages)
299
- process.stdout.write(line + "\n");
300
- }
229
+ if (event) gcEvents.push(event);
230
+ else if (line.trim()) process.stdout.write(line + "\n");
301
231
  }
302
232
  });
303
233
  }
304
234
 
305
- /** Create cleanup for timeout and termination */
306
- function createCleanup(
307
- worker: ReturnType<typeof createWorkerProcess>,
308
- specName: string,
309
- reject: (error: Error) => void,
310
- ) {
311
- const timeoutId = setTimeout(() => {
312
- cleanup();
313
- reject(new Error(`Benchmark "${specName}" timed out after 60 seconds`));
314
- }, 60000);
315
- const cleanup = () => {
316
- clearTimeout(timeoutId);
317
- if (!worker.killed) worker.kill("SIGTERM");
318
- };
319
- return cleanup;
320
- }
321
-
322
- /** Handle worker messages (results or errors) */
323
- function createMessageHandler(
324
- specName: string,
325
- cleanup: () => void,
326
- resolve: (results: MeasuredResults[], heapProfile?: HeapProfile) => void,
327
- reject: (error: Error) => void,
328
- ) {
329
- return (msg: ResultMessage | ErrorMessage) => {
330
- cleanup();
331
- if (msg.type === "result") {
332
- resolve(msg.results, msg.heapProfile);
333
- } else if (msg.type === "error") {
334
- const error = new Error(`Benchmark "${specName}" failed: ${msg.error}`);
335
- if (msg.stack) error.stack = msg.stack;
336
- reject(error);
337
- }
338
- };
339
- }
340
-
341
- /** Handle worker process errors */
342
- function createErrorHandler(
343
- specName: string,
344
- cleanup: () => void,
345
- reject: (error: Error) => void,
346
- ) {
347
- return (error: Error) => {
348
- cleanup();
349
- reject(
350
- new Error(
351
- `Worker process failed for benchmark "${specName}": ${error.message}`,
352
- ),
353
- );
354
- };
355
- }
356
-
357
- /** Handle worker process exit */
358
- function createExitHandler(
359
- specName: string,
360
- cleanup: () => void,
361
- reject: (error: Error) => void,
362
- ) {
363
- return (code: number | null, _signal: NodeJS.Signals | null) => {
364
- if (code !== 0 && code !== null) {
365
- cleanup();
366
- const msg = `Worker exited with code ${code} for benchmark "${specName}"`;
367
- reject(new Error(msg));
368
- }
235
+ /** Attach profiling data collected by the worker to each result. */
236
+ function attachProfilingData(
237
+ results: MeasuredResults[],
238
+ gcEvents: GcEvent[] | undefined,
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;
369
249
  };
250
+ attach("gcStats", gcStats);
251
+ attach("heapProfile", heapProfile);
252
+ attach("timeProfile", timeProfile);
253
+ attach("coverage", coverage);
370
254
  }
371
255
 
372
256
  /** Resolve WorkerScript path for dev (.ts) or dist (.mjs) */
@@ -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
+ }
@@ -0,0 +1,100 @@
1
+ import {
2
+ coefficientOfVariation,
3
+ median,
4
+ medianAbsoluteDeviation,
5
+ percentile,
6
+ } from "../stats/StatisticalUtils.ts";
7
+ import {
8
+ type MeasuredResults,
9
+ type OptStatusInfo,
10
+ optStatusNames,
11
+ } from "./MeasuredResults.ts";
12
+
13
+ /** Compute percentiles, CV, MAD, and outlier rate from timing samples. */
14
+ export function computeStats(samples: number[]): MeasuredResults["time"] {
15
+ let min = Number.POSITIVE_INFINITY;
16
+ let max = Number.NEGATIVE_INFINITY;
17
+ let sum = 0;
18
+ for (const s of samples) {
19
+ if (s < min) min = s;
20
+ if (s > max) max = s;
21
+ sum += s;
22
+ }
23
+ const sorted = [...samples].sort((a, b) => a - b);
24
+ const pct = (p: number) =>
25
+ sorted[Math.max(0, Math.ceil(sorted.length * p) - 1)];
26
+ return {
27
+ min,
28
+ max,
29
+ avg: sum / samples.length,
30
+ p25: pct(0.25),
31
+ p50: pct(0.5),
32
+ p75: pct(0.75),
33
+ p95: pct(0.95),
34
+ p99: pct(0.99),
35
+ p999: pct(0.999),
36
+ cv: coefficientOfVariation(samples),
37
+ mad: medianAbsoluteDeviation(samples),
38
+ outlierRate: outlierImpactRatio(samples),
39
+ };
40
+ }
41
+
42
+ /** Measure outlier impact as proportion of excess time above 1.5*IQR threshold. */
43
+ export function outlierImpactRatio(samples: number[]): number {
44
+ if (samples.length === 0) return 0;
45
+ const med = median(samples);
46
+ const q75 = percentile(samples, 0.75);
47
+ const threshold = med + 1.5 * (q75 - med);
48
+
49
+ let excessTime = 0;
50
+ for (const sample of samples) {
51
+ if (sample > threshold) excessTime += sample - med;
52
+ }
53
+ const total = samples.reduce((a, b) => a + b, 0);
54
+ return total > 0 ? excessTime / total : 0;
55
+ }
56
+
57
+ /** Group samples by V8 optimization tier and count deopts. */
58
+ export function analyzeOptStatus(
59
+ samples: number[],
60
+ statuses: number[],
61
+ ): OptStatusInfo | undefined {
62
+ if (statuses.length === 0 || statuses[0] === undefined) return undefined;
63
+
64
+ const byStatus = new Map<number, number[]>();
65
+ let deoptCount = 0;
66
+ for (let i = 0; i < samples.length; i++) {
67
+ const status = statuses[i];
68
+ if (status === undefined) continue;
69
+ if (status & 8) deoptCount++; // deopt flag (bit 3)
70
+ const group = byStatus.get(status);
71
+ if (group) group.push(samples[i]);
72
+ else byStatus.set(status, [samples[i]]);
73
+ }
74
+
75
+ const entries = [...byStatus].map(([status, times]) => {
76
+ const name = optStatusNames[status] || `status=${status}`;
77
+ return [name, { count: times.length, medianMs: median(times) }] as const;
78
+ });
79
+ return { byTier: Object.fromEntries(entries), deoptCount };
80
+ }
81
+
82
+ /** @return runtime gc() function, or a no-op if --expose-gc wasn't passed. */
83
+ export function gcFunction(): () => void {
84
+ const gc = globalThis.gc ?? (globalThis as any).__gc;
85
+ if (gc) return gc;
86
+ console.warn("gc() not available, run node/bun with --expose-gc");
87
+ return () => {};
88
+ }
89
+
90
+ /** @return function that reads V8 optimization status via %GetOptimizationStatus. */
91
+ export function createOptStatusGetter(): ((fn: unknown) => number) | undefined {
92
+ try {
93
+ // %GetOptimizationStatus returns a bitmask
94
+ const fn = new Function("f", "return %GetOptimizationStatus(f)");
95
+ fn(() => {});
96
+ return fn as (fn: unknown) => number;
97
+ } catch {
98
+ return undefined;
99
+ }
100
+ }