benchforge 0.1.9 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +99 -260
  3. package/bin/benchforge +1 -2
  4. package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
  5. package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
  6. package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
  7. package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
  8. package/dist/BenchRunner-DglX1NOn.d.mts +302 -0
  9. package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
  10. package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
  11. package/dist/Formatters-BWj3d4sv.mjs +95 -0
  12. package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
  13. package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
  14. package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
  15. package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
  16. package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
  17. package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
  18. package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
  19. package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
  20. package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
  21. package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
  22. package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
  23. package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
  24. package/dist/bin/benchforge.mjs +4 -5
  25. package/dist/bin/benchforge.mjs.map +1 -1
  26. package/dist/index.d.mts +731 -522
  27. package/dist/index.mjs +98 -3
  28. package/dist/index.mjs.map +1 -0
  29. package/dist/runners/WorkerScript.d.mts +12 -4
  30. package/dist/runners/WorkerScript.mjs +92 -120
  31. package/dist/runners/WorkerScript.mjs.map +1 -1
  32. package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
  33. package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
  34. package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
  35. package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
  36. package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
  37. package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
  38. package/dist/viewer/index.html +19 -0
  39. package/dist/viewer/speedscope/LICENSE +21 -0
  40. package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
  41. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
  42. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
  43. package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
  44. package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
  45. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
  46. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
  47. package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
  48. package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
  49. package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
  50. package/dist/viewer/speedscope/file-format-schema.json +274 -0
  51. package/dist/viewer/speedscope/index.html +19 -0
  52. package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
  53. package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
  54. package/dist/viewer/speedscope/release.txt +3 -0
  55. package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
  56. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
  57. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
  58. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
  59. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
  60. package/package.json +52 -26
  61. package/src/bin/benchforge.ts +2 -2
  62. package/src/cli/AnalyzeArchive.ts +232 -0
  63. package/src/cli/BrowserBench.ts +322 -0
  64. package/src/cli/CliArgs.ts +164 -48
  65. package/src/cli/CliExport.ts +179 -0
  66. package/src/cli/CliOptions.ts +147 -0
  67. package/src/cli/CliReport.ts +197 -0
  68. package/src/cli/FilterBenchmarks.ts +18 -30
  69. package/src/cli/RunBenchCLI.ts +138 -844
  70. package/src/cli/SuiteRunner.ts +160 -0
  71. package/src/cli/ViewerServer.ts +282 -0
  72. package/src/export/AllocExport.ts +121 -0
  73. package/src/export/ArchiveExport.ts +146 -0
  74. package/src/export/ArchiveFormat.ts +50 -0
  75. package/src/export/CoverageExport.ts +148 -0
  76. package/src/export/EditorUri.ts +10 -0
  77. package/src/export/PerfettoExport.ts +91 -126
  78. package/src/export/SpeedscopeTypes.ts +98 -0
  79. package/src/export/TimeExport.ts +115 -0
  80. package/src/index.ts +87 -62
  81. package/src/matrix/BenchMatrix.ts +230 -0
  82. package/src/matrix/CaseLoader.ts +8 -6
  83. package/src/matrix/MatrixDirRunner.ts +153 -0
  84. package/src/matrix/MatrixFilter.ts +55 -53
  85. package/src/matrix/MatrixInlineRunner.ts +50 -0
  86. package/src/matrix/MatrixReport.ts +94 -254
  87. package/src/matrix/VariantLoader.ts +9 -9
  88. package/src/profiling/browser/BenchLoop.ts +51 -0
  89. package/src/profiling/browser/BrowserCDP.ts +133 -0
  90. package/src/profiling/browser/BrowserGcStats.ts +33 -0
  91. package/src/profiling/browser/BrowserProfiler.ts +160 -0
  92. package/src/profiling/browser/CdpClient.ts +82 -0
  93. package/src/profiling/browser/CdpPage.ts +138 -0
  94. package/src/profiling/browser/ChromeLauncher.ts +158 -0
  95. package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
  96. package/src/profiling/browser/PageLoadMode.ts +61 -0
  97. package/src/profiling/node/CoverageSampler.ts +27 -0
  98. package/src/profiling/node/CoverageTypes.ts +23 -0
  99. package/src/profiling/node/HeapSampleReport.ts +261 -0
  100. package/src/{heap-sample → profiling/node}/HeapSampler.ts +55 -13
  101. package/src/profiling/node/ResolvedProfile.ts +98 -0
  102. package/src/profiling/node/TimeSampler.ts +57 -0
  103. package/src/report/BenchmarkReport.ts +146 -0
  104. package/src/report/Colors.ts +9 -0
  105. package/src/report/Formatters.ts +110 -0
  106. package/src/report/GcSections.ts +151 -0
  107. package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
  108. package/src/report/HtmlReport.ts +223 -0
  109. package/src/report/ParseStats.ts +73 -0
  110. package/src/report/StandardSections.ts +147 -0
  111. package/src/report/ViewerSections.ts +286 -0
  112. package/src/report/text/TableReport.ts +253 -0
  113. package/src/report/text/TextReport.ts +123 -0
  114. package/src/runners/AdaptiveWrapper.ts +167 -287
  115. package/src/runners/BenchRunner.ts +27 -22
  116. package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
  117. package/src/runners/CreateRunner.ts +5 -7
  118. package/src/runners/GcStats.ts +58 -61
  119. package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
  120. package/src/runners/MergeBatches.ts +123 -0
  121. package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
  122. package/src/runners/RunnerOrchestrator.ts +180 -296
  123. package/src/runners/RunnerUtils.ts +75 -1
  124. package/src/runners/SampleStats.ts +100 -0
  125. package/src/runners/TimingRunner.ts +244 -0
  126. package/src/runners/TimingUtils.ts +3 -2
  127. package/src/runners/WorkerScript.ts +162 -178
  128. package/src/stats/BootstrapDifference.ts +282 -0
  129. package/src/{PermutationTest.ts → stats/PermutationTest.ts} +31 -40
  130. package/src/stats/StatisticalUtils.ts +445 -0
  131. package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
  132. package/src/test/AdaptiveRunner.test.ts +39 -41
  133. package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
  134. package/src/test/AdaptiveStatistics.integration.ts +9 -41
  135. package/src/{tests → test}/BenchMatrix.test.ts +31 -28
  136. package/src/test/BenchmarkReport.test.ts +63 -13
  137. package/src/test/BrowserBench.e2e.test.ts +186 -17
  138. package/src/test/BrowserBench.test.ts +10 -5
  139. package/src/test/BuildTimeSection.test.ts +130 -0
  140. package/src/test/CapSamples.test.ts +82 -0
  141. package/src/test/CoverageExport.test.ts +115 -0
  142. package/src/test/CoverageSampler.test.ts +33 -0
  143. package/src/test/HeapAttribution.test.ts +51 -0
  144. package/src/{tests → test}/MatrixFilter.test.ts +16 -16
  145. package/src/{tests → test}/MatrixReport.test.ts +1 -1
  146. package/src/test/PermutationTest.test.ts +1 -1
  147. package/src/{tests → test}/RealDataValidation.test.ts +6 -6
  148. package/src/test/RunBenchCLI.test.ts +57 -56
  149. package/src/test/RunnerOrchestrator.test.ts +12 -12
  150. package/src/test/StatisticalUtils.test.ts +48 -12
  151. package/src/{table-util/test → test}/TableReport.test.ts +2 -2
  152. package/src/test/TestUtils.ts +35 -30
  153. package/src/test/TimeExport.test.ts +139 -0
  154. package/src/test/TimeSampler.test.ts +37 -0
  155. package/src/test/ViewerLive.e2e.test.ts +159 -0
  156. package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
  157. package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
  158. package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
  159. package/src/test/fixtures/cases/asyncCases.ts +9 -0
  160. package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
  161. package/src/test/fixtures/cases/variants/product.ts +2 -0
  162. package/src/test/fixtures/cases/variants/sum.ts +2 -0
  163. package/src/test/fixtures/discover/fast.ts +1 -0
  164. package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
  165. package/src/test/fixtures/invalid/bad.ts +1 -0
  166. package/src/test/fixtures/loader/fast.ts +1 -0
  167. package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
  168. package/src/test/fixtures/loader/stateful.ts +2 -0
  169. package/src/test/fixtures/stateful/stateful.ts +2 -0
  170. package/src/test/fixtures/variants/extra.ts +1 -0
  171. package/src/test/fixtures/variants/impl.ts +1 -0
  172. package/src/test/fixtures/worker/fast.ts +1 -0
  173. package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
  174. package/src/viewer/DateFormat.ts +30 -0
  175. package/src/viewer/Helpers.ts +23 -0
  176. package/src/viewer/LineData.ts +120 -0
  177. package/src/viewer/Providers.ts +191 -0
  178. package/src/viewer/ReportData.ts +123 -0
  179. package/src/viewer/State.ts +49 -0
  180. package/src/viewer/Theme.ts +15 -0
  181. package/src/viewer/components/App.tsx +73 -0
  182. package/src/viewer/components/DropZone.tsx +71 -0
  183. package/src/viewer/components/LazyPlot.ts +33 -0
  184. package/src/viewer/components/SamplesPanel.tsx +214 -0
  185. package/src/viewer/components/Shell.tsx +26 -0
  186. package/src/viewer/components/SourcePanel.tsx +216 -0
  187. package/src/viewer/components/SummaryPanel.tsx +332 -0
  188. package/src/viewer/components/TabBar.tsx +131 -0
  189. package/src/viewer/components/TabContent.tsx +46 -0
  190. package/src/viewer/components/ThemeToggle.tsx +50 -0
  191. package/src/viewer/index.html +20 -0
  192. package/src/viewer/main.tsx +4 -0
  193. package/src/viewer/plots/CIPlot.ts +313 -0
  194. package/src/{html/browser → viewer/plots}/HistogramKde.ts +42 -47
  195. package/src/viewer/plots/LegendUtils.ts +134 -0
  196. package/src/viewer/plots/PlotTypes.ts +85 -0
  197. package/src/viewer/plots/RenderPlots.ts +230 -0
  198. package/src/viewer/plots/SampleTimeSeries.ts +306 -0
  199. package/src/viewer/plots/SvgHelpers.ts +136 -0
  200. package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
  201. package/src/viewer/report.css +427 -0
  202. package/src/viewer/shell.css +357 -0
  203. package/src/viewer/tsconfig.json +11 -0
  204. package/dist/BenchRunner-CSKN9zPy.d.mts +0 -225
  205. package/dist/BrowserHeapSampler-DCeL42RE.mjs +0 -202
  206. package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
  207. package/dist/GcStats-ByEovUi1.mjs +0 -77
  208. package/dist/GcStats-ByEovUi1.mjs.map +0 -1
  209. package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
  210. package/dist/TimingUtils-ClclVQ7E.mjs +0 -597
  211. package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
  212. package/dist/browser/index.js +0 -914
  213. package/dist/src-Cf_LXwlp.mjs +0 -2873
  214. package/dist/src-Cf_LXwlp.mjs.map +0 -1
  215. package/src/BenchMatrix.ts +0 -380
  216. package/src/BenchmarkReport.ts +0 -156
  217. package/src/HtmlDataPrep.ts +0 -148
  218. package/src/StandardSections.ts +0 -261
  219. package/src/StatisticalUtils.ts +0 -176
  220. package/src/TypeUtil.ts +0 -8
  221. package/src/browser/BrowserGcStats.ts +0 -44
  222. package/src/browser/BrowserHeapSampler.ts +0 -271
  223. package/src/export/JsonExport.ts +0 -103
  224. package/src/export/JsonFormat.ts +0 -91
  225. package/src/heap-sample/HeapSampleReport.ts +0 -196
  226. package/src/html/HtmlReport.ts +0 -131
  227. package/src/html/HtmlTemplate.ts +0 -284
  228. package/src/html/Types.ts +0 -88
  229. package/src/html/browser/CIPlot.ts +0 -287
  230. package/src/html/browser/LegendUtils.ts +0 -163
  231. package/src/html/browser/RenderPlots.ts +0 -263
  232. package/src/html/browser/SampleTimeSeries.ts +0 -389
  233. package/src/html/browser/Types.ts +0 -96
  234. package/src/html/browser/index.ts +0 -1
  235. package/src/html/index.ts +0 -17
  236. package/src/runners/BasicRunner.ts +0 -364
  237. package/src/table-util/ConvergenceFormatters.ts +0 -19
  238. package/src/table-util/Formatters.ts +0 -152
  239. package/src/table-util/README.md +0 -70
  240. package/src/table-util/TableReport.ts +0 -293
  241. package/src/tests/fixtures/cases/asyncCases.ts +0 -7
  242. package/src/tests/fixtures/cases/variants/product.ts +0 -2
  243. package/src/tests/fixtures/cases/variants/sum.ts +0 -2
  244. package/src/tests/fixtures/discover/fast.ts +0 -1
  245. package/src/tests/fixtures/invalid/bad.ts +0 -1
  246. package/src/tests/fixtures/loader/fast.ts +0 -1
  247. package/src/tests/fixtures/loader/stateful.ts +0 -2
  248. package/src/tests/fixtures/stateful/stateful.ts +0 -2
  249. package/src/tests/fixtures/variants/extra.ts +0 -1
  250. package/src/tests/fixtures/variants/impl.ts +0 -1
  251. package/src/tests/fixtures/worker/fast.ts +0 -1
  252. package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
  253. package/src/{table-util/test → test}/TableValueExtractor.ts +9 -9
