benchforge 0.1.11 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +99 -294
  3. package/bin/benchforge +1 -2
  4. package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
  5. package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
  6. package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
  7. package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
  8. package/dist/{BenchRunner-BzyUfiyB.d.mts → BenchRunner-DglX1NOn.d.mts} +119 -66
  9. package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
  10. package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
  11. package/dist/Formatters-BWj3d4sv.mjs +95 -0
  12. package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
  13. package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
  14. package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
  15. package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
  16. package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
  17. package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
  18. package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
  19. package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
  20. package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
  21. package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
  22. package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
  23. package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
  24. package/dist/bin/benchforge.mjs +4 -5
  25. package/dist/bin/benchforge.mjs.map +1 -1
  26. package/dist/index.d.mts +711 -558
  27. package/dist/index.mjs +98 -3
  28. package/dist/index.mjs.map +1 -0
  29. package/dist/runners/WorkerScript.d.mts +12 -4
  30. package/dist/runners/WorkerScript.mjs +77 -105
  31. package/dist/runners/WorkerScript.mjs.map +1 -1
  32. package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
  33. package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
  34. package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
  35. package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
  36. package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
  37. package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
  38. package/dist/viewer/index.html +19 -0
  39. package/dist/viewer/speedscope/LICENSE +21 -0
  40. package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
  41. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
  42. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
  43. package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
  44. package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
  45. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
  46. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
  47. package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
  48. package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
  49. package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
  50. package/dist/viewer/speedscope/file-format-schema.json +274 -0
  51. package/dist/viewer/speedscope/index.html +19 -0
  52. package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
  53. package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
  54. package/dist/viewer/speedscope/release.txt +3 -0
  55. package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
  56. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
  57. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
  58. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
  59. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
  60. package/package.json +52 -27
  61. package/src/bin/benchforge.ts +2 -2
  62. package/src/cli/AnalyzeArchive.ts +232 -0
  63. package/src/cli/BrowserBench.ts +322 -0
  64. package/src/cli/CliArgs.ts +164 -51
  65. package/src/cli/CliExport.ts +179 -0
  66. package/src/cli/CliOptions.ts +147 -0
  67. package/src/cli/CliReport.ts +197 -0
  68. package/src/cli/FilterBenchmarks.ts +18 -30
  69. package/src/cli/RunBenchCLI.ts +132 -866
  70. package/src/cli/SuiteRunner.ts +160 -0
  71. package/src/cli/ViewerServer.ts +282 -0
  72. package/src/export/AllocExport.ts +121 -0
  73. package/src/export/ArchiveExport.ts +146 -0
  74. package/src/export/ArchiveFormat.ts +50 -0
  75. package/src/export/CoverageExport.ts +148 -0
  76. package/src/export/EditorUri.ts +10 -0
  77. package/src/export/PerfettoExport.ts +64 -99
  78. package/src/export/SpeedscopeTypes.ts +98 -0
  79. package/src/export/TimeExport.ts +115 -0
  80. package/src/index.ts +86 -67
  81. package/src/matrix/BenchMatrix.ts +230 -0
  82. package/src/matrix/CaseLoader.ts +8 -6
  83. package/src/matrix/MatrixDirRunner.ts +153 -0
  84. package/src/matrix/MatrixFilter.ts +49 -47
  85. package/src/matrix/MatrixInlineRunner.ts +50 -0
  86. package/src/matrix/MatrixReport.ts +90 -250
  87. package/src/matrix/VariantLoader.ts +5 -5
  88. package/src/profiling/browser/BenchLoop.ts +51 -0
  89. package/src/profiling/browser/BrowserCDP.ts +133 -0
  90. package/src/profiling/browser/BrowserGcStats.ts +33 -0
  91. package/src/profiling/browser/BrowserProfiler.ts +160 -0
  92. package/src/profiling/browser/CdpClient.ts +82 -0
  93. package/src/profiling/browser/CdpPage.ts +138 -0
  94. package/src/profiling/browser/ChromeLauncher.ts +158 -0
  95. package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
  96. package/src/profiling/browser/PageLoadMode.ts +61 -0
  97. package/src/profiling/node/CoverageSampler.ts +27 -0
  98. package/src/profiling/node/CoverageTypes.ts +23 -0
  99. package/src/profiling/node/HeapSampleReport.ts +261 -0
  100. package/src/{heap-sample → profiling/node}/HeapSampler.ts +1 -2
  101. package/src/{heap-sample → profiling/node}/ResolvedProfile.ts +18 -9
  102. package/src/profiling/node/TimeSampler.ts +57 -0
  103. package/src/report/BenchmarkReport.ts +146 -0
  104. package/src/report/Colors.ts +9 -0
  105. package/src/report/Formatters.ts +110 -0
  106. package/src/report/GcSections.ts +151 -0
  107. package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
  108. package/src/report/HtmlReport.ts +223 -0
  109. package/src/report/ParseStats.ts +73 -0
  110. package/src/report/StandardSections.ts +147 -0
  111. package/src/report/ViewerSections.ts +286 -0
  112. package/src/report/text/TableReport.ts +253 -0
  113. package/src/report/text/TextReport.ts +123 -0
  114. package/src/runners/AdaptiveWrapper.ts +116 -236
  115. package/src/runners/BenchRunner.ts +20 -15
  116. package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
  117. package/src/runners/CreateRunner.ts +5 -7
  118. package/src/runners/GcStats.ts +47 -50
  119. package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
  120. package/src/runners/MergeBatches.ts +123 -0
  121. package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
  122. package/src/runners/RunnerOrchestrator.ts +127 -243
  123. package/src/runners/RunnerUtils.ts +75 -1
  124. package/src/runners/SampleStats.ts +100 -0
  125. package/src/runners/TimingRunner.ts +244 -0
  126. package/src/runners/TimingUtils.ts +3 -2
  127. package/src/runners/WorkerScript.ts +135 -151
  128. package/src/stats/BootstrapDifference.ts +282 -0
  129. package/src/{PermutationTest.ts → stats/PermutationTest.ts} +8 -17
  130. package/src/stats/StatisticalUtils.ts +445 -0
  131. package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
  132. package/src/test/AdaptiveRunner.test.ts +39 -41
  133. package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
  134. package/src/test/AdaptiveStatistics.integration.ts +2 -2
  135. package/src/{tests → test}/BenchMatrix.test.ts +19 -16
  136. package/src/test/BenchmarkReport.test.ts +63 -13
  137. package/src/test/BrowserBench.e2e.test.ts +186 -17
  138. package/src/test/BrowserBench.test.ts +10 -5
  139. package/src/test/BuildTimeSection.test.ts +130 -0
  140. package/src/test/CapSamples.test.ts +82 -0
  141. package/src/test/CoverageExport.test.ts +115 -0
  142. package/src/test/CoverageSampler.test.ts +33 -0
  143. package/src/test/HeapAttribution.test.ts +14 -14
  144. package/src/{tests → test}/MatrixFilter.test.ts +1 -1
  145. package/src/{tests → test}/MatrixReport.test.ts +1 -1
  146. package/src/test/PermutationTest.test.ts +1 -1
  147. package/src/{tests → test}/RealDataValidation.test.ts +6 -6
  148. package/src/test/RunBenchCLI.test.ts +39 -38
  149. package/src/test/RunnerOrchestrator.test.ts +12 -12
  150. package/src/test/StatisticalUtils.test.ts +48 -12
  151. package/src/{table-util/test → test}/TableReport.test.ts +2 -2
  152. package/src/test/TestUtils.ts +12 -7
  153. package/src/test/TimeExport.test.ts +139 -0
  154. package/src/test/TimeSampler.test.ts +37 -0
  155. package/src/test/ViewerLive.e2e.test.ts +159 -0
  156. package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
  157. package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
  158. package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
  159. package/src/test/fixtures/cases/asyncCases.ts +9 -0
  160. package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
  161. package/src/test/fixtures/cases/variants/product.ts +2 -0
  162. package/src/test/fixtures/cases/variants/sum.ts +2 -0
  163. package/src/test/fixtures/discover/fast.ts +1 -0
  164. package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
  165. package/src/test/fixtures/invalid/bad.ts +1 -0
  166. package/src/test/fixtures/loader/fast.ts +1 -0
  167. package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
  168. package/src/test/fixtures/loader/stateful.ts +2 -0
  169. package/src/test/fixtures/stateful/stateful.ts +2 -0
  170. package/src/test/fixtures/variants/extra.ts +1 -0
  171. package/src/test/fixtures/variants/impl.ts +1 -0
  172. package/src/test/fixtures/worker/fast.ts +1 -0
  173. package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
  174. package/src/viewer/DateFormat.ts +30 -0
  175. package/src/viewer/Helpers.ts +23 -0
  176. package/src/viewer/LineData.ts +120 -0
  177. package/src/viewer/Providers.ts +191 -0
  178. package/src/viewer/ReportData.ts +123 -0
  179. package/src/viewer/State.ts +49 -0
  180. package/src/viewer/Theme.ts +15 -0
  181. package/src/viewer/components/App.tsx +73 -0
  182. package/src/viewer/components/DropZone.tsx +71 -0
  183. package/src/viewer/components/LazyPlot.ts +33 -0
  184. package/src/viewer/components/SamplesPanel.tsx +214 -0
  185. package/src/viewer/components/Shell.tsx +26 -0
  186. package/src/viewer/components/SourcePanel.tsx +216 -0
  187. package/src/viewer/components/SummaryPanel.tsx +332 -0
  188. package/src/viewer/components/TabBar.tsx +131 -0
  189. package/src/viewer/components/TabContent.tsx +46 -0
  190. package/src/viewer/components/ThemeToggle.tsx +50 -0
  191. package/src/viewer/index.html +20 -0
  192. package/src/viewer/main.tsx +4 -0
  193. package/src/viewer/plots/CIPlot.ts +313 -0
  194. package/src/{html/browser → viewer/plots}/HistogramKde.ts +33 -38
  195. package/src/viewer/plots/LegendUtils.ts +134 -0
  196. package/src/viewer/plots/PlotTypes.ts +85 -0
  197. package/src/viewer/plots/RenderPlots.ts +230 -0
  198. package/src/viewer/plots/SampleTimeSeries.ts +306 -0
  199. package/src/viewer/plots/SvgHelpers.ts +136 -0
  200. package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
  201. package/src/viewer/report.css +427 -0
  202. package/src/viewer/shell.css +357 -0
  203. package/src/viewer/tsconfig.json +11 -0
  204. package/dist/BrowserHeapSampler-B6asLKWQ.mjs +0 -202
  205. package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +0 -1
  206. package/dist/GcStats-wX7Xyblu.mjs +0 -77
  207. package/dist/GcStats-wX7Xyblu.mjs.map +0 -1
  208. package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
  209. package/dist/TimingUtils-DwOwkc8G.mjs +0 -597
  210. package/dist/TimingUtils-DwOwkc8G.mjs.map +0 -1
  211. package/dist/browser/index.js +0 -914
  212. package/dist/src-B-DDaCa9.mjs +0 -3108
  213. package/dist/src-B-DDaCa9.mjs.map +0 -1
  214. package/src/BenchMatrix.ts +0 -380
  215. package/src/BenchmarkReport.ts +0 -161
  216. package/src/HtmlDataPrep.ts +0 -148
  217. package/src/StandardSections.ts +0 -261
  218. package/src/StatisticalUtils.ts +0 -175
  219. package/src/TypeUtil.ts +0 -8
  220. package/src/browser/BrowserGcStats.ts +0 -44
  221. package/src/browser/BrowserHeapSampler.ts +0 -271
  222. package/src/export/JsonExport.ts +0 -103
  223. package/src/export/JsonFormat.ts +0 -91
  224. package/src/export/SpeedscopeExport.ts +0 -202
  225. package/src/heap-sample/HeapSampleReport.ts +0 -269
  226. package/src/html/HtmlReport.ts +0 -131
  227. package/src/html/HtmlTemplate.ts +0 -284
  228. package/src/html/Types.ts +0 -88
  229. package/src/html/browser/CIPlot.ts +0 -287
  230. package/src/html/browser/LegendUtils.ts +0 -163
  231. package/src/html/browser/RenderPlots.ts +0 -263
  232. package/src/html/browser/SampleTimeSeries.ts +0 -389
  233. package/src/html/browser/Types.ts +0 -96
  234. package/src/html/browser/index.ts +0 -1
  235. package/src/html/index.ts +0 -17
  236. package/src/runners/BasicRunner.ts +0 -364
  237. package/src/table-util/ConvergenceFormatters.ts +0 -19
  238. package/src/table-util/Formatters.ts +0 -157
  239. package/src/table-util/README.md +0 -70
  240. package/src/table-util/TableReport.ts +0 -293
  241. package/src/tests/fixtures/cases/asyncCases.ts +0 -7
  242. package/src/tests/fixtures/cases/variants/product.ts +0 -2
  243. package/src/tests/fixtures/cases/variants/sum.ts +0 -2
  244. package/src/tests/fixtures/discover/fast.ts +0 -1
  245. package/src/tests/fixtures/invalid/bad.ts +0 -1
  246. package/src/tests/fixtures/loader/fast.ts +0 -1
  247. package/src/tests/fixtures/loader/stateful.ts +0 -2
  248. package/src/tests/fixtures/stateful/stateful.ts +0 -2
  249. package/src/tests/fixtures/variants/extra.ts +0 -1
  250. package/src/tests/fixtures/variants/impl.ts +0 -1
  251. package/src/tests/fixtures/worker/fast.ts +0 -1
  252. /package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
  253. /package/src/{table-util/test → test}/TableValueExtractor.ts +0 -0
