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,364 +0,0 @@
1
- import { getHeapStatistics } from "node:v8";
2
- import type { BenchmarkSpec } from "../Benchmark.ts";
3
- import type {
4
- MeasuredResults,
5
- OptStatusInfo,
6
- PausePoint,
7
- } from "../MeasuredResults.ts";
8
- import type { BenchRunner, RunnerOptions } from "./BenchRunner.ts";
9
- import { executeBenchmark } from "./BenchRunner.ts";
10
-
11
- /**
12
- * Wait time after gc() for V8 to stabilize (ms).
13
- *
14
- * V8 has 4 compilation tiers: Ignition (interpreter) -> Sparkplug (baseline) ->
15
- * Maglev (mid-tier optimizer) -> TurboFan (full optimizer). Tiering thresholds:
16
- * - Ignition -> Sparkplug: 8 invocations
17
- * - Sparkplug -> Maglev: 500 invocations
18
- * - Maglev -> TurboFan: 6000 invocations
19
- *
20
- * Optimization compilation happens on background threads and requires idle time
21
- * on the main thread to complete. Without sufficient warmup + settle time,
22
- * benchmarks exhibit bimodal timing: slow Sparkplug samples (~30% slower) mixed
23
- * with fast optimized samples.
24
- *
25
- * The warmup iterations trigger the optimization decision, then gcSettleTime
26
- * provides idle time for background compilation to finish before measurement.
27
- *
28
- * @see https://v8.dev/blog/sparkplug
29
- * @see https://v8.dev/blog/maglev
30
- * @see https://v8.dev/blog/background-compilation
31
- */
32
- const gcSettleTime = 1000;
33
-
34
- type CollectParams<T = unknown> = {
35
- benchmark: BenchmarkSpec<T>;
36
- maxTime: number;
37
- maxIterations: number;
38
- warmup: number;
39
- params?: T;
40
- skipWarmup?: boolean;
41
- traceOpt?: boolean;
42
- noSettle?: boolean;
43
- pauseFirst?: number;
44
- pauseInterval?: number;
45
- pauseDuration?: number;
46
- };
47
-
48
- type CollectResult = {
49
- samples: number[];
50
- warmupSamples: number[]; // timing of warmup iterations
51
- heapGrowth: number; // amortized KB per sample
52
- heapSamples?: number[]; // heap size per sample (bytes)
53
- timestamps?: number[]; // wall-clock μs per sample for Perfetto
54
- optStatus?: OptStatusInfo;
55
- optSamples?: number[]; // per-sample V8 opt status codes
56
- pausePoints: PausePoint[]; // where pauses occurred
57
- };
58
-
59
- export type SampleTimeStats = {
60
- min: number;
61
- max: number;
62
- avg: number;
63
- p50: number;
64
- p75: number;
65
- p99: number;
66
- p999: number;
67
- };
68
-
69
- /** @return runner with time and iteration limits */
70
- export class BasicRunner implements BenchRunner {
71
- async runBench<T = unknown>(
72
- benchmark: BenchmarkSpec<T>,
73
- options: RunnerOptions,
74
- params?: T,
75
- ): Promise<MeasuredResults[]> {
76
- const opts = { ...defaultCollectOptions, ...(options as any) };
77
- const collected = await collectSamples({ benchmark, params, ...opts });
78
- return [buildMeasuredResults(benchmark.name, collected)];
79
- }
80
- }
81
-
82
- const defaultCollectOptions = {
83
- maxTime: 5000,
84
- maxIterations: 1000000,
85
- warmup: 0,
86
- traceOpt: false,
87
- noSettle: false,
88
- };
89
-
90
- function buildMeasuredResults(name: string, c: CollectResult): MeasuredResults {
91
- const time = computeStats(c.samples);
92
- return {
93
- name,
94
- samples: c.samples,
95
- warmupSamples: c.warmupSamples,
96
- heapSamples: c.heapSamples,
97
- timestamps: c.timestamps,
98
- time,
99
- heapSize: { avg: c.heapGrowth, min: c.heapGrowth, max: c.heapGrowth },
100
- optStatus: c.optStatus,
101
- optSamples: c.optSamples,
102
- pausePoints: c.pausePoints,
103
- };
104
- }
105
-
106
- /** @return timing samples and amortized allocation from benchmark execution */
107
- async function collectSamples<T>(p: CollectParams<T>): Promise<CollectResult> {
108
- if (!p.maxIterations && !p.maxTime) {
109
- throw new Error(`At least one of maxIterations or maxTime must be set`);
110
- }
111
- const warmupSamples = p.skipWarmup ? [] : await runWarmup(p);
112
- const heapBefore = process.memoryUsage().heapUsed;
113
- const { samples, heapSamples, timestamps, optStatuses, pausePoints } =
114
- await runSampleLoop(p);
115
- const heapGrowth =
116
- Math.max(0, process.memoryUsage().heapUsed - heapBefore) /
117
- 1024 /
118
- samples.length;
119
- if (samples.length === 0) {
120
- throw new Error(`No samples collected for benchmark: ${p.benchmark.name}`);
121
- }
122
- const optStatus = p.traceOpt
123
- ? analyzeOptStatus(samples, optStatuses)
124
- : undefined;
125
- const optSamples =
126
- p.traceOpt && optStatuses.length > 0 ? optStatuses : undefined;
127
- return {
128
- samples,
129
- warmupSamples,
130
- heapGrowth,
131
- heapSamples,
132
- timestamps,
133
- optStatus,
134
- optSamples,
135
- pausePoints,
136
- };
137
- }
138
-
139
- /** Run warmup iterations with gc + settle time for V8 optimization */
140
- async function runWarmup<T>(p: CollectParams<T>): Promise<number[]> {
141
- const gc = gcFunction();
142
- const samples = new Array<number>(p.warmup);
143
- for (let i = 0; i < p.warmup; i++) {
144
- const start = performance.now();
145
- executeBenchmark(p.benchmark, p.params);
146
- samples[i] = performance.now() - start;
147
- }
148
- gc();
149
- if (!p.noSettle) {
150
- await new Promise(r => setTimeout(r, gcSettleTime));
151
- gc();
152
- }
153
- return samples;
154
- }
155
-
156
- type SampleLoopResult = {
157
- samples: number[];
158
- heapSamples?: number[];
159
- timestamps?: number[];
160
- optStatuses: number[];
161
- pausePoints: PausePoint[];
162
- };
163
-
164
- /** Estimate sample count for pre-allocation */
165
- function estimateSampleCount(maxTime: number, maxIterations: number): number {
166
- return maxIterations || Math.ceil(maxTime / 0.1); // assume 0.1ms per iteration minimum
167
- }
168
-
169
- type SampleArrays = {
170
- samples: number[];
171
- timestamps: number[];
172
- heapSamples: number[];
173
- optStatuses: number[];
174
- pausePoints: PausePoint[];
175
- };
176
-
177
- /** Pre-allocate arrays to reduce GC pressure during measurement */
178
- function createSampleArrays(
179
- n: number,
180
- trackHeap: boolean,
181
- trackOpt: boolean,
182
- ): SampleArrays {
183
- const arr = (track: boolean) => (track ? new Array<number>(n) : []);
184
- return {
185
- samples: new Array<number>(n),
186
- timestamps: new Array<number>(n),
187
- heapSamples: arr(trackHeap),
188
- optStatuses: arr(trackOpt),
189
- pausePoints: [],
190
- };
191
- }
192
-
193
- /** Trim arrays to actual sample count */
194
- function trimArrays(
195
- a: SampleArrays,
196
- count: number,
197
- trackHeap: boolean,
198
- trackOpt: boolean,
199
- ): void {
200
- a.samples.length = a.timestamps.length = count;
201
- if (trackHeap) a.heapSamples.length = count;
202
- if (trackOpt) a.optStatuses.length = count;
203
- }
204
-
205
- /** Collect timing samples with periodic pauses for V8 optimization */
206
- async function runSampleLoop<T>(
207
- p: CollectParams<T>,
208
- ): Promise<SampleLoopResult> {
209
- const {
210
- maxTime,
211
- maxIterations,
212
- pauseFirst,
213
- pauseInterval = 0,
214
- pauseDuration = 100,
215
- } = p;
216
- const trackHeap = true; // Always track heap for charts
217
- const getOptStatus = p.traceOpt ? createOptStatusGetter() : undefined;
218
- const estimated = estimateSampleCount(maxTime, maxIterations);
219
- const a = createSampleArrays(estimated, trackHeap, !!getOptStatus);
220
-
221
- let count = 0;
222
- let elapsed = 0;
223
- let totalPauseTime = 0;
224
- const loopStart = performance.now();
225
-
226
- while (
227
- (!maxIterations || count < maxIterations) &&
228
- (!maxTime || elapsed < maxTime)
229
- ) {
230
- const start = performance.now();
231
- executeBenchmark(p.benchmark, p.params);
232
- const end = performance.now();
233
- a.samples[count] = end - start;
234
- a.timestamps[count] = Number(process.hrtime.bigint() / 1000n);
235
- if (trackHeap) a.heapSamples[count] = getHeapStatistics().used_heap_size;
236
- if (getOptStatus) a.optStatuses[count] = getOptStatus(p.benchmark.fn);
237
- count++;
238
-
239
- if (shouldPause(count, pauseFirst, pauseInterval)) {
240
- a.pausePoints.push({ sampleIndex: count - 1, durationMs: pauseDuration });
241
- const pauseStart = performance.now();
242
- await new Promise(r => setTimeout(r, pauseDuration));
243
- totalPauseTime += performance.now() - pauseStart;
244
- }
245
- elapsed = performance.now() - loopStart - totalPauseTime;
246
- }
247
-
248
- trimArrays(a, count, trackHeap, !!getOptStatus);
249
- return {
250
- samples: a.samples,
251
- heapSamples: trackHeap ? a.heapSamples : undefined,
252
- timestamps: a.timestamps,
253
- optStatuses: a.optStatuses,
254
- pausePoints: a.pausePoints,
255
- };
256
- }
257
-
258
- /** Check if we should pause at this iteration for V8 optimization */
259
- function shouldPause(
260
- iter: number,
261
- first: number | undefined,
262
- interval: number,
263
- ): boolean {
264
- if (first !== undefined && iter === first) return true;
265
- if (interval <= 0) return false;
266
- if (first === undefined) return iter % interval === 0;
267
- return (iter - first) % interval === 0;
268
- }
269
-
270
- /** @return percentiles and basic statistics */
271
- export function computeStats(samples: number[]): SampleTimeStats {
272
- const sorted = [...samples].sort((a, b) => a - b);
273
- const avg = samples.reduce((sum, s) => sum + s, 0) / samples.length;
274
- return {
275
- min: sorted[0],
276
- max: sorted[sorted.length - 1],
277
- avg,
278
- p50: percentile(sorted, 0.5),
279
- p75: percentile(sorted, 0.75),
280
- p99: percentile(sorted, 0.99),
281
- p999: percentile(sorted, 0.999),
282
- };
283
- }
284
-
285
- /** @return percentile value with linear interpolation */
286
- function percentile(sortedArray: number[], p: number): number {
287
- const index = (sortedArray.length - 1) * p;
288
- const lower = Math.floor(index);
289
- const upper = Math.ceil(index);
290
- const weight = index % 1;
291
-
292
- if (upper >= sortedArray.length) return sortedArray[sortedArray.length - 1];
293
-
294
- return sortedArray[lower] * (1 - weight) + sortedArray[upper] * weight;
295
- }
296
-
297
- /** @return runtime gc() function, or no-op if unavailable */
298
- function gcFunction(): () => void {
299
- const gc = globalThis.gc || (globalThis as any).__gc;
300
- if (gc) return gc;
301
- console.warn("gc() not available, run node/bun with --expose-gc");
302
- return () => {};
303
- }
304
-
305
- /** @return function to get V8 optimization status (requires --allow-natives-syntax) */
306
- function createOptStatusGetter(): ((fn: unknown) => number) | undefined {
307
- try {
308
- // %GetOptimizationStatus returns a bitmask
309
- const getter = new Function("f", "return %GetOptimizationStatus(f)");
310
- getter(() => {});
311
- return getter as (fn: unknown) => number;
312
- } catch {
313
- return undefined;
314
- }
315
- }
316
-
317
- /**
318
- * V8 optimization status bit meanings:
319
- * Bit 0 (1): is_function
320
- * Bit 4 (16): is_optimized (TurboFan)
321
- * Bit 5 (32): is_optimized (Maglev)
322
- * Bit 7 (128): is_baseline (Sparkplug)
323
- * Bit 3 (8): maybe_deoptimized
324
- */
325
- const statusNames: Record<number, string> = {
326
- 1: "interpreted",
327
- 129: "sparkplug", // 1 + 128
328
- 17: "turbofan", // 1 + 16
329
- 33: "maglev", // 1 + 32
330
- 49: "turbofan+maglev", // 1 + 16 + 32
331
- 32769: "optimized", // common optimized status
332
- };
333
-
334
- /** @return analysis of V8 optimization status per sample */
335
- function analyzeOptStatus(
336
- samples: number[],
337
- statuses: number[],
338
- ): OptStatusInfo | undefined {
339
- if (statuses.length === 0 || statuses[0] === undefined) return undefined;
340
-
341
- const byStatusCode = new Map<number, number[]>();
342
- let deoptCount = 0;
343
-
344
- for (let i = 0; i < samples.length; i++) {
345
- const status = statuses[i];
346
- if (status === undefined) continue;
347
-
348
- // Check deopt flag (bit 3)
349
- if (status & 8) deoptCount++;
350
-
351
- if (!byStatusCode.has(status)) byStatusCode.set(status, []);
352
- byStatusCode.get(status)!.push(samples[i]);
353
- }
354
-
355
- const byTier: Record<string, { count: number; medianMs: number }> = {};
356
- for (const [status, times] of byStatusCode) {
357
- const name = statusNames[status] || `status=${status}`;
358
- const sorted = [...times].sort((a, b) => a - b);
359
- const median = sorted[Math.floor(sorted.length / 2)];
360
- byTier[name] = { count: times.length, medianMs: median };
361
- }
362
-
363
- return { byTier, deoptCount };
364
- }
@@ -1,19 +0,0 @@
1
- import pico from "picocolors";
2
-
3
- const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
4
- const { red } = isTest ? { red: (str: string) => str } : pico;
5
-
6
- const lowConfidence = 80;
7
-
8
- /** @return convergence percentage with color for low values */
9
- export function formatConvergence(v: unknown): string {
10
- if (typeof v !== "number") return "—";
11
- const pct = `${Math.round(v)}%`;
12
- return v < lowConfidence ? red(pct) : pct;
13
- }
14
-
15
- /** @return coefficient of variation as ±percentage */
16
- export function formatCV(v: unknown): string {
17
- if (typeof v !== "number") return "";
18
- return `±${(v * 100).toFixed(1)}%`;
19
- }
@@ -1,152 +0,0 @@
1
- import pico from "picocolors";
2
- import type { CIDirection, DifferenceCI } from "../StatisticalUtils.ts";
3
-
4
- const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
5
- const { red, green } = isTest
6
- ? { red: (str: string) => str, green: (str: string) => str }
7
- : pico;
8
-
9
- /** Format floats with custom precision */
10
- export function floatPrecision(precision: number) {
11
- return (x: unknown): string | null => {
12
- if (typeof x !== "number") return null;
13
- return x.toFixed(precision).replace(/\.?0+$/, "");
14
- };
15
- }
16
-
17
- /** Format percentages with custom precision */
18
- export function percentPrecision(precision: number) {
19
- return (x: unknown): string | null => {
20
- if (typeof x !== "number") return null;
21
- return percent(x, precision);
22
- };
23
- }
24
-
25
- /** Format duration in milliseconds with appropriate units */
26
- export function duration(ms: unknown): string | null {
27
- if (typeof ms !== "number") return null;
28
- if (ms < 0.001) return `${(ms * 1000000).toFixed(0)}ns`;
29
- if (ms < 1) return `${(ms * 1000).toFixed(1)}μs`;
30
- if (ms < 1000) return `${ms.toFixed(2)}ms`;
31
- return `${(ms / 1000).toFixed(2)}s`;
32
- }
33
-
34
- /** Format time in milliseconds, showing very small values with units */
35
- export function timeMs(ms: unknown): string | null {
36
- if (typeof ms !== "number") return null;
37
- if (ms < 0.001) return `${(ms * 1000000).toFixed(0)}ns`;
38
- if (ms < 0.01) return `${(ms * 1000).toFixed(1)}μs`;
39
- if (ms >= 10) return ms.toFixed(0);
40
- return ms.toFixed(2);
41
- }
42
-
43
- /** Format as rate (value per unit) */
44
- export function rate(unit: string): (value: unknown) => string | null {
45
- return (value: unknown) => {
46
- if (typeof value !== "number") return null;
47
- return `${integer(value)}/${unit}`;
48
- };
49
- }
50
-
51
- /** Format integer with thousand separators */
52
- export function integer(x: unknown): string | null {
53
- if (typeof x !== "number") return null;
54
- return new Intl.NumberFormat("en-US").format(Math.round(x));
55
- }
56
-
57
- /** Format fraction as percentage (0.473 → 47.3%) */
58
- export function percent(fraction: unknown, precision = 1): string | null {
59
- if (typeof fraction !== "number") return null;
60
- return `${Math.abs(fraction * 100).toFixed(precision)}%`;
61
- }
62
-
63
- /** Format percentage difference between two values */
64
- export function diffPercent(main: unknown, base: unknown): string {
65
- if (typeof main !== "number" || typeof base !== "number") return " ";
66
- const diff = main - base;
67
- return coloredPercent(diff, base);
68
- }
69
-
70
- /** Format percentage difference for benchmarks (lower is better) */
71
- export function diffPercentBenchmark(main: unknown, base: unknown): string {
72
- if (typeof main !== "number" || typeof base !== "number") return " ";
73
- const diff = main - base;
74
- return coloredPercent(diff, base, false); // negative is good for benchmarks
75
- }
76
-
77
- /** Format fraction as colored +/- percentage */
78
- function coloredPercent(
79
- numerator: number,
80
- denominator: number,
81
- positiveIsGreen = true,
82
- ): string {
83
- const fraction = numerator / denominator;
84
- if (Number.isNaN(fraction) || !Number.isFinite(fraction)) {
85
- return " ";
86
- }
87
- const positive = fraction >= 0;
88
- const sign = positive ? "+" : "-";
89
- const percentStr = `${sign}${percent(fraction)}`;
90
- const isGood = positive === positiveIsGreen;
91
- return isGood ? green(percentStr) : red(percentStr);
92
- }
93
-
94
- /** Format memory size in KB with appropriate units */
95
- export function memoryKB(kb: unknown): string | null {
96
- if (typeof kb !== "number") return null;
97
- if (kb < 1024) return `${kb.toFixed(0)}KB`;
98
- return `${(kb / 1024).toFixed(1)}MB`;
99
- }
100
-
101
- /** Format bytes with appropriate units (B, KB, MB, GB) */
102
- export function formatBytes(bytes: unknown): string | null {
103
- if (typeof bytes !== "number") return null;
104
- if (bytes < 1024) return `${bytes.toFixed(0)}B`;
105
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
106
- if (bytes < 1024 * 1024 * 1024)
107
- return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
108
- return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}GB`;
109
- }
110
-
111
- /** Format percentage difference with confidence interval */
112
- export function formatDiffWithCI(value: unknown): string | null {
113
- if (!isDifferenceCI(value)) return null;
114
- const { percent, ci, direction } = value;
115
- return colorByDirection(diffCIText(percent, ci), direction);
116
- }
117
-
118
- /** Format percentage difference with CI for throughput metrics (higher is better) */
119
- export function formatDiffWithCIHigherIsBetter(value: unknown): string | null {
120
- if (!isDifferenceCI(value)) return null;
121
- const { percent, ci, direction } = value;
122
- // Flip percent sign for "higher is better" metrics (direction stays same)
123
- return colorByDirection(diffCIText(-percent, [-ci[1], -ci[0]]), direction);
124
- }
125
-
126
- /** @return formatted "pct [lo, hi]" text for a diff with CI */
127
- function diffCIText(pct: number, ci: [number, number]): string {
128
- return `${formatBound(pct)} [${formatBound(ci[0])}, ${formatBound(ci[1])}]`;
129
- }
130
-
131
- /** @return text colored green for faster, red for slower */
132
- function colorByDirection(text: string, direction: CIDirection): string {
133
- if (direction === "faster") return green(text);
134
- if (direction === "slower") return red(text);
135
- return text;
136
- }
137
-
138
- /** @return signed percentage string (e.g. "+1.2%", "-3.4%") */
139
- function formatBound(v: number): string {
140
- const sign = v >= 0 ? "+" : "";
141
- return `${sign}${v.toFixed(1)}%`;
142
- }
143
-
144
- /** @return true if value is a DifferenceCI object */
145
- function isDifferenceCI(x: unknown): x is DifferenceCI {
146
- return typeof x === "object" && x !== null && "ci" in x && "direction" in x;
147
- }
148
-
149
- /** @return truncated string with ellipsis if over maxLen */
150
- export function truncate(str: string, maxLen = 30): string {
151
- return str.length > maxLen ? str.slice(0, maxLen - 3) + "..." : str;
152
- }
@@ -1,70 +0,0 @@
1
- # TableReport
2
-
3
- Utilities for creating formatted text-based tables.
4
- Under the hood, TableReport uses the npm [table](https://www.npmjs.com/package/table) library.
5
-
6
- ### Features
7
-
8
- * **Column Grouping:** Group related columns under a common header.
9
- * **Difference Columns:** Automatically generate columns that show the percentage difference between a value and a baseline value.
10
- * **Custom Formatting:** Declaratively provide custom formatters for columns.
11
- * **Simplified Configuration:** A higher-level API for the `table` library.
12
-
13
- ## `TableReport.ts`
14
-
15
- The `buildTable` function in `TableReport.ts` is the main entry point for creating a table.
16
- It takes a configuration object for columns and an array of data records for rows.
17
-
18
- ### Example
19
-
20
- Here's a simplified example of how to use `buildTable`:
21
-
22
- ```typescript
23
- import { buildTable, ColumnGroup } from './TableReport';
24
- import { integer, floatPrecision } from './Formatters';
25
-
26
- interface MyData {
27
- name: string;
28
- value: number;
29
- score: number;
30
- }
31
-
32
- const data: MyData[] = [
33
- { name: 'test A', value: 123, score: 45.6 },
34
- { name: 'test B', value: 456, score: 78.9 },
35
- ];
36
-
37
- const baselineData: MyData[] = [
38
- { name: 'test A', value: 100, score: 50.0 },
39
- { name: 'test B', value: 500, score: 75.0 },
40
- ];
41
-
42
- const tableConfig: ColumnGroup<MyData>[] = [
43
- {
44
- columns: [{ key: 'name', title: 'Name' }],
45
- },
46
- {
47
- groupTitle: 'Metrics',
48
- columns: [
49
- { key: 'value', title: 'Value', formatter: integer },
50
- { key: 'value_diff', title: 'Δ%', diffKey: 'value' },
51
- { key: 'score', title: 'Score', formatter: floatPrecision(1) },
52
- { key: 'score_diff', title: 'Δ%', diffKey: 'score' },
53
- ],
54
- },
55
- ];
56
-
57
- const table = buildTable(tableConfig, data, baselineData);
58
- console.log(table);
59
- ```
60
-
61
- For a more complex example, see `BenchmarkReport.ts`, specifically the `mostlyFullRow` and `tableConfig` variables.
62
-
63
- ## `Formatters.ts`
64
-
65
- This file contains various utility functions for formatting numbers and strings, such as:
66
-
67
- * `float`, `integer`: Format numbers to a specific precision.
68
- * `percent`: Format a number as a percentage.
69
- * `diffPercent`, `diffPercentNegative`: Format the percentage difference between two numbers, with color-coding for positive/negative changes.
70
- * `bytes`, `duration`, `rate`: Format numbers with appropriate units.