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
package/dist/browser/index.js
DELETED
|
@@ -1,914 +0,0 @@
|
|
|
1
|
-
import * as Plot from "@observablehq/plot";
|
|
2
|
-
import * as d3 from "d3";
|
|
3
|
-
|
|
4
|
-
//#region src/html/browser/CIPlot.ts
|
|
5
|
-
const defaultMargin = {
|
|
6
|
-
top: 22,
|
|
7
|
-
right: 12,
|
|
8
|
-
bottom: 22,
|
|
9
|
-
left: 12
|
|
10
|
-
};
|
|
11
|
-
const defaultOpts = {
|
|
12
|
-
width: 260,
|
|
13
|
-
height: 85,
|
|
14
|
-
title: "p50 Δ%",
|
|
15
|
-
smooth: false,
|
|
16
|
-
direction: "uncertain"
|
|
17
|
-
};
|
|
18
|
-
const colors = {
|
|
19
|
-
faster: {
|
|
20
|
-
fill: "#dcfce7",
|
|
21
|
-
stroke: "#22c55e"
|
|
22
|
-
},
|
|
23
|
-
slower: {
|
|
24
|
-
fill: "#fee2e2",
|
|
25
|
-
stroke: "#ef4444"
|
|
26
|
-
},
|
|
27
|
-
uncertain: {
|
|
28
|
-
fill: "#dbeafe",
|
|
29
|
-
stroke: "#3b82f6"
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
const formatPct$1 = (v) => (v >= 0 ? "+" : "") + v.toFixed(0) + "%";
|
|
33
|
-
/** Create a small distribution plot showing histogram with CI shading */
|
|
34
|
-
function createDistributionPlot(histogram, ci, pointEstimate, options = {}) {
|
|
35
|
-
const opts = {
|
|
36
|
-
...defaultOpts,
|
|
37
|
-
...options
|
|
38
|
-
};
|
|
39
|
-
const layout = buildLayout(opts.width, opts.height);
|
|
40
|
-
const svg = createSvg(layout.width, layout.height);
|
|
41
|
-
if (!histogram?.length) return svg;
|
|
42
|
-
const { fill, stroke } = colors[opts.direction];
|
|
43
|
-
const scales = buildScales(histogram, ci, layout);
|
|
44
|
-
drawTitle(svg, opts.title, layout.margin.left);
|
|
45
|
-
drawCIRegion(svg, ci, scales, layout, fill);
|
|
46
|
-
opts.smooth ? drawSmoothedDist(svg, histogram, scales, stroke) : drawHistogramBars(svg, histogram, scales, layout, stroke);
|
|
47
|
-
drawZeroLine(svg, scales, layout);
|
|
48
|
-
drawPointEstimate(svg, pointEstimate, scales, layout, stroke);
|
|
49
|
-
drawCILabels(svg, ci, scales, layout.height);
|
|
50
|
-
return svg;
|
|
51
|
-
}
|
|
52
|
-
/** Convenience wrapper for ComparisonCI data */
|
|
53
|
-
function createCIPlot(ci, options = {}) {
|
|
54
|
-
if (!ci.histogram) return createSvg(0, 0);
|
|
55
|
-
return createDistributionPlot(ci.histogram, ci.ci, ci.percent, {
|
|
56
|
-
direction: ci.direction,
|
|
57
|
-
...options
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
function buildLayout(width, height) {
|
|
61
|
-
const margin = defaultMargin;
|
|
62
|
-
return {
|
|
63
|
-
width,
|
|
64
|
-
height,
|
|
65
|
-
margin,
|
|
66
|
-
plot: {
|
|
67
|
-
w: width - margin.left - margin.right,
|
|
68
|
-
h: height - margin.top - margin.bottom
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
function createSvg(w, h) {
|
|
73
|
-
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
74
|
-
svg.setAttribute("width", String(w));
|
|
75
|
-
svg.setAttribute("height", String(h));
|
|
76
|
-
if (w && h) svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
|
|
77
|
-
return svg;
|
|
78
|
-
}
|
|
79
|
-
/** Compute x/y scale functions mapping data values to SVG coordinates */
|
|
80
|
-
function buildScales(histogram, ci, layout) {
|
|
81
|
-
const { margin, plot } = layout;
|
|
82
|
-
const xMin = Math.min(...histogram.map((b) => b.x), ci[0], 0);
|
|
83
|
-
const xMax = Math.max(...histogram.map((b) => b.x), ci[1], 0);
|
|
84
|
-
const yMax = Math.max(...histogram.map((b) => b.count));
|
|
85
|
-
return {
|
|
86
|
-
x: (v) => margin.left + (v - xMin) / (xMax - xMin) * plot.w,
|
|
87
|
-
y: (v) => margin.top + plot.h - v / yMax * plot.h
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
function drawTitle(svg, title, x) {
|
|
91
|
-
svg.appendChild(text(x, 14, title, "start", "13", "#333", "600"));
|
|
92
|
-
}
|
|
93
|
-
function drawCIRegion(svg, ci, scales, layout, fill) {
|
|
94
|
-
const x = scales.x(ci[0]);
|
|
95
|
-
const w = scales.x(ci[1]) - x;
|
|
96
|
-
svg.appendChild(rect(x, layout.margin.top, w, layout.plot.h, {
|
|
97
|
-
fill,
|
|
98
|
-
opacity: "0.5"
|
|
99
|
-
}));
|
|
100
|
-
}
|
|
101
|
-
function drawSmoothedDist(svg, histogram, scales, stroke) {
|
|
102
|
-
const smoothed = gaussianSmooth([...histogram].sort((a, b) => a.x - b.x), 2);
|
|
103
|
-
const pts = smoothed.map((b) => `${scales.x(b.x)},${scales.y(b.count)}`);
|
|
104
|
-
const baseline = scales.y(0);
|
|
105
|
-
svg.appendChild(path(`M${scales.x(smoothed[0].x)},${baseline}L${pts.join("L")}L${scales.x(smoothed.at(-1).x)},${baseline}Z`, {
|
|
106
|
-
fill: stroke,
|
|
107
|
-
opacity: "0.3"
|
|
108
|
-
}));
|
|
109
|
-
svg.appendChild(path(`M${pts.join("L")}`, {
|
|
110
|
-
stroke,
|
|
111
|
-
fill: "none",
|
|
112
|
-
strokeWidth: "1.5"
|
|
113
|
-
}));
|
|
114
|
-
}
|
|
115
|
-
function drawHistogramBars(svg, histogram, scales, layout, stroke) {
|
|
116
|
-
const sorted = [...histogram].sort((a, b) => a.x - b.x);
|
|
117
|
-
const binW = sorted.length > 1 ? sorted[1].x - sorted[0].x : 1;
|
|
118
|
-
const yMax = Math.max(...histogram.map((b) => b.count));
|
|
119
|
-
const xRange = scales.x(sorted.at(-1).x) - scales.x(sorted[0].x) + binW;
|
|
120
|
-
for (const bin of sorted) {
|
|
121
|
-
const barW = binW / xRange * layout.plot.w * .9;
|
|
122
|
-
const barH = bin.count / yMax * layout.plot.h;
|
|
123
|
-
svg.appendChild(rect(scales.x(bin.x) - barW / 2, layout.margin.top + layout.plot.h - barH, barW, barH, {
|
|
124
|
-
fill: stroke,
|
|
125
|
-
opacity: "0.6"
|
|
126
|
-
}));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
function drawZeroLine(svg, scales, layout) {
|
|
130
|
-
const zeroX = scales.x(0);
|
|
131
|
-
if (zeroX < layout.margin.left || zeroX > layout.width - layout.margin.right) return;
|
|
132
|
-
const top = layout.margin.top;
|
|
133
|
-
svg.appendChild(line(zeroX, top, zeroX, top + layout.plot.h, {
|
|
134
|
-
stroke: "#666",
|
|
135
|
-
strokeWidth: "1",
|
|
136
|
-
strokeDasharray: "3,2"
|
|
137
|
-
}));
|
|
138
|
-
}
|
|
139
|
-
function drawPointEstimate(svg, pt, scales, layout, stroke) {
|
|
140
|
-
const x = scales.x(pt);
|
|
141
|
-
const top = layout.margin.top;
|
|
142
|
-
svg.appendChild(line(x, top, x, top + layout.plot.h, {
|
|
143
|
-
stroke,
|
|
144
|
-
strokeWidth: "2"
|
|
145
|
-
}));
|
|
146
|
-
}
|
|
147
|
-
function drawCILabels(svg, ci, scales, height) {
|
|
148
|
-
svg.appendChild(text(scales.x(ci[0]), height - 4, formatPct$1(ci[0]), "middle", "12"));
|
|
149
|
-
svg.appendChild(text(scales.x(ci[1]), height - 4, formatPct$1(ci[1]), "middle", "12"));
|
|
150
|
-
}
|
|
151
|
-
function text(x, y, content, anchor = "start", size = "9", fill = "#666", weight = "400") {
|
|
152
|
-
const el = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
153
|
-
el.setAttribute("x", String(x));
|
|
154
|
-
el.setAttribute("y", String(y));
|
|
155
|
-
el.setAttribute("text-anchor", anchor);
|
|
156
|
-
el.setAttribute("font-size", size);
|
|
157
|
-
el.setAttribute("font-weight", weight);
|
|
158
|
-
el.setAttribute("fill", fill);
|
|
159
|
-
el.textContent = content;
|
|
160
|
-
return el;
|
|
161
|
-
}
|
|
162
|
-
function rect(x, y, w, h, attrs) {
|
|
163
|
-
const el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
164
|
-
el.setAttribute("x", String(x));
|
|
165
|
-
el.setAttribute("y", String(y));
|
|
166
|
-
el.setAttribute("width", String(w));
|
|
167
|
-
el.setAttribute("height", String(h));
|
|
168
|
-
setAttrs(el, attrs);
|
|
169
|
-
return el;
|
|
170
|
-
}
|
|
171
|
-
/** Apply gaussian kernel smoothing to histogram bins */
|
|
172
|
-
function gaussianSmooth(bins, sigma) {
|
|
173
|
-
return bins.map((bin, i) => {
|
|
174
|
-
let sum = 0;
|
|
175
|
-
let wt = 0;
|
|
176
|
-
for (let j = 0; j < bins.length; j++) {
|
|
177
|
-
const w = Math.exp(-((i - j) ** 2) / (2 * sigma ** 2));
|
|
178
|
-
sum += bins[j].count * w;
|
|
179
|
-
wt += w;
|
|
180
|
-
}
|
|
181
|
-
return {
|
|
182
|
-
x: bin.x,
|
|
183
|
-
count: sum / wt
|
|
184
|
-
};
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
function path(d, attrs) {
|
|
188
|
-
const el = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
189
|
-
el.setAttribute("d", d);
|
|
190
|
-
setAttrs(el, attrs);
|
|
191
|
-
return el;
|
|
192
|
-
}
|
|
193
|
-
function line(x1, y1, x2, y2, attrs) {
|
|
194
|
-
const el = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
195
|
-
el.setAttribute("x1", String(x1));
|
|
196
|
-
el.setAttribute("y1", String(y1));
|
|
197
|
-
el.setAttribute("x2", String(x2));
|
|
198
|
-
el.setAttribute("y2", String(y2));
|
|
199
|
-
setAttrs(el, attrs);
|
|
200
|
-
return el;
|
|
201
|
-
}
|
|
202
|
-
/** Set SVG attributes, converting camelCase keys to kebab-case */
|
|
203
|
-
function setAttrs(el, attrs) {
|
|
204
|
-
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()), v);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
//#endregion
|
|
208
|
-
//#region src/html/browser/LegendUtils.ts
|
|
209
|
-
/** Build complete legend marks array */
|
|
210
|
-
function buildLegend(bounds, items) {
|
|
211
|
-
const xRange = bounds.xMax - bounds.xMin;
|
|
212
|
-
const legendX = bounds.xMin + xRange * .68;
|
|
213
|
-
const textX = legendX + xRange * .04;
|
|
214
|
-
const getY = (i) => bounds.yMax * .98 - i * (bounds.yMax * .08);
|
|
215
|
-
const marks = [legendBackground(bounds)];
|
|
216
|
-
for (let i = 0; i < items.length; i++) {
|
|
217
|
-
const pos = {
|
|
218
|
-
legendX,
|
|
219
|
-
y: getY(i),
|
|
220
|
-
textX,
|
|
221
|
-
xRange,
|
|
222
|
-
yMax: bounds.yMax
|
|
223
|
-
};
|
|
224
|
-
marks.push(symbolMark(pos, items[i]), textMark(pos, items[i].label));
|
|
225
|
-
}
|
|
226
|
-
return marks;
|
|
227
|
-
}
|
|
228
|
-
/** Draw a semi-transparent white background behind the legend area */
|
|
229
|
-
function legendBackground(bounds) {
|
|
230
|
-
const xRange = bounds.xMax - bounds.xMin;
|
|
231
|
-
const data = [{
|
|
232
|
-
x1: bounds.xMin + xRange * .65,
|
|
233
|
-
x2: bounds.xMin + xRange * 1.05,
|
|
234
|
-
y1: bounds.yMax * .65,
|
|
235
|
-
y2: bounds.yMax * 1.05
|
|
236
|
-
}];
|
|
237
|
-
return Plot.rect(data, {
|
|
238
|
-
x1: "x1",
|
|
239
|
-
x2: "x2",
|
|
240
|
-
y1: "y1",
|
|
241
|
-
y2: "y2",
|
|
242
|
-
fill: "white",
|
|
243
|
-
fillOpacity: .9,
|
|
244
|
-
stroke: "#ddd",
|
|
245
|
-
strokeWidth: 1
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
function symbolMark(pos, item) {
|
|
249
|
-
switch (item.style) {
|
|
250
|
-
case "filled-dot": return dotMark(pos.legendX, pos.y, item.color, true);
|
|
251
|
-
case "hollow-dot": return dotMark(pos.legendX, pos.y, item.color, false);
|
|
252
|
-
case "vertical-bar": return verticalBarMark(pos, item.color);
|
|
253
|
-
case "vertical-line": return verticalLineMark(pos, item.color, item.strokeDash);
|
|
254
|
-
case "rect": return rectMark(pos, item.color);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
function textMark(pos, label) {
|
|
258
|
-
const data = [{
|
|
259
|
-
x: pos.textX,
|
|
260
|
-
y: pos.y,
|
|
261
|
-
text: label
|
|
262
|
-
}];
|
|
263
|
-
return Plot.text(data, {
|
|
264
|
-
x: "x",
|
|
265
|
-
y: "y",
|
|
266
|
-
text: "text",
|
|
267
|
-
fontSize: 11,
|
|
268
|
-
textAnchor: "start",
|
|
269
|
-
fill: "#333"
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
function dotMark(x, y, color, filled) {
|
|
273
|
-
return Plot.dot([{
|
|
274
|
-
x,
|
|
275
|
-
y
|
|
276
|
-
}], filled ? {
|
|
277
|
-
x: "x",
|
|
278
|
-
y: "y",
|
|
279
|
-
fill: color,
|
|
280
|
-
r: 4
|
|
281
|
-
} : {
|
|
282
|
-
x: "x",
|
|
283
|
-
y: "y",
|
|
284
|
-
stroke: color,
|
|
285
|
-
fill: "none",
|
|
286
|
-
strokeWidth: 1.5,
|
|
287
|
-
r: 4
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
function verticalBarMark(pos, color) {
|
|
291
|
-
const { legendX, y, xRange, yMax } = pos;
|
|
292
|
-
const w = xRange * .012;
|
|
293
|
-
const h = yMax * .05;
|
|
294
|
-
const data = [{
|
|
295
|
-
x1: legendX - w / 2,
|
|
296
|
-
x2: legendX + w / 2,
|
|
297
|
-
y1: y - h / 2,
|
|
298
|
-
y2: y + h / 2
|
|
299
|
-
}];
|
|
300
|
-
return Plot.rect(data, {
|
|
301
|
-
x1: "x1",
|
|
302
|
-
x2: "x2",
|
|
303
|
-
y1: "y1",
|
|
304
|
-
y2: "y2",
|
|
305
|
-
fill: color,
|
|
306
|
-
fillOpacity: .6
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
function verticalLineMark(pos, color, strokeDash) {
|
|
310
|
-
const { legendX, y, yMax } = pos;
|
|
311
|
-
return Plot.ruleX([legendX], {
|
|
312
|
-
y1: y - yMax * .025,
|
|
313
|
-
y2: y + yMax * .025,
|
|
314
|
-
stroke: color,
|
|
315
|
-
strokeWidth: 2,
|
|
316
|
-
strokeDasharray: strokeDash
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
function rectMark(pos, color) {
|
|
320
|
-
const { legendX, y, xRange, yMax } = pos;
|
|
321
|
-
const data = [{
|
|
322
|
-
x1: legendX - xRange * .01,
|
|
323
|
-
x2: legendX + xRange * .03,
|
|
324
|
-
y1: y - yMax * .02,
|
|
325
|
-
y2: y + yMax * .02
|
|
326
|
-
}];
|
|
327
|
-
const opts = {
|
|
328
|
-
x1: "x1",
|
|
329
|
-
x2: "x2",
|
|
330
|
-
y1: "y1",
|
|
331
|
-
y2: "y2",
|
|
332
|
-
fill: color,
|
|
333
|
-
fillOpacity: .3,
|
|
334
|
-
stroke: color,
|
|
335
|
-
strokeWidth: 1
|
|
336
|
-
};
|
|
337
|
-
return Plot.rect(data, opts);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
//#endregion
|
|
341
|
-
//#region src/html/browser/HistogramKde.ts
|
|
342
|
-
/** Create histogram + KDE plot for sample distribution */
|
|
343
|
-
function createHistogramKde(allSamples, benchmarkNames) {
|
|
344
|
-
const { barData, binMin, binMax, yMax } = buildBarData(allSamples, benchmarkNames);
|
|
345
|
-
const { colorMap, legendItems } = buildColorData(benchmarkNames);
|
|
346
|
-
const xMax = binMax + (binMax - binMin) * .45;
|
|
347
|
-
return Plot.plot({
|
|
348
|
-
marginTop: 24,
|
|
349
|
-
marginLeft: 70,
|
|
350
|
-
marginRight: 10,
|
|
351
|
-
marginBottom: 60,
|
|
352
|
-
width: 550,
|
|
353
|
-
height: 300,
|
|
354
|
-
style: { fontSize: "14px" },
|
|
355
|
-
x: {
|
|
356
|
-
label: "Time (ms)",
|
|
357
|
-
labelAnchor: "center",
|
|
358
|
-
domain: [binMin, xMax],
|
|
359
|
-
labelOffset: 45,
|
|
360
|
-
tickFormat: (d) => d.toFixed(1),
|
|
361
|
-
ticks: 5
|
|
362
|
-
},
|
|
363
|
-
y: {
|
|
364
|
-
label: "Count",
|
|
365
|
-
labelAnchor: "top",
|
|
366
|
-
labelArrow: false,
|
|
367
|
-
grid: true,
|
|
368
|
-
domain: [0, yMax]
|
|
369
|
-
},
|
|
370
|
-
marks: [
|
|
371
|
-
Plot.rectY(barData, {
|
|
372
|
-
x1: "x1",
|
|
373
|
-
x2: "x2",
|
|
374
|
-
y: "count",
|
|
375
|
-
fill: (d) => colorMap.get(d.benchmark),
|
|
376
|
-
fillOpacity: .6,
|
|
377
|
-
tip: true,
|
|
378
|
-
title: (d) => `${d.benchmark}: ${d.count}`
|
|
379
|
-
}),
|
|
380
|
-
Plot.ruleY([0]),
|
|
381
|
-
...buildLegend({
|
|
382
|
-
xMin: binMin,
|
|
383
|
-
xMax,
|
|
384
|
-
yMax
|
|
385
|
-
}, legendItems)
|
|
386
|
-
]
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
/** Bin samples into grouped histogram bars for each benchmark */
|
|
390
|
-
function buildBarData(allSamples, benchmarkNames) {
|
|
391
|
-
const sorted = allSamples.map((d) => d.value).sort((a, b) => a - b);
|
|
392
|
-
const binMin = d3.quantile(sorted, .01);
|
|
393
|
-
const binMax = d3.quantile(sorted, .99);
|
|
394
|
-
const binCount = 25;
|
|
395
|
-
const step = (binMax - binMin) / binCount;
|
|
396
|
-
const thresholds = d3.range(1, binCount).map((i) => binMin + i * step);
|
|
397
|
-
const plotWidth = 550;
|
|
398
|
-
const bins = d3.bin().domain([binMin, binMax]).thresholds(thresholds).value((d) => d.value)(allSamples);
|
|
399
|
-
const barData = [];
|
|
400
|
-
const n = benchmarkNames.length;
|
|
401
|
-
const unitsPerPx = (binMax - binMin) / plotWidth;
|
|
402
|
-
const groupGapPx = 8;
|
|
403
|
-
for (const bin of bins) {
|
|
404
|
-
const counts = /* @__PURE__ */ new Map();
|
|
405
|
-
for (const d of bin) counts.set(d.benchmark, (counts.get(d.benchmark) || 0) + 1);
|
|
406
|
-
const full = bin.x1 - bin.x0;
|
|
407
|
-
const groupGap = Math.min(full * .5, unitsPerPx * groupGapPx);
|
|
408
|
-
const start = bin.x0 + groupGap / 2;
|
|
409
|
-
const w = (full - groupGap) / n;
|
|
410
|
-
benchmarkNames.forEach((benchmark, i) => {
|
|
411
|
-
const x1 = start + i * w;
|
|
412
|
-
const x2 = start + (i + 1) * w;
|
|
413
|
-
barData.push({
|
|
414
|
-
benchmark,
|
|
415
|
-
count: counts.get(benchmark) || 0,
|
|
416
|
-
x1,
|
|
417
|
-
x2
|
|
418
|
-
});
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
return {
|
|
422
|
-
barData,
|
|
423
|
-
binMin,
|
|
424
|
-
binMax,
|
|
425
|
-
yMax: (d3.max(barData, (d) => d.count) || 1) * 1.15
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
function buildColorData(benchmarkNames) {
|
|
429
|
-
const scheme = d3.schemeObservable10;
|
|
430
|
-
return {
|
|
431
|
-
colorMap: new Map(benchmarkNames.map((name, i) => [name, scheme[i % 10]])),
|
|
432
|
-
legendItems: benchmarkNames.map((name, i) => ({
|
|
433
|
-
color: scheme[i % 10],
|
|
434
|
-
label: name,
|
|
435
|
-
style: "vertical-bar"
|
|
436
|
-
}))
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
//#endregion
|
|
441
|
-
//#region src/html/browser/SampleTimeSeries.ts
|
|
442
|
-
const OPT_STATUS_NAMES = {
|
|
443
|
-
1: "interpreted",
|
|
444
|
-
129: "sparkplug",
|
|
445
|
-
17: "turbofan",
|
|
446
|
-
33: "maglev",
|
|
447
|
-
49: "turbofan+maglev",
|
|
448
|
-
32769: "optimized"
|
|
449
|
-
};
|
|
450
|
-
const OPT_TIER_COLORS = {
|
|
451
|
-
turbofan: "#22c55e",
|
|
452
|
-
optimized: "#22c55e",
|
|
453
|
-
"turbofan+maglev": "#22c55e",
|
|
454
|
-
maglev: "#eab308",
|
|
455
|
-
sparkplug: "#f97316",
|
|
456
|
-
interpreted: "#dc3545"
|
|
457
|
-
};
|
|
458
|
-
/** Create sample time series showing each sample in order */
|
|
459
|
-
function createSampleTimeSeries(timeSeries, gcEvents = [], pausePoints = [], heapSeries = []) {
|
|
460
|
-
const ctx = buildPlotContext(timeSeries);
|
|
461
|
-
const heapData = prepareHeapData(heapSeries, ctx.yMin, ctx.yMax);
|
|
462
|
-
return Plot.plot({
|
|
463
|
-
marginTop: 24,
|
|
464
|
-
marginLeft: 70,
|
|
465
|
-
marginBottom: 60,
|
|
466
|
-
marginRight: 110,
|
|
467
|
-
width: 550,
|
|
468
|
-
height: 300,
|
|
469
|
-
style: { fontSize: "14px" },
|
|
470
|
-
x: {
|
|
471
|
-
label: "Sample",
|
|
472
|
-
labelAnchor: "center",
|
|
473
|
-
labelOffset: 45,
|
|
474
|
-
grid: true,
|
|
475
|
-
domain: [ctx.xMin, ctx.xMax]
|
|
476
|
-
},
|
|
477
|
-
y: {
|
|
478
|
-
label: `Time (${ctx.unitSuffix})`,
|
|
479
|
-
labelAnchor: "top",
|
|
480
|
-
labelArrow: false,
|
|
481
|
-
grid: true,
|
|
482
|
-
domain: [ctx.yMin, ctx.yMax],
|
|
483
|
-
tickFormat: ctx.formatValue
|
|
484
|
-
},
|
|
485
|
-
color: {
|
|
486
|
-
legend: false,
|
|
487
|
-
scheme: "observable10"
|
|
488
|
-
},
|
|
489
|
-
marks: [
|
|
490
|
-
...heapMarks(heapData, ctx.yMin),
|
|
491
|
-
...ctx.hasWarmup ? [Plot.ruleX([0], {
|
|
492
|
-
stroke: "#999",
|
|
493
|
-
strokeWidth: 1,
|
|
494
|
-
strokeDasharray: "4,4"
|
|
495
|
-
})] : [],
|
|
496
|
-
gcMark(gcEvents, ctx.yMin, ctx.convertValue),
|
|
497
|
-
...pauseMarks(pausePoints, ctx.yMin, ctx.yMax),
|
|
498
|
-
...sampleDotMarks(ctx),
|
|
499
|
-
Plot.ruleY([ctx.yMin], {
|
|
500
|
-
stroke: "black",
|
|
501
|
-
strokeWidth: 1
|
|
502
|
-
}),
|
|
503
|
-
...buildLegend({
|
|
504
|
-
xMin: ctx.xMin,
|
|
505
|
-
xMax: ctx.xMax,
|
|
506
|
-
yMax: ctx.yMax
|
|
507
|
-
}, buildLegendItems(ctx.hasWarmup, gcEvents.length, pausePoints.length, heapData.length > 0, ctx.optTiers, ctx.benchmarks))
|
|
508
|
-
]
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
function buildPlotContext(timeSeries) {
|
|
512
|
-
const benchmarks = [...new Set(timeSeries.map((d) => d.benchmark))];
|
|
513
|
-
const sampleData = buildSampleData(timeSeries, benchmarks);
|
|
514
|
-
const { unitSuffix, convertValue, formatValue } = getTimeUnit(sampleData.map((d) => d.value));
|
|
515
|
-
const convertedData = sampleData.map((d) => ({
|
|
516
|
-
...d,
|
|
517
|
-
displayValue: convertValue(d.value)
|
|
518
|
-
}));
|
|
519
|
-
const { yMin, yMax } = computeYRange(convertedData.map((d) => d.displayValue));
|
|
520
|
-
return {
|
|
521
|
-
convertedData,
|
|
522
|
-
xMin: d3.min(convertedData, (d) => d.sample),
|
|
523
|
-
xMax: d3.max(convertedData, (d) => d.sample),
|
|
524
|
-
yMin,
|
|
525
|
-
yMax,
|
|
526
|
-
unitSuffix,
|
|
527
|
-
formatValue,
|
|
528
|
-
convertValue,
|
|
529
|
-
hasWarmup: convertedData.some((d) => d.isWarmup),
|
|
530
|
-
optTiers: [...new Set(convertedData.filter((d) => d.optTier && !d.isWarmup).map((d) => d.optTier))].filter((t) => t !== null),
|
|
531
|
-
benchmarks
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
/** Scale heap byte values into the plot's Y coordinate range */
|
|
535
|
-
function prepareHeapData(heapSeries, yMin, yMax) {
|
|
536
|
-
if (heapSeries.length === 0) return [];
|
|
537
|
-
const heapMin = d3.min(heapSeries, (d) => d.value);
|
|
538
|
-
const heapRange = d3.max(heapSeries, (d) => d.value) - heapMin || 1;
|
|
539
|
-
const scale = (yMax - yMin) * .25 / heapRange;
|
|
540
|
-
return heapSeries.map((d) => ({
|
|
541
|
-
sample: d.iteration,
|
|
542
|
-
y: yMin + (d.value - heapMin) * scale,
|
|
543
|
-
heapMB: d.value / 1024 / 1024
|
|
544
|
-
}));
|
|
545
|
-
}
|
|
546
|
-
function heapMarks(heapData, yMin) {
|
|
547
|
-
if (heapData.length === 0) return [];
|
|
548
|
-
return [Plot.areaY(heapData, {
|
|
549
|
-
x: "sample",
|
|
550
|
-
y: "y",
|
|
551
|
-
y1: yMin,
|
|
552
|
-
fill: "#9333ea",
|
|
553
|
-
fillOpacity: .15,
|
|
554
|
-
stroke: "#9333ea",
|
|
555
|
-
strokeWidth: 1,
|
|
556
|
-
strokeOpacity: .4
|
|
557
|
-
}), Plot.tip(heapData, Plot.pointerX({
|
|
558
|
-
x: "sample",
|
|
559
|
-
y: "y",
|
|
560
|
-
title: (d) => `Heap: ${d.heapMB.toFixed(1)} MB`
|
|
561
|
-
}))];
|
|
562
|
-
}
|
|
563
|
-
function gcMark(gcEvents, yMin, convertValue) {
|
|
564
|
-
const data = gcEvents.map((gc) => ({
|
|
565
|
-
x1: gc.sampleIndex,
|
|
566
|
-
y1: yMin,
|
|
567
|
-
x2: gc.sampleIndex,
|
|
568
|
-
y2: yMin + convertValue(gc.duration),
|
|
569
|
-
duration: gc.duration
|
|
570
|
-
}));
|
|
571
|
-
return Plot.link(data, {
|
|
572
|
-
x1: "x1",
|
|
573
|
-
y1: "y1",
|
|
574
|
-
x2: "x2",
|
|
575
|
-
y2: "y2",
|
|
576
|
-
stroke: "#22c55e",
|
|
577
|
-
strokeWidth: 2,
|
|
578
|
-
strokeOpacity: .8,
|
|
579
|
-
title: (d) => `GC: ${d.duration.toFixed(2)}ms`
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
function pauseMarks(pausePoints, yMin, yMax) {
|
|
583
|
-
return pausePoints.map((p) => Plot.ruleX([p.sampleIndex], {
|
|
584
|
-
y1: yMin,
|
|
585
|
-
y2: yMax,
|
|
586
|
-
stroke: "#888",
|
|
587
|
-
strokeWidth: 1,
|
|
588
|
-
strokeDasharray: "4,4",
|
|
589
|
-
strokeOpacity: .7,
|
|
590
|
-
title: `Pause: ${p.durationMs}ms`
|
|
591
|
-
}));
|
|
592
|
-
}
|
|
593
|
-
function sampleDotMarks(ctx) {
|
|
594
|
-
const { convertedData, unitSuffix, formatValue } = ctx;
|
|
595
|
-
const tipTitle = (d) => d.optTier ? `Sample ${d.sample}: ${formatValue(d.displayValue)}${unitSuffix} [${d.optTier}]` : `Sample ${d.sample}: ${formatValue(d.displayValue)}${unitSuffix}`;
|
|
596
|
-
return [
|
|
597
|
-
Plot.dot(convertedData.filter((d) => d.isWarmup), {
|
|
598
|
-
x: "sample",
|
|
599
|
-
y: "displayValue",
|
|
600
|
-
stroke: "#dc3545",
|
|
601
|
-
fill: "none",
|
|
602
|
-
strokeWidth: 1.5,
|
|
603
|
-
r: 3,
|
|
604
|
-
opacity: .7,
|
|
605
|
-
title: (d) => `Warmup ${d.sample}: ${formatValue(d.displayValue)}${unitSuffix}`
|
|
606
|
-
}),
|
|
607
|
-
Plot.dot(convertedData.filter((d) => d.isBaseline && !d.isWarmup), {
|
|
608
|
-
x: "sample",
|
|
609
|
-
y: "displayValue",
|
|
610
|
-
stroke: "#ffa500",
|
|
611
|
-
fill: "none",
|
|
612
|
-
strokeWidth: 2,
|
|
613
|
-
r: 3,
|
|
614
|
-
opacity: .8,
|
|
615
|
-
title: tipTitle
|
|
616
|
-
}),
|
|
617
|
-
Plot.dot(convertedData.filter((d) => !d.isBaseline && !d.isWarmup), {
|
|
618
|
-
x: "sample",
|
|
619
|
-
y: "displayValue",
|
|
620
|
-
fill: (d) => d.optTier ? OPT_TIER_COLORS[d.optTier] || "#4682b4" : "#4682b4",
|
|
621
|
-
r: 3,
|
|
622
|
-
opacity: .8,
|
|
623
|
-
title: tipTitle
|
|
624
|
-
})
|
|
625
|
-
];
|
|
626
|
-
}
|
|
627
|
-
function buildLegendItems(hasWarmup, gcCount, pauseCount, hasHeap, optTiers, benchmarks) {
|
|
628
|
-
const items = [];
|
|
629
|
-
if (hasWarmup) items.push({
|
|
630
|
-
color: "#dc3545",
|
|
631
|
-
label: "warmup",
|
|
632
|
-
style: "hollow-dot"
|
|
633
|
-
});
|
|
634
|
-
if (gcCount > 0) items.push({
|
|
635
|
-
color: "#22c55e",
|
|
636
|
-
label: `gc (${gcCount})`,
|
|
637
|
-
style: "vertical-line"
|
|
638
|
-
});
|
|
639
|
-
if (pauseCount > 0) items.push({
|
|
640
|
-
color: "#888",
|
|
641
|
-
label: `pause (${pauseCount})`,
|
|
642
|
-
style: "vertical-line",
|
|
643
|
-
strokeDash: "4,4"
|
|
644
|
-
});
|
|
645
|
-
if (hasHeap) items.push({
|
|
646
|
-
color: "#9333ea",
|
|
647
|
-
label: "heap",
|
|
648
|
-
style: "rect"
|
|
649
|
-
});
|
|
650
|
-
for (const tier of optTiers) items.push({
|
|
651
|
-
color: OPT_TIER_COLORS[tier] || "#4682b4",
|
|
652
|
-
label: tier,
|
|
653
|
-
style: "filled-dot"
|
|
654
|
-
});
|
|
655
|
-
if (optTiers.length === 0) {
|
|
656
|
-
const sorted = [...benchmarks].sort((a, b) => {
|
|
657
|
-
const aBase = a.includes("(baseline)");
|
|
658
|
-
return aBase === b.includes("(baseline)") ? 0 : aBase ? 1 : -1;
|
|
659
|
-
});
|
|
660
|
-
for (const bm of sorted) {
|
|
661
|
-
const isBase = bm.includes("(baseline)");
|
|
662
|
-
items.push({
|
|
663
|
-
color: isBase ? "#ffa500" : "#4682b4",
|
|
664
|
-
label: bm,
|
|
665
|
-
style: isBase ? "hollow-dot" : "filled-dot"
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
return items;
|
|
670
|
-
}
|
|
671
|
-
function buildSampleData(timeSeries, benchmarks) {
|
|
672
|
-
const result = [];
|
|
673
|
-
for (const benchmark of benchmarks) {
|
|
674
|
-
const isBaseline = benchmark.includes("(baseline)");
|
|
675
|
-
for (const d of timeSeries.filter((t) => t.benchmark === benchmark)) {
|
|
676
|
-
const optTier = d.optStatus !== void 0 ? OPT_STATUS_NAMES[d.optStatus] || "unknown" : null;
|
|
677
|
-
result.push({
|
|
678
|
-
benchmark,
|
|
679
|
-
sample: d.iteration,
|
|
680
|
-
value: d.value,
|
|
681
|
-
isBaseline,
|
|
682
|
-
isWarmup: d.isWarmup || false,
|
|
683
|
-
optTier
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
return result;
|
|
688
|
-
}
|
|
689
|
-
/** Pick display unit (ns/us/ms) based on average value magnitude */
|
|
690
|
-
function getTimeUnit(values) {
|
|
691
|
-
const avg = d3.mean(values);
|
|
692
|
-
const fmt0 = (d) => d3.format(",.0f")(d);
|
|
693
|
-
const fmt1 = (d) => d3.format(",.1f")(d);
|
|
694
|
-
if (avg < .001) return {
|
|
695
|
-
unitSuffix: "ns",
|
|
696
|
-
convertValue: (ms) => ms * 1e6,
|
|
697
|
-
formatValue: fmt0
|
|
698
|
-
};
|
|
699
|
-
if (avg < 1) return {
|
|
700
|
-
unitSuffix: "μs",
|
|
701
|
-
convertValue: (ms) => ms * 1e3,
|
|
702
|
-
formatValue: fmt1
|
|
703
|
-
};
|
|
704
|
-
return {
|
|
705
|
-
unitSuffix: "ms",
|
|
706
|
-
convertValue: (ms) => ms,
|
|
707
|
-
formatValue: fmt1
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
/** Compute Y axis range with padding, snapping yMin to a round number */
|
|
711
|
-
function computeYRange(values) {
|
|
712
|
-
const dataMin = d3.min(values);
|
|
713
|
-
const dataMax = d3.max(values);
|
|
714
|
-
const dataRange = dataMax - dataMin;
|
|
715
|
-
let yMin = dataMin - dataRange * .15;
|
|
716
|
-
const magnitude = 10 ** Math.floor(Math.log10(Math.abs(yMin)));
|
|
717
|
-
yMin = Math.floor(yMin / magnitude) * magnitude;
|
|
718
|
-
if (dataMin > 0 && yMin < 0) yMin = 0;
|
|
719
|
-
return {
|
|
720
|
-
yMin,
|
|
721
|
-
yMax: dataMax + dataRange * .05
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
//#endregion
|
|
726
|
-
//#region src/html/browser/RenderPlots.ts
|
|
727
|
-
/** Render all plots for the benchmark report */
|
|
728
|
-
function renderPlots(data) {
|
|
729
|
-
const gcEnabled = data.metadata.gcTrackingEnabled ?? false;
|
|
730
|
-
data.groups.forEach((group, groupIndex) => {
|
|
731
|
-
try {
|
|
732
|
-
renderGroup(group, groupIndex, gcEnabled);
|
|
733
|
-
} catch (error) {
|
|
734
|
-
console.error("Error rendering plots for group", groupIndex, error);
|
|
735
|
-
showError(groupIndex, `Error rendering visualizations: ${error.message}`);
|
|
736
|
-
}
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
function renderGroup(group, groupIndex, gcEnabled) {
|
|
740
|
-
const benchmarks = prepareBenchmarks(group);
|
|
741
|
-
if (benchmarks.length === 0 || !benchmarks[0].samples?.length) {
|
|
742
|
-
showError(groupIndex, "No sample data available for visualization");
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
const flattened = flattenSamples(benchmarks);
|
|
746
|
-
const benchmarkNames = benchmarks.map((b) => b.name);
|
|
747
|
-
const currentBenchmark = benchmarks.find((b) => !b.isBaseline);
|
|
748
|
-
if (currentBenchmark?.comparisonCI?.histogram) renderToContainer(`#ci-plot-${groupIndex}`, true, () => createCIPlot(currentBenchmark.comparisonCI));
|
|
749
|
-
renderToContainer(`#histogram-${groupIndex}`, flattened.allSamples.length > 0, () => createHistogramKde(flattened.allSamples, benchmarkNames));
|
|
750
|
-
const { timeSeries, allGcEvents, allPausePoints, heapSeries } = flattened;
|
|
751
|
-
renderToContainer(`#sample-timeseries-${groupIndex}`, timeSeries.length > 0, () => createSampleTimeSeries(timeSeries, allGcEvents, allPausePoints, heapSeries));
|
|
752
|
-
const statsContainer = document.querySelector(`#stats-${groupIndex}`);
|
|
753
|
-
if (statsContainer) statsContainer.innerHTML = benchmarks.map((b) => generateStatsHtml(b, gcEnabled)).join("");
|
|
754
|
-
}
|
|
755
|
-
function showError(groupIndex, message) {
|
|
756
|
-
const container = document.querySelector(`#group-${groupIndex}`);
|
|
757
|
-
if (container) container.innerHTML = `<div class="error">${message}</div>`;
|
|
758
|
-
}
|
|
759
|
-
/** Combine baseline and benchmarks into a single list with display names */
|
|
760
|
-
function prepareBenchmarks(group) {
|
|
761
|
-
const benchmarks = [];
|
|
762
|
-
if (group.baseline) {
|
|
763
|
-
const name = group.baseline.name + " (baseline)";
|
|
764
|
-
benchmarks.push({
|
|
765
|
-
...group.baseline,
|
|
766
|
-
name,
|
|
767
|
-
isBaseline: true
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
for (const b of group.benchmarks) benchmarks.push({
|
|
771
|
-
...b,
|
|
772
|
-
isBaseline: false
|
|
773
|
-
});
|
|
774
|
-
return benchmarks;
|
|
775
|
-
}
|
|
776
|
-
function flattenSamples(benchmarks) {
|
|
777
|
-
const result = {
|
|
778
|
-
allSamples: [],
|
|
779
|
-
timeSeries: [],
|
|
780
|
-
heapSeries: [],
|
|
781
|
-
allGcEvents: [],
|
|
782
|
-
allPausePoints: []
|
|
783
|
-
};
|
|
784
|
-
for (const b of benchmarks) if (b.samples?.length) flattenBenchmark(b, result);
|
|
785
|
-
return result;
|
|
786
|
-
}
|
|
787
|
-
/** Clear a container element and append a freshly created plot */
|
|
788
|
-
function renderToContainer(selector, condition, create) {
|
|
789
|
-
const container = document.querySelector(selector);
|
|
790
|
-
if (!container || !condition) return;
|
|
791
|
-
container.innerHTML = "";
|
|
792
|
-
container.appendChild(create());
|
|
793
|
-
}
|
|
794
|
-
function generateStatsHtml(b, gcEnabled) {
|
|
795
|
-
const ciHtml = generateCIHtml(b.comparisonCI);
|
|
796
|
-
if (b.sectionStats?.length) {
|
|
797
|
-
const statsHtml = (gcEnabled ? b.sectionStats : b.sectionStats.filter((s) => s.groupTitle !== "gc")).map((stat) => `
|
|
798
|
-
<div class="stat-item">
|
|
799
|
-
<div class="stat-label">${stat.groupTitle ? stat.groupTitle + " " : ""}${stat.label}</div>
|
|
800
|
-
<div class="stat-value">${stat.value}</div>
|
|
801
|
-
</div>
|
|
802
|
-
`).join("");
|
|
803
|
-
return `
|
|
804
|
-
<div class="summary-stats">
|
|
805
|
-
<h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
|
|
806
|
-
<div class="stats-grid">${ciHtml}${statsHtml}</div>
|
|
807
|
-
</div>
|
|
808
|
-
`;
|
|
809
|
-
}
|
|
810
|
-
return `
|
|
811
|
-
<div class="summary-stats">
|
|
812
|
-
<h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
|
|
813
|
-
<div class="stats-grid">
|
|
814
|
-
${ciHtml}
|
|
815
|
-
<div class="stat-item">
|
|
816
|
-
<div class="stat-label">Min</div>
|
|
817
|
-
<div class="stat-value">${b.stats.min.toFixed(3)}ms</div>
|
|
818
|
-
</div>
|
|
819
|
-
<div class="stat-item">
|
|
820
|
-
<div class="stat-label">Median</div>
|
|
821
|
-
<div class="stat-value">${b.stats.p50.toFixed(3)}ms</div>
|
|
822
|
-
</div>
|
|
823
|
-
<div class="stat-item">
|
|
824
|
-
<div class="stat-label">Mean</div>
|
|
825
|
-
<div class="stat-value">${b.stats.avg.toFixed(3)}ms</div>
|
|
826
|
-
</div>
|
|
827
|
-
<div class="stat-item">
|
|
828
|
-
<div class="stat-label">Max</div>
|
|
829
|
-
<div class="stat-value">${b.stats.max.toFixed(3)}ms</div>
|
|
830
|
-
</div>
|
|
831
|
-
<div class="stat-item">
|
|
832
|
-
<div class="stat-label">P75</div>
|
|
833
|
-
<div class="stat-value">${b.stats.p75.toFixed(3)}ms</div>
|
|
834
|
-
</div>
|
|
835
|
-
<div class="stat-item">
|
|
836
|
-
<div class="stat-label">P99</div>
|
|
837
|
-
<div class="stat-value">${b.stats.p99.toFixed(3)}ms</div>
|
|
838
|
-
</div>
|
|
839
|
-
</div>
|
|
840
|
-
</div>
|
|
841
|
-
`;
|
|
842
|
-
}
|
|
843
|
-
/** Extract time series, heap, GC, and pause data from one benchmark */
|
|
844
|
-
function flattenBenchmark(b, out) {
|
|
845
|
-
const warmupCount = b.warmupSamples?.length || 0;
|
|
846
|
-
b.warmupSamples?.forEach((value, i) => {
|
|
847
|
-
out.timeSeries.push({
|
|
848
|
-
benchmark: b.name,
|
|
849
|
-
iteration: i - warmupCount,
|
|
850
|
-
value,
|
|
851
|
-
isWarmup: true
|
|
852
|
-
});
|
|
853
|
-
});
|
|
854
|
-
const sampleEndTimes = cumulativeSum(b.samples);
|
|
855
|
-
b.samples.forEach((value, i) => {
|
|
856
|
-
out.allSamples.push({
|
|
857
|
-
benchmark: b.name,
|
|
858
|
-
value,
|
|
859
|
-
iteration: i
|
|
860
|
-
});
|
|
861
|
-
out.timeSeries.push({
|
|
862
|
-
benchmark: b.name,
|
|
863
|
-
iteration: i,
|
|
864
|
-
value,
|
|
865
|
-
isWarmup: false,
|
|
866
|
-
optStatus: b.optSamples?.[i]
|
|
867
|
-
});
|
|
868
|
-
if (b.heapSamples?.[i] !== void 0) out.heapSeries.push({
|
|
869
|
-
benchmark: b.name,
|
|
870
|
-
iteration: i,
|
|
871
|
-
value: b.heapSamples[i]
|
|
872
|
-
});
|
|
873
|
-
});
|
|
874
|
-
b.gcEvents?.forEach((gc) => {
|
|
875
|
-
const idx = sampleEndTimes.findIndex((t) => t >= gc.offset);
|
|
876
|
-
out.allGcEvents.push({
|
|
877
|
-
benchmark: b.name,
|
|
878
|
-
sampleIndex: idx >= 0 ? idx : b.samples.length - 1,
|
|
879
|
-
duration: gc.duration
|
|
880
|
-
});
|
|
881
|
-
});
|
|
882
|
-
b.pausePoints?.forEach((p) => {
|
|
883
|
-
out.allPausePoints.push({
|
|
884
|
-
benchmark: b.name,
|
|
885
|
-
sampleIndex: p.sampleIndex,
|
|
886
|
-
durationMs: p.durationMs
|
|
887
|
-
});
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
function generateCIHtml(ci) {
|
|
891
|
-
if (!ci) return "";
|
|
892
|
-
const text = `${formatPct(ci.percent)} [${formatPct(ci.ci[0])}, ${formatPct(ci.ci[1])}]`;
|
|
893
|
-
return `
|
|
894
|
-
<div class="stat-item">
|
|
895
|
-
<div class="stat-label">vs Baseline</div>
|
|
896
|
-
<div class="stat-value ci-${ci.direction}">${text}</div>
|
|
897
|
-
</div>
|
|
898
|
-
`;
|
|
899
|
-
}
|
|
900
|
-
function cumulativeSum(arr) {
|
|
901
|
-
const result = [];
|
|
902
|
-
let sum = 0;
|
|
903
|
-
for (const v of arr) {
|
|
904
|
-
sum += v;
|
|
905
|
-
result.push(sum);
|
|
906
|
-
}
|
|
907
|
-
return result;
|
|
908
|
-
}
|
|
909
|
-
function formatPct(v) {
|
|
910
|
-
return (v >= 0 ? "+" : "") + v.toFixed(1) + "%";
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
//#endregion
|
|
914
|
-
export { renderPlots };
|