benchforge 0.1.11 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +99 -294
  3. package/bin/benchforge +1 -2
  4. package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
  5. package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
  6. package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
  7. package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
  8. package/dist/{BenchRunner-BzyUfiyB.d.mts → BenchRunner-DglX1NOn.d.mts} +119 -66
  9. package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
  10. package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
  11. package/dist/Formatters-BWj3d4sv.mjs +95 -0
  12. package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
  13. package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
  14. package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
  15. package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
  16. package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
  17. package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
  18. package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
  19. package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
  20. package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
  21. package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
  22. package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
  23. package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
  24. package/dist/bin/benchforge.mjs +4 -5
  25. package/dist/bin/benchforge.mjs.map +1 -1
  26. package/dist/index.d.mts +711 -558
  27. package/dist/index.mjs +98 -3
  28. package/dist/index.mjs.map +1 -0
  29. package/dist/runners/WorkerScript.d.mts +12 -4
  30. package/dist/runners/WorkerScript.mjs +77 -105
  31. package/dist/runners/WorkerScript.mjs.map +1 -1
  32. package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
  33. package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
  34. package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
  35. package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
  36. package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
  37. package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
  38. package/dist/viewer/index.html +19 -0
  39. package/dist/viewer/speedscope/LICENSE +21 -0
  40. package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
  41. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
  42. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
  43. package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
  44. package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
  45. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
  46. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
  47. package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
  48. package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
  49. package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
  50. package/dist/viewer/speedscope/file-format-schema.json +274 -0
  51. package/dist/viewer/speedscope/index.html +19 -0
  52. package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
  53. package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
  54. package/dist/viewer/speedscope/release.txt +3 -0
  55. package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
  56. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
  57. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
  58. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
  59. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
  60. package/package.json +52 -27
  61. package/src/bin/benchforge.ts +2 -2
  62. package/src/cli/AnalyzeArchive.ts +232 -0
  63. package/src/cli/BrowserBench.ts +322 -0
  64. package/src/cli/CliArgs.ts +164 -51
  65. package/src/cli/CliExport.ts +179 -0
  66. package/src/cli/CliOptions.ts +147 -0
  67. package/src/cli/CliReport.ts +197 -0
  68. package/src/cli/FilterBenchmarks.ts +18 -30
  69. package/src/cli/RunBenchCLI.ts +132 -866
  70. package/src/cli/SuiteRunner.ts +160 -0
  71. package/src/cli/ViewerServer.ts +282 -0
  72. package/src/export/AllocExport.ts +121 -0
  73. package/src/export/ArchiveExport.ts +146 -0
  74. package/src/export/ArchiveFormat.ts +50 -0
  75. package/src/export/CoverageExport.ts +148 -0
  76. package/src/export/EditorUri.ts +10 -0
  77. package/src/export/PerfettoExport.ts +64 -99
  78. package/src/export/SpeedscopeTypes.ts +98 -0
  79. package/src/export/TimeExport.ts +115 -0
  80. package/src/index.ts +86 -67
  81. package/src/matrix/BenchMatrix.ts +230 -0
  82. package/src/matrix/CaseLoader.ts +8 -6
  83. package/src/matrix/MatrixDirRunner.ts +153 -0
  84. package/src/matrix/MatrixFilter.ts +49 -47
  85. package/src/matrix/MatrixInlineRunner.ts +50 -0
  86. package/src/matrix/MatrixReport.ts +90 -250
  87. package/src/matrix/VariantLoader.ts +5 -5
  88. package/src/profiling/browser/BenchLoop.ts +51 -0
  89. package/src/profiling/browser/BrowserCDP.ts +133 -0
  90. package/src/profiling/browser/BrowserGcStats.ts +33 -0
  91. package/src/profiling/browser/BrowserProfiler.ts +160 -0
  92. package/src/profiling/browser/CdpClient.ts +82 -0
  93. package/src/profiling/browser/CdpPage.ts +138 -0
  94. package/src/profiling/browser/ChromeLauncher.ts +158 -0
  95. package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
  96. package/src/profiling/browser/PageLoadMode.ts +61 -0
  97. package/src/profiling/node/CoverageSampler.ts +27 -0
  98. package/src/profiling/node/CoverageTypes.ts +23 -0
  99. package/src/profiling/node/HeapSampleReport.ts +261 -0
  100. package/src/{heap-sample → profiling/node}/HeapSampler.ts +1 -2
  101. package/src/{heap-sample → profiling/node}/ResolvedProfile.ts +18 -9
  102. package/src/profiling/node/TimeSampler.ts +57 -0
  103. package/src/report/BenchmarkReport.ts +146 -0
  104. package/src/report/Colors.ts +9 -0
  105. package/src/report/Formatters.ts +110 -0
  106. package/src/report/GcSections.ts +151 -0
  107. package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
  108. package/src/report/HtmlReport.ts +223 -0
  109. package/src/report/ParseStats.ts +73 -0
  110. package/src/report/StandardSections.ts +147 -0
  111. package/src/report/ViewerSections.ts +286 -0
  112. package/src/report/text/TableReport.ts +253 -0
  113. package/src/report/text/TextReport.ts +123 -0
  114. package/src/runners/AdaptiveWrapper.ts +116 -236
  115. package/src/runners/BenchRunner.ts +20 -15
  116. package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
  117. package/src/runners/CreateRunner.ts +5 -7
  118. package/src/runners/GcStats.ts +47 -50
  119. package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
  120. package/src/runners/MergeBatches.ts +123 -0
  121. package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
  122. package/src/runners/RunnerOrchestrator.ts +127 -243
  123. package/src/runners/RunnerUtils.ts +75 -1
  124. package/src/runners/SampleStats.ts +100 -0
  125. package/src/runners/TimingRunner.ts +244 -0
  126. package/src/runners/TimingUtils.ts +3 -2
  127. package/src/runners/WorkerScript.ts +135 -151
  128. package/src/stats/BootstrapDifference.ts +282 -0
  129. package/src/{PermutationTest.ts → stats/PermutationTest.ts} +8 -17
  130. package/src/stats/StatisticalUtils.ts +445 -0
  131. package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
  132. package/src/test/AdaptiveRunner.test.ts +39 -41
  133. package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
  134. package/src/test/AdaptiveStatistics.integration.ts +2 -2
  135. package/src/{tests → test}/BenchMatrix.test.ts +19 -16
  136. package/src/test/BenchmarkReport.test.ts +63 -13
  137. package/src/test/BrowserBench.e2e.test.ts +186 -17
  138. package/src/test/BrowserBench.test.ts +10 -5
  139. package/src/test/BuildTimeSection.test.ts +130 -0
  140. package/src/test/CapSamples.test.ts +82 -0
  141. package/src/test/CoverageExport.test.ts +115 -0
  142. package/src/test/CoverageSampler.test.ts +33 -0
  143. package/src/test/HeapAttribution.test.ts +14 -14
  144. package/src/{tests → test}/MatrixFilter.test.ts +1 -1
  145. package/src/{tests → test}/MatrixReport.test.ts +1 -1
  146. package/src/test/PermutationTest.test.ts +1 -1
  147. package/src/{tests → test}/RealDataValidation.test.ts +6 -6
  148. package/src/test/RunBenchCLI.test.ts +39 -38
  149. package/src/test/RunnerOrchestrator.test.ts +12 -12
  150. package/src/test/StatisticalUtils.test.ts +48 -12
  151. package/src/{table-util/test → test}/TableReport.test.ts +2 -2
  152. package/src/test/TestUtils.ts +12 -7
  153. package/src/test/TimeExport.test.ts +139 -0
  154. package/src/test/TimeSampler.test.ts +37 -0
  155. package/src/test/ViewerLive.e2e.test.ts +159 -0
  156. package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
  157. package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
  158. package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
  159. package/src/test/fixtures/cases/asyncCases.ts +9 -0
  160. package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
  161. package/src/test/fixtures/cases/variants/product.ts +2 -0
  162. package/src/test/fixtures/cases/variants/sum.ts +2 -0
  163. package/src/test/fixtures/discover/fast.ts +1 -0
  164. package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
  165. package/src/test/fixtures/invalid/bad.ts +1 -0
  166. package/src/test/fixtures/loader/fast.ts +1 -0
  167. package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
  168. package/src/test/fixtures/loader/stateful.ts +2 -0
  169. package/src/test/fixtures/stateful/stateful.ts +2 -0
  170. package/src/test/fixtures/variants/extra.ts +1 -0
  171. package/src/test/fixtures/variants/impl.ts +1 -0
  172. package/src/test/fixtures/worker/fast.ts +1 -0
  173. package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
  174. package/src/viewer/DateFormat.ts +30 -0
  175. package/src/viewer/Helpers.ts +23 -0
  176. package/src/viewer/LineData.ts +120 -0
  177. package/src/viewer/Providers.ts +191 -0
  178. package/src/viewer/ReportData.ts +123 -0
  179. package/src/viewer/State.ts +49 -0
  180. package/src/viewer/Theme.ts +15 -0
  181. package/src/viewer/components/App.tsx +73 -0
  182. package/src/viewer/components/DropZone.tsx +71 -0
  183. package/src/viewer/components/LazyPlot.ts +33 -0
  184. package/src/viewer/components/SamplesPanel.tsx +214 -0
  185. package/src/viewer/components/Shell.tsx +26 -0
  186. package/src/viewer/components/SourcePanel.tsx +216 -0
  187. package/src/viewer/components/SummaryPanel.tsx +332 -0
  188. package/src/viewer/components/TabBar.tsx +131 -0
  189. package/src/viewer/components/TabContent.tsx +46 -0
  190. package/src/viewer/components/ThemeToggle.tsx +50 -0
  191. package/src/viewer/index.html +20 -0
  192. package/src/viewer/main.tsx +4 -0
  193. package/src/viewer/plots/CIPlot.ts +313 -0
  194. package/src/{html/browser → viewer/plots}/HistogramKde.ts +33 -38
  195. package/src/viewer/plots/LegendUtils.ts +134 -0
  196. package/src/viewer/plots/PlotTypes.ts +85 -0
  197. package/src/viewer/plots/RenderPlots.ts +230 -0
  198. package/src/viewer/plots/SampleTimeSeries.ts +306 -0
  199. package/src/viewer/plots/SvgHelpers.ts +136 -0
  200. package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
  201. package/src/viewer/report.css +427 -0
  202. package/src/viewer/shell.css +357 -0
  203. package/src/viewer/tsconfig.json +11 -0
  204. package/dist/BrowserHeapSampler-B6asLKWQ.mjs +0 -202
  205. package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +0 -1
  206. package/dist/GcStats-wX7Xyblu.mjs +0 -77
  207. package/dist/GcStats-wX7Xyblu.mjs.map +0 -1
  208. package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
  209. package/dist/TimingUtils-DwOwkc8G.mjs +0 -597
  210. package/dist/TimingUtils-DwOwkc8G.mjs.map +0 -1
  211. package/dist/browser/index.js +0 -914
  212. package/dist/src-B-DDaCa9.mjs +0 -3108
  213. package/dist/src-B-DDaCa9.mjs.map +0 -1
  214. package/src/BenchMatrix.ts +0 -380
  215. package/src/BenchmarkReport.ts +0 -161
  216. package/src/HtmlDataPrep.ts +0 -148
  217. package/src/StandardSections.ts +0 -261
  218. package/src/StatisticalUtils.ts +0 -175
  219. package/src/TypeUtil.ts +0 -8
  220. package/src/browser/BrowserGcStats.ts +0 -44
  221. package/src/browser/BrowserHeapSampler.ts +0 -271
  222. package/src/export/JsonExport.ts +0 -103
  223. package/src/export/JsonFormat.ts +0 -91
  224. package/src/export/SpeedscopeExport.ts +0 -202
  225. package/src/heap-sample/HeapSampleReport.ts +0 -269
  226. package/src/html/HtmlReport.ts +0 -131
  227. package/src/html/HtmlTemplate.ts +0 -284
  228. package/src/html/Types.ts +0 -88
  229. package/src/html/browser/CIPlot.ts +0 -287
  230. package/src/html/browser/LegendUtils.ts +0 -163
  231. package/src/html/browser/RenderPlots.ts +0 -263
  232. package/src/html/browser/SampleTimeSeries.ts +0 -389
  233. package/src/html/browser/Types.ts +0 -96
  234. package/src/html/browser/index.ts +0 -1
  235. package/src/html/index.ts +0 -17
  236. package/src/runners/BasicRunner.ts +0 -364
  237. package/src/table-util/ConvergenceFormatters.ts +0 -19
  238. package/src/table-util/Formatters.ts +0 -157
  239. package/src/table-util/README.md +0 -70
  240. package/src/table-util/TableReport.ts +0 -293
  241. package/src/tests/fixtures/cases/asyncCases.ts +0 -7
  242. package/src/tests/fixtures/cases/variants/product.ts +0 -2
  243. package/src/tests/fixtures/cases/variants/sum.ts +0 -2
  244. package/src/tests/fixtures/discover/fast.ts +0 -1
  245. package/src/tests/fixtures/invalid/bad.ts +0 -1
  246. package/src/tests/fixtures/loader/fast.ts +0 -1
  247. package/src/tests/fixtures/loader/stateful.ts +0 -2
  248. package/src/tests/fixtures/stateful/stateful.ts +0 -2
  249. package/src/tests/fixtures/variants/extra.ts +0 -1
  250. package/src/tests/fixtures/variants/impl.ts +0 -1
  251. package/src/tests/fixtures/worker/fast.ts +0 -1
  252. /package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
  253. /package/src/{table-util/test → test}/TableValueExtractor.ts +0 -0
