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
@@ -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
+ }
@@ -0,0 +1,244 @@
1
+ import { getHeapStatistics } from "node:v8";
2
+ import type { BenchmarkSpec } from "./BenchmarkSpec.ts";
3
+ import type { BenchRunner, RunnerOptions } from "./BenchRunner.ts";
4
+ import { executeBenchmark } from "./BenchRunner.ts";
5
+ import type {
6
+ MeasuredResults,
7
+ OptStatusInfo,
8
+ PausePoint,
9
+ } from "./MeasuredResults.ts";
10
+ import {
11
+ analyzeOptStatus,
12
+ computeStats,
13
+ createOptStatusGetter,
14
+ gcFunction,
15
+ } from "./SampleStats.ts";
16
+
17
+ type CollectParams<T = unknown> = {
18
+ benchmark: BenchmarkSpec<T>;
19
+ maxTime: number;
20
+ maxIterations: number;
21
+ warmup: number;
22
+ params?: T;
23
+ skipWarmup?: boolean;
24
+ traceOpt?: boolean;
25
+ pauseWarmup?: number;
26
+ pauseFirst?: number;
27
+ pauseInterval?: number;
28
+ pauseDuration?: number;
29
+ };
30
+
31
+ type CollectResult = {
32
+ samples: number[];
33
+ warmupSamples: number[];
34
+ heapGrowth: number;
35
+ heapSamples: number[];
36
+ startTime: number;
37
+ optStatus?: OptStatusInfo;
38
+ optSamples?: number[];
39
+ pausePoints: PausePoint[];
40
+ };
41
+
42
+ type SampleArrays = {
43
+ samples: number[];
44
+ heapSamples: number[];
45
+ optStatuses: number[];
46
+ pausePoints: PausePoint[];
47
+ };
48
+
49
+ const defaultCollectOptions = {
50
+ maxTime: 5000,
51
+ maxIterations: 1000000,
52
+ warmup: 0,
53
+ traceOpt: false,
54
+ pauseWarmup: 0,
55
+ };
56
+
57
+ /**
58
+ * Timing-based runner that collects samples within time/iteration limits.
59
+ * Handles warmup, heap tracking, V8 optimization tracing, and periodic pauses.
60
+ */
61
+ export class TimingRunner implements BenchRunner {
62
+ async runBench<T = unknown>(
63
+ benchmark: BenchmarkSpec<T>,
64
+ options: RunnerOptions,
65
+ params?: T,
66
+ ): Promise<MeasuredResults[]> {
67
+ const opts = { ...defaultCollectOptions, ...(options as any) };
68
+ const collected = await collectSamples({ benchmark, params, ...opts });
69
+ return [buildMeasuredResults(benchmark.name, collected)];
70
+ }
71
+ }
72
+
73
+ /** Collect timing samples with warmup, heap tracking, and optional V8 opt tracing. */
74
+ async function collectSamples<T>(
75
+ config: CollectParams<T>,
76
+ ): Promise<CollectResult> {
77
+ if (!config.maxIterations && !config.maxTime) {
78
+ throw new Error(`At least one of maxIterations or maxTime must be set`);
79
+ }
80
+ const warmupSamples = config.skipWarmup ? [] : await runWarmup(config);
81
+ const heapBefore = process.memoryUsage().heapUsed;
82
+ const { samples, heapSamples, optStatuses, pausePoints, startTime } =
83
+ await runSampleLoop(config);
84
+ if (samples.length === 0)
85
+ throw new Error(
86
+ `No samples collected for benchmark: ${config.benchmark.name}`,
87
+ );
88
+ const heapAfter = process.memoryUsage().heapUsed;
89
+ const heapGrowth =
90
+ Math.max(0, heapAfter - heapBefore) / 1024 / samples.length;
91
+ const optStatus = config.traceOpt
92
+ ? analyzeOptStatus(samples, optStatuses)
93
+ : undefined;
94
+ const optSamples =
95
+ config.traceOpt && optStatuses.length > 0 ? optStatuses : undefined;
96
+ return {
97
+ samples,
98
+ warmupSamples,
99
+ heapGrowth,
100
+ heapSamples,
101
+ startTime,
102
+ optStatus,
103
+ optSamples,
104
+ pausePoints,
105
+ };
106
+ }
107
+
108
+ /** Assemble CollectResult into a MeasuredResults record. */
109
+ function buildMeasuredResults(
110
+ name: string,
111
+ collected: CollectResult,
112
+ ): MeasuredResults {
113
+ const { samples, warmupSamples, heapSamples } = collected;
114
+ const { optStatus, optSamples, pausePoints, heapGrowth, startTime } =
115
+ collected;
116
+ const time = computeStats(samples);
117
+ const heapSize = { avg: heapGrowth, min: heapGrowth, max: heapGrowth };
118
+ return {
119
+ name,
120
+ samples,
121
+ warmupSamples,
122
+ heapSamples,
123
+ time,
124
+ heapSize,
125
+ startTime,
126
+ optStatus,
127
+ optSamples,
128
+ pausePoints,
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Run warmup iterations with gc + settle time for V8 optimization. Returns warmup timings.
134
+ *
135
+ * V8 has 4 compilation tiers: Ignition (interpreter) ==> Sparkplug (baseline) ==>
136
+ * Maglev (mid-tier optimizer) ==> TurboFan (full optimizer). Tiering thresholds:
137
+ * - Ignition ==> Sparkplug: 8 invocations
138
+ * - Sparkplug ==> Maglev: 500 invocations
139
+ * - Maglev ==> TurboFan: 6000 invocations
140
+ *
141
+ * Optimization compilation happens on background threads and requires idle time
142
+ * on the main thread to complete. Without sufficient warmup + settle time,
143
+ * benchmarks exhibit bimodal timing: slow Sparkplug samples (~30% slower) mixed
144
+ * with fast optimized samples.
145
+ *
146
+ * The warmup iterations trigger the optimization decision, then settle time
147
+ * provides idle time for background compilation to finish before measurement.
148
+ *
149
+ * @see https://v8.dev/blog/sparkplug
150
+ * @see https://v8.dev/blog/maglev
151
+ * @see https://v8.dev/blog/background-compilation
152
+ */
153
+ async function runWarmup<T>(config: CollectParams<T>): Promise<number[]> {
154
+ const gc = gcFunction();
155
+ const samples = new Array<number>(config.warmup);
156
+ for (let i = 0; i < config.warmup; i++) {
157
+ const start = performance.now();
158
+ executeBenchmark(config.benchmark, config.params);
159
+ samples[i] = performance.now() - start;
160
+ }
161
+ gc();
162
+ if (config.pauseWarmup) {
163
+ await new Promise(r => setTimeout(r, config.pauseWarmup));
164
+ gc();
165
+ }
166
+ return samples;
167
+ }
168
+
169
+ /** Collect timing samples with optional periodic pauses for V8 background compilation to complete. */
170
+ async function runSampleLoop<T>(
171
+ config: CollectParams<T>,
172
+ ): Promise<SampleArrays & { startTime: number }> {
173
+ const { maxTime, maxIterations, pauseFirst } = config;
174
+ const { pauseInterval = 0, pauseDuration = 100 } = config;
175
+ const getOptStatus = config.traceOpt ? createOptStatusGetter() : undefined;
176
+ const trackOpt = !!getOptStatus;
177
+ const estimated = maxIterations || Math.ceil(maxTime / 0.1);
178
+ const arrays = createSampleArrays(estimated, trackOpt);
179
+
180
+ let count = 0;
181
+ let elapsed = 0;
182
+ let totalPauseTime = 0;
183
+ const startTime = Number(process.hrtime.bigint() / 1000n);
184
+ const loopStart = performance.now();
185
+
186
+ while (
187
+ (!maxIterations || count < maxIterations) &&
188
+ (!maxTime || elapsed < maxTime)
189
+ ) {
190
+ const start = performance.now();
191
+ executeBenchmark(config.benchmark, config.params);
192
+ const end = performance.now();
193
+ arrays.samples[count] = end - start;
194
+ arrays.heapSamples[count] = getHeapStatistics().used_heap_size;
195
+ if (getOptStatus)
196
+ arrays.optStatuses[count] = getOptStatus(config.benchmark.fn);
197
+ count++;
198
+
199
+ if (shouldPause(count, pauseFirst, pauseInterval)) {
200
+ const sampleIndex = count - 1;
201
+ arrays.pausePoints.push({ sampleIndex, durationMs: pauseDuration });
202
+ const pauseStart = performance.now();
203
+ await new Promise(r => setTimeout(r, pauseDuration));
204
+ totalPauseTime += performance.now() - pauseStart;
205
+ }
206
+ elapsed = performance.now() - loopStart - totalPauseTime;
207
+ }
208
+
209
+ trimArrays(arrays, count, trackOpt);
210
+ return { ...arrays, startTime };
211
+ }
212
+
213
+ /** Pre-allocate sample arrays to reduce GC pressure during measurement. */
214
+ function createSampleArrays(n: number, trackOpt: boolean): SampleArrays {
215
+ const arr = () => new Array<number>(n);
216
+ return {
217
+ samples: arr(),
218
+ heapSamples: arr(),
219
+ optStatuses: trackOpt ? arr() : [],
220
+ pausePoints: [],
221
+ };
222
+ }
223
+
224
+ /** @return true if this iteration should pause for V8 background compilation. */
225
+ function shouldPause(
226
+ iter: number,
227
+ first: number | undefined,
228
+ interval: number,
229
+ ): boolean {
230
+ if (first !== undefined && iter === first) return true;
231
+ if (interval <= 0) return false;
232
+ if (first === undefined) return iter % interval === 0;
233
+ return (iter - first) % interval === 0;
234
+ }
235
+
236
+ /** Trim pre-allocated arrays to the actual sample count. */
237
+ function trimArrays(
238
+ arrays: SampleArrays,
239
+ count: number,
240
+ trackOpt: boolean,
241
+ ): void {
242
+ arrays.samples.length = arrays.heapSamples.length = count;
243
+ if (trackOpt) arrays.optStatuses.length = count;
244
+ }
@@ -1,11 +1,12 @@
1
+ /** Toggle for worker process timing logs (manual, not exposed as CLI flag) */
1
2
  export const debugWorkerTiming = false;
2
3
 
3
- /** Get current time or 0 if debugging disabled */
4
+ /** Current time in ms, or 0 when debug timing is off (zero-cost no-op) */
4
5
  export function getPerfNow(): number {
5
6
  return debugWorkerTiming ? performance.now() : 0;
6
7
  }
7
8
 
8
- /** Calculate elapsed milliseconds between marks */
9
+ /** Elapsed ms between marks, or 0 when debug timing is off */
9
10
  export function getElapsed(startMark: number, endMark?: number): number {
10
11
  if (!debugWorkerTiming) return 0;
11
12
  const end = endMark ?? performance.now();