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
@@ -0,0 +1,130 @@
1
+ import { expect, test } from "vitest";
2
+ import type { BenchmarkReport } from "../report/BenchmarkReport.ts";
3
+ import { computeColumnValues } from "../report/BenchmarkReport.ts";
4
+ import { buildTimeSection } from "../report/StandardSections.ts";
5
+ import { reportResults, valuesForReports } from "../report/text/TextReport.ts";
6
+ import type { MeasuredResults } from "../runners/MeasuredResults.ts";
7
+
8
+ /** @return minimal MeasuredResults with the given samples (time fields derived trivially). */
9
+ function measured(samples: number[]): MeasuredResults {
10
+ const sorted = [...samples].sort((a, b) => a - b);
11
+ return {
12
+ name: "t",
13
+ samples,
14
+ time: {
15
+ min: sorted[0],
16
+ max: sorted[sorted.length - 1],
17
+ avg: samples.reduce((a, b) => a + b, 0) / samples.length,
18
+ p50: sorted[Math.floor(sorted.length * 0.5)],
19
+ p75: sorted[Math.floor(sorted.length * 0.75)],
20
+ p99: sorted[Math.floor(sorted.length * 0.99)],
21
+ p999: sorted[Math.floor(sorted.length * 0.999)],
22
+ },
23
+ };
24
+ }
25
+
26
+ function report(name: string, samples: number[]): BenchmarkReport {
27
+ return { name, measuredResults: measured(samples) };
28
+ }
29
+
30
+ function range(n: number): number[] {
31
+ return Array.from({ length: n }, (_, i) => i + 1);
32
+ }
33
+
34
+ test("default buildTimeSection produces mean, p50, p99 columns", () => {
35
+ const section = buildTimeSection();
36
+ expect(section.columns.map(c => c.key ?? c.title)).toEqual([
37
+ "mean",
38
+ "p50",
39
+ "p99",
40
+ ]);
41
+ });
42
+
43
+ test("computeColumnValues computes values from samples", () => {
44
+ const section = buildTimeSection("mean,p50,max,min");
45
+ const row = computeColumnValues(section, measured([10, 20, 30, 40, 50]));
46
+ expect(row.mean).toBe(30);
47
+ expect(row.min).toBe(10);
48
+ expect(row.max).toBe(50);
49
+ expect(row.p50).toBeGreaterThanOrEqual(20);
50
+ expect(row.p50).toBeLessThanOrEqual(40);
51
+ });
52
+
53
+ test("p70 returns value near 70th percentile of [1..100]", () => {
54
+ const section = buildTimeSection("p70");
55
+ const row = computeColumnValues(section, measured(range(100)));
56
+ expect(row.p70).toBeGreaterThanOrEqual(69);
57
+ expect(row.p70).toBeLessThanOrEqual(71);
58
+ });
59
+
60
+ test("p999 uses divide-by-1000 convention", () => {
61
+ const section = buildTimeSection("p999");
62
+ const row = computeColumnValues(section, measured(range(1000)));
63
+ expect(row.p999).toBeGreaterThanOrEqual(999);
64
+ });
65
+
66
+ test("p9999 uses divide-by-10000 convention", () => {
67
+ const section = buildTimeSection("p9999");
68
+ const row = computeColumnValues(section, measured(range(10000)));
69
+ expect(row.p9999).toBeGreaterThanOrEqual(9999);
70
+ });
71
+
72
+ test("median and p50 produce the same value", () => {
73
+ const a = computeColumnValues(
74
+ buildTimeSection("median"),
75
+ measured(range(100)),
76
+ );
77
+ const b = computeColumnValues(buildTimeSection("p50"), measured(range(100)));
78
+ expect(a.p50).toBe(b.p50);
79
+ });
80
+
81
+ test("mean and avg dedupe to a single column", () => {
82
+ const section = buildTimeSection("mean,avg");
83
+ expect(section.columns.length).toBe(1);
84
+ });
85
+
86
+ test("min and max return exact values", () => {
87
+ const section = buildTimeSection("min,max");
88
+ const row = computeColumnValues(section, measured([5, 1, 9, 3, 7]));
89
+ expect(row.min).toBe(1);
90
+ expect(row.max).toBe(9);
91
+ });
92
+
93
+ test("empty stats string throws", () => {
94
+ expect(() => buildTimeSection("")).toThrow(/at least one column/);
95
+ expect(() => buildTimeSection(" , ")).toThrow(/at least one column/);
96
+ });
97
+
98
+ test("unknown token throws with vocabulary hint", () => {
99
+ expect(() => buildTimeSection("wat")).toThrow(
100
+ /expected mean, median, min, max, or p<N>/,
101
+ );
102
+ });
103
+
104
+ test("single-digit percentile token is rejected", () => {
105
+ expect(() => buildTimeSection("p5")).toThrow(/at least 2 digits/);
106
+ });
107
+
108
+ test("3+ digit percentile tokens not starting with 9 are rejected", () => {
109
+ expect(() => buildTimeSection("p100")).toThrow(/must start with 9/);
110
+ expect(() => buildTimeSection("p500")).toThrow(/must start with 9/);
111
+ expect(() => buildTimeSection("p1000")).toThrow(/must start with 9/);
112
+ });
113
+
114
+ test("reportResults renders user-chosen columns as table headers", () => {
115
+ const groups = [{ name: "g", reports: [report("bench", range(100))] }];
116
+ const table = reportResults(groups, [buildTimeSection("p70,p95")]);
117
+ expect(table).toContain("p70");
118
+ expect(table).toContain("p95");
119
+ });
120
+
121
+ test("valuesForReports extracts user-chosen keys", () => {
122
+ const rows = valuesForReports(
123
+ [report("bench", range(100))],
124
+ [buildTimeSection("p70,p95")],
125
+ );
126
+ expect(rows[0].p70).toBeGreaterThanOrEqual(69);
127
+ expect(rows[0].p70).toBeLessThanOrEqual(71);
128
+ expect(rows[0].p95).toBeGreaterThanOrEqual(94);
129
+ expect(rows[0].p95).toBeLessThanOrEqual(96);
130
+ });
@@ -0,0 +1,82 @@
1
+ import { expect, test } from "vitest";
2
+ import { sampleDifferenceCI } from "../stats/BootstrapDifference.ts";
3
+ import {
4
+ average,
5
+ maxBootstrapInput,
6
+ percentile,
7
+ sampleBootstrap,
8
+ } from "../stats/StatisticalUtils.ts";
9
+
10
+ test("sampleBootstrap uses full samples for point estimate", () => {
11
+ const samples = Array.from({ length: 5000 }, (_, i) => i);
12
+ const result = sampleBootstrap(samples, average, { resamples: 100 });
13
+ expect(result.estimate).toBe(average(samples));
14
+ });
15
+
16
+ test("sampleDifferenceCI preserves point estimate", () => {
17
+ const a = Array.from({ length: 5000 }, () => 50 + Math.random() * 10);
18
+ const b = a.map(v => v * 1.1);
19
+ const result = sampleDifferenceCI(a, b, average, { resamples: 100 });
20
+ const expected = ((average(b) - average(a)) / average(a)) * 100;
21
+ expect(result.percent).toBeCloseTo(expected, 10);
22
+ });
23
+
24
+ test("sampleBootstrap point estimate uses full array when capped", () => {
25
+ const n = maxBootstrapInput + 5000;
26
+ const samples = Array.from({ length: n }, (_, i) => i);
27
+ const result = sampleBootstrap(samples, average, { resamples: 50 });
28
+ expect(result.estimate).toBe(average(samples));
29
+ expect(result.subsampled).toBe(n);
30
+ });
31
+
32
+ test("sampleBootstrap does not set subsampled when under cap", () => {
33
+ const samples = Array.from({ length: 100 }, (_, i) => i);
34
+ const result = sampleBootstrap(samples, average, { resamples: 50 });
35
+ expect(result.subsampled).toBeUndefined();
36
+ });
37
+
38
+ test("sampleDifferenceCI sets subsampled when inputs exceed cap", () => {
39
+ const n = maxBootstrapInput + 1000;
40
+ const a = Array.from({ length: n }, () => 50 + Math.random() * 10);
41
+ const b = a.map(v => v * 1.1);
42
+ const result = sampleDifferenceCI(a, b, average, { resamples: 50 });
43
+ expect(result.percent).toBeCloseTo(10, 0);
44
+ expect(result.subsampled).toBe(n);
45
+ });
46
+
47
+ test("sampleDifferenceCI no subsampled flag when under cap", () => {
48
+ const a = Array.from({ length: 100 }, () => 50 + Math.random() * 10);
49
+ const b = a.map(v => v * 1.1);
50
+ const result = sampleDifferenceCI(a, b, average, { resamples: 50 });
51
+ expect(result.subsampled).toBeUndefined();
52
+ });
53
+
54
+ test("quickselect-based percentile matches sorted percentile", () => {
55
+ const data = Array.from({ length: 1000 }, () => Math.random() * 100);
56
+ const sorted = [...data].sort((a, b) => a - b);
57
+ for (const p of [0.25, 0.5, 0.75, 0.99]) {
58
+ const k = Math.max(0, Math.ceil(sorted.length * p) - 1);
59
+ expect(percentile(data, p)).toBe(sorted[k]);
60
+ }
61
+ });
62
+
63
+ test("quickselect handles small arrays", () => {
64
+ expect(percentile([42], 0.5)).toBe(42);
65
+ expect(percentile([1, 2], 0.5)).toBe(1);
66
+ expect(percentile([1, 2], 1.0)).toBe(2);
67
+ });
68
+
69
+ test("quickselect handles duplicate values", () => {
70
+ const data = [5, 5, 5, 5, 5, 10, 10, 10, 10, 10];
71
+ expect(percentile(data, 0.5)).toBe(5);
72
+ expect(percentile(data, 0.99)).toBe(10);
73
+ });
74
+
75
+ test("sampleBootstrap reuses buffer (no per-iteration allocation)", () => {
76
+ const samples = [10, 20, 30, 40, 50];
77
+ const result = sampleBootstrap(samples, average, { resamples: 50 });
78
+ expect(result.estimate).toBe(average(samples));
79
+ expect(result.samples).toHaveLength(50);
80
+ expect(result.ci[0]).toBeLessThanOrEqual(result.estimate);
81
+ expect(result.ci[1]).toBeGreaterThanOrEqual(result.estimate);
82
+ });
@@ -0,0 +1,115 @@
1
+ import { expect, test } from "vitest";
2
+ import {
3
+ annotateFramesWithCounts,
4
+ buildCoverageMap,
5
+ } from "../export/CoverageExport.ts";
6
+ import type { CoverageData } from "../profiling/node/CoverageTypes.ts";
7
+
8
+ const source = `function foo() {
9
+ return 1;
10
+ }
11
+ function bar() {
12
+ return 2;
13
+ }
14
+ const baz = () => 3;
15
+ `;
16
+
17
+ const coverage: CoverageData = {
18
+ scripts: [
19
+ {
20
+ url: "file:///test.js",
21
+ functions: [
22
+ {
23
+ functionName: "foo",
24
+ ranges: [{ startOffset: 0, endOffset: 30, count: 10 }],
25
+ },
26
+ {
27
+ functionName: "bar",
28
+ ranges: [{ startOffset: 31, endOffset: 60, count: 5 }],
29
+ },
30
+ {
31
+ functionName: "",
32
+ ranges: [{ startOffset: 61, endOffset: 80, count: 3 }],
33
+ },
34
+ ],
35
+ },
36
+ ],
37
+ };
38
+
39
+ test("buildCoverageMap resolves offsets to lines", () => {
40
+ const result = buildCoverageMap(coverage, { "file:///test.js": source });
41
+
42
+ expect(result.map.has("file:///test.js")).toBe(true);
43
+ const entries = result.map.get("file:///test.js")!;
44
+ expect(entries).toHaveLength(3);
45
+
46
+ const foo = entries.find(e => e.functionName === "foo");
47
+ expect(foo).toBeDefined();
48
+ expect(foo!.startLine).toBe(1);
49
+ expect(foo!.count).toBe(10);
50
+
51
+ const bar = entries.find(e => e.functionName === "bar");
52
+ expect(bar).toBeDefined();
53
+ expect(bar!.startLine).toBe(4);
54
+ expect(bar!.count).toBe(5);
55
+
56
+ // byName aggregates across all scripts
57
+ expect(result.byName.get("foo")).toBe(10);
58
+ expect(result.byName.get("bar")).toBe(5);
59
+ });
60
+
61
+ test("annotateFramesWithCounts appends [N] to matched frames", () => {
62
+ const result = buildCoverageMap(coverage, { "file:///test.js": source });
63
+
64
+ const frames = [
65
+ { name: "foo", file: "file:///test.js", line: 1 },
66
+ { name: "bar", file: "file:///test.js", line: 4 },
67
+ { name: "unmatched", file: "file:///other.js", line: 1 },
68
+ ];
69
+
70
+ annotateFramesWithCounts(frames, result);
71
+
72
+ expect(frames[0].name).toBe("foo [10]");
73
+ expect(frames[1].name).toBe("bar [5]");
74
+ expect(frames[2].name).toBe("unmatched"); // no coverage data for this file
75
+ });
76
+
77
+ test("annotateFramesWithCounts falls back to name-only for frames without file", () => {
78
+ const result = buildCoverageMap(coverage, { "file:///test.js": source });
79
+
80
+ const frames = [
81
+ { name: "foo" }, // no file — should match by name
82
+ { name: "bar" },
83
+ { name: "(anonymous)" }, // anonymous — should not match by name
84
+ ];
85
+
86
+ annotateFramesWithCounts(frames, result);
87
+
88
+ expect(frames[0].name).toBe("foo [10]");
89
+ expect(frames[1].name).toBe("bar [5]");
90
+ expect(frames[2].name).toBe("(anonymous)");
91
+ });
92
+
93
+ test("annotateFramesWithCounts formats large counts", () => {
94
+ const bigCoverage: CoverageData = {
95
+ scripts: [
96
+ {
97
+ url: "file:///big.js",
98
+ functions: [
99
+ {
100
+ functionName: "hot",
101
+ ranges: [{ startOffset: 0, endOffset: 10, count: 1_500_000 }],
102
+ },
103
+ ],
104
+ },
105
+ ],
106
+ };
107
+ const result = buildCoverageMap(bigCoverage, {
108
+ "file:///big.js": "function hot() {}",
109
+ });
110
+ const frames = [{ name: "hot", file: "file:///big.js", line: 1 }];
111
+
112
+ annotateFramesWithCounts(frames, result);
113
+
114
+ expect(frames[0].name).toBe("hot [1.5M]");
115
+ });
@@ -0,0 +1,33 @@
1
+ import { expect, test } from "vitest";
2
+ import { withCoverageProfiling } from "../profiling/node/CoverageSampler.ts";
3
+
4
+ test("withCoverageProfiling returns function execution counts", async () => {
5
+ function hotFunction() {
6
+ let sum = 0;
7
+ for (let i = 0; i < 100; i++) sum += i;
8
+ return sum;
9
+ }
10
+
11
+ const { result, coverage } = await withCoverageProfiling(_session => {
12
+ for (let i = 0; i < 10; i++) hotFunction();
13
+ return 42;
14
+ });
15
+
16
+ expect(result).toBe(42);
17
+ expect(coverage.scripts.length).toBeGreaterThan(0);
18
+
19
+ // Find our test file in the coverage data
20
+ const thisScript = coverage.scripts.find(s =>
21
+ s.url.includes("CoverageSampler.test"),
22
+ );
23
+ expect(thisScript).toBeDefined();
24
+ expect(thisScript!.functions.length).toBeGreaterThan(0);
25
+
26
+ // Find hotFunction and verify its count
27
+ const hotFn = thisScript!.functions.find(
28
+ f => f.functionName === "hotFunction",
29
+ );
30
+ expect(hotFn).toBeDefined();
31
+ const count = hotFn!.ranges[0].count;
32
+ expect(count).toBe(10);
33
+ });
@@ -2,12 +2,12 @@ import { expect, test } from "vitest";
2
2
  import {
3
3
  aggregateSites,
4
4
  type HeapSite,
5
- } from "../heap-sample/HeapSampleReport.ts";
5
+ } from "../profiling/node/HeapSampleReport.ts";
6
6
 