@@ -1,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 = {
@@ -49,8 +49,26 @@ const suiteWithSetup: BenchSuite = {
49
49
  ],
50
50
  };
51
51
 
52
+ /** Execute test fixture script and return output */
53
+ function executeTestScript(args = ""): string {
54
+ const script = path.join(
55
+ import.meta.dirname!,
56
+ "fixtures/test-bench-script.ts",
57
+ );
58
+ return execSync(`node --expose-gc --allow-natives-syntax ${script} ${args}`, {
59
+ encoding: "utf8",
60
+ });
61
+ }
62
+
63
+ /** Run a fixture file via bin/benchforge and return output */
64
+ function executeBenchforgeFile(file: string, args = ""): string {
65
+ const bin = path.join(import.meta.dirname!, "../../bin/benchforge");
66
+ const fixture = path.join(import.meta.dirname!, "fixtures", file);
67
+ return execSync(`${bin} ${fixture} ${args}`, { encoding: "utf8" });
68
+ }
69
+
52
70
  test("runs all benchmarks", { timeout: 30000 }, async () => {
53
- const output = await runBenchCLITest(testSuite, "--time 0.1");
71
+ const output = await runBenchCLITest(testSuite, "--duration 0.1");
54
72
 
55
73
  expect(output).toContain("concatenation");
56
74
  expect(output).toContain("template literal");
@@ -61,7 +79,10 @@ test("runs all benchmarks", { timeout: 30000 }, async () => {
61
79
  });
62
80
 
63
81
  test("filters by substring", { timeout: 15000 }, async () => {
64
- const output = await runBenchCLITest(testSuite, "--filter concat --time 0.1");
82
+ const output = await runBenchCLITest(
83
+ testSuite,
84
+ "--filter concat --duration 0.1",
85
+ );
65
86
 
66
87
  expect(output).toContain("concatenation");
67
88
  expect(output).not.toContain("addition");
@@ -70,7 +91,7 @@ test("filters by substring", { timeout: 15000 }, async () => {
70
91
  test("filters by regex", { timeout: 15000 }, async () => {
71
92
  const output = await runBenchCLITest(
72
93
  testSuite,
73
- "--filter ^template --time 0.1",
94
+ "--filter ^template --duration 0.1",
74
95
  );
75
96
  expect(output).toContain("template literal");
76
97
  expect(output).not.toContain("addition");
@@ -87,26 +108,8 @@ test("filter preserves suite structure", () => {
87
108
  expect(filtered.groups[1].benchmarks).toHaveLength(0);
88
109
  });
89
110
 
90
- /** Execute test fixture script and return output */
91
- function executeTestScript(args = ""): string {
92
- const script = path.join(
93
- import.meta.dirname!,
94
- "fixtures/test-bench-script.ts",
95
- );
96
- return execSync(`node --expose-gc --allow-natives-syntax ${script} ${args}`, {
97
- encoding: "utf8",
98
- });
99
- }
100
-
101
- /** Run a fixture file via bin/benchforge and return output */
102
- function executeBenchforgeFile(file: string, args = ""): string {
103
- const bin = path.join(import.meta.dirname!, "../../bin/benchforge");
104
- const fixture = path.join(import.meta.dirname!, "fixtures", file);
105
- return execSync(`${bin} ${fixture} ${args}`, { encoding: "utf8" });
106
- }
107
-
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(
@@ -1,5 +1,5 @@
1
1
  import { expect, test } from "vitest";
2
- import type { BenchmarkSpec } from "../Benchmark.ts";
2
+ import type { BenchmarkSpec } from "../runners/BenchmarkSpec.ts";
3
3
  import { runBenchmark } from "../runners/RunnerOrchestrator.ts";
4
4
 
5
5
  /** lightweight function for testing worker communication */
@@ -9,15 +9,15 @@ function simpleTestFunction(): number {
9
9
  return sum;
10
10
  }
11
11
 
12
- test("BasicRunner runs benchmark in worker mode", async () => {
12
+ test("TimingRunner runs benchmark in worker mode", async () => {
13
13
  const spec: BenchmarkSpec = {
14
- name: "basic-worker-test",
14
+ name: "timing-worker-test",
15
15
  fn: simpleTestFunction,
16
16
  };
17
17
 
18
18
  const results = await runBenchmark({
19
19
  spec,
20
- runner: "basic",
20
+ runner: "timing",
21
21
  options: {
22
22
  maxTime: 5,
23
23
  maxIterations: 50,
@@ -28,7 +28,7 @@ test("BasicRunner runs benchmark in worker mode", async () => {
28
28
  expect(results).toHaveLength(1);
29
29
  const result = results[0];
30
30
 
31
- expect(result.name).toBe("basic-worker-test");
31
+ expect(result.name).toBe("timing-worker-test");
32
32
  expect(result.samples.length).toBeGreaterThan(0);
33
33
  expect(result.samples.length).toBeLessThanOrEqual(500);
34
34
  expect(result.time.min).toBeGreaterThan(0);
@@ -38,15 +38,15 @@ test("BasicRunner runs benchmark in worker mode", async () => {
38
38
  expect(result.time.p99).toBeGreaterThan(0);
39
39
  });
40
40
 
41
- test("BasicRunner runs benchmark in non-worker mode", async () => {
41
+ test("TimingRunner runs benchmark in non-worker mode", async () => {
42
42
  const spec: BenchmarkSpec = {
43
- name: "basic-test",
43
+ name: "timing-test",
44
44
  fn: simpleTestFunction,
45
45
  };
46
46
 
47
47
  const results = await runBenchmark({
48
48
  spec,
49
- runner: "basic",
49
+ runner: "timing",
50
50
  options: {
51
51
  maxTime: 5,
52
52
  maxIterations: 50,
@@ -57,12 +57,12 @@ test("BasicRunner runs benchmark in non-worker mode", async () => {
57
57
  expect(results).toHaveLength(1);
58
58
  const result = results[0];
59
59
 
60
- expect(result.name).toBe("basic-test");
60
+ expect(result.name).toBe("timing-test");
61
61
  expect(result.samples.length).toBeGreaterThan(0);
62
62
  expect(result.time.p50).toBeGreaterThan(0);
63
63
  });
64
64
 
65
- test("BasicRunner with parameterized benchmark", async () => {
65
+ test("TimingRunner with parameterized benchmark", async () => {
66
66
  const spec: BenchmarkSpec<number> = {
67
67
  name: "parameterized-test",
68
68
  fn: (n: number) => {
@@ -74,7 +74,7 @@ test("BasicRunner with parameterized benchmark", async () => {
74
74
 
75
75
  const results = await runBenchmark({
76
76
  spec,
77
- runner: "basic",
77
+ runner: "timing",
78
78
  options: { maxTime: 5, maxIterations: 20 },
79
79
  useWorker: false,
80
80
  params: 100,
@@ -94,7 +94,7 @@ test("RunnerOrchestrator propagates errors from worker", async () => {
94
94
 
95
95
  const promise = runBenchmark({
96
96
  spec,
97
- runner: "basic",
97
+ runner: "timing",
98
98
  options: { maxTime: 1, maxIterations: 1 },
99
99
  useWorker: true,
100
100
  });
@@ -1,14 +1,19 @@
1
1
  import { expect, test } from "vitest";
2
+ import {
3
+ blockDifferenceCI,
4
+ sampleDifferenceCI,
5
+ } from "../stats/BootstrapDifference.ts";
2
6
  import {
3
7
  average,
4
- bootstrapDifferenceCI,
5
- bootstrapMedian,
8
+ blockBootstrap,
6
9
  coefficientOfVariation,
7
10
  findOutliers,
11
+ median,
8
12
  medianAbsoluteDeviation,
9
13
  percentile,
14
+ sampleBootstrap,
10
15
  standardDeviation,
11
- } from "../StatisticalUtils.ts";
16
+ } from "../stats/StatisticalUtils.ts";
12
17
  import { assertValid, getSampleData } from "./TestUtils.ts";
13
18
 
14
19
  test("calculates mean correctly", () => {
@@ -68,11 +73,21 @@ test("identifies outliers in mixed data", () => {
68
73
  expect(outliers.indices).toContain(51);
69
74
  });
70
75
 
71
- test("bootstrap estimates median with confidence intervals", () => {
76
+ test("sampleBootstrap estimates median with CI", () => {
77
+ const stable = getSampleData(400, 450);
78
+ const result = sampleBootstrap(stable, median, { resamples: 1000 });
79
+ expect(result.ciLevel).toBe("sample");
80
+ expect(result.ci[0]).toBeLessThanOrEqual(result.estimate);
81
+ expect(result.ci[1]).toBeGreaterThanOrEqual(result.estimate);
82
+ });
83
+
84
+ test("blockBootstrap estimates median with confidence intervals", () => {
72
85
  const stable = getSampleData(400, 450);
73
86
  const actual = percentile(stable, 0.5);
74
- const result = bootstrapMedian(stable, { resamples: 1000 });
87
+ const blocks = Array.from({ length: 5 }, (_, i) => i * 10);
88
+ const result = blockBootstrap(stable, blocks, median, { resamples: 1000 });
75
89
 
90
+ expect(result.ciLevel).toBe("block");
76
91
  expect(result.estimate).toBeCloseTo(actual, 1);
77
92
  expect(result.ci[0]).toBeLessThanOrEqual(result.estimate);
78
93
  expect(result.ci[1]).toBeGreaterThanOrEqual(result.estimate);
@@ -80,32 +95,53 @@ test("bootstrap estimates median with confidence intervals", () => {
80
95
  expect(result.samples).toHaveLength(1000);
81
96
  });
82
97
 
83
- test("bootstrapDifferenceCI detects improvement", () => {
98
+ test("sampleDifferenceCI detects improvement", () => {
99
+ const baseline = getSampleData(0, 100);
100
+ const improved = baseline.map(v => v * 0.8);
101
+ const result = sampleDifferenceCI(baseline, improved, median, {
102
+ resamples: 1000,
103
+ });
104
+ expect(result.ciLevel).toBe("sample");
105
+ expect(result.percent).toBeCloseTo(-20, 0);
106
+ });
107
+
108
+ test("blockDifferenceCI detects improvement", () => {
84
109
  const baseline = getSampleData(0, 100);
85
110
  const improved = baseline.map(v => v * 0.8);
86
- const result = bootstrapDifferenceCI(baseline, improved, { resamples: 1000 });
111
+ const blocks = Array.from({ length: 10 }, (_, i) => i * 10);
112
+ const result = blockDifferenceCI(baseline, blocks, improved, median, {
113
+ resamples: 1000,
114
+ });
87
115
 
116
+ expect(result.ciLevel).toBe("block");
88
117
  expect(result.percent).toBeCloseTo(-20, 0);
89
118
  expect(result.ci[1]).toBeLessThan(0);
90
119
  expect(result.direction).toBe("faster");
91
120
  });
92
121
 
93
- test("bootstrapDifferenceCI detects regression", () => {
122
+ test("blockDifferenceCI detects regression", () => {
94
123
  const baseline = getSampleData(0, 100);
95
124
  const slower = baseline.map(v => v * 1.2);
96
- const result = bootstrapDifferenceCI(baseline, slower, { resamples: 1000 });
125
+ const blocks = Array.from({ length: 10 }, (_, i) => i * 10);
126
+ const result = blockDifferenceCI(baseline, blocks, slower, median, {
127
+ resamples: 1000,
128
+ });
97
129
 
130
+ expect(result.ciLevel).toBe("block");
98
131
  expect(result.percent).toBeCloseTo(20, 0);
99
132
  expect(result.ci[0]).toBeGreaterThan(0);
100
133
  expect(result.direction).toBe("slower");
101
134
  });
102
135
 
103
- test("bootstrapDifferenceCI shows uncertainty for noise", () => {
136
+ test("blockDifferenceCI shows uncertainty for noise", () => {
104
137
  const baseline = getSampleData(0, 100);
105
138
  const noisy = baseline.map(v => v + (Math.random() - 0.5) * 2);
106
- const result = bootstrapDifferenceCI(baseline, noisy, { resamples: 1000 });
139
+ const blocks = Array.from({ length: 10 }, (_, i) => i * 10);
140
+ const result = blockDifferenceCI(baseline, blocks, noisy, median, {
141
+ resamples: 1000,
142
+ });
107
143
 
108
- // CI should span zero for no real change
144
+ expect(result.ciLevel).toBe("block");
109
145
  expect(result.ci[0]).toBeLessThanOrEqual(0);
110
146
  expect(result.ci[1]).toBeGreaterThanOrEqual(0);
111
147
  expect(result.direction).toBe("uncertain");
@@ -1,10 +1,10 @@
1
1
  import { expect, test } from "vitest";
2
- import { integer } from "../Formatters.ts";
2
+ import { integer } from "../report/Formatters.ts";
3
3
  import {
4
4
  buildTable,
5
5
  type ColumnGroup,
6
6
  type ResultGroup,
7
- } from "../TableReport.ts";
7
+ } from "../report/text/TableReport.ts";
8
8
 
9
9
  interface TestRecord {
10
10
  name: string;
@@ -1,11 +1,40 @@
1
- import type { BenchSuite } from "../Benchmark.ts";
2
- import type { BenchmarkReport } from "../BenchmarkReport.ts";
3
1
  import type { Configure, DefaultCliArgs } from "../cli/CliArgs.ts";
4
2
  import { parseCliArgs } from "../cli/CliArgs.ts";
5
- import { defaultReport, runBenchmarks } from "../cli/RunBenchCLI.ts";
6
- import type { MeasuredResults } from "../MeasuredResults.ts";
7
- import { average, percentile } from "../StatisticalUtils.ts";
8
- import { bevy30SamplesMs } from "../tests/fixtures/bevy30-samples.ts";
3
+ import { defaultReport } from "../cli/CliReport.ts";
4
+ import { runBenchmarks } from "../cli/SuiteRunner.ts";
5
+ import type { BenchmarkReport } from "../report/BenchmarkReport.ts";
6
+ import type { BenchSuite } from "../runners/BenchmarkSpec.ts";
7
+ import type { MeasuredResults } from "../runners/MeasuredResults.ts";
8
+ import { average, percentile } from "../stats/StatisticalUtils.ts";
9
+ import { bevy30SamplesMs } from "./fixtures/bevy30-samples.ts";
10
+
11
+ /** Validation helpers for statistical tests */
12
+ export const assertValid: {
13
+ pValue: (value: number) => void;
14
+ percentileOrder: (p25: number, p50: number, p75: number, p99: number) => void;
15
+ significance: (level: string) => void;
16
+ } = {
17
+ pValue: (value: number) => {
18
+ if (value < 0 || value > 1) {
19
+ throw new Error(`Expected p-value between 0 and 1, got ${value}`);
20
+ }
21
+ },
22
+
23
+ percentileOrder: (p25: number, p50: number, p75: number, p99: number) => {
24
+ if (!(p25 <= p50 && p50 <= p75 && p75 <= p99)) {
25
+ throw new Error(
26
+ `Percentiles not ordered: p25=${p25}, p50=${p50}, p75=${p75}, p99=${p99}`,
27
+ );
28
+ }
29
+ },
30
+
31
+ significance: (level: string) => {
32
+ const valid = ["none", "weak", "good", "strong"];
33
+ if (!valid.includes(level)) {
34
+ throw new Error(`Invalid significance level: ${level}`);
35
+ }
36
+ },
37
+ };
9
38
 
10
39
  /** @return formatted benchmark output for CLI testing */
11
40
  export async function runBenchCLITest<T = DefaultCliArgs>(
@@ -67,27 +96,3 @@ export function createBenchmarkReport(
67
96
  measuredResults: createMeasuredResults(sampleRange, overrides),
68
97
  };
69
98
  }
70
-
71
- /** Validation helpers for statistical tests */
72
- export const assertValid = {
73
- pValue: (value: number) => {
74
- if (value < 0 || value > 1) {
75
- throw new Error(`Expected p-value between 0 and 1, got ${value}`);
76
- }
77
- },
78
-
79
- percentileOrder: (p25: number, p50: number, p75: number, p99: number) => {
80
- if (!(p25 <= p50 && p50 <= p75 && p75 <= p99)) {
81
- throw new Error(
82
- `Percentiles not ordered: p25=${p25}, p50=${p50}, p75=${p75}, p99=${p99}`,
83
- );
84
- }
85
- },
86
-
87
- significance: (level: string) => {
88
- const valid = ["none", "weak", "good", "strong"];
89
- if (!valid.includes(level)) {
90
- throw new Error(`Invalid significance level: ${level}`);
91
- }
92
- },
93
- };
@@ -0,0 +1,139 @@
1
+ import { expect, test } from "vitest";
2
+ import { timeProfileToSpeedscope } from "../export/TimeExport.ts";
3
+ import type { TimeProfile } from "../profiling/node/TimeSampler.ts";
4
+
5
+ /** Build a minimal TimeProfile for testing */
6
+ function mockProfile(): TimeProfile {
7
+ return {
8
+ nodes: [
9
+ {
10
+ id: 1,
11
+ callFrame: { functionName: "", url: "", lineNumber: -1 },
12
+ children: [2],
13
+ },
14
+ {
15
+ id: 2,
16
+ callFrame: {
17
+ functionName: "main",
18
+ url: "file:///app.ts",
19
+ lineNumber: 9,
20
+ columnNumber: 0,
21
+ },
22
+ children: [3],
23
+ },
24
+ {
25
+ id: 3,
26
+ callFrame: {
27
+ functionName: "compute",
28
+ url: "file:///app.ts",
29
+ lineNumber: 19,
30
+ columnNumber: 4,
31
+ },
32
+ hitCount: 5,
33
+ },
34
+ ],
35
+ startTime: 0,
36
+ endTime: 5000,
37
+ samples: [3, 3, 2, 3, 3],
38
+ timeDeltas: [1000, 1000, 1000, 1000, 1000],
39
+ };
40
+ }
41
+
42
+ test("converts TimeProfile to valid SpeedScope format", () => {
43
+ const profile = mockProfile();
44
+ const file = timeProfileToSpeedscope("test-bench", profile);
45
+
46
+ expect(file.$schema).toBe(
47
+ "https://www.speedscope.app/file-format-schema.json",
48
+ );
49
+ expect(file.exporter).toBe("benchforge");
50
+ expect(file.profiles).toHaveLength(1);
51
+
52
+ const p = file.profiles[0];
53
+ expect(p.type).toBe("sampled");
54
+ expect(p.name).toBe("test-bench");
55
+ expect(p.unit).toBe("microseconds");
56
+ expect(p.samples).toHaveLength(5);
57
+ expect(p.weights).toEqual([1000, 1000, 1000, 1000, 1000]);
58
+ expect(p.endValue).toBe(5000);
59
+ });
60
+
61
+ test("resolves stacks from leaf to root (root-first order)", () => {
62
+ const profile = mockProfile();
63
+ const file = timeProfileToSpeedscope("test", profile);
64
+
65
+ const p = file.profiles[0];
66
+ const frames = file.shared.frames;
67
+
68
+ // Sample at node 3 (compute) should have stack: [main, compute]
69
+ const deepStack = p.samples[0];
70
+ expect(deepStack).toHaveLength(2); // root is skipped
71
+ expect(frames[deepStack[0]].name).toBe("main");
72
+ expect(frames[deepStack[1]].name).toBe("compute");
73
+
74
+ // Sample at node 2 (main) should have stack: [main]
75
+ const shallowStack = p.samples[2];
76
+ expect(shallowStack).toHaveLength(1);
77
+ expect(frames[shallowStack[0]].name).toBe("main");
78
+ });
79
+
80
+ test("deduplicates shared frames", () => {
81
+ const profile = mockProfile();
82
+ const file = timeProfileToSpeedscope("test", profile);
83
+
84
+ // "main" and "compute" — only 2 unique frames
85
+ expect(file.shared.frames).toHaveLength(2);
86
+ });
87
+
88
+ test("handles empty samples gracefully", () => {
89
+ const profile: TimeProfile = {
90
+ nodes: [
91
+ { id: 1, callFrame: { functionName: "", url: "", lineNumber: -1 } },
92
+ ],
93
+ startTime: 0,
94
+ endTime: 0,
95
+ samples: [],
96
+ timeDeltas: [],
97
+ };
98
+ const file = timeProfileToSpeedscope("empty", profile);
99
+
100
+ expect(file.profiles[0].samples).toHaveLength(0);
101
+ expect(file.profiles[0].weights).toHaveLength(0);
102
+ expect(file.profiles[0].endValue).toBe(0);
103
+ });
104
+
105
+ test("converts 0-indexed V8 lines to 1-indexed", () => {
106
+ const profile = mockProfile();
107
+ const file = timeProfileToSpeedscope("test", profile);
108
+
109
+ const mainFrame = file.shared.frames.find(f => f.name === "main")!;
110
+ expect(mainFrame.line).toBe(10); // lineNumber 9 -> line 10
111
+ expect(mainFrame.col).toBe(1); // columnNumber 0 -> col 1
112
+ });
113
+
114
+ test("anonymous functions get location hint in name", () => {
115
+ const profile: TimeProfile = {
116
+ nodes: [
117
+ {
118
+ id: 1,
119
+ callFrame: { functionName: "", url: "", lineNumber: -1 },
120
+ children: [2],
121
+ },
122
+ {
123
+ id: 2,
124
+ callFrame: {
125
+ functionName: "",
126
+ url: "file:///lib/utils.ts",
127
+ lineNumber: 41,
128
+ },
129
+ },
130
+ ],
131
+ startTime: 0,
132
+ endTime: 1000,
133
+ samples: [2],
134
+ timeDeltas: [1000],
135
+ };
136
+ const file = timeProfileToSpeedscope("test", profile);
137
+
138
+ expect(file.shared.frames[0].name).toBe("(anonymous utils.ts:42)");
139
+ });
@@ -0,0 +1,37 @@
1
+ import { expect, test } from "vitest";
2
+ import { withTimeProfiling } from "../profiling/node/TimeSampler.ts";
3
+
4
+ test("withTimeProfiling returns valid V8 CPU profile", async () => {
5
+ // Burn some CPU to produce samples
6
+ const { result, profile } = await withTimeProfiling({}, () => {
7
+ let sum = 0;
8
+ for (let i = 0; i < 1e6; i++) sum += Math.sqrt(i);
9
+ return sum;
10
+ });
11
+
12
+ expect(result).toBeGreaterThan(0);
13
+ expect(profile.nodes.length).toBeGreaterThan(0);
14
+ expect(profile.startTime).toBeLessThan(profile.endTime);
15
+ expect(profile.samples).toBeDefined();
16
+ expect(profile.timeDeltas).toBeDefined();
17
+ expect(profile.samples!.length).toBe(profile.timeDeltas!.length);
18
+
19
+ // Verify node structure
20
+ const node = profile.nodes[0];
21
+ expect(node).toHaveProperty("id");
22
+ expect(node).toHaveProperty("callFrame");
23
+ expect(node.callFrame).toHaveProperty("functionName");
24
+ expect(node.callFrame).toHaveProperty("url");
25
+ expect(node.callFrame).toHaveProperty("lineNumber");
26
+ });
27
+
28
+ test("withTimeProfiling respects custom interval", async () => {
29
+ const { profile } = await withTimeProfiling({ interval: 100 }, () => {
30
+ let sum = 0;
31
+ for (let i = 0; i < 1e6; i++) sum += Math.sqrt(i);
32
+ return sum;
33
+ });
34
+
35
+ // Finer interval should produce more samples for the same work
36
+ expect(profile.samples!.length).toBeGreaterThan(0);
37
+ });