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,96 +0,0 @@
1
- export interface Sample {
2
- benchmark: string;
3
- value: number;
4
- iteration: number;
5
- }
6
-
7
- export interface TimeSeriesPoint {
8
- benchmark: string;
9
- iteration: number;
10
- value: number;
11
- isWarmup: boolean;
12
- optStatus?: number;
13
- }
14
-
15
- export interface GcEvent {
16
- benchmark: string;
17
- sampleIndex: number;
18
- duration: number;
19
- }
20
-
21
- export interface PausePoint {
22
- benchmark: string;
23
- sampleIndex: number;
24
- durationMs: number;
25
- }
26
-
27
- export interface HeapPoint {
28
- benchmark: string;
29
- iteration: number;
30
- value: number;
31
- }
32
-
33
- export interface BenchmarkStats {
34
- min: number;
35
- max: number;
36
- avg: number;
37
- p50: number;
38
- p75: number;
39
- p99: number;
40
- }
41
-
42
- export interface SectionStat {
43
- groupTitle?: string;
44
- label: string;
45
- value: string;
46
- }
47
-
48
- export interface HistogramBin {
49
- x: number;
50
- count: number;
51
- }
52
-
53
- /** Bootstrap confidence interval for A/B comparison */
54
- export interface ComparisonCI {
55
- percent: number;
56
- ci: [number, number];
57
- direction: "faster" | "slower" | "uncertain";
58
- histogram?: HistogramBin[];
59
- }
60
-
61
- /** One benchmark's raw data, statistics, and optional comparison results */
62
- export interface BenchmarkEntry {
63
- name: string;
64
- samples: number[];
65
- warmupSamples?: number[];
66
- heapSamples?: number[];
67
- gcEvents?: { offset: number; duration: number }[];
68
- optSamples?: number[];
69
- pausePoints?: { sampleIndex: number; durationMs: number }[];
70
- stats: BenchmarkStats;
71
- sectionStats?: SectionStat[];
72
- comparisonCI?: ComparisonCI;
73
- isBaseline: boolean;
74
- }
75
-
76
- export interface BenchmarkGroup {
77
- baseline?: BenchmarkEntry;
78
- benchmarks: BenchmarkEntry[];
79
- }
80
-
81
- export interface GitVersion {
82
- hash: string;
83
- date: string;
84
- dirty?: boolean;
85
- }
86
-
87
- /** Top-level data structure for the HTML benchmark report */
88
- export interface ReportData {
89
- metadata: {
90
- cliArgs?: Record<string, unknown>;
91
- gcTrackingEnabled?: boolean;
92
- currentVersion?: GitVersion;
93
- baselineVersion?: GitVersion;
94
- };
95
- groups: BenchmarkGroup[];
96
- }
@@ -1 +0,0 @@
1
- export { renderPlots } from "./RenderPlots.ts";
package/src/html/index.ts DELETED
@@ -1,17 +0,0 @@
1
- export { generateHtmlReport } from "./HtmlReport.ts";
2
- export {
3
- formatDateWithTimezone,
4
- formatRelativeTime,
5
- } from "./HtmlTemplate.ts";
6
- export type {
7
- BenchmarkData,
8
- DifferenceCI,
9
- FormattedStat,
10
- GcEvent,
11
- GitVersion,
12
- GroupData,
13
- HtmlReportOptions,
14
- HtmlReportResult,
15
- PausePoint,
16
- ReportData,
17
- } from "./Types.ts";
@@ -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
- export type SampleTimeStats = {
12
- min: number;
13
- max: number;
14
- avg: number;
15
- p50: number;
16
- p75: number;
17
- p99: number;
18
- p999: number;
19
- };
20
-
21
- type CollectParams<T = unknown> = {
22
- benchmark: BenchmarkSpec<T>;
23
- maxTime: number;
24
- maxIterations: number;
25
- warmup: number;
26
- params?: T;
27
- skipWarmup?: boolean;
28
- traceOpt?: boolean;
29
- noSettle?: boolean;
30
- pauseFirst?: number;
31
- pauseInterval?: number;
32
- pauseDuration?: number;
33
- };
34
-
35
- type CollectResult = {
36
- samples: number[];
37
- warmupSamples: number[]; // timing of warmup iterations
38
- heapGrowth: number; // amortized KB per sample
39
- heapSamples?: number[]; // heap size per sample (bytes)
40
- timestamps?: number[]; // wall-clock μs per sample for Perfetto
41
- optStatus?: OptStatusInfo;
42
- optSamples?: number[]; // per-sample V8 opt status codes
43
- pausePoints: PausePoint[]; // where pauses occurred
44
- };
45
-
46
- type SampleLoopResult = {
47
- samples: number[];
48
- heapSamples?: number[];
49
- timestamps?: number[];
50
- optStatuses: number[];
51
- pausePoints: PausePoint[];
52
- };
53
-
54
- type SampleArrays = {
55
- samples: number[];
56
- timestamps: number[];
57
- heapSamples: number[];
58
- optStatuses: number[];
59
- pausePoints: PausePoint[];
60
- };
61
-
62
- /**
63
- * Wait time after gc() for V8 to stabilize (ms).
64
- *
65
- * V8 has 4 compilation tiers: Ignition (interpreter) -> Sparkplug (baseline) ->
66
- * Maglev (mid-tier optimizer) -> TurboFan (full optimizer). Tiering thresholds:
67
- * - Ignition -> Sparkplug: 8 invocations
68
- * - Sparkplug -> Maglev: 500 invocations
69
- * - Maglev -> TurboFan: 6000 invocations
70
- *
71
- * Optimization compilation happens on background threads and requires idle time
72
- * on the main thread to complete. Without sufficient warmup + settle time,
73
- * benchmarks exhibit bimodal timing: slow Sparkplug samples (~30% slower) mixed
74
- * with fast optimized samples.
75
- *
76
- * The warmup iterations trigger the optimization decision, then gcSettleTime
77
- * provides idle time for background compilation to finish before measurement.
78
- *
79
- * @see https://v8.dev/blog/sparkplug
80
- * @see https://v8.dev/blog/maglev
81
- * @see https://v8.dev/blog/background-compilation
82
- */
83
- const gcSettleTime = 1000;
84
-
85
- const defaultCollectOptions = {
86
- maxTime: 5000,
87
- maxIterations: 1000000,
88
- warmup: 0,
89
- traceOpt: false,
90
- noSettle: false,
91
- };
92
-
93
- /**
94
- * V8 optimization status bit meanings:
95
- * Bit 0 (1): is_function
96
- * Bit 4 (16): is_optimized (TurboFan)
97
- * Bit 5 (32): is_optimized (Maglev)
98
- * Bit 7 (128): is_baseline (Sparkplug)
99
- * Bit 3 (8): maybe_deoptimized
100
- */
101
- const statusNames: Record<number, string> = {
102
- 1: "interpreted",
103
- 129: "sparkplug", // 1 + 128
104
- 17: "turbofan", // 1 + 16
105
- 33: "maglev", // 1 + 32
106
- 49: "turbofan+maglev", // 1 + 16 + 32
107
- 32769: "optimized", // common optimized status
108
- };
109
-
110
- /** @return runner with time and iteration limits */
111
- export class BasicRunner implements BenchRunner {
112
- async runBench<T = unknown>(
113
- benchmark: BenchmarkSpec<T>,
114
- options: RunnerOptions,
115
- params?: T,
116
- ): Promise<MeasuredResults[]> {
117
- const opts = { ...defaultCollectOptions, ...(options as any) };
118
- const collected = await collectSamples({ benchmark, params, ...opts });
119
- return [buildMeasuredResults(benchmark.name, collected)];
120
- }
121
- }
122
-
123
- /** @return percentiles and basic statistics */
124
- export function computeStats(samples: number[]): SampleTimeStats {
125
- const sorted = [...samples].sort((a, b) => a - b);
126
- const avg = samples.reduce((sum, s) => sum + s, 0) / samples.length;
127
- return {
128
- min: sorted[0],
129
- max: sorted[sorted.length - 1],
130
- avg,
131
- p50: percentile(sorted, 0.5),
132
- p75: percentile(sorted, 0.75),
133
- p99: percentile(sorted, 0.99),
134
- p999: percentile(sorted, 0.999),
135
- };
136
- }
137
-
138
- /** @return timing samples and amortized allocation from benchmark execution */
139
- async function collectSamples<T>(p: CollectParams<T>): Promise<CollectResult> {
140
- if (!p.maxIterations && !p.maxTime) {
141
- throw new Error(`At least one of maxIterations or maxTime must be set`);
142
- }
143
- const warmupSamples = p.skipWarmup ? [] : await runWarmup(p);
144
- const heapBefore = process.memoryUsage().heapUsed;
145
- const { samples, heapSamples, timestamps, optStatuses, pausePoints } =
146
- await runSampleLoop(p);
147
- const heapGrowth =
148
- Math.max(0, process.memoryUsage().heapUsed - heapBefore) /
149
- 1024 /
150
- samples.length;
151
- if (samples.length === 0) {
152
- throw new Error(`No samples collected for benchmark: ${p.benchmark.name}`);
153
- }
154
- const optStatus = p.traceOpt
155
- ? analyzeOptStatus(samples, optStatuses)
156
- : undefined;
157
- const optSamples =
158
- p.traceOpt && optStatuses.length > 0 ? optStatuses : undefined;
159
- return {
160
- samples,
161
- warmupSamples,
162
- heapGrowth,
163
- heapSamples,
164
- timestamps,
165
- optStatus,
166
- optSamples,
167
- pausePoints,
168
- };
169
- }
170
-
171
- function buildMeasuredResults(name: string, c: CollectResult): MeasuredResults {
172
- const time = computeStats(c.samples);
173
- return {
174
- name,
175
- samples: c.samples,
176
- warmupSamples: c.warmupSamples,
177
- heapSamples: c.heapSamples,
178
- timestamps: c.timestamps,
179
- time,
180
- heapSize: { avg: c.heapGrowth, min: c.heapGrowth, max: c.heapGrowth },
181
- optStatus: c.optStatus,
182
- optSamples: c.optSamples,
183
- pausePoints: c.pausePoints,
184
- };
185
- }
186
-
187
- /** @return percentile value with linear interpolation */
188
- function percentile(sortedArray: number[], p: number): number {
189
- const index = (sortedArray.length - 1) * p;
190
- const lower = Math.floor(index);
191
- const upper = Math.ceil(index);
192
- const weight = index % 1;
193
-
194
- if (upper >= sortedArray.length) return sortedArray[sortedArray.length - 1];
195
-
196
- return sortedArray[lower] * (1 - weight) + sortedArray[upper] * weight;
197
- }
198
-
199
- /** Run warmup iterations with gc + settle time for V8 optimization */
200
- async function runWarmup<T>(p: CollectParams<T>): Promise<number[]> {
201
- const gc = gcFunction();
202
- const samples = new Array<number>(p.warmup);
203
- for (let i = 0; i < p.warmup; i++) {
204
- const start = performance.now();
205
- executeBenchmark(p.benchmark, p.params);
206
- samples[i] = performance.now() - start;
207
- }
208
- gc();
209
- if (!p.noSettle) {
210
- await new Promise(r => setTimeout(r, gcSettleTime));
211
- gc();
212
- }
213
- return samples;
214
- }
215
-
216
- /** Collect timing samples with periodic pauses for V8 optimization */
217
- async function runSampleLoop<T>(
218
- p: CollectParams<T>,
219
- ): Promise<SampleLoopResult> {
220
- const {
221
- maxTime,
222
- maxIterations,
223
- pauseFirst,
224
- pauseInterval = 0,
225
- pauseDuration = 100,
226
- } = p;
227
- const trackHeap = true; // Always track heap for charts
228
- const getOptStatus = p.traceOpt ? createOptStatusGetter() : undefined;
229
- const estimated = estimateSampleCount(maxTime, maxIterations);
230
- const a = createSampleArrays(estimated, trackHeap, !!getOptStatus);
231
-
232
- let count = 0;
233
- let elapsed = 0;
234
- let totalPauseTime = 0;
235
- const loopStart = performance.now();
236
-
237
- while (
238
- (!maxIterations || count < maxIterations) &&
239
- (!maxTime || elapsed < maxTime)
240
- ) {
241
- const start = performance.now();
242
- executeBenchmark(p.benchmark, p.params);
243
- const end = performance.now();
244
- a.samples[count] = end - start;
245
- a.timestamps[count] = Number(process.hrtime.bigint() / 1000n);
246
- if (trackHeap) a.heapSamples[count] = getHeapStatistics().used_heap_size;
247
- if (getOptStatus) a.optStatuses[count] = getOptStatus(p.benchmark.fn);
248
- count++;
249
-
250
- if (shouldPause(count, pauseFirst, pauseInterval)) {
251
- a.pausePoints.push({ sampleIndex: count - 1, durationMs: pauseDuration });
252
- const pauseStart = performance.now();
253
- await new Promise(r => setTimeout(r, pauseDuration));
254
- totalPauseTime += performance.now() - pauseStart;
255
- }
256
- elapsed = performance.now() - loopStart - totalPauseTime;
257
- }
258
-
259
- trimArrays(a, count, trackHeap, !!getOptStatus);
260
- return {
261
- samples: a.samples,
262
- heapSamples: trackHeap ? a.heapSamples : undefined,
263
- timestamps: a.timestamps,
264
- optStatuses: a.optStatuses,
265
- pausePoints: a.pausePoints,
266
- };
267
- }
268
-
269
- /** @return analysis of V8 optimization status per sample */
270
- function analyzeOptStatus(
271
- samples: number[],
272
- statuses: number[],
273
- ): OptStatusInfo | undefined {
274
- if (statuses.length === 0 || statuses[0] === undefined) return undefined;
275
-
276
- const byStatusCode = new Map<number, number[]>();
277
- let deoptCount = 0;
278
-
279
- for (let i = 0; i < samples.length; i++) {
280
- const status = statuses[i];
281
- if (status === undefined) continue;
282
-
283
- // Check deopt flag (bit 3)
284
- if (status & 8) deoptCount++;
285
-
286
- if (!byStatusCode.has(status)) byStatusCode.set(status, []);
287
- byStatusCode.get(status)!.push(samples[i]);
288
- }
289
-
290
- const byTier: Record<string, { count: number; medianMs: number }> = {};
291
- for (const [status, times] of byStatusCode) {
292
- const name = statusNames[status] || `status=${status}`;
293
- const sorted = [...times].sort((a, b) => a - b);
294
- const median = sorted[Math.floor(sorted.length / 2)];
295
- byTier[name] = { count: times.length, medianMs: median };
296
- }
297
-
298
- return { byTier, deoptCount };
299
- }
300
-
301
- /** @return runtime gc() function, or no-op if unavailable */
302
- function gcFunction(): () => void {
303
- const gc = globalThis.gc || (globalThis as any).__gc;
304
- if (gc) return gc;
305
- console.warn("gc() not available, run node/bun with --expose-gc");
306
- return () => {};
307
- }
308
-
309
- /** @return function to get V8 optimization status (requires --allow-natives-syntax) */
310
- function createOptStatusGetter(): ((fn: unknown) => number) | undefined {
311
- try {
312
- // %GetOptimizationStatus returns a bitmask
313
- const getter = new Function("f", "return %GetOptimizationStatus(f)");
314
- getter(() => {});
315
- return getter as (fn: unknown) => number;
316
- } catch {
317
- return undefined;
318
- }
319
- }
320
-
321
- /** Estimate sample count for pre-allocation */
322
- function estimateSampleCount(maxTime: number, maxIterations: number): number {
323
- return maxIterations || Math.ceil(maxTime / 0.1); // assume 0.1ms per iteration minimum
324
- }
325
-
326
- /** Pre-allocate arrays to reduce GC pressure during measurement */
327
- function createSampleArrays(
328
- n: number,
329
- trackHeap: boolean,
330
- trackOpt: boolean,
331
- ): SampleArrays {
332
- const arr = (track: boolean) => (track ? new Array<number>(n) : []);
333
- return {
334
- samples: new Array<number>(n),
335
- timestamps: new Array<number>(n),
336
- heapSamples: arr(trackHeap),
337
- optStatuses: arr(trackOpt),
338
- pausePoints: [],
339
- };
340
- }
341
-
342
- /** Check if we should pause at this iteration for V8 optimization */
343
- function shouldPause(
344
- iter: number,
345
- first: number | undefined,
346
- interval: number,
347
- ): boolean {
348
- if (first !== undefined && iter === first) return true;
349
- if (interval <= 0) return false;
350
- if (first === undefined) return iter % interval === 0;
351
- return (iter - first) % interval === 0;
352
- }
353
-
354
- /** Trim arrays to actual sample count */
355
- function trimArrays(
356
- a: SampleArrays,
357
- count: number,
358
- trackHeap: boolean,
359
- trackOpt: boolean,
360
- ): void {
361
- a.samples.length = a.timestamps.length = count;
362
- if (trackHeap) a.heapSamples.length = count;
363
- if (trackOpt) a.optStatuses.length = count;
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,157 +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 memory size in KB with appropriate units */
78
- export function memoryKB(kb: unknown): string | null {
79
- if (typeof kb !== "number") return null;
80
- if (kb < 1024) return `${kb.toFixed(0)}KB`;
81
- return `${(kb / 1024).toFixed(1)}MB`;
82
- }
83
-
84
- /** Format bytes with appropriate units (B, KB, MB, GB).
85
- * Use `space: true` for human-readable console output (`1.5 KB`). */
86
- export function formatBytes(
87
- bytes: unknown,
88
- opts?: { space?: boolean },
89
- ): string | null {
90
- if (typeof bytes !== "number") return null;
91
- const s = opts?.space ? " " : "";
92
- if (bytes < 1024) return `${bytes.toFixed(0)}${s}B`;
93
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}${s}KB`;
94
- if (bytes < 1024 * 1024 * 1024)
95
- return `${(bytes / 1024 / 1024).toFixed(1)}${s}MB`;
96
- return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}${s}GB`;
97
- }
98
-
99
- /** Format percentage difference with confidence interval */
100
- export function formatDiffWithCI(value: unknown): string | null {
101
- if (!isDifferenceCI(value)) return null;
102
- const { percent, ci, direction } = value;
103
- return colorByDirection(diffCIText(percent, ci), direction);
104
- }
105
-
106
- /** Format percentage difference with CI for throughput metrics (higher is better) */
107
- export function formatDiffWithCIHigherIsBetter(value: unknown): string | null {
108
- if (!isDifferenceCI(value)) return null;
109
- const { percent, ci, direction } = value;
110
- // Flip percent sign for "higher is better" metrics (direction stays same)
111
- return colorByDirection(diffCIText(-percent, [-ci[1], -ci[0]]), direction);
112
- }
113
-
114
- /** @return truncated string with ellipsis if over maxLen */
115
- export function truncate(str: string, maxLen = 30): string {
116
- return str.length > maxLen ? str.slice(0, maxLen - 3) + "..." : str;
117
- }
118
-
119
- /** Format fraction as colored +/- percentage */
120
- function coloredPercent(
121
- numerator: number,
122
- denominator: number,
123
- positiveIsGreen = true,
124
- ): string {
125
- const fraction = numerator / denominator;
126
- if (Number.isNaN(fraction) || !Number.isFinite(fraction)) {
127
- return " ";
128
- }
129
- const positive = fraction >= 0;
130
- const sign = positive ? "+" : "-";
131
- const percentStr = `${sign}${percent(fraction)}`;
132
- const isGood = positive === positiveIsGreen;
133
- return isGood ? green(percentStr) : red(percentStr);
134
- }
135
-
136
- /** @return true if value is a DifferenceCI object */
137
- function isDifferenceCI(x: unknown): x is DifferenceCI {
138
- return typeof x === "object" && x !== null && "ci" in x && "direction" in x;
139
- }
140
-
141
- /** @return text colored green for faster, red for slower */
142
- function colorByDirection(text: string, direction: CIDirection): string {
143
- if (direction === "faster") return green(text);
144
- if (direction === "slower") return red(text);
145
- return text;
146
- }
147
-
148
- /** @return formatted "pct [lo, hi]" text for a diff with CI */
149
- function diffCIText(pct: number, ci: [number, number]): string {
150
- return `${formatBound(pct)} [${formatBound(ci[0])}, ${formatBound(ci[1])}]`;
151
- }
152
-
153
- /** @return signed percentage string (e.g. "+1.2%", "-3.4%") */
154
- function formatBound(v: number): string {
155
- const sign = v >= 0 ? "+" : "";
156
- return `${sign}${v.toFixed(1)}%`;
157
- }