@@ -0,0 +1,445 @@
1
+ /** Whether CI was computed from block-level or sample-level resampling */
2
+ export type CILevel = "block" | "sample";
3
+
4
+ /** Stat descriptor for multi-bootstrap: known stat kinds enable zero-alloc inner loops */
5
+ export type StatKind = "mean" | "min" | "max" | { percentile: number };
6
+
7
+ /** Bootstrap estimate with confidence interval and raw resample distribution */
8
+ export interface BootstrapResult {
9
+ /** Point estimate from the original sample */
10
+ estimate: number;
11
+ /** Confidence interval [lower, upper] from bootstrap resampling */
12
+ ci: [number, number];
13
+ /** Bootstrap resample distribution (for visualization) */
14
+ samples: number[];
15
+ /** Block-level (between-run) or sample-level (within-run) resampling */
16
+ ciLevel: CILevel;
17
+ /** Original sample count before subsampling (set only when cap applied) */
18
+ subsampled?: number;
19
+ }
20
+
21
+ export type CIDirection = "faster" | "slower" | "uncertain" | "equivalent";
22
+
23
+ /** Binned histogram for efficient transfer to browser */
24
+ export interface HistogramBin {
25
+ /** Bin center value */
26
+ x: number;
27
+ count: number;
28
+ }
29
+
30
+ /**
31
+ * Bootstrap confidence interval for percentage difference between two sample medians.
32
+ * Used for baseline comparisons: negative percent means current is faster.
33
+ */
34
+ export interface DifferenceCI {
35
+ /** Observed percentage difference (current - baseline) / baseline */
36
+ percent: number;
37
+ /** Confidence interval [lower, upper] in percent */
38
+ ci: [number, number];
39
+ /** Whether the CI excludes zero: "faster", "slower", or "uncertain" */
40
+ direction: CIDirection;
41
+ /** Bootstrap distribution histogram for visualization */
42
+ histogram?: HistogramBin[];
43
+ /** Label for the CI plot title (e.g. "mean Δ%") */
44
+ label?: string;
45
+ /** Blocks trimmed per side [baseline, current] via Tukey fences */
46
+ trimmed?: [number, number];
47
+ /** Block-level (between-run) or sample-level (within-run) resampling */
48
+ ciLevel?: CILevel;
49
+ /** false when batch count is too low for reliable CI */
50
+ ciReliable?: boolean;
51
+ /** Original sample count before subsampling (set only when cap applied) */
52
+ subsampled?: number;
53
+ }
54
+
55
+ /** Options for bootstrap resampling */
56
+ type BootstrapOptions = {
57
+ /** Number of bootstrap resamples (default: 10000) */
58
+ resamples?: number;
59
+ /** Confidence level 0-1 (default: 0.95) */
60
+ confidence?: number;
61
+ };
62
+
63
+ interface StatOp {
64
+ origIndex: number;
65
+ compute: (buf: number[]) => number;
66
+ pointEstimate: (s: number[]) => number;
67
+ }
68
+
69
+ export const defaultConfidence = 0.95;
70
+ export const bootstrapSamples = 10000;
71
+ export const maxBootstrapInput = 10_000;
72
+ const outlierMultiplier = 1.5;
73
+
74
+ /** Swap direction labels for higher-is-better metrics (positive = faster) */
75
+ export function swapDirection(ci: DifferenceCI): DifferenceCI {
76
+ const swap: Record<CIDirection, CIDirection> = {
77
+ faster: "slower",
78
+ slower: "faster",
79
+ uncertain: "uncertain",
80
+ equivalent: "equivalent",
81
+ };
82
+ return { ...ci, direction: swap[ci.direction] };
83
+ }
84
+
85
+ /** Negate percent and CI for "higher is better" metrics (e.g., throughput) */
86
+ export function flipCI(ci: DifferenceCI): DifferenceCI {
87
+ return {
88
+ ...ci,
89
+ percent: -ci.percent,
90
+ ci: [-ci.ci[1], -ci.ci[0]],
91
+ histogram: ci.histogram?.map(bin => ({ x: -bin.x, count: bin.count })),
92
+ };
93
+ }
94
+
95
+ /** Compute a statistic from samples by kind */
96
+ export function computeStat(samples: number[], kind: StatKind): number {
97
+ if (kind === "mean") return average(samples);
98
+ if (kind === "min") return minOf(samples);
99
+ if (kind === "max") return maxOf(samples);
100
+ return percentile(samples, kind.percentile);
101
+ }
102
+
103
+ /** @return true if the stat kind supports bootstrap CI (min/max don't) */
104
+ export function isBootstrappable(kind: StatKind): boolean {
105
+ return kind !== "min" && kind !== "max";
106
+ }
107
+
108
+ /** @return smallest value in samples (loop to avoid spread-arg limits) */
109
+ export function minOf(samples: number[]): number {
110
+ let min = samples[0];
111
+ for (let i = 1; i < samples.length; i++) {
112
+ if (samples[i] < min) min = samples[i];
113
+ }
114
+ return min;
115
+ }
116
+
117
+ /** @return largest value in samples (loop to avoid spread-arg limits) */
118
+ export function maxOf(samples: number[]): number {
119
+ let max = samples[0];
120
+ for (let i = 1; i < samples.length; i++) {
121
+ if (samples[i] > max) max = samples[i];
122
+ }
123
+ return max;
124
+ }
125
+
126
+ /** @return relative standard deviation (coefficient of variation) */
127
+ export function coefficientOfVariation(samples: number[]): number {
128
+ const mean = average(samples);
129
+ if (mean === 0) return 0;
130
+ const stdDev = standardDeviation(samples);
131
+ return stdDev / mean;
132
+ }
133
+
134
+ /** @return median absolute deviation for robust variability measure */
135
+ export function medianAbsoluteDeviation(samples: number[]): number {
136
+ const med = median(samples);
137
+ const deviations = samples.map(x => Math.abs(x - med));
138
+ return median(deviations);
139
+ }
140
+
141
+ /** @return outliers detected via Tukey's interquartile range method */
142
+ export function findOutliers(samples: number[]): {
143
+ rate: number;
144
+ indices: number[];
145
+ } {
146
+ const [lo, hi] = tukeyFences(samples, outlierMultiplier);
147
+ const indices = samples.flatMap((v, i) => (v < lo || v > hi ? [i] : []));
148
+ return { rate: indices.length / samples.length, indices };
149
+ }
150
+
151
+ /** Sample-level bootstrap CI: resample individual samples with replacement. */
152
+ export function sampleBootstrap(
153
+ samples: number[],
154
+ statFn: (s: number[]) => number,
155
+ options: BootstrapOptions = {},
156
+ ): BootstrapResult {
157
+ const { resamples = bootstrapSamples, confidence: conf = defaultConfidence } =
158
+ options;
159
+ const sub = subsample(samples, maxBootstrapInput);
160
+ const buf = new Array(sub.length);
161
+ const stats = Array.from({ length: resamples }, () => {
162
+ resampleInto(sub, buf);
163
+ return statFn(buf);
164
+ });
165
+ return {
166
+ estimate: statFn(samples),
167
+ ci: computeInterval(stats, conf),
168
+ samples: stats,
169
+ ciLevel: "sample",
170
+ ...(sub !== samples && { subsampled: samples.length }),
171
+ };
172
+ }
173
+
174
+ /** Shared-resample bootstrap: one resample per iteration, all stats computed on it.
175
+ * Mean is computed first (non-destructive), then percentiles via in-place quickSelect. */
176
+ export function multiSampleBootstrap(
177
+ samples: number[],
178
+ stats: StatKind[],
179
+ options: BootstrapOptions = {},
180
+ ): BootstrapResult[] {
181
+ const { resamples = bootstrapSamples, confidence: conf = defaultConfidence } =
182
+ options;
183
+ const sub = subsample(samples, maxBootstrapInput);
184
+ const n = sub.length;
185
+ const buf = new Array(n);
186
+ const ops = buildStatOps(stats, n);
187
+ const allStats = ops.map(() => new Array<number>(resamples));
188
+
189
+ for (let i = 0; i < resamples; i++) {
190
+ resampleInto(sub, buf);
191
+ for (let j = 0; j < ops.length; j++) {
192
+ allStats[j][i] = ops[j].compute(buf);
193
+ }
194
+ }
195
+
196
+ const capped = sub !== samples;
197
+ const results = new Array<BootstrapResult>(stats.length);
198
+ for (let j = 0; j < ops.length; j++) {
199
+ results[ops[j].origIndex] = {
200
+ estimate: ops[j].pointEstimate(samples),
201
+ ci: computeInterval(allStats[j], conf),
202
+ samples: allStats[j],
203
+ ciLevel: "sample",
204
+ ...(capped && { subsampled: samples.length }),
205
+ };
206
+ }
207
+ return results;
208
+ }
209
+
210
+ /** Bootstrap CIs for multiple stats, dispatching block vs sample automatically.
211
+ * Returns undefined for non-bootstrappable stats (min/max). */
212
+ export function bootstrapCIs(
213
+ samples: number[],
214
+ batchOffsets: number[] | undefined,
215
+ stats: StatKind[],
216
+ options?: BootstrapOptions,
217
+ ): (BootstrapResult | undefined)[] {
218
+ const bsStats = stats.filter(isBootstrappable);
219
+ if (bsStats.length === 0) return stats.map(() => undefined);
220
+
221
+ const hasBlocks = (batchOffsets?.length ?? 0) >= 2;
222
+ const bsResults = hasBlocks
223
+ ? bsStats.map(s =>
224
+ blockBootstrap(samples, batchOffsets!, statKindToFn(s), options),
225
+ )
226
+ : multiSampleBootstrap(samples, bsStats, options);
227
+
228
+ const results: (BootstrapResult | undefined)[] = new Array(stats.length);
229
+ let bi = 0;
230
+ for (let i = 0; i < stats.length; i++) {
231
+ results[i] = isBootstrappable(stats[i]) ? bsResults[bi++] : undefined;
232
+ }
233
+ return results;
234
+ }
235
+
236
+ /** Convert StatKind to a stat function */
237
+ export function statKindToFn(kind: StatKind): (s: number[]) => number {
238
+ if (kind === "mean") return average;
239
+ if (kind === "min") return minOf;
240
+ if (kind === "max") return maxOf;
241
+ const p = kind.percentile;
242
+ return (s: number[]) => percentile(s, p);
243
+ }
244
+
245
+ /** Block bootstrap CI: Tukey-trim outlier batches, then resample per-block
246
+ * statFn values as independent observations. Requires 2+ blocks. */
247
+ export function blockBootstrap(
248
+ samples: number[],
249
+ blocks: number[],
250
+ statFn: (s: number[]) => number,
251
+ options: BootstrapOptions = {},
252
+ ): BootstrapResult {
253
+ const { resamples = bootstrapSamples, confidence: conf = defaultConfidence } =
254
+ options;
255
+ const side = prepareBlocks(samples, blocks, statFn);
256
+ const stats = Array.from({ length: resamples }, () =>
257
+ average(createResample(side.blockVals)),
258
+ );
259
+ return {
260
+ estimate: statFn(side.filtered),
261
+ ci: computeInterval(stats, conf),
262
+ samples: stats,
263
+ ciLevel: "block",
264
+ };
265
+ }
266
+
267
+ /** @return mean of values */
268
+ export function average(values: number[]): number {
269
+ const sum = values.reduce((a, b) => a + b, 0);
270
+ return sum / values.length;
271
+ }
272
+
273
+ /** @return median (50th percentile) of values */
274
+ export function median(values: number[]): number {
275
+ return percentile(values, 0.5);
276
+ }
277
+
278
+ /** @return standard deviation with Bessel's correction */
279
+ export function standardDeviation(samples: number[]): number {
280
+ if (samples.length <= 1) return 0;
281
+ const mean = average(samples);
282
+ const variance =
283
+ samples.reduce((sum, x) => sum + (x - mean) ** 2, 0) / (samples.length - 1);
284
+ return Math.sqrt(variance);
285
+ }
286
+
287
+ /** @return value at percentile p (0-1), using O(N) quickselect */
288
+ export function percentile(values: number[], p: number): number {
289
+ const copy = values.slice();
290
+ const k = Math.max(0, Math.ceil(copy.length * p) - 1);
291
+ return quickSelect(copy, k);
292
+ }
293
+
294
+ /** Hoare's selection: O(N) average k-th smallest element. Mutates arr. */
295
+ export function quickSelect(arr: number[], k: number): number {
296
+ let lo = 0;
297
+ let hi = arr.length - 1;
298
+ while (lo < hi) {
299
+ const [i, j] = partition(arr, lo, hi);
300
+ if (k <= j) hi = j;
301
+ else if (k >= i) lo = i;
302
+ else break;
303
+ }
304
+ return arr[k];
305
+ }
306
+
307
+ /** Fill buf in-place with bootstrap resample (with replacement) from source */
308
+ export function resampleInto(source: number[], buf: number[]): void {
309
+ const n = source.length;
310
+ for (let i = 0; i < n; i++) {
311
+ buf[i] = source[Math.floor(Math.random() * n)];
312
+ }
313
+ }
314
+
315
+ /** @return bootstrap resample with replacement */
316
+ export function createResample(samples: number[]): number[] {
317
+ const n = samples.length;
318
+ return Array.from(
319
+ { length: n },
320
+ () => samples[Math.floor(Math.random() * n)],
321
+ );
322
+ }
323
+
324
+ /** @return Tukey fence bounds [lo, hi] for the given IQR multiplier.
325
+ * minIqr prevents degenerate fences when values are tightly clustered. */
326
+ export function tukeyFences(
327
+ values: number[],
328
+ multiplier = 3,
329
+ minIqr = 0,
330
+ ): [lo: number, hi: number] {
331
+ const q1 = percentile(values, 0.25);
332
+ const q3 = percentile(values, 0.75);
333
+ const iqr = Math.max(q3 - q1, minIqr);
334
+ return [q1 - multiplier * iqr, q3 + multiplier * iqr];
335
+ }
336
+
337
+ /** @return indices of values below the upper 3x IQR Tukey fence.
338
+ * Only trims slow outliers — fast batches reflect less environmental noise, not errors.
339
+ * Floors IQR at 2% of median to avoid over-trimming tightly clustered batch means. */
340
+ export function tukeyKeep(values: number[]): number[] {
341
+ if (values.length < 4) return values.map((_, i) => i);
342
+ const minIqr = median(values) * 0.02;
343
+ const [, hi] = tukeyFences(values, 3, minIqr);
344
+ return values.flatMap((v, i) => (v <= hi ? [i] : []));
345
+ }
346
+
347
+ /** @return samples split into blocks by offset boundaries */
348
+ export function splitByOffsets(
349
+ samples: number[],
350
+ offsets: number[],
351
+ ): number[][] {
352
+ return offsets.map((start, i) => {
353
+ const end = i + 1 < offsets.length ? offsets[i + 1] : samples.length;
354
+ return samples.slice(start, end);
355
+ });
356
+ }
357
+
358
+ /** @return per-block statistic values from sample data split by offsets */
359
+ export function blockValues(
360
+ samples: number[],
361
+ offsets: number[],
362
+ fn: (s: number[]) => number,
363
+ ): number[] {
364
+ return splitByOffsets(samples, offsets).map(fn);
365
+ }
366
+
367
+ /** Tukey-trim outlier blocks and compute per-block statistic for one side */
368
+ export function prepareBlocks(
369
+ samples: number[],
370
+ offsets: number[],
371
+ fn: (s: number[]) => number,
372
+ noTrim?: boolean,
373
+ ): { blockVals: number[]; filtered: number[]; trimCount: number } {
374
+ const splits = splitByOffsets(samples, offsets);
375
+ const means = splits.map(average);
376
+ const keep = noTrim ? means.map((_, i) => i) : tukeyKeep(means);
377
+ return {
378
+ blockVals: keep.map(i => fn(splits[i])),
379
+ filtered: keep.flatMap(i => splits[i]),
380
+ trimCount: means.length - keep.length,
381
+ };
382
+ }
383
+
384
+ /** Random subsample without replacement via partial Fisher-Yates. Returns original if n <= max. */
385
+ export function subsample(samples: number[], max: number): number[] {
386
+ if (samples.length <= max) return samples;
387
+ const copy = samples.slice();
388
+ for (let i = 0; i < max; i++) {
389
+ const j = i + Math.floor(Math.random() * (copy.length - i));
390
+ [copy[i], copy[j]] = [copy[j], copy[i]];
391
+ }
392
+ return copy.slice(0, max);
393
+ }
394
+
395
+ /** @return confidence interval [lower, upper] */
396
+ export function computeInterval(
397
+ values: number[],
398
+ conf: number,
399
+ ): [number, number] {
400
+ const alpha = (1 - conf) / 2;
401
+ return [percentile(values, alpha), percentile(values, 1 - alpha)];
402
+ }
403
+
404
+ /** Build stat operations in safe order: mean/min/max first (non-destructive),
405
+ * then percentiles ascending (use quickSelect which mutates buf) */
406
+ function buildStatOps(stats: StatKind[], n: number): StatOp[] {
407
+ const simple = (order: number, i: number, fn: (s: number[]) => number) => ({
408
+ order,
409
+ compute: fn,
410
+ pointEstimate: fn,
411
+ origIndex: i,
412
+ });
413
+ const ops = stats.map((s, i): StatOp & { order: number } => {
414
+ if (s === "mean") return simple(-3, i, average);
415
+ if (s === "min") return simple(-2, i, minOf);
416
+ if (s === "max") return simple(-1, i, maxOf);
417
+ const p = s.percentile;
418
+ const k = Math.max(0, Math.ceil(n * p) - 1);
419
+ return {
420
+ order: p,
421
+ origIndex: i,
422
+ compute: (buf: number[]) => quickSelect(buf, k),
423
+ pointEstimate: (v: number[]) => percentile(v, p),
424
+ };
425
+ });
426
+ ops.sort((a, b) => a.order - b.order);
427
+ return ops;
428
+ }
429
+
430
+ /** Hoare partition around the midpoint pivot. @return [i, j] boundary indices. */
431
+ function partition(arr: number[], lo: number, hi: number): [number, number] {
432
+ const pivot = arr[lo + ((hi - lo) >> 1)];
433
+ let i = lo;
434
+ let j = hi;
435
+ while (i <= j) {
436
+ while (arr[i] < pivot) i++;
437
+ while (arr[j] > pivot) j--;
438
+ if (i <= j) {
439
+ [arr[i], arr[j]] = [arr[j], arr[i]];
440
+ i++;
441
+ j--;
442
+ }
443
+ }
444
+ return [i, j];
445
+ }
@@ -2,7 +2,7 @@ import { test } from "vitest";
2
2
  import { checkConvergence } from "../runners/AdaptiveWrapper.ts";