7
7
  test("unknown column does not merge distinct functions on same line", () => {
8
8
  const sites: HeapSite[] = [
9
- { fn: "Foo", url: "test.ts", line: 10, col: undefined, bytes: 100 },
10
- { fn: "Bar", url: "test.ts", line: 10, col: undefined, bytes: 200 },
9
+ { name: "Foo", url: "test.ts", line: 10, col: undefined, bytes: 100 },
10
+ { name: "Bar", url: "test.ts", line: 10, col: undefined, bytes: 200 },
11
11
  ];
12
12
  const aggregated = aggregateSites(sites);
13
13
  expect(aggregated).toHaveLength(2);
@@ -15,8 +15,8 @@ test("unknown column does not merge distinct functions on same line", () => {
15
15
 
16
16
  test("same column merges regardless of function name", () => {
17
17
  const sites: HeapSite[] = [
18
- { fn: "Foo", url: "test.ts", line: 10, col: 5, bytes: 100 },
19
- { fn: "Foo", url: "test.ts", line: 10, col: 5, bytes: 200 },
18
+ { name: "Foo", url: "test.ts", line: 10, col: 5, bytes: 100 },
19
+ { name: "Foo", url: "test.ts", line: 10, col: 5, bytes: 200 },
20
20
  ];
21
21
  const aggregated = aggregateSites(sites);
22
22
  expect(aggregated).toHaveLength(1);
@@ -25,18 +25,18 @@ test("same column merges regardless of function name", () => {
25
25
 
26
26
  test("aggregation preserves distinct caller stacks", () => {
27
27
  const stackA = [
28
- { fn: "root", url: "a.ts", line: 1, col: 0 },
29
- { fn: "foo", url: "a.ts", line: 10, col: 0 },
30
- { fn: "alloc", url: "a.ts", line: 20, col: 5 },
28
+ { name: "root", url: "a.ts", line: 1, col: 0 },
29
+ { name: "foo", url: "a.ts", line: 10, col: 0 },
30
+ { name: "alloc", url: "a.ts", line: 20, col: 5 },
31
31
  ];
32
32
  const stackB = [
33
- { fn: "root", url: "a.ts", line: 1, col: 0 },
34
- { fn: "bar", url: "a.ts", line: 15, col: 0 },
35
- { fn: "alloc", url: "a.ts", line: 20, col: 5 },
33
+ { name: "root", url: "a.ts", line: 1, col: 0 },
34
+ { name: "bar", url: "a.ts", line: 15, col: 0 },
35
+ { name: "alloc", url: "a.ts", line: 20, col: 5 },
36
36
  ];
37
37
  const sites: HeapSite[] = [
38
- { fn: "alloc", url: "a.ts", line: 20, col: 5, bytes: 800, stack: stackA },
39
- { fn: "alloc", url: "a.ts", line: 20, col: 5, bytes: 200, stack: stackB },
38
+ { name: "alloc", url: "a.ts", line: 20, col: 5, bytes: 800, stack: stackA },
39
+ { name: "alloc", url: "a.ts", line: 20, col: 5, bytes: 200, stack: stackB },
40
40
  ];
41
41
  const aggregated = aggregateSites(sites);
42
42
 
@@ -44,7 +44,7 @@ test("aggregation preserves distinct caller stacks", () => {
44
44
  expect(aggregated[0].bytes).toBe(1000);
45
45
  expect(aggregated[0].callers).toHaveLength(2);
46
46
  // Primary stack should be the highest-bytes path (foo)
47
- expect(aggregated[0].stack![1].fn).toBe("foo");
47
+ expect(aggregated[0].stack![1].name).toBe("foo");
48
48
  // Callers sorted by bytes descending
49
49
  expect(aggregated[0].callers![0].bytes).toBe(800);
50
50
  expect(aggregated[0].callers![1].bytes).toBe(200);
@@ -1,5 +1,5 @@
1
1
  import { expect, test } from "vitest";
2
- import type { BenchMatrix } from "../BenchMatrix.ts";
2
+ import type { BenchMatrix } from "../matrix/BenchMatrix.ts";
3
3
  import { filterMatrix, parseMatrixFilter } from "../matrix/MatrixFilter.ts";
4
4
 
5
5
  const inlineMatrix: BenchMatrix<string> = {
@@ -1,5 +1,5 @@
1
1
  import { expect, test } from "vitest";
2
- import type { CaseResult, MatrixResults } from "../BenchMatrix.ts";
2
+ import type { CaseResult, MatrixResults } from "../matrix/BenchMatrix.ts";
3
3
  import { reportMatrixResults } from "../matrix/MatrixReport.ts";
4
4
 
5
5
  /** Create simple measured results for testing */
@@ -1,5 +1,5 @@
1
1
  import { expect, test } from "vitest";
2
- import { compareWithBaseline } from "../PermutationTest.ts";
2
+ import { compareWithBaseline } from "../stats/PermutationTest.ts";
3
3
  import { assertValid, getSampleData } from "./TestUtils.ts";
4
4
 
5
5
  test("detects 20% performance improvement", () => {
@@ -4,10 +4,10 @@ import {
4
4
  coefficientOfVariation,
5
5
  medianAbsoluteDeviation,
6
6
  percentile,
7
- } from "../StatisticalUtils.ts";
7
+ } from "../stats/StatisticalUtils.ts";
8
8
  import { bevy30SamplesMs, bevy30SamplesNs } from "./fixtures/bevy30-samples.ts";
9
9
 
10
- test("bevy30 data characteristics", () => {
10
+ test.skip("bevy30 data characteristics", () => {
11
11
  const sortedMs = [...bevy30SamplesMs].sort((a, b) => a - b);
12
12
 
13
13
  const stats = {
@@ -48,7 +48,7 @@ test("bevy30 data characteristics", () => {
48
48
  if (stats.cv > 0.5) console.warn("Very high variation - may be unstable");
49
49
  });
50
50
 
51
- test("convergence at different time points matches CLI behavior", () => {
51
+ test.skip("convergence at different time points matches CLI behavior", () => {
52
52
  // Simulate 5-second run (approximately 100 samples at ~50ms each)
53
53
  const samples5s = bevy30SamplesNs.slice(0, 100);
54
54
  const result5s = checkConvergence(samples5s);
@@ -76,7 +76,7 @@ test("convergence at different time points matches CLI behavior", () => {
76
76
  );
77
77
  });
78
78
 
79
- test("warm-up detection in real data", () => {
79
+ test.skip("warm-up detection in real data", () => {
80
80
  const windowSize = 20;
81
81
  const windows: Array<{ start: number; median: number }> = [];
82
82
 
@@ -104,7 +104,7 @@ test("warm-up detection in real data", () => {
104
104
  }
105
105
  });
106
106
 
107
- test("convergence stability over sliding windows", () => {
107
+ test.skip("convergence stability over sliding windows", () => {
108
108
  const windowSize = 100;
109
109
  const step = 50;
110
110
  const history: Array<{ start: number; confidence: number }> = [];
@@ -132,7 +132,7 @@ test("convergence stability over sliding windows", () => {
132
132
  }
133
133
  });
134
134
 
135
- test("adaptive algorithm would stop at correct time", () => {
135
+ test.skip("adaptive algorithm would stop at correct time", () => {
136
136
  const target = 95;
137
137
  const fallback = 80;
138
138
  const minSamples = 50;
@@ -1,8 +1,8 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import { expect, test } from "vitest";
4
- import type { BenchSuite } from "../Benchmark.ts";
5
4
  import { filterBenchmarks } from "../cli/FilterBenchmarks.ts";
5
+ import type { BenchSuite } from "../runners/BenchmarkSpec.ts";
6
6
  import { runBenchCLITest } from "./TestUtils.ts";
7
7
 
8
8
  const testSuite: BenchSuite = {
@@ -68,7 +68,7 @@ function executeBenchforgeFile(file: string, args = ""): string {
68
68
  }
69
69
 
70
70
  test("runs all benchmarks", { timeout: 30000 }, async () => {
71
- const output = await runBenchCLITest(testSuite, "--time 0.1");
71
+ const output = await runBenchCLITest(testSuite, "--duration 0.1");
72
72
 
73
73
  expect(output).toContain("concatenation");
74
74
  expect(output).toContain("template literal");
@@ -79,7 +79,10 @@ test("runs all benchmarks", { timeout: 30000 }, async () => {
79
79
  });
80
80
 
81
81
  test("filters by substring", { timeout: 15000 }, async () => {
82
- const output = await runBenchCLITest(testSuite, "--filter concat --time 0.1");
82
+ const output = await runBenchCLITest(
83
+ testSuite,
84
+ "--filter concat --duration 0.1",
85
+ );
83
86
 
84
87
  expect(output).toContain("concatenation");
85
88
  expect(output).not.toContain("addition");
@@ -88,7 +91,7 @@ test("filters by substring", { timeout: 15000 }, async () => {
88
91
  test("filters by regex", { timeout: 15000 }, async () => {
89
92
  const output = await runBenchCLITest(
90
93
  testSuite,
91
- "--filter ^template --time 0.1",
94
+ "--filter ^template --duration 0.1",
92
95
  );
93
96
  expect(output).toContain("template literal");
94
97
  expect(output).not.toContain("addition");
@@ -106,7 +109,7 @@ test("filter preserves suite structure", () => {
106
109
  });
107
110
 
108
111
  test("e2e: runs user script", { timeout: 30000 }, () => {
109
- const output = executeTestScript("--time 0.1");
112
+ const output = executeTestScript("--duration 0.1");
110
113
 
111
114
  expect(output).toContain("plus");
112
115
  expect(output).toContain("multiply");
@@ -122,14 +125,14 @@ test("e2e: runs user script", { timeout: 30000 }, () => {
122
125
  });
123
126
 
124
127
  test("e2e: filter flag", { timeout: 30000 }, () => {
125
- const output = executeTestScript('--filter "plus" --time 0.1');
128
+ const output = executeTestScript('--filter "plus" --duration 0.1');
126
129
 
127
130
  expect(output).toContain("plus");
128
131
  expect(output).not.toContain("multiply");
129
132
  });
130
133
 
131
134
  test("runs benchmarks with setup function", { timeout: 30000 }, async () => {
132
- const output = await runBenchCLITest(suiteWithSetup, "--time 0.1");
135
+ const output = await runBenchCLITest(suiteWithSetup, "--duration 0.1");
133
136
 
134
137
  expect(output).toContain("sum numbers");
135
138
  expect(output).toContain("join strings");
@@ -137,40 +140,38 @@ test("runs benchmarks with setup function", { timeout: 30000 }, async () => {
137
140
  expect(output).toContain("runs");
138
141
  });
139
142
 
140
- test(
141
- "runs benchmarks with baseline comparison",
142
- { timeout: 30000 },
143
- async () => {
144
- const suiteWithBaseline: BenchSuite = {
145
- name: "Baseline Test",
146
- groups: [
147
- {
148
- name: "Sort Comparison",
149
- setup: () => ({
150
- data: Array.from({ length: 10 }, () => Math.random()),
151
- }),
152
- baseline: {
153
- name: "baseline sort",
154
- fn: ({ data }: any) => [...data].sort(),
155
- },
156
- benchmarks: [
157
- {
158
- name: "optimized sort",
159
- fn: ({ data }: any) => [...data].sort((a, b) => a - b),
160
- },
161
- ],
143
+ test("runs benchmarks with baseline comparison", {
144
+ timeout: 30000,
145
+ }, async () => {
146
+ const suiteWithBaseline: BenchSuite = {
147
+ name: "Baseline Test",
148
+ groups: [
149
+ {
150
+ name: "Sort Comparison",
151
+ setup: () => ({
152
+ data: Array.from({ length: 10 }, () => Math.random()),
153
+ }),
154
+ baseline: {
155
+ name: "baseline sort",
156
+ fn: ({ data }: any) => [...data].sort(),
162
157
  },
163
- ],
164
- };
158
+ benchmarks: [
159
+ {
160
+ name: "optimized sort",
161
+ fn: ({ data }: any) => [...data].sort((a, b) => a - b),
162
+ },
163
+ ],
164
+ },
165
+ ],
166
+ };
165
167
 
166
- const output = await runBenchCLITest(suiteWithBaseline, "--time 0.01");
168
+ const output = await runBenchCLITest(suiteWithBaseline, "--iterations 20");
167
169
 
168
- expect(output).toContain("baseline sort");
169
- expect(output).toContain("optimized sort");
170
- expect(output).toContain("Δ%"); // Diff column should appear
171
- expect(output).toContain("mean");
172
- },
173
- );
170
+ expect(output).toContain("baseline sort");
171
+ expect(output).toContain("optimized sort");
172
+ expect(output).toContain("Δ%"); // Diff column should appear
173
+ expect(output).toContain("mean");
174
+ });
174
175
 
175
176
  test("file mode: BenchSuite export", { timeout: 30000 }, () => {
176
177
  const output = executeBenchforgeFile(