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,19 +1,22 @@
1
- import type { BenchmarkSpec } from "../Benchmark.ts";
2
- import type { MeasuredResults } from "../MeasuredResults.ts";
3
- import {
4
- coefficientOfVariation,
5
- medianAbsoluteDeviation,
6
- percentile,
7
- } from "../StatisticalUtils.ts";
1
+ import { median } from "../stats/StatisticalUtils.ts";
2
+ import type { BenchmarkSpec } from "./BenchmarkSpec.ts";
8
3
  import type { BenchRunner, RunnerOptions } from "./BenchRunner.ts";
4
+ import type { MeasuredResults } from "./MeasuredResults.ts";
9
5
  import { msToNs } from "./RunnerUtils.ts";
6
+ import { computeStats, outlierImpactRatio } from "./SampleStats.ts";
10
7
 
8
+ /** Options for adaptive sampling: collects until statistical convergence or timeout. */
11
9
  export interface AdaptiveOptions extends RunnerOptions {
10
+ /** Enable adaptive sampling (default: true when using adaptive runner) */
12
11
  adaptive?: boolean;
12
+ /** Minimum measurement time in ms before convergence can stop sampling (default: 1000) */
13
13
  minTime?: number;
14
+ /** Maximum measurement time in ms, hard stop (default: 10000) */
14
15
  maxTime?: number;
16
+ /** Target confidence percentage to stop early (default: 95) */
15
17
  targetConfidence?: number;
16
- convergence?: number; // Confidence threshold (0-100)
18
+ /** Confidence threshold 0-100 (alias for targetConfidence) */
19
+ convergence?: number;
17
20
  }
18
21
 
