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
@@ -2,8 +2,8 @@ import { expect, test } from "vitest";
2
2
  import {
3
3
  browserGcStats,
4
4
  parseGcTraceEvents,
5
- type TraceEvent,
6
- } from "../browser/BrowserGcStats.ts";
5
+ } from "../profiling/browser/BrowserGcStats.ts";
6
+ import type { TraceEvent } from "../profiling/browser/ChromeTraceEvent.ts";
7
7
 
8
8
  test("parseGcTraceEvents parses MinorGC and MajorGC events", () => {
9
9
  const events: TraceEvent[] = [
@@ -11,6 +11,7 @@ test("parseGcTraceEvents parses MinorGC and MajorGC events", () => {
11
11
  cat: "v8.gc",
12
12
  name: "MinorGC",
13
13
  ph: "X",
14
+ ts: 0,
14
15
  dur: 500,
15
16
  args: { usedHeapSizeBefore: 10000, usedHeapSizeAfter: 8000 },
16
17
  },
@@ -18,6 +19,7 @@ test("parseGcTraceEvents parses MinorGC and MajorGC events", () => {
18
19
  cat: "v8.gc",
19
20
  name: "MajorGC",
20
21
  ph: "X",
22
+ ts: 0,
21
23
  dur: 12000,
22
24
  args: { usedHeapSizeBefore: 50000, usedHeapSizeAfter: 30000 },
23
25
  },
@@ -38,9 +40,9 @@ test("parseGcTraceEvents parses MinorGC and MajorGC events", () => {
38
40
 
39
41
  test("parseGcTraceEvents ignores non-complete and non-GC events", () => {
40
42
  const events: TraceEvent[] = [
41
- { cat: "v8.gc", name: "MinorGC", ph: "B", dur: 500 }, // not complete
42
- { cat: "v8", name: "V8.Execute", ph: "X", dur: 100 }, // not GC
43
- { cat: "v8.gc", name: "MinorGC", ph: "X" }, // valid, missing dur/args
43
+ { cat: "v8.gc", name: "MinorGC", ph: "B", ts: 0, dur: 500 }, // not complete
44
+ { cat: "v8", name: "V8.Execute", ph: "X", ts: 0, dur: 100 }, // not GC
45
+ { cat: "v8.gc", name: "MinorGC", ph: "X", ts: 0 }, // valid, missing dur/args
44
46
  ];
45
47
  const parsed = parseGcTraceEvents(events);
46
48
  expect(parsed).toHaveLength(1);
@@ -53,6 +55,7 @@ test("browserGcStats aggregates trace events into GcStats", () => {
53
55
  cat: "v8.gc",
54
56
  name: "MinorGC",
55
57
  ph: "X",
58
+ ts: 0,
56
59
  dur: 300,
57
60
  args: { usedHeapSizeBefore: 5000, usedHeapSizeAfter: 3000 },
58
61
  },
@@ -60,6 +63,7 @@ test("browserGcStats aggregates trace events into GcStats", () => {
60
63
  cat: "v8.gc",
61
64
  name: "MinorGC",
62
65
  ph: "X",
66
+ ts: 0,
63
67
  dur: 200,
64
68
  args: { usedHeapSizeBefore: 6000, usedHeapSizeAfter: 4000 },
65
69
  },
@@ -67,6 +71,7 @@ test("browserGcStats aggregates trace events into GcStats", () => {
67
71
  cat: "v8.gc",
68
72
  name: "MajorGC",
69
73
  ph: "X",
74
+ ts: 0,
70
75
  dur: 8000,
71
76
  args: { usedHeapSizeBefore: 40000, usedHeapSizeAfter: 20000 },
72
77
  },
@@ -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
+ });
@@ -0,0 +1,51 @@
1
+ import { expect, test } from "vitest";
2
+ import {
3
+ aggregateSites,
4
+ type HeapSite,
5
+ } from "../profiling/node/HeapSampleReport.ts";
6
+
7
+ test("unknown column does not merge distinct functions on same line", () => {
8
+ const sites: HeapSite[] = [
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
+ ];
12
+ const aggregated = aggregateSites(sites);
13
+ expect(aggregated).toHaveLength(2);
14
+ });
15
+
16
+ test("same column merges regardless of function name", () => {
17
+ const sites: HeapSite[] = [
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
+ ];
21
+ const aggregated = aggregateSites(sites);
22
+ expect(aggregated).toHaveLength(1);
23
+ expect(aggregated[0].bytes).toBe(300);
24
+ });
25
+
26
+ test("aggregation preserves distinct caller stacks", () => {
27
+ const stackA = [
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
+ ];
32
+ const stackB = [
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
+ ];
37
+ const sites: HeapSite[] = [
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
+ ];
41
+ const aggregated = aggregateSites(sites);
42
+
43
+ expect(aggregated).toHaveLength(1);
44
+ expect(aggregated[0].bytes).toBe(1000);
45
+ expect(aggregated[0].callers).toHaveLength(2);
46
+ // Primary stack should be the highest-bytes path (foo)
47
+ expect(aggregated[0].stack![1].name).toBe("foo");
48
+ // Callers sorted by bytes descending
49
+ expect(aggregated[0].callers![0].bytes).toBe(800);
50
+ expect(aggregated[0].callers![1].bytes).toBe(200);
51
+ });
@@ -1,7 +1,22 @@
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
+ const inlineMatrix: BenchMatrix<string> = {
6
+ name: "Test",
7
+ variants: {
8
+ fast: (s: string) => s.toUpperCase(),
9
+ slow: (s: string) => s.toLowerCase(),
10
+ medium: (s: string) => s,
11
+ },
12
+ cases: ["small", "large", "bevy_env_map"],
13
+ };
14
+
15
+ const noExplicitCases: BenchMatrix = {
16
+ name: "NoCase",
17
+ variants: { fast: () => {} },
18
+ };
19
+
5
20
  test("parseMatrixFilter: case/variant", () => {
6
21
  expect(parseMatrixFilter("bevy/link")).toEqual({
7
22
  case: "bevy",
@@ -34,16 +49,6 @@ test("parseMatrixFilter: empty parts", () => {
34
49
  });
35
50
  });
36
51
 
37
- const inlineMatrix: BenchMatrix<string> = {
38
- name: "Test",
39
- variants: {
40
- fast: (s: string) => s.toUpperCase(),
41
- slow: (s: string) => s.toLowerCase(),
42
- medium: (s: string) => s,
43
- },
44
- cases: ["small", "large", "bevy_env_map"],
45
- };
46
-
47
52
  test("filterMatrix: no filter returns original", async () => {
48
53
  const result = await filterMatrix(inlineMatrix, undefined);
49
54
  expect(result).toBe(inlineMatrix);
@@ -106,11 +111,6 @@ test("filterMatrix: multiple matching variants", async () => {
106
111
  expect(result.filteredVariants?.sort()).toEqual(["fast", "slow"]);
107
112
  });
108
113
 
109
- const noExplicitCases: BenchMatrix = {
110
- name: "NoCase",
111
- variants: { fast: () => {} },
112
- };
113
-
114
114
  test("filterMatrix: implicit default case returns default", async () => {
115
115
  const result = await filterMatrix(noExplicitCases, { case: "default" });
116
116
  expect(result.filteredCases).toEqual(["default"]);
@@ -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;