@@ -1,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,14 +1,19 @@
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";
9
10
 
10
11
  /** Validation helpers for statistical tests */
11
- export const assertValid = {
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
+ } = {
12
17
  pValue: (value: number) => {
13
18
  if (value < 0 || value > 1) {
14
19
  throw new Error(`Expected p-value between 0 and 1, got ${value}`);
@@ -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
+ });
@@ -0,0 +1,159 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ import { spawn } from "node:child_process";
3
+ import path from "node:path";
4
+ import type { Browser } from "playwright";
5
+ import { chromium } from "playwright";
6
+ import { afterAll, beforeAll, expect, test } from "vitest";
7
+
8
+ const binPath = path.resolve(import.meta.dirname!, "../../bin/benchforge");
9
+ const examplePath = path.resolve(
10
+ import.meta.dirname!,
11
+ "../../examples/simple-cli.ts",
12
+ );
13
+
14
+ let proc: ChildProcess;
15
+ let port: number;
16
+ let browser: Browser;
17
+
18
+ test("live viewer: summary tab shows stats", {
19
+ timeout: 30_000,
20
+ }, async () => {
21
+ const consoleErrors: string[] = [];
22
+ const page = await browser.newPage();
23
+ try {
24
+ page.on("console", msg => {
25
+ if (msg.type() === "error" && !msg.text().includes("WebGL"))
26
+ consoleErrors.push(msg.text());
27
+ });
28
+ await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
29
+
30
+ const summaryPanel = page.locator("#summary-panel");
31
+ const panel = summaryPanel.locator(".section-panel").first();
32
+ await panel.waitFor({ state: "visible", timeout: 15_000 });
33
+ const statRows = await summaryPanel.locator(".stat-row").count();
34
+ expect(statRows).toBeGreaterThan(0);
35
+ } finally {
36
+ await page.close();
37
+ }
38
+ expect(consoleErrors).toEqual([]);
39
+ });
40
+
41
+ test("live viewer: samples tab shows chart SVG", {
42
+ timeout: 30_000,
43
+ }, async () => {
44
+ const consoleErrors: string[] = [];
45
+ const page = await browser.newPage();
46
+ try {
47
+ page.on("console", msg => {
48
+ if (msg.type() === "error" && !msg.text().includes("WebGL"))
49
+ consoleErrors.push(msg.text());
50
+ });
51
+ await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
52
+
53
+ // Wait for summary to load (samples tab becomes enabled)
54
+ await page
55
+ .locator("#summary-panel .section-panel")
56
+ .first()
57
+ .waitFor({ state: "visible", timeout: 15_000 });
58
+
59
+ await page.locator("#tab-samples").click();
60
+
61
+ const samplesPanel = page.locator("#samples-panel");
62
+ const svg = samplesPanel.locator("svg").first();
63
+ await svg.waitFor({ state: "visible", timeout: 15_000 });
64
+ const childCount = await svg
65
+ .locator("path, rect, circle, line, text")
66
+ .count();
67
+ expect(childCount).toBeGreaterThan(0);
68
+ } finally {
69
+ await page.close();
70
+ }
71
+ expect(consoleErrors).toEqual([]);
72
+ });
73
+
74
+ test("live viewer: allocation tab has speedscope content", {
75
+ timeout: 30_000,
76
+ }, async () => {
77
+ const page = await browser.newPage();
78
+ try {
79
+ await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
80
+
81
+ await page.locator("#tab-flamechart").click();
82
+ const frame = page.frameLocator("#speedscope-iframe");
83
+ await frame
84
+ .locator("body *")
85
+ .first()
86
+ .waitFor({ state: "visible", timeout: 15_000 });
87
+ } finally {
88
+ await page.close();
89
+ }
90
+ });
91
+
92
+ test("live viewer: timing tab has speedscope content", {
93
+ timeout: 30_000,
94
+ }, async () => {
95
+ const page = await browser.newPage();
96
+ try {
97
+ await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
98
+
99
+ await page.locator("#tab-time-flamechart").click();
100
+ const frame = page.frameLocator("#time-speedscope-iframe");
101
+ await frame
102
+ .locator("body *")
103
+ .first()
104
+ .waitFor({ state: "visible", timeout: 15_000 });
105
+ } finally {
106
+ await page.close();
107
+ }
108
+ });
109
+
110
+ beforeAll(async () => {
111
+ const args = [
112
+ examplePath,
113
+ "--view-serve",
114
+ "--alloc",
115
+ "--profile",
116
+ "--iterations",
117
+ "3",
118
+ "--warmup",
119
+ "0",
120
+ ];
121
+
122
+ proc = spawn(binPath, args, {
123
+ stdio: ["ignore", "pipe", "pipe"],
124
+ });
125
+
126
+ // Parse port from stdout line like "Viewer: http://localhost:3939"
127
+ const portP = new Promise<number>((resolve, reject) => {
128
+ let stdout = "";
129
+ proc.stdout!.on("data", (chunk: Buffer) => {
130
+ stdout += chunk.toString();
131
+ const match = stdout.match(/Viewer: http:\/\/localhost:(\d+)/);
132
+ if (match) resolve(Number(match[1]));
133
+ });
134
+ proc.on("error", reject);
135
+ proc.on("exit", code => {
136
+ if (!port)
137
+ reject(
138
+ new Error(
139
+ `Process exited (${code}) before viewer started.\nstdout: ${stdout}`,
140
+ ),
141
+ );
142
+ });
143
+ setTimeout(
144
+ () =>
145
+ reject(new Error(`Timed out waiting for viewer.\nstdout: ${stdout}`)),
146
+ 60_000,
147
+ );
148
+ });
149
+
150
+ [port, browser] = await Promise.all([
151
+ portP,
152
+ chromium.launch({ headless: true }),
153
+ ]);
154
+ }, 90_000);
155
+
156
+ afterAll(async () => {
157
+ await browser?.close();
158
+ proc?.kill();
159
+ });