19
22
  type Metrics = {
@@ -39,101 +42,85 @@ const initialBatch = 100;
39
42
  const continueBatch = 100;
40
43
  const continueIterations = 10;
41
44
 
42
- /** @return adaptive sampling runner wrapper */
45
+ /** Wrap a runner with adaptive sampling (convergence detection or timeout). */
43
46
  export function createAdaptiveWrapper(
44
47
  baseRunner: BenchRunner,
45
48
  options: AdaptiveOptions,
46
49
  ): BenchRunner {
47
50
  return {
48
51
  async runBench<T = unknown>(
49
- benchmark: BenchmarkSpec<T>,
50
- runnerOptions: RunnerOptions,
52
+ bench: BenchmarkSpec<T>,
53
+ opts: RunnerOptions,
51
54
  params?: T,
52
55
  ): Promise<MeasuredResults[]> {
53
- return runAdaptiveBench(
54
- baseRunner,
55
- benchmark,
56
- runnerOptions,
57
- options,
58
- params,
59
- );
56
+ return runAdaptiveBench(baseRunner, bench, opts, options, params);
60
57
  },
61
58
  };
62
59
  }
63
60
 
64
- /** @return convergence based on window stability */
61
+ /** Check convergence by comparing sliding windows of samples for stability. */
65
62
  export function checkConvergence(samples: number[]): ConvergenceResult {
66
63
  const windowSize = getWindowSize(samples);
67
64
  const minSamples = windowSize * 2;
68
-
69
65
  if (samples.length < minSamples) {
70
- return buildProgressResult(samples.length, minSamples);
66
+ const confidence = (samples.length / minSamples) * 100;
67
+ const reason = `Collecting samples: ${samples.length}/${minSamples}`;
68
+ return { converged: false, confidence, reason };
71
69
  }
72
-
73
- const metrics = getStability(samples, windowSize);
74
- return buildConvergence(metrics);
70
+ return buildConvergence(getStability(samples, windowSize));
75
71
  }
76
72
 
77
- /** @return results using adaptive sampling strategy */
73
+ /** Run benchmark with adaptive sampling until convergence or timeout. */
78
74
  async function runAdaptiveBench<T>(
79
- baseRunner: BenchRunner,
80
- benchmark: BenchmarkSpec<T>,
81
- runnerOptions: RunnerOptions,
82
- options: AdaptiveOptions,
75
+ runner: BenchRunner,
76
+ bench: BenchmarkSpec<T>,
77
+ opts: RunnerOptions,
78
+ adaptive: AdaptiveOptions,
83
79
  params?: T,
84
80
  ): Promise<MeasuredResults[]> {
85
- const {
86
- minTime: min = options.minTime ?? minTime,
87
- maxTime: max = options.maxTime ?? maxTime,
88
- targetConfidence: target = options.convergence ?? targetConfidence,
89
- } = runnerOptions as AdaptiveOptions;
81
+ const overrides = opts as AdaptiveOptions;
82
+ const min = overrides.minTime ?? adaptive.minTime ?? minTime;
83
+ const max = overrides.maxTime ?? adaptive.maxTime ?? maxTime;
84
+ const target =
85
+ overrides.convergence ?? adaptive.convergence ?? targetConfidence;
90
86
  const allSamples: number[] = [];
91
87
 
92
- // Collect initial batch (includes warmup + settle)
93
- const warmup = await collectInitial(
94
- baseRunner,
95
- benchmark,
96
- runnerOptions,
88
+ const { warmup, startTime: hrtimeStart } = await collectInitial(
89
+ runner,
90
+ bench,
91
+ opts,
97
92
  params,
98
93
  allSamples,
99
94
  );
100
-
101
- // Start timing AFTER warmup - warmup time doesn't count against maxTime
95
+ // Start timing after warmup so warmup time doesn't count against maxTime
102
96
  const startTime = performance.now();
103
-
104
97
  const limits = {
105
98
  minTime: min,
106
99
  maxTime: max,
107
100
  targetConfidence: target,
108
101
  startTime,
109
102
  };
110
- await collectAdaptive(
111
- baseRunner,
112
- benchmark,
113
- runnerOptions,
114
- params,
115
- allSamples,
116
- limits,
117
- );
103
+ await collectAdaptive(runner, bench, opts, params, allSamples, limits);
118
104
 
119
- const convergence = checkConvergence(allSamples.map(s => s * msToNs));
105
+ const samplesNs = allSamples.map(s => s * msToNs);
106
+ const convergence = checkConvergence(samplesNs);
120
107
  return buildResults(
121
108
  allSamples,
122
109
  startTime,
123
110
  convergence,
124
- benchmark.name,
111
+ bench.name,
125
112
  warmup,
113
+ hrtimeStart,
126
114
  );
127
115
  }
128
116
 
129
- /** @return window size scaled to execution time */
117
+ /** Scale window size inversely with execution time -- fast ops need more samples. */
130
118
  function getWindowSize(samples: number[]): number {
131
- if (samples.length < 20) return windowSize; // Default for initial samples
119
+ if (samples.length < 20) return windowSize;
132
120
 
133
121
  const recentMs = samples.slice(-20).map(s => s / msToNs);
134
- const recentMedian = percentile(recentMs, 0.5);
122
+ const recentMedian = median(recentMs);
135
123
 
136
- // Inverse scaling with execution time
137
124
  if (recentMedian < 0.01) return 200; // <10μs
138
125
  if (recentMedian < 0.1) return 100; // <100μs
139
126
  if (recentMedian < 1) return 50; // <1ms
@@ -141,91 +128,67 @@ function getWindowSize(samples: number[]): number {
141
128
  return 20; // >10ms
142
129
  }
143
130
 
144
- /** @return progress when samples insufficient */
145
- function buildProgressResult(
146
- currentSamples: number,
147
- minSamples: number,
148
- ): ConvergenceResult {
149
- return {
150
- converged: false,
151
- confidence: (currentSamples / minSamples) * 100,
152
- reason: `Collecting samples: ${currentSamples}/${minSamples}`,
153
- };
154
- }
155
-
156
- /** @return stability metrics between windows */
157
- function getStability(samples: number[], windowSize: number): Metrics {
158
- const recent = samples.slice(-windowSize);
159
- const previous = samples.slice(-windowSize * 2, -windowSize);
160
-
161
- const recentMs = recent.map(s => s / msToNs);
162
- const previousMs = previous.map(s => s / msToNs);
163
-
164
- const medianRecent = percentile(recentMs, 0.5);
165
- const medianPrevious = percentile(previousMs, 0.5);
166
- const medianDrift = Math.abs(medianRecent - medianPrevious) / medianPrevious;
167
-
168
- const impactRecent = getOutlierImpact(recentMs);
169
- const impactPrevious = getOutlierImpact(previousMs);
170
- const impactDrift = Math.abs(impactRecent.ratio - impactPrevious.ratio);
171
-
172
- return {
173
- medianDrift,
174
- impactDrift,
175
- medianStable: medianDrift < stability,
176
- impactStable: impactDrift < stability,
177
- };
178
- }
179
-
180
- /** @return convergence from stability metrics */
131
+ /** Convert stability metrics to a convergence result with confidence score. */
181
132
  function buildConvergence(metrics: Metrics): ConvergenceResult {
182
133
  const { medianDrift, impactDrift, medianStable, impactStable } = metrics;
183
-
184
- if (medianStable && impactStable) {
134
+ if (medianStable && impactStable)
185
135
  return {
186
136
  converged: true,
187
137
  confidence: 100,
188
138
  reason: "Stable performance pattern",
189
139
  };
190
- }
191
-
192
- const confidence = Math.min(
193
- 100,
194
- (1 - medianDrift / stability) * 50 + (1 - impactDrift / stability) * 50,
195
- );
196
-
140
+ const raw =
141
+ (1 - medianDrift / stability) * 50 + (1 - impactDrift / stability) * 50;
142
+ const confidence = Math.max(0, Math.min(100, raw));
197
143
  const reason =
198
144
  medianDrift > impactDrift
199
145
  ? `Median drifting: ${(medianDrift * 100).toFixed(1)}%`
200
146
  : `Outlier impact changing: ${(impactDrift * 100).toFixed(1)}%`;
147
+ return { converged: false, confidence, reason };
148
+ }
149
+
150
+ /** Compare median and outlier-impact drift between recent and previous windows. */
151
+ function getStability(samples: number[], windowSize: number): Metrics {
152
+ const toMs = (s: number) => s / msToNs;
153
+ const recentMs = samples.slice(-windowSize).map(toMs);
154
+ const previousMs = samples.slice(-windowSize * 2, -windowSize).map(toMs);
201
155
 
202
- return { converged: false, confidence: Math.max(0, confidence), reason };
156
+ const medianRecent = median(recentMs);
157
+ const medianPrevious = median(previousMs);
158
+ const medianDrift = Math.abs(medianRecent - medianPrevious) / medianPrevious;
159
+
160
+ const impactRecent = outlierImpactRatio(recentMs);
161
+ const impactPrevious = outlierImpactRatio(previousMs);
162
+ const impactDrift = Math.abs(impactRecent - impactPrevious);
163
+
164
+ const medianStable = medianDrift < stability;
165
+ const impactStable = impactDrift < stability;
166
+ return { medianDrift, impactDrift, medianStable, impactStable };
203
167
  }
204
168
 
205
- /** @return warmupSamples from initial batch */
169
+ /** Collect the initial batch (warmup + settle), returning warmup samples. */
206
170
  async function collectInitial<T>(
207
- baseRunner: BenchRunner,
208
- benchmark: BenchmarkSpec<T>,
209
- runnerOptions: RunnerOptions,
171
+ runner: BenchRunner,
172
+ bench: BenchmarkSpec<T>,
173
+ opts: RunnerOptions,
210
174
  params: T | undefined,
211
175
  allSamples: number[],
212
- ): Promise<number[] | undefined> {
213
- // Don't pass adaptive flag to base runner to avoid double wrapping
214
- const opts = {
215
- ...(runnerOptions as any),
176
+ ): Promise<{ warmup?: number[]; startTime?: number }> {
177
+ const batchOpts = {
178
+ ...(opts as any),
216
179
  maxTime: initialBatch,
217
180
  maxIterations: undefined,
218
181
  };
219
- const results = await baseRunner.runBench(benchmark, opts, params);
182
+ const results = await runner.runBench(bench, batchOpts, params);
220
183
  appendSamples(results[0], allSamples);
221
- return results[0].warmupSamples;
184
+ return { warmup: results[0].warmupSamples, startTime: results[0].startTime };
222
185
  }
223
186
 
224
- /** @return samples until convergence or timeout */
187
+ /** Collect batches until convergence or timeout, with progress logging. */
225
188
  async function collectAdaptive<T>(
226
- baseRunner: BenchRunner,
227
- benchmark: BenchmarkSpec<T>,
228
- runnerOptions: RunnerOptions,
189
+ runner: BenchRunner,
190
+ bench: BenchmarkSpec<T>,
191
+ opts: RunnerOptions,
229
192
  params: T | undefined,
230
193
  allSamples: number[],
231
194
  limits: {
@@ -242,150 +205,67 @@ async function collectAdaptive<T>(
242
205
  const convergence = checkConvergence(samplesNs);
243
206
  const elapsed = performance.now() - startTime;
244
207
 
245
- if (elapsed - lastLog > 1000) {
246
- const elapsedSec = (elapsed / 1000).toFixed(1);
247
- const conf = convergence.confidence.toFixed(0);
248
- process.stderr.write(
249
- `\r◊ ${benchmark.name}: ${conf}% confident (${elapsedSec}s) `,
250
- );
251
- lastLog = elapsed;
252
- }
253
-
254
- if (shouldStop(convergence, targetConfidence, elapsed, minTime)) {
255
- break;
256
- }
208
+ lastLog = logProgress(bench.name, convergence, elapsed, lastLog);
209
+ if (shouldStop(convergence, targetConfidence, elapsed, minTime)) break;
257
210
 
258
- // Skip warmup for continuation batches (warmup done in initial batch)
259
- const opts = {
260
- ...(runnerOptions as any),
211
+ const batch = {
212
+ ...(opts as any),
261
213
  maxTime: continueBatch,
262
214
  maxIterations: continueIterations,
263
215
  skipWarmup: true,
264
216
  };
265
- const batchResults = await baseRunner.runBench(benchmark, opts, params);
266
- appendSamples(batchResults[0], allSamples);
217
+ const results = await runner.runBench(bench, batch, params);
218
+ appendSamples(results[0], allSamples);
267
219
  }
268
220
  process.stderr.write("\r" + " ".repeat(60) + "\r");
269
221
  }
270
222
 
271
- /** @return measured results with convergence metrics */
223
+ /** Build final MeasuredResults from collected samples and convergence state. */
272
224
  function buildResults(
273
- samplesMs: number[],
274
- startTime: number,
225
+ samples: number[],
226
+ elapsedStart: number,
275
227
  convergence: ConvergenceResult,
276
228
  name: string,
277
229
  warmupSamples?: number[],
230
+ startTime?: number,
278
231
  ): MeasuredResults[] {
279
- const totalTime = (performance.now() - startTime) / 1000;
280
- const samplesNs = samplesMs.map(s => s * msToNs);
281
- const timeStats = computeTimeStats(samplesNs);
282
-
232
+ const totalTime = (performance.now() - elapsedStart) / 1000;
233
+ const time = computeStats(samples);
283
234
  return [
284
- {
285
- name,
286
- samples: samplesMs,
287
- warmupSamples,
288
- time: timeStats,
289
- totalTime,
290
- convergence,
291
- },
235
+ { name, samples, warmupSamples, time, totalTime, startTime, convergence },
292
236
  ];
293
237
  }
294
238
 
295
- /** @return outlier impact as proportion of total time */
296
- function getOutlierImpact(samples: number[]): { ratio: number; count: number } {
297
- if (samples.length === 0) return { ratio: 0, count: 0 };
298
-
299
- const median = percentile(samples, 0.5);
300
- const q75 = percentile(samples, 0.75);
301
- const threshold = median + 1.5 * (q75 - median);
302
-
303
- let excessTime = 0;
304
- let count = 0;
305
-
306
- for (const sample of samples) {
307
- if (sample > threshold) {
308
- excessTime += sample - median;
309
- count++;
310
- }
311
- }
312
-
313
- const totalTime = samples.reduce((a, b) => a + b, 0);
314
- return {
315
- ratio: totalTime > 0 ? excessTime / totalTime : 0,
316
- count,
317
- };
318
- }
319
-
320
- /** Append samples one-by-one to avoid stack overflow from spread on large arrays */
239
+ /** Append samples one-by-one to avoid stack overflow from spread on large arrays. */
321
240
  function appendSamples(result: MeasuredResults, samples: number[]): void {
322
241
  if (!result.samples?.length) return;
323
242
  for (const sample of result.samples) samples.push(sample);
324
243
  }
325
244
 
326
- /** @return true if convergence reached or timeout */
327
- function shouldStop(
245
+ /** Log adaptive sampling progress at ~1s intervals. */
246
+ function logProgress(
247
+ name: string,
328
248
  convergence: ConvergenceResult,
329
- targetConfidence: number,
330
- elapsedTime: number,
331
- minTime: number,
332
- ): boolean {
333
- if (convergence.converged && convergence.confidence >= targetConfidence) {
334
- return true;
335
- }
336
- // After minTime, accept whichever is higher: targetConfidence or fallbackThreshold
337
- const threshold = Math.max(targetConfidence, fallbackThreshold);
338
- return elapsedTime >= minTime && convergence.confidence >= threshold;
249
+ elapsed: number,
250
+ lastLog: number,
251
+ ): number {
252
+ if (elapsed - lastLog <= 1000) return lastLog;
253
+ const sec = (elapsed / 1000).toFixed(1);
254
+ const conf = convergence.confidence.toFixed(0);
255
+ process.stderr.write(`\r◊ ${name}: ${conf}% confident (${sec}s) `);
256
+ return elapsed;
339
257
  }
340
258
 
341
- /** @return time percentiles and statistics in ms */
342
- function computeTimeStats(samplesNs: number[]) {
343
- const samplesMs = samplesNs.map(s => s / msToNs);
344
- const { min, max, sum } = getMinMaxSum(samplesNs);
345
- const percentiles = getPercentiles(samplesNs);
346
- const robust = getRobustMetrics(samplesMs);
347
-
348
- return {
349
- min: min / msToNs,
350
- max: max / msToNs,
351
- avg: sum / samplesNs.length / msToNs,
352
- ...percentiles,
353
- ...robust,
354
- };
355
- }
356
-
357
- /** @return min, max, sum of samples */
358
- function getMinMaxSum(samples: number[]) {
359
- const min = samples.reduce(
360
- (a, b) => Math.min(a, b),
361
- Number.POSITIVE_INFINITY,
362
- );
363
- const max = samples.reduce(
364
- (a, b) => Math.max(a, b),
365
- Number.NEGATIVE_INFINITY,
259
+ /** @return true if convergence target met, or minTime elapsed with fallback confidence. */
260
+ function shouldStop(
261
+ convergence: ConvergenceResult,
262
+ target: number,
263
+ elapsed: number,
264
+ minElapsed: number,
265
+ ): boolean {
266
+ if (convergence.converged && convergence.confidence >= target) return true;
267
+ return (
268
+ elapsed >= minElapsed &&
269
+ convergence.confidence >= Math.max(target, fallbackThreshold)
366
270
  );
367
- const sum = samples.reduce((a, b) => a + b, 0);
368
- return { min, max, sum };
369
- }
370
-
371
- /** @return percentiles in ms */
372
- function getPercentiles(samples: number[]) {
373
- return {
374
- p25: percentile(samples, 0.25) / msToNs,
375
- p50: percentile(samples, 0.5) / msToNs,
376
- p75: percentile(samples, 0.75) / msToNs,
377
- p95: percentile(samples, 0.95) / msToNs,
378
- p99: percentile(samples, 0.99) / msToNs,
379
- p999: percentile(samples, 0.999) / msToNs,
380
- };
381
- }
382
-
383
- /** @return robust variability metrics */
384
- function getRobustMetrics(samplesMs: number[]) {
385
- const impact = getOutlierImpact(samplesMs);
386
- return {
387
- cv: coefficientOfVariation(samplesMs),
388
- mad: medianAbsoluteDeviation(samplesMs),
389
- outlierRate: impact.ratio,
390
- };
391
271
  }
@@ -1,7 +1,7 @@
1
- import type { BenchmarkSpec } from "../Benchmark.ts";
2
- import type { MeasuredResults } from "../MeasuredResults.ts";
1
+ import type { BenchmarkSpec } from "./BenchmarkSpec.ts";
2
+ import type { MeasuredResults } from "./MeasuredResults.ts";
3
3
 
4
- /** Interface for benchmark execution libraries */
4
+ /** Benchmark execution strategy that collects timing samples. */
5
5
  export interface BenchRunner {
6
6
  runBench<T = unknown>(
7
7
  benchmark: BenchmarkSpec<T>,
@@ -10,6 +10,7 @@ export interface BenchRunner {
10
10
  ): Promise<MeasuredResults[]>;
11
11
  }
12
12
 
13
+ /** Configuration for benchmark execution: timing limits, warmup, profiling, and V8 options. */
13
14
  export interface RunnerOptions {
14
15
  /** Minimum time to run each benchmark (milliseconds) */
15
16
  minTime?: number;
@@ -28,13 +29,11 @@ export interface RunnerOptions {
28
29
  /** Minimum samples required - mitata only */
29
30
  minSamples?: number;
30
31
  /** Force GC after each iteration (requires --expose-gc) */
31
- collect?: boolean;
32
- /** Enable CPU performance counters (requires root access) */
33
- cpuCounters?: boolean;
32
+ gcForce?: boolean;
34
33
  /** Trace V8 optimization tiers (requires --allow-natives-syntax) */
35
34
  traceOpt?: boolean;
36
- /** Skip post-warmup settle time (default: false) */
37
- noSettle?: boolean;
35
+ /** Post-warmup settle time in ms for V8 background compilation (0 to skip) */
36
+ pauseWarmup?: number;
38
37
  /** Iterations before first pause (then pauseInterval applies) */
39
38
  pauseFirst?: number;
40
39
  /** Iterations between pauses for V8 optimization (0 to disable) */
@@ -43,15 +42,21 @@ export interface RunnerOptions {
43
42
  pauseDuration?: number;
44
43
  /** Collect GC stats via --trace-gc-nvp (requires worker mode) */
45
44
  gcStats?: boolean;
46
- /** Heap sampling allocation attribution */
47
- heapSample?: boolean;
48
- /** Heap sampling interval in bytes */
49
- heapInterval?: number;
50
- /** Heap sampling stack depth */
51
- heapDepth?: number;
45
+ /** Allocation sampling attribution */
46
+ alloc?: boolean;
47
+ /** Allocation sampling interval in bytes */
48
+ allocInterval?: number;
49
+ /** Allocation sampling stack depth */
50
+ allocDepth?: number;
51
+ /** V8 CPU time sampling */
52
+ profile?: boolean;
53
+ /** CPU sampling interval in microseconds (default 1000) */
54
+ profileInterval?: number;
55
+ /** Collect per-function execution counts via V8 precise coverage */
56
+ callCounts?: boolean;
52
57
  }
53
58
 
54
- /** Execute benchmark with optional parameters */
59
+ /** Invoke the benchmark function, forwarding setup params. */
55
60
  export function executeBenchmark<T>(
56
61
  benchmark: BenchmarkSpec<T>,
57
62
  params?: T,
@@ -1,4 +1,4 @@
1
- /** Single benchmark function specification */
1
+ /** Benchmark function with optional module path for worker-mode serialization. */
2
2
  export interface BenchmarkSpec<T = unknown> {
3
3
  name: string;
4
4
  fn: BenchmarkFunction<T>;
@@ -10,23 +10,22 @@ export interface BenchmarkSpec<T = unknown> {
10
10
  setupExportName?: string;
11
11
  }
12
12
 
13
+ /** Benchmark function, optionally receiving setup parameters from the group. */
13
14
  export type BenchmarkFunction<T = unknown> =
14
15
  | ((params: T) => void)
15
16
  | (() => void);
16
17
 
17
- /** Group of benchmarks with shared setup */
18
+ /** Group of benchmarks with shared setup and optional baseline. */
18
19
  export interface BenchGroup<T = unknown> {
19
20
  name: string;
20
- /** Prepare parameters for all benchmarks in this group */
21
21
  setup?: () => T | Promise<T>;
22
22
  benchmarks: BenchmarkSpec<T>[];
23
- /** Baseline benchmark for comparison */
24
23
  baseline?: BenchmarkSpec<T>;
25
- /** Metadata for reporting (e.g., lines of code) */
24
+ /** Metadata for reporting (e.g. lines of code). */
26
25
  metadata?: Record<string, any>;
27
26
  }
28
27
 
29
- /** Collection of benchmark groups */
28
+ /** Named collection of benchmark groups. */
30
29
  export interface BenchSuite {
31
30
  name: string;
32
31
  groups: BenchGroup<any>[];
@@ -1,11 +1,9 @@
1
- import { BasicRunner } from "./BasicRunner.ts";
2
1
  import type { BenchRunner } from "./BenchRunner.ts";
2
+ import { TimingRunner } from "./TimingRunner.ts";
3
3
 
4
- export type KnownRunner = "basic";
4
+ export type KnownRunner = "timing";
5
5
 
6
- /** @return benchmark runner */
7
- export async function createRunner(
8
- _runnerName: KnownRunner,
9
- ): Promise<BenchRunner> {
10
- return new BasicRunner();
6
+ /** Create a benchmark runner by name. */
7
+ export async function createRunner(_name: KnownRunner): Promise<BenchRunner> {
8
+ return new TimingRunner();
11
9
  }