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,46 +1,54 @@
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";
11
+ createBenchRunner,
12
+ importBenchFn,
13
+ resolveVariantFn,
14
+ } from "./RunnerUtils.ts";
12
15
  import { debugWorkerTiming, getElapsed, getPerfNow } from "./TimingUtils.ts";
13
16
 
14
- const workerStartTime = getPerfNow();
15
- const maxLifetime = 5 * 60 * 1000; // 5 minutes
16
-
17
17
  /** Message sent to worker process to start a benchmark run. */
18
18
  export interface RunMessage {
19
19
  type: "run";
20
20
  spec: BenchmarkSpec;
21
21
  runnerName: KnownRunner;
22
22
  options: RunnerOptions;
23
- fnCode?: string; // Made optional - either fnCode or modulePath is required
24
- modulePath?: string; // Path to module for dynamic import
25
- exportName?: string; // Export name from module
26
- 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;
27
30
  params?: unknown;
28
- // Variant directory mode (BenchMatrix)
29
- variantDir?: string; // Directory URL containing variant .ts files
30
- variantId?: string; // Variant filename (without .ts)
31
- caseData?: unknown; // Data to pass to variant
32
- caseId?: string; // Case identifier
33
- 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;
34
40
  }
35
41
 
36
- /** Message returned from worker process with benchmark results. */
42
+ /** Benchmark results returned from worker process. */
37
43
  export interface ResultMessage {
38
44
  type: "result";
39
45
  results: MeasuredResults[];
40
46
  heapProfile?: HeapProfile;
47
+ timeProfile?: TimeProfile;
48
+ coverage?: CoverageData;
41
49
  }
42
50
 
