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.
- package/LICENSE +20 -0
- package/README.md +99 -294
- package/bin/benchforge +1 -2
- package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
- package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
- package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
- package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
- package/dist/{BenchRunner-BzyUfiyB.d.mts → BenchRunner-DglX1NOn.d.mts} +119 -66
- package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
- package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
- package/dist/Formatters-BWj3d4sv.mjs +95 -0
- package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
- package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
- package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
- package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
- package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
- package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
- package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
- package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
- package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
- package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
- package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
- package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
- package/dist/bin/benchforge.mjs +4 -5
- package/dist/bin/benchforge.mjs.map +1 -1
- package/dist/index.d.mts +711 -558
- package/dist/index.mjs +98 -3
- package/dist/index.mjs.map +1 -0
- package/dist/runners/WorkerScript.d.mts +12 -4
- package/dist/runners/WorkerScript.mjs +77 -105
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
- package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
- package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
- package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
- package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
- package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
- package/dist/viewer/index.html +19 -0
- package/dist/viewer/speedscope/LICENSE +21 -0
- package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
- package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
- package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
- package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
- package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
- package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
- package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
- package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
- package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
- package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
- package/dist/viewer/speedscope/file-format-schema.json +274 -0
- package/dist/viewer/speedscope/index.html +19 -0
- package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
- package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
- package/dist/viewer/speedscope/release.txt +3 -0
- package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
- package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
- package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
- package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
- package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
- package/package.json +52 -27
- package/src/bin/benchforge.ts +2 -2
- package/src/cli/AnalyzeArchive.ts +232 -0
- package/src/cli/BrowserBench.ts +322 -0
- package/src/cli/CliArgs.ts +164 -51
- package/src/cli/CliExport.ts +179 -0
- package/src/cli/CliOptions.ts +147 -0
- package/src/cli/CliReport.ts +197 -0
- package/src/cli/FilterBenchmarks.ts +18 -30
- package/src/cli/RunBenchCLI.ts +132 -866
- package/src/cli/SuiteRunner.ts +160 -0
- package/src/cli/ViewerServer.ts +282 -0
- package/src/export/AllocExport.ts +121 -0
- package/src/export/ArchiveExport.ts +146 -0
- package/src/export/ArchiveFormat.ts +50 -0
- package/src/export/CoverageExport.ts +148 -0
- package/src/export/EditorUri.ts +10 -0
- package/src/export/PerfettoExport.ts +64 -99
- package/src/export/SpeedscopeTypes.ts +98 -0
- package/src/export/TimeExport.ts +115 -0
- package/src/index.ts +86 -67
- package/src/matrix/BenchMatrix.ts +230 -0
- package/src/matrix/CaseLoader.ts +8 -6
- package/src/matrix/MatrixDirRunner.ts +153 -0
- package/src/matrix/MatrixFilter.ts +49 -47
- package/src/matrix/MatrixInlineRunner.ts +50 -0
- package/src/matrix/MatrixReport.ts +90 -250
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/profiling/browser/BenchLoop.ts +51 -0
- package/src/profiling/browser/BrowserCDP.ts +133 -0
- package/src/profiling/browser/BrowserGcStats.ts +33 -0
- package/src/profiling/browser/BrowserProfiler.ts +160 -0
- package/src/profiling/browser/CdpClient.ts +82 -0
- package/src/profiling/browser/CdpPage.ts +138 -0
- package/src/profiling/browser/ChromeLauncher.ts +158 -0
- package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
- package/src/profiling/browser/PageLoadMode.ts +61 -0
- package/src/profiling/node/CoverageSampler.ts +27 -0
- package/src/profiling/node/CoverageTypes.ts +23 -0
- package/src/profiling/node/HeapSampleReport.ts +261 -0
- package/src/{heap-sample → profiling/node}/HeapSampler.ts +1 -2
- package/src/{heap-sample → profiling/node}/ResolvedProfile.ts +18 -9
- package/src/profiling/node/TimeSampler.ts +57 -0
- package/src/report/BenchmarkReport.ts +146 -0
- package/src/report/Colors.ts +9 -0
- package/src/report/Formatters.ts +110 -0
- package/src/report/GcSections.ts +151 -0
- package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
- package/src/report/HtmlReport.ts +223 -0
- package/src/report/ParseStats.ts +73 -0
- package/src/report/StandardSections.ts +147 -0
- package/src/report/ViewerSections.ts +286 -0
- package/src/report/text/TableReport.ts +253 -0
- package/src/report/text/TextReport.ts +123 -0
- package/src/runners/AdaptiveWrapper.ts +116 -236
- package/src/runners/BenchRunner.ts +20 -15
- package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
- package/src/runners/CreateRunner.ts +5 -7
- package/src/runners/GcStats.ts +47 -50
- package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
- package/src/runners/MergeBatches.ts +123 -0
- package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
- package/src/runners/RunnerOrchestrator.ts +127 -243
- package/src/runners/RunnerUtils.ts +75 -1
- package/src/runners/SampleStats.ts +100 -0
- package/src/runners/TimingRunner.ts +244 -0
- package/src/runners/TimingUtils.ts +3 -2
- package/src/runners/WorkerScript.ts +135 -151
- package/src/stats/BootstrapDifference.ts +282 -0
- package/src/{PermutationTest.ts → stats/PermutationTest.ts} +8 -17
- package/src/stats/StatisticalUtils.ts +445 -0
- package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
- package/src/test/AdaptiveRunner.test.ts +39 -41
- package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +2 -2
- package/src/{tests → test}/BenchMatrix.test.ts +19 -16
- package/src/test/BenchmarkReport.test.ts +63 -13
- package/src/test/BrowserBench.e2e.test.ts +186 -17
- package/src/test/BrowserBench.test.ts +10 -5
- package/src/test/BuildTimeSection.test.ts +130 -0
- package/src/test/CapSamples.test.ts +82 -0
- package/src/test/CoverageExport.test.ts +115 -0
- package/src/test/CoverageSampler.test.ts +33 -0
- package/src/test/HeapAttribution.test.ts +14 -14
- package/src/{tests → test}/MatrixFilter.test.ts +1 -1
- package/src/{tests → test}/MatrixReport.test.ts +1 -1
- package/src/test/PermutationTest.test.ts +1 -1
- package/src/{tests → test}/RealDataValidation.test.ts +6 -6
- package/src/test/RunBenchCLI.test.ts +39 -38
- package/src/test/RunnerOrchestrator.test.ts +12 -12
- package/src/test/StatisticalUtils.test.ts +48 -12
- package/src/{table-util/test → test}/TableReport.test.ts +2 -2
- package/src/test/TestUtils.ts +12 -7
- package/src/test/TimeExport.test.ts +139 -0
- package/src/test/TimeSampler.test.ts +37 -0
- package/src/test/ViewerLive.e2e.test.ts +159 -0
- package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
- package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
- package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
- package/src/test/fixtures/cases/asyncCases.ts +9 -0
- package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
- package/src/test/fixtures/cases/variants/product.ts +2 -0
- package/src/test/fixtures/cases/variants/sum.ts +2 -0
- package/src/test/fixtures/discover/fast.ts +1 -0
- package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
- package/src/test/fixtures/invalid/bad.ts +1 -0
- package/src/test/fixtures/loader/fast.ts +1 -0
- package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
- package/src/test/fixtures/loader/stateful.ts +2 -0
- package/src/test/fixtures/stateful/stateful.ts +2 -0
- package/src/test/fixtures/variants/extra.ts +1 -0
- package/src/test/fixtures/variants/impl.ts +1 -0
- package/src/test/fixtures/worker/fast.ts +1 -0
- package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
- package/src/viewer/DateFormat.ts +30 -0
- package/src/viewer/Helpers.ts +23 -0
- package/src/viewer/LineData.ts +120 -0
- package/src/viewer/Providers.ts +191 -0
- package/src/viewer/ReportData.ts +123 -0
- package/src/viewer/State.ts +49 -0
- package/src/viewer/Theme.ts +15 -0
- package/src/viewer/components/App.tsx +73 -0
- package/src/viewer/components/DropZone.tsx +71 -0
- package/src/viewer/components/LazyPlot.ts +33 -0
- package/src/viewer/components/SamplesPanel.tsx +214 -0
- package/src/viewer/components/Shell.tsx +26 -0
- package/src/viewer/components/SourcePanel.tsx +216 -0
- package/src/viewer/components/SummaryPanel.tsx +332 -0
- package/src/viewer/components/TabBar.tsx +131 -0
- package/src/viewer/components/TabContent.tsx +46 -0
- package/src/viewer/components/ThemeToggle.tsx +50 -0
- package/src/viewer/index.html +20 -0
- package/src/viewer/main.tsx +4 -0
- package/src/viewer/plots/CIPlot.ts +313 -0
- package/src/{html/browser → viewer/plots}/HistogramKde.ts +33 -38
- package/src/viewer/plots/LegendUtils.ts +134 -0
- package/src/viewer/plots/PlotTypes.ts +85 -0
- package/src/viewer/plots/RenderPlots.ts +230 -0
- package/src/viewer/plots/SampleTimeSeries.ts +306 -0
- package/src/viewer/plots/SvgHelpers.ts +136 -0
- package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
- package/src/viewer/report.css +427 -0
- package/src/viewer/shell.css +357 -0
- package/src/viewer/tsconfig.json +11 -0
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs +0 -202
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +0 -1
- package/dist/GcStats-wX7Xyblu.mjs +0 -77
- package/dist/GcStats-wX7Xyblu.mjs.map +0 -1
- package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
- package/dist/TimingUtils-DwOwkc8G.mjs +0 -597
- package/dist/TimingUtils-DwOwkc8G.mjs.map +0 -1
- package/dist/browser/index.js +0 -914
- package/dist/src-B-DDaCa9.mjs +0 -3108
- package/dist/src-B-DDaCa9.mjs.map +0 -1
- package/src/BenchMatrix.ts +0 -380
- package/src/BenchmarkReport.ts +0 -161
- package/src/HtmlDataPrep.ts +0 -148
- package/src/StandardSections.ts +0 -261
- package/src/StatisticalUtils.ts +0 -175
- package/src/TypeUtil.ts +0 -8
- package/src/browser/BrowserGcStats.ts +0 -44
- package/src/browser/BrowserHeapSampler.ts +0 -271
- package/src/export/JsonExport.ts +0 -103
- package/src/export/JsonFormat.ts +0 -91
- package/src/export/SpeedscopeExport.ts +0 -202
- package/src/heap-sample/HeapSampleReport.ts +0 -269
- package/src/html/HtmlReport.ts +0 -131
- package/src/html/HtmlTemplate.ts +0 -284
- package/src/html/Types.ts +0 -88
- package/src/html/browser/CIPlot.ts +0 -287
- package/src/html/browser/LegendUtils.ts +0 -163
- package/src/html/browser/RenderPlots.ts +0 -263
- package/src/html/browser/SampleTimeSeries.ts +0 -389
- package/src/html/browser/Types.ts +0 -96
- package/src/html/browser/index.ts +0 -1
- package/src/html/index.ts +0 -17
- package/src/runners/BasicRunner.ts +0 -364
- package/src/table-util/ConvergenceFormatters.ts +0 -19
- package/src/table-util/Formatters.ts +0 -157
- package/src/table-util/README.md +0 -70
- package/src/table-util/TableReport.ts +0 -293
- package/src/tests/fixtures/cases/asyncCases.ts +0 -7
- package/src/tests/fixtures/cases/variants/product.ts +0 -2
- package/src/tests/fixtures/cases/variants/sum.ts +0 -2
- package/src/tests/fixtures/discover/fast.ts +0 -1
- package/src/tests/fixtures/invalid/bad.ts +0 -1
- package/src/tests/fixtures/loader/fast.ts +0 -1
- package/src/tests/fixtures/loader/stateful.ts +0 -2
- package/src/tests/fixtures/stateful/stateful.ts +0 -2
- package/src/tests/fixtures/variants/extra.ts +0 -1
- package/src/tests/fixtures/variants/impl.ts +0 -1
- package/src/tests/fixtures/worker/fast.ts +0 -1
- /package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
- /package/src/{table-util/test → test}/TableValueExtractor.ts +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as Plot from "@observablehq/plot";
|
|
2
|
+
|
|
3
|
+
/** Plot data bounds used to position the legend overlay */
|
|
4
|
+
export interface LegendBounds {
|
|
5
|
+
xMin: number;
|
|
6
|
+
xMax: number;
|
|
7
|
+
yMin?: number;
|
|
8
|
+
yMax: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A single entry in the plot legend with color, label, and symbol style */
|
|
12
|
+
export interface LegendItem {
|
|
13
|
+
color: string;
|
|
14
|
+
label: string;
|
|
15
|
+
style:
|
|
16
|
+
| "filled-dot"
|
|
17
|
+
| "hollow-dot"
|
|
18
|
+
| "vertical-bar"
|
|
19
|
+
| "vertical-line"
|
|
20
|
+
| "rect";
|
|
21
|
+
strokeDash?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface LegendPos {
|
|
25
|
+
legendX: number;
|
|
26
|
+
y: number;
|
|
27
|
+
textX: number;
|
|
28
|
+
xRange: number;
|
|
29
|
+
yRange: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const rectFields = { x1: "x1", x2: "x2", y1: "y1", y2: "y2" } as const;
|
|
33
|
+
|
|
34
|
+
/** Build complete legend marks array, positioned in the right margin */
|
|
35
|
+
export function buildLegend(bounds: LegendBounds, items: LegendItem[]): any[] {
|
|
36
|
+
const xRange = Math.max(bounds.xMax - bounds.xMin, bounds.xMax * 0.1 || 1);
|
|
37
|
+
const yRange = bounds.yMax - (bounds.yMin ?? 0);
|
|
38
|
+
const legendX = bounds.xMax + xRange * 0.04;
|
|
39
|
+
const textX = legendX + xRange * 0.03;
|
|
40
|
+
const itemHeight = yRange * 0.07;
|
|
41
|
+
const topY = bounds.yMax - yRange * 0.02;
|
|
42
|
+
|
|
43
|
+
const pos = (i: number): LegendPos => ({
|
|
44
|
+
legendX,
|
|
45
|
+
y: topY - i * itemHeight,
|
|
46
|
+
textX,
|
|
47
|
+
xRange,
|
|
48
|
+
yRange,
|
|
49
|
+
});
|
|
50
|
+
return items.flatMap((item, i) => [
|
|
51
|
+
symbolMark(pos(i), item),
|
|
52
|
+
textMark(pos(i), item.label),
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function symbolMark(pos: LegendPos, item: LegendItem): any {
|
|
57
|
+
switch (item.style) {
|
|
58
|
+
case "filled-dot":
|
|
59
|
+
return dotMark(pos.legendX, pos.y, item.color, true);
|
|
60
|
+
case "hollow-dot":
|
|
61
|
+
return dotMark(pos.legendX, pos.y, item.color, false);
|
|
62
|
+
case "vertical-bar":
|
|
63
|
+
return verticalBarMark(pos, item.color);
|
|
64
|
+
case "vertical-line":
|
|
65
|
+
return verticalLineMark(pos, item.color, item.strokeDash);
|
|
66
|
+
case "rect":
|
|
67
|
+
return rectMark(pos, item.color);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function textMark(pos: LegendPos, label: string): any {
|
|
72
|
+
return Plot.text([{ x: pos.textX, y: pos.y, text: label }], {
|
|
73
|
+
x: "x",
|
|
74
|
+
y: "y",
|
|
75
|
+
text: "text",
|
|
76
|
+
fontSize: 11,
|
|
77
|
+
textAnchor: "start",
|
|
78
|
+
fill: "#333",
|
|
79
|
+
clip: false,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function dotMark(x: number, y: number, color: string, filled: boolean): any {
|
|
84
|
+
const base = { x: "x", y: "y", r: 4, clip: false };
|
|
85
|
+
const style = filled
|
|
86
|
+
? { ...base, fill: color }
|
|
87
|
+
: { ...base, stroke: color, fill: "none", strokeWidth: 1.5 };
|
|
88
|
+
return Plot.dot([{ x, y }], style);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function verticalBarMark(pos: LegendPos, color: string): any {
|
|
92
|
+
const { legendX, y, xRange, yRange } = pos;
|
|
93
|
+
const hw = xRange * 0.006;
|
|
94
|
+
const hh = yRange * 0.025;
|
|
95
|
+
const data = [{ x1: legendX - hw, x2: legendX + hw, y1: y - hh, y2: y + hh }];
|
|
96
|
+
return Plot.rect(data, {
|
|
97
|
+
...rectFields,
|
|
98
|
+
fill: color,
|
|
99
|
+
fillOpacity: 0.6,
|
|
100
|
+
clip: false,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function verticalLineMark(
|
|
105
|
+
pos: LegendPos,
|
|
106
|
+
color: string,
|
|
107
|
+
strokeDash?: string,
|
|
108
|
+
): any {
|
|
109
|
+
const { legendX, y, yRange } = pos;
|
|
110
|
+
const half = yRange * 0.025;
|
|
111
|
+
return Plot.ruleX([legendX], {
|
|
112
|
+
y1: y - half,
|
|
113
|
+
y2: y + half,
|
|
114
|
+
stroke: color,
|
|
115
|
+
strokeWidth: 2,
|
|
116
|
+
strokeDasharray: strokeDash,
|
|
117
|
+
clip: false,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function rectMark(pos: LegendPos, color: string): any {
|
|
122
|
+
const { legendX, y, xRange, yRange } = pos;
|
|
123
|
+
const hw = xRange * 0.015;
|
|
124
|
+
const hh = yRange * 0.02;
|
|
125
|
+
const data = [{ x1: legendX - hw, x2: legendX + hw, y1: y - hh, y2: y + hh }];
|
|
126
|
+
return Plot.rect(data, {
|
|
127
|
+
...rectFields,
|
|
128
|
+
fill: color,
|
|
129
|
+
fillOpacity: 0.3,
|
|
130
|
+
stroke: color,
|
|
131
|
+
strokeWidth: 1,
|
|
132
|
+
clip: false,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** A single timing sample from a benchmark run */
|
|
2
|
+
export interface Sample {
|
|
3
|
+
benchmark: string;
|
|
4
|
+
value: number;
|
|
5
|
+
iteration: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** A sample with warmup/optimization metadata for time series plots */
|
|
9
|
+
export interface TimeSeriesPoint {
|
|
10
|
+
benchmark: string;
|
|
11
|
+
iteration: number;
|
|
12
|
+
value: number;
|
|
13
|
+
isWarmup: boolean;
|
|
14
|
+
isBaseline?: boolean;
|
|
15
|
+
isRejected?: boolean;
|
|
16
|
+
/** V8 optimization status code (e.g. 17=turbofan, 33=maglev) */
|
|
17
|
+
optStatus?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Heap usage sample (in bytes) at a given iteration */
|
|
21
|
+
export interface HeapPoint {
|
|
22
|
+
benchmark: string;
|
|
23
|
+
iteration: number;
|
|
24
|
+
value: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** GcEvent flattened with benchmark name for multi-series plots */
|
|
28
|
+
export interface FlatGcEvent {
|
|
29
|
+
benchmark: string;
|
|
30
|
+
sampleIndex: number;
|
|
31
|
+
duration: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** PausePoint flattened with benchmark name for multi-series plots */
|
|
35
|
+
export interface FlatPausePoint {
|
|
36
|
+
benchmark: string;
|
|
37
|
+
sampleIndex: number;
|
|
38
|
+
durationMs: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Display unit (ns/us/ms) with conversion and formatting functions */
|
|
42
|
+
export interface TimeUnit {
|
|
43
|
+
unitSuffix: string;
|
|
44
|
+
convertValue: (ms: number) => number;
|
|
45
|
+
formatValue: (d: number) => string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Shared Observable Plot layout: margins, dimensions, font size */
|
|
49
|
+
export const plotLayout = {
|
|
50
|
+
marginTop: 24,
|
|
51
|
+
marginLeft: 70,
|
|
52
|
+
marginRight: 110,
|
|
53
|
+
marginBottom: 60,
|
|
54
|
+
width: 550,
|
|
55
|
+
height: 300,
|
|
56
|
+
style: { fontSize: "14px" },
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
/** Format a number as a signed percentage string (e.g. "+1.2%", "-3.4%") */
|
|
60
|
+
export function formatPct(v: number, precision = 1): string {
|
|
61
|
+
const sign = v >= 0 ? "+" : "";
|
|
62
|
+
return `${sign}${v.toFixed(precision)}%`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Pick display unit (ns/us/ms) based on average value magnitude (in ms) */
|
|
66
|
+
export function getTimeUnit(values: number[]): TimeUnit {
|
|
67
|
+
const avg = values.reduce((s, v) => s + v, 0) / values.length;
|
|
68
|
+
const locale = (digits: number) => (d: number) =>
|
|
69
|
+
d.toLocaleString("en-US", { maximumFractionDigits: digits });
|
|
70
|
+
const fmt0 = locale(0);
|
|
71
|
+
const fmt1 = locale(1);
|
|
72
|
+
if (avg < 0.001)
|
|
73
|
+
return {
|
|
74
|
+
unitSuffix: "ns",
|
|
75
|
+
convertValue: ms => ms * 1e6,
|
|
76
|
+
formatValue: fmt0,
|
|
77
|
+
};
|
|
78
|
+
if (avg < 1)
|
|
79
|
+
return {
|
|
80
|
+
unitSuffix: "\u00b5s",
|
|
81
|
+
convertValue: ms => ms * 1e3,
|
|
82
|
+
formatValue: fmt1,
|
|
83
|
+
};
|
|
84
|
+
return { unitSuffix: "ms", convertValue: ms => ms, formatValue: fmt1 };
|
|
85
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {
|
|
2
|
+
average,
|
|
3
|
+
splitByOffsets,
|
|
4
|
+
tukeyKeep,
|
|
5
|
+
} from "../../stats/StatisticalUtils.ts";
|
|
6
|
+
import type { BenchmarkEntry, ReportData } from "../ReportData.ts";
|
|
7
|
+
import type {
|
|
8
|
+
FlatGcEvent,
|
|
9
|
+
FlatPausePoint,
|
|
10
|
+
HeapPoint,
|
|
11
|
+
Sample,
|
|
12
|
+
TimeSeriesPoint,
|
|
13
|
+
} from "./PlotTypes.ts";
|
|
14
|
+
|
|
15
|
+
/** Benchmark entry tagged with whether it's the baseline for comparison */
|
|
16
|
+
export interface PreparedBenchmark extends BenchmarkEntry {
|
|
17
|
+
isBaseline: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** All sample data flattened across benchmarks into arrays for plotting */
|
|
21
|
+
export interface FlattenedData {
|
|
22
|
+
allSamples: Sample[];
|
|
23
|
+
timeSeries: TimeSeriesPoint[];
|
|
24
|
+
heapSeries: HeapPoint[];
|
|
25
|
+
baselineHeapSeries: HeapPoint[];
|
|
26
|
+
allGcEvents: FlatGcEvent[];
|
|
27
|
+
allPausePoints: FlatPausePoint[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Combine baseline and benchmarks into a single list with display names */
|
|
31
|
+
export function prepareBenchmarks(
|
|
32
|
+
group: ReportData["groups"][0],
|
|
33
|
+
): PreparedBenchmark[] {
|
|
34
|
+
const base = group.baseline;
|
|
35
|
+
const current = group.benchmarks.map(b => ({ ...b, isBaseline: false }));
|
|
36
|
+
if (!base) return current;
|
|
37
|
+
|
|
38
|
+
const baseName = base.name.endsWith("(baseline)")
|
|
39
|
+
? base.name
|
|
40
|
+
: base.name + " (baseline)";
|
|
41
|
+
return [{ ...base, name: baseName, isBaseline: true }, ...current];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Collect all sample data across benchmarks into flat arrays for plotting */
|
|
45
|
+
export function flattenSamples(benchmarks: PreparedBenchmark[]): FlattenedData {
|
|
46
|
+
const out: FlattenedData = {
|
|
47
|
+
allSamples: [],
|
|
48
|
+
timeSeries: [],
|
|
49
|
+
heapSeries: [],
|
|
50
|
+
baselineHeapSeries: [],
|
|
51
|
+
allGcEvents: [],
|
|
52
|
+
allPausePoints: [],
|
|
53
|
+
};
|
|
54
|
+
for (const b of benchmarks) {
|
|
55
|
+
if (b.samples?.length) flattenBenchmark(b, out);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @return batch count from the first benchmark with batchOffsets, or 0 */
|
|
61
|
+
export function batchCount(benchmarks: PreparedBenchmark[]): number {
|
|
62
|
+
return (
|
|
63
|
+
benchmarks.find(b => b.batchOffsets?.length)?.batchOffsets?.length ?? 0
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Filter flattened data to a single batch, re-indexing iterations from 0 */
|
|
68
|
+
export function filterToBatch(
|
|
69
|
+
flat: FlattenedData,
|
|
70
|
+
benchmarks: PreparedBenchmark[],
|
|
71
|
+
batchIndex: number,
|
|
72
|
+
): FlattenedData {
|
|
73
|
+
const ranges = new Map<string, [number, number]>();
|
|
74
|
+
for (const b of benchmarks) {
|
|
75
|
+
const offsets = b.batchOffsets;
|
|
76
|
+
if (!offsets?.length) continue;
|
|
77
|
+
const start = offsets[batchIndex];
|
|
78
|
+
const end =
|
|
79
|
+
batchIndex + 1 < offsets.length
|
|
80
|
+
? offsets[batchIndex + 1]
|
|
81
|
+
: b.samples.length;
|
|
82
|
+
ranges.set(b.name, [start, end]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const inBatch = (name: string, iter: number) => {
|
|
86
|
+
const r = ranges.get(name);
|
|
87
|
+
return r ? iter >= r[0] && iter < r[1] : true;
|
|
88
|
+
};
|
|
89
|
+
const reindex = (name: string, iter: number) => {
|
|
90
|
+
const r = ranges.get(name);
|
|
91
|
+
return r ? iter - r[0] : iter;
|
|
92
|
+
};
|
|
93
|
+
const sliceIter = <T extends { benchmark: string; iteration: number }>(
|
|
94
|
+
arr: T[],
|
|
95
|
+
) =>
|
|
96
|
+
arr
|
|
97
|
+
.filter(d => inBatch(d.benchmark, d.iteration))
|
|
98
|
+
.map(d => ({ ...d, iteration: reindex(d.benchmark, d.iteration) }));
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
allSamples: sliceIter(flat.allSamples),
|
|
102
|
+
timeSeries: sliceIter(flat.timeSeries.filter(d => !d.isWarmup)),
|
|
103
|
+
heapSeries: sliceIter(flat.heapSeries),
|
|
104
|
+
baselineHeapSeries: sliceIter(flat.baselineHeapSeries),
|
|
105
|
+
allGcEvents: flat.allGcEvents.filter(d =>
|
|
106
|
+
inBatch(d.benchmark, d.sampleIndex),
|
|
107
|
+
),
|
|
108
|
+
allPausePoints: flat.allPausePoints.filter(d =>
|
|
109
|
+
inBatch(d.benchmark, d.sampleIndex),
|
|
110
|
+
),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Extract time series, heap, GC, and pause data from one benchmark */
|
|
115
|
+
function flattenBenchmark(b: PreparedBenchmark, out: FlattenedData): void {
|
|
116
|
+
flattenWarmup(b, b.name, out);
|
|
117
|
+
flattenSamplesAndHeap(b, b.name, out);
|
|
118
|
+
flattenGcEvents(b, b.name, out);
|
|
119
|
+
flattenPausePoints(b, b.name, out);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Warmup samples get negative iteration indices so they appear left of zero */
|
|
123
|
+
function flattenWarmup(
|
|
124
|
+
b: PreparedBenchmark,
|
|
125
|
+
name: string,
|
|
126
|
+
out: FlattenedData,
|
|
127
|
+
): void {
|
|
128
|
+
const warmupCount = b.warmupSamples?.length || 0;
|
|
129
|
+
b.warmupSamples?.forEach((value, i) => {
|
|
130
|
+
out.timeSeries.push({
|
|
131
|
+
benchmark: name,
|
|
132
|
+
iteration: i - warmupCount,
|
|
133
|
+
value,
|
|
134
|
+
isWarmup: true,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Populate timeSeries, allSamples (excluding rejected), and heap data */
|
|
140
|
+
function flattenSamplesAndHeap(
|
|
141
|
+
b: PreparedBenchmark,
|
|
142
|
+
name: string,
|
|
143
|
+
out: FlattenedData,
|
|
144
|
+
): void {
|
|
145
|
+
const rejected = rejectedIndices(b);
|
|
146
|
+
const isBase = b.isBaseline || undefined;
|
|
147
|
+
b.samples.forEach((value, i) => {
|
|
148
|
+
const isRejected = rejected?.has(i) || undefined;
|
|
149
|
+
if (!isRejected)
|
|
150
|
+
out.allSamples.push({ benchmark: name, value, iteration: i });
|
|
151
|
+
const optStatus = b.optSamples?.[i];
|
|
152
|
+
out.timeSeries.push({
|
|
153
|
+
benchmark: name,
|
|
154
|
+
iteration: i,
|
|
155
|
+
value,
|
|
156
|
+
isWarmup: false,
|
|
157
|
+
isBaseline: isBase,
|
|
158
|
+
isRejected,
|
|
159
|
+
optStatus,
|
|
160
|
+
});
|
|
161
|
+
if (b.heapSamples?.[i] !== undefined) {
|
|
162
|
+
const target = b.isBaseline ? out.baselineHeapSeries : out.heapSeries;
|
|
163
|
+
target.push({ benchmark: name, iteration: i, value: b.heapSamples[i] });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Map GC events to sample indices using cumulative sample durations */
|
|
169
|
+
function flattenGcEvents(
|
|
170
|
+
b: PreparedBenchmark,
|
|
171
|
+
name: string,
|
|
172
|
+
out: FlattenedData,
|
|
173
|
+
): void {
|
|
174
|
+
if (!b.gcEvents?.length) return;
|
|
175
|
+
const endTimes = cumulativeSum(b.samples);
|
|
176
|
+
for (const gc of b.gcEvents) {
|
|
177
|
+
const idx = endTimes.findIndex(t => t >= gc.offset);
|
|
178
|
+
const sampleIndex = idx >= 0 ? idx : b.samples.length - 1;
|
|
179
|
+
out.allGcEvents.push({
|
|
180
|
+
benchmark: name,
|
|
181
|
+
sampleIndex,
|
|
182
|
+
duration: gc.duration,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Flatten benchmark pause points into the shared output arrays */
|
|
188
|
+
function flattenPausePoints(
|
|
189
|
+
b: PreparedBenchmark,
|
|
190
|
+
name: string,
|
|
191
|
+
out: FlattenedData,
|
|
192
|
+
): void {
|
|
193
|
+
if (!b.pausePoints) return;
|
|
194
|
+
for (const p of b.pausePoints)
|
|
195
|
+
out.allPausePoints.push({
|
|
196
|
+
benchmark: name,
|
|
197
|
+
sampleIndex: p.sampleIndex,
|
|
198
|
+
durationMs: p.durationMs,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** @return sample indices in Tukey-rejected batches, or undefined if none */
|
|
203
|
+
function rejectedIndices(b: PreparedBenchmark): Set<number> | undefined {
|
|
204
|
+
const offsets = b.batchOffsets;
|
|
205
|
+
if (!offsets || offsets.length < 4) return undefined;
|
|
206
|
+
|
|
207
|
+
const means = splitByOffsets(b.samples, offsets).map(s => average(s));
|
|
208
|
+
const kept = new Set(tukeyKeep(means));
|
|
209
|
+
|
|
210
|
+
const rejected = new Set<number>();
|
|
211
|
+
for (let bi = 0; bi < means.length; bi++) {
|
|
212
|
+
if (!kept.has(bi)) {
|
|
213
|
+
const start = offsets[bi];
|
|
214
|
+
const end = bi + 1 < offsets.length ? offsets[bi + 1] : b.samples.length;
|
|
215
|
+
for (let j = start; j < end; j++) rejected.add(j);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return rejected.size > 0 ? rejected : undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Running total of sample durations, used to map GC offsets to sample indices */
|
|
222
|
+
function cumulativeSum(arr: number[]): number[] {
|
|
223
|
+
const result: number[] = [];
|
|
224
|
+
let sum = 0;
|
|
225
|
+
for (const v of arr) {
|
|
226
|
+
sum += v;
|
|
227
|
+
result.push(sum);
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
}
|