3
3
  import { bevy30SamplesNs } from "./fixtures/bevy30-samples.ts";
4
4
 
5
- test("convergence with insufficient samples", () => {
5
+ test.skip("convergence with insufficient samples", () => {
6
6
  const samples = [1e6, 2e6, 3e6]; // 3 samples in nanoseconds
7
7
  const result = checkConvergence(samples);
8
8
 
@@ -14,7 +14,7 @@ test("convergence with insufficient samples", () => {
14
14
  }
15
15
  });
16
16
 
17
- test("convergence with stable samples", () => {
17
+ test.skip("convergence with stable samples", () => {
18
18
  // Create very stable samples (all within 1% of each other)
19
19
  const base = 50e6; // 50ms in nanoseconds
20
20
  const samples = Array.from(
@@ -30,7 +30,7 @@ test("convergence with stable samples", () => {
30
30
  }
31
31
  });
32
32
 
33
- test("convergence with drifting median", () => {
33
+ test.skip("convergence with drifting median", () => {
34
34
  // Create samples with increasing median over time
35
35
  const samples = Array.from(
36
36
  { length: 200 },
@@ -48,7 +48,7 @@ test("convergence with drifting median", () => {
48
48
  }
49
49
  });
50
50
 
51
- test("convergence with outliers", () => {
51
+ test.skip("convergence with outliers", () => {
52
52
  // Create stable samples with occasional outliers every 20 samples
53
53
  const base = 50e6;
54
54
  const samples = Array.from({ length: 200 }, (_, i) =>
@@ -63,7 +63,7 @@ test("convergence with outliers", () => {
63
63
  }
64
64
  });
65
65
 
66
- test("convergence with real bevy30 data - early samples", () => {
66
+ test.skip("convergence with real bevy30 data - early samples", () => {
67
67
  // Test with first 100 samples (should show initial instability)
68
68
  const early = bevy30SamplesNs.slice(0, 100);
69
69
  const result = checkConvergence(early);
@@ -78,7 +78,7 @@ test("convergence with real bevy30 data - early samples", () => {
78
78
  );
79
79
  });
80
80
 
81
- test("convergence with real bevy30 data - middle samples", () => {
81
+ test.skip("convergence with real bevy30 data - middle samples", () => {
82
82
  // Test with middle 200 samples (should be more stable)
83
83
  const middle = bevy30SamplesNs.slice(200, 400);
84
84
  const result = checkConvergence(middle);
@@ -92,7 +92,7 @@ test("convergence with real bevy30 data - middle samples", () => {
92
92
  );
93
93
  });
94
94
 
95
- test("convergence with real bevy30 data - all samples", () => {
95
+ test.skip("convergence with real bevy30 data - all samples", () => {
96
96
  const result = checkConvergence(bevy30SamplesNs);
97
97
 
98
98
  if (result.confidence > 100 || result.confidence < 0) {
@@ -109,7 +109,7 @@ test("convergence with real bevy30 data - all samples", () => {
109
109
  );
110
110
  });
111
111
 
112
- test("convergence progression over time", () => {
112
+ test.skip("convergence progression over time", () => {
113
113
  const checkpoints = [50, 100, 150, 200, 300, 400, 500, 610];
114
114
  const progressions = checkpoints.map(n => {
115
115
  const result = checkConvergence(bevy30SamplesNs.slice(0, n));
@@ -132,7 +132,7 @@ test("convergence progression over time", () => {
132
132
  }
133
133
  });
134
134
 
135
- test("window size adaptation for different execution times", () => {
135
+ test.skip("window size adaptation for different execution times", () => {
136
136
  // Fast samples (microseconds)
137
137
  const fastSamples = Array.from(
138
138
  { length: 100 },
@@ -155,7 +155,7 @@ test("window size adaptation for different execution times", () => {
155
155
  }
156
156
  });
157
157
 
158
- test("outlier impact calculation", () => {
158
+ test.skip("outlier impact calculation", () => {
159
159
  // 95 stable samples + 5 outliers (2x slower)
160
160
  const base = 50e6; // 50ms
161
161
  const stable = Array.from(
@@ -1,42 +1,40 @@
1
1
  import { expect, test } from "vitest";
2
- import type { BenchmarkSpec } from "../Benchmark.ts";
3
2
  import {
4
3
  checkConvergence,
5
4
  createAdaptiveWrapper,
6
5
  } from "../runners/AdaptiveWrapper.ts";
7
- import { BasicRunner } from "../runners/BasicRunner.ts";
8
-
9
- test(
10
- "adaptive runner collects samples for minimum time",
11
- { timeout: 10000 },
12
- async () => {
13
- const runner = new BasicRunner();
14
- const adaptive = createAdaptiveWrapper(runner, {
15
- minTime: 100,
16
- maxTime: 300,
17
- });
18
-
19
- const benchmark: BenchmarkSpec = {
20
- name: "test-min-time",
21
- fn: () => {
22
- let sum = 0;
23
- for (let i = 0; i < 1000; i++) sum += i;
24
- return sum;
25
- },
26
- };
27
-
28
- const start = performance.now();
29
- const results = await adaptive.runBench(benchmark, { minTime: 100 });
30
- const elapsed = performance.now() - start;
31
-
32
- expect(results).toHaveLength(1);
33
- expect(results[0].samples.length).toBeGreaterThan(0);
34
- expect(elapsed).toBeGreaterThanOrEqual(100);
35
- },
36
- );
37
-
38
- test("adaptive runner respects max time limit", async () => {
39
- const runner = new BasicRunner();
6
+ import type { BenchmarkSpec } from "../runners/BenchmarkSpec.ts";
7
+ import { TimingRunner } from "../runners/TimingRunner.ts";
8
+
9
+ test.skip("adaptive runner collects samples for minimum time", {
10
+ timeout: 10000,
11
+ }, async () => {
12
+ const runner = new TimingRunner();
13
+ const adaptive = createAdaptiveWrapper(runner, {
14
+ minTime: 100,
15
+ maxTime: 300,
16
+ });
17
+
18
+ const benchmark: BenchmarkSpec = {
19
+ name: "test-min-time",
20
+ fn: () => {
21
+ let sum = 0;
22
+ for (let i = 0; i < 1000; i++) sum += i;
23
+ return sum;
24
+ },
25
+ };
26
+
27
+ const start = performance.now();
28
+ const results = await adaptive.runBench(benchmark, { minTime: 100 });
29
+ const elapsed = performance.now() - start;
30
+
31
+ expect(results).toHaveLength(1);
32
+ expect(results[0].samples.length).toBeGreaterThan(0);
33
+ expect(elapsed).toBeGreaterThanOrEqual(100);
34
+ });
35
+
36
+ test.skip("adaptive runner respects max time limit", async () => {
37
+ const runner = new TimingRunner();
40
38
  const adaptive = createAdaptiveWrapper(runner, {
41
39
  minTime: 100,
42
40
  maxTime: 2000,
@@ -62,8 +60,8 @@ test("adaptive runner respects max time limit", async () => {
62
60
  expect(results[0].totalTime).toBeLessThanOrEqual(2.0);
63
61
  });
64
62
 
65
- test("adaptive runner merges results correctly", async () => {
66
- const runner = new BasicRunner();
63
+ test.skip("adaptive runner merges results correctly", async () => {
64
+ const runner = new TimingRunner();
67
65
  const adaptive = createAdaptiveWrapper(runner, {
68
66
  minTime: 100,
69
67
  maxTime: 200,
@@ -101,8 +99,8 @@ test("adaptive runner merges results correctly", async () => {
101
99
  expect(result.totalTime).toBeGreaterThan(0);
102
100
  }, 10000);
103
101
 
104
- test("convergence detection with stable benchmark", async () => {
105
- const runner = new BasicRunner();
102
+ test.skip("convergence detection with stable benchmark", async () => {
103
+ const runner = new TimingRunner();
106
104
  const adaptive = createAdaptiveWrapper(runner, {
107
105
  minTime: 100,
108
106
  maxTime: 2000,
@@ -129,8 +127,8 @@ test("convergence detection with stable benchmark", async () => {
129
127
  expect(result.convergence?.reason).toBeDefined();
130
128
  });
131
129
 
132
- test("convergence detection with variable benchmark", async () => {
133
- const runner = new BasicRunner();
130
+ test.skip("convergence detection with variable benchmark", async () => {
131
+ const runner = new TimingRunner();
134
132
  const adaptive = createAdaptiveWrapper(runner, {
135
133
  minTime: 100,
136
134
  maxTime: 1000,
@@ -162,7 +160,7 @@ test("convergence detection with variable benchmark", async () => {
162
160
  expect(result.convergence?.confidence).toBeLessThanOrEqual(100);
163
161
  });
164
162
 
165
- test("checkConvergence function basics", () => {
163
+ test.skip("checkConvergence function basics", () => {
166
164
  // Not enough samples
167
165
  const fewSamples = [1e6, 1.1e6, 1e6];
168
166
  const fewResult = checkConvergence(fewSamples);