43
- /** Message returned from worker process when benchmark fails. */
51
+ /** Error returned from worker process when benchmark fails. */
44
52
  export interface ErrorMessage {
45
53
  type: "error";
46
54
  error: string;
@@ -49,83 +57,30 @@ export interface ErrorMessage {
49
57
 
50
58
  export type WorkerMessage = RunMessage | ResultMessage | ErrorMessage;
51
59
 
52
- /**
53
- * Worker process for isolated benchmark execution.
54
- * Uses eval() safely in isolated child process with trusted code.
55
- */
56
- process.on("message", async (message: RunMessage) => {
57
- if (message.type !== "run") return;
58
-
59
- logTiming(`Processing ${message.spec.name} with ${message.runnerName}`);
60
-
61
- try {
62
- const start = getPerfNow();
63
- const baseRunner = await createRunner(message.runnerName);
64
-
65
- const runner = (message.options as any).adaptive
66
- ? createAdaptiveWrapper(baseRunner, message.options as AdaptiveOptions)
67
- : baseRunner;
68
-
69
- logTiming("Runner created in", getElapsed(start));
70
-
71
- const benchStart = getPerfNow();
72
-
73
- // Run with heap sampling if enabled (covers module import + execution)
74
- if (message.options.heapSample) {
75
- const { withHeapSampling } = await import(
76
- "../heap-sample/HeapSampler.ts"
77
- );
78
- const heapOpts = {
79
- samplingInterval: message.options.heapInterval,
80
- stackDepth: message.options.heapDepth,
81
- };
82
- const { result: results, profile: heapProfile } = await withHeapSampling(
83
- heapOpts,
84
- async () => {
85
- const { fn, params } = await resolveBenchmarkFn(message);
86
- return runner.runBench(
87
- { ...message.spec, fn },
88
- message.options,
89
- params,
90
- );
91
- },
92
- );
93
- logTiming("Benchmark execution took", getElapsed(benchStart));
94
- sendAndExit({ type: "result", results, heapProfile }, 0);
95
- } else {
96
- const { fn, params } = await resolveBenchmarkFn(message);
97
- const results = await runner.runBench(
98
- { ...message.spec, fn },
99
- message.options,
100
- params,
101
- );
102
- logTiming("Benchmark execution took", getElapsed(benchStart));
103
- sendAndExit({ type: "result", results }, 0);
104
- }
105
- } catch (error) {
106
- sendAndExit(createErrorMessage(error), 1);
107
- }
108
- });
60
+ interface BenchmarkImportResult {
61
+ fn: BenchmarkFunction;
62
+ params: unknown;
63
+ }
109
64
 
110
- // Exit after 5 minutes to prevent zombie processes
111
- setTimeout(() => {
112
- console.error("WorkerScript: Maximum lifetime exceeded, exiting");
113
- process.exit(1);
114
- }, maxLifetime);
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
+ }
115
73
 
116
- process.stdin.pause();
74
+ const workerStartTime = getPerfNow();
75
+ const maxLifetime = 5 * 60 * 1000;
117
76
 
118
- /** Log timing with consistent format */
119
77
  const logTiming = debugWorkerTiming ? _logTiming : () => {};
120
78
  function _logTiming(operation: string, duration?: number) {
121
- if (duration === undefined) {
122
- console.log(`[Worker] ${operation}`);
123
- } else {
124
- console.log(`[Worker] ${operation} ${duration.toFixed(1)}ms`);
125
- }
79
+ const suffix = duration !== undefined ? ` ${duration.toFixed(1)}ms` : "";
80
+ console.log(`[Worker] ${operation}${suffix}`);
126
81
  }
127
82
 
128
- /** Send message and exit with duration log */
83
+ /** Send IPC message to parent then exit the worker process */
129
84
  function sendAndExit(msg: ResultMessage | ErrorMessage, exitCode: number) {
130
85
  process.send!(msg, undefined, undefined, (err: Error | null): void => {
131
86
  if (err) {
@@ -138,11 +93,6 @@ function sendAndExit(msg: ResultMessage | ErrorMessage, exitCode: number) {
138
93
  });
139
94
  }
140
95
 
141
- interface BenchmarkImportResult {
142
- fn: BenchmarkFunction;
143
- params: unknown;
144
- }
145
-
146
96
  /** Resolve benchmark function from message (variant dir, module path, or fnCode) */
147
97
  async function resolveBenchmarkFn(
148
98
  message: RunMessage,
@@ -151,7 +101,12 @@ async function resolveBenchmarkFn(
151
101
  return importVariantModule(message);
152
102
  }
153
103
  if (message.modulePath) {
154
- 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);
155
110
  }
156
111
  return { fn: reconstructFunction(message.fnCode!), params: message.params };
157
112
  }
@@ -160,97 +115,126 @@ async function resolveBenchmarkFn(
160
115
  async function importVariantModule(
161
116
  message: RunMessage,
162
117
  ): Promise<BenchmarkImportResult> {
163
- const { variantDir, variantId, caseId, casesModule } = message;
164
- let { caseData } = message;
165
- const moduleUrl = variantModuleUrl(variantDir!, variantId!);
118
+ const { variantDir, variantId } = message;
166
119
  logTiming(`Importing variant ${variantId} from ${variantDir}`);
120
+ return resolveVariantFn({
121
+ ...message,
122
+ variantDir: variantDir!,
123
+ variantId: variantId!,
124
+ });
125
+ }
167
126
 
168
- if (casesModule && caseId) {
169
- caseData = (await loadCaseFromModule(casesModule, caseId)).data;
170
- }
171
-
172
- const module = await import(moduleUrl);
173
- const { setup, run } = module;
174
-
175
- if (typeof run !== "function") {
176
- throw new Error(`Variant '${variantId}' must export 'run' function`);
177
- }
178
-
179
- // Stateful variant: setup returns state, run receives state
180
- if (typeof setup === "function") {
181
- logTiming(`Calling setup for ${variantId}`);
182
- const state = await setup(caseData);
183
- return { fn: () => run(state), params: undefined };
127
+ /** Eval serialized function body back into a callable */
128
+ function reconstructFunction(fnCode: string): BenchmarkFunction {
129
+ // biome-ignore lint/security/noGlobalEval: Necessary for worker process isolation, code is from trusted source
130
+ const fn = eval(`(${fnCode})`); // eslint-disable-line no-eval
131
+ if (typeof fn !== "function") {
132
+ throw new Error("Reconstructed code is not a function");
184
133
  }
185
-
186
- // Stateless variant: run receives caseData directly
187
- return { fn: () => run(caseData), params: undefined };
134
+ return fn;
188
135
  }
189
136
 
190
- /** Load case data from a cases module */
191
- async function loadCaseFromModule(
192
- casesModuleUrl: string,
193
- caseId: string,
194
- ): Promise<{ data: unknown; metadata?: Record<string, unknown> }> {
195
- logTiming(`Loading case '${caseId}' from ${casesModuleUrl}`);
196
- const module = await import(casesModuleUrl);
197
- if (typeof module.loadCase === "function") {
198
- 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 };
199
148
  }
200
- return { data: caseId };
149
+
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 };
201
159
  }
202
160
 
203
- /** Import benchmark function and optionally run setup */
204
- async function importBenchmarkWithSetup(
161
+ /** Build nested profiling wrappers: outer heap, inner time */
162
+ function buildProfilingChain(
205
163
  message: RunMessage,
206
- ): Promise<BenchmarkImportResult> {
207
- const { modulePath, exportName, setupExportName, params } = message;
208
- logTiming(
209
- `Importing from ${modulePath}${exportName ? ` (${exportName})` : ""}`,
210
- );
211
- const module = await import(modulePath!);
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);
173
+ };
212
174
 
213
- const fn = getModuleExport(module, exportName, modulePath!);
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;
204
+ }
214
205
 
215
- if (setupExportName) {
216
- logTiming(`Calling setup: ${setupExportName}`);
217
- const setupFn = getModuleExport(module, setupExportName, modulePath!);
218
- const setupResult = await setupFn(params);
219
- return { fn, params: setupResult };
220
- }
206
+ process.on("message", async (message: RunMessage) => {
207
+ if (message.type !== "run") return;
221
208
 
222
- return { fn, params };
223
- }
209
+ logTiming(`Processing ${message.spec.name} with ${message.runnerName}`);
224
210
 
225
- /** Get named or default export from module */
226
- function getModuleExport(
227
- module: any,
228
- exportName: string | undefined,
229
- modulePath: string,
230
- ): BenchmarkFunction {
231
- const fn = exportName ? module[exportName] : module.default || module;
232
- if (typeof fn !== "function") {
233
- const name = exportName || "default";
234
- throw new Error(`Export '${name}' from ${modulePath} is not a function`);
235
- }
236
- return fn;
237
- }
211
+ try {
212
+ const start = getPerfNow();
213
+ const runner = await createBenchRunner(message.runnerName, message.options);
214
+ logTiming("Runner created in", getElapsed(start));
238
215
 
239
- /** Reconstruct function from string code */
240
- function reconstructFunction(fnCode: string): BenchmarkFunction {
241
- // biome-ignore lint/security/noGlobalEval: Necessary for worker process isolation, code is from trusted source
242
- const fn = eval(`(${fnCode})`); // eslint-disable-line no-eval
243
- if (typeof fn !== "function") {
244
- throw new Error("Reconstructed code is not a function");
216
+ const benchStart = getPerfNow();
217
+ const result = await runWithProfiling(message, runner);
218
+ logTiming("Benchmark execution took", getElapsed(benchStart));
219
+ sendAndExit(result, 0);
220
+ } catch (error) {
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
+ );
245
230
  }
246
- return fn;
247
- }
231
+ });
248
232
 
249
- /** Create error message from exception */
250
- function createErrorMessage(error: unknown): ErrorMessage {
251
- return {
252
- type: "error",
253
- error: error instanceof Error ? error.message : String(error),
254
- stack: error instanceof Error ? error.stack : undefined,
255
- };
256
- }
233
+ // Prevent zombie processes
234
+ setTimeout(() => {
235
+ console.error("WorkerScript: Maximum lifetime exceeded, exiting");
236
+ process.exit(1);
237
+ }, maxLifetime);
238
+
239
+ // Prevent stdin from keeping the worker process alive
240
+ process.stdin.pause();