benchforge 0.1.9 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +99 -260
- 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-DglX1NOn.d.mts +302 -0
- 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 +731 -522
- 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 +92 -120
- 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 -26
- 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 -48
- 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 +138 -844
- 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 +91 -126
- package/src/export/SpeedscopeTypes.ts +98 -0
- package/src/export/TimeExport.ts +115 -0
- package/src/index.ts +87 -62
- 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 +55 -53
- package/src/matrix/MatrixInlineRunner.ts +50 -0
- package/src/matrix/MatrixReport.ts +94 -254
- package/src/matrix/VariantLoader.ts +9 -9
- 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 +55 -13
- package/src/profiling/node/ResolvedProfile.ts +98 -0
- 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 +167 -287
- package/src/runners/BenchRunner.ts +27 -22
- package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
- package/src/runners/CreateRunner.ts +5 -7
- package/src/runners/GcStats.ts +58 -61
- 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 +180 -296
- 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 +162 -178
- package/src/stats/BootstrapDifference.ts +282 -0
- package/src/{PermutationTest.ts → stats/PermutationTest.ts} +31 -40
- 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 +9 -41
- package/src/{tests → test}/BenchMatrix.test.ts +31 -28
- 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 +51 -0
- package/src/{tests → test}/MatrixFilter.test.ts +16 -16
- 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 +57 -56
- 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 +35 -30
- 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 +42 -47
- 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/BenchRunner-CSKN9zPy.d.mts +0 -225
- package/dist/BrowserHeapSampler-DCeL42RE.mjs +0 -202
- package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
- package/dist/GcStats-ByEovUi1.mjs +0 -77
- package/dist/GcStats-ByEovUi1.mjs.map +0 -1
- package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
- package/dist/TimingUtils-ClclVQ7E.mjs +0 -597
- package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
- package/dist/browser/index.js +0 -914
- package/dist/src-Cf_LXwlp.mjs +0 -2873
- package/dist/src-Cf_LXwlp.mjs.map +0 -1
- package/src/BenchMatrix.ts +0 -380
- package/src/BenchmarkReport.ts +0 -156
- package/src/HtmlDataPrep.ts +0 -148
- package/src/StandardSections.ts +0 -261
- package/src/StatisticalUtils.ts +0 -176
- 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/heap-sample/HeapSampleReport.ts +0 -196
- 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 -152
- 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 +9 -9
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CILevel,
|
|
3
|
+
DifferenceCI,
|
|
4
|
+
HistogramBin,
|
|
5
|
+
} from "../../stats/StatisticalUtils.ts";
|
|
6
|
+
import { formatPct } from "./PlotTypes.ts";
|
|
7
|
+
import {
|
|
8
|
+
createSvg,
|
|
9
|
+
ensureHatchPattern,
|
|
10
|
+
ensureSketchFilter,
|
|
11
|
+
line,
|
|
12
|
+
path,
|
|
13
|
+
rect,
|
|
14
|
+
text,
|
|
15
|
+
} from "./SvgHelpers.ts";
|
|
16
|
+
|
|
17
|
+
export interface DistributionPlotOptions {
|
|
18
|
+
width?: number;
|
|
19
|
+
height?: number;
|
|
20
|
+
title?: string;
|
|
21
|
+
smooth?: boolean;
|
|
22
|
+
direction?: "faster" | "slower" | "uncertain" | "equivalent";
|
|
23
|
+
/** Pre-formatted CI bound labels (overrides default formatPct) */
|
|
24
|
+
ciLabels?: [string, string];
|
|
25
|
+
/** Include zero in x scale (default true, set false for absolute-value plots) */
|
|
26
|
+
includeZero?: boolean;
|
|
27
|
+
/** Centered label above chart (e.g., the formatted point estimate) */
|
|
28
|
+
pointLabel?: string;
|
|
29
|
+
/** Equivalence margin in percent (draws shaded band at +/- margin) */
|
|
30
|
+
equivMargin?: number;
|
|
31
|
+
/** Block-level or sample-level CI */
|
|
32
|
+
ciLevel?: CILevel;
|
|
33
|
+
/** false ==> dashed border (insufficient batches for reliable CI) */
|
|
34
|
+
ciReliable?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Scales = { x: (v: number) => number; y: (v: number) => number };
|
|
38
|
+
type Layout = {
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
margin: { top: number; right: number; bottom: number; left: number };
|
|
42
|
+
plot: { w: number; h: number };
|
|
43
|
+
};
|
|
44
|
+
const defaultMargin = { top: 22, right: 12, bottom: 22, left: 12 };
|
|
45
|
+
|
|
46
|
+
const defaultOpts = {
|
|
47
|
+
width: 260,
|
|
48
|
+
height: 85,
|
|
49
|
+
title: "Δ%",
|
|
50
|
+
smooth: true,
|
|
51
|
+
direction: "uncertain" as const,
|
|
52
|
+
includeZero: true,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const colors = {
|
|
56
|
+
faster: { fill: "#bbf7d0", stroke: "#22c55e" },
|
|
57
|
+
slower: { fill: "#fee2e2", stroke: "#ef4444" },
|
|
58
|
+
uncertain: { fill: "#dbeafe", stroke: "#3b82f6" },
|
|
59
|
+
equivalent: { fill: "#dcfce7", stroke: "#86efac" },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Create a small distribution plot showing histogram with CI shading */
|
|
63
|
+
export function createDistributionPlot(
|
|
64
|
+
histogram: HistogramBin[],
|
|
65
|
+
ci: [number, number],
|
|
66
|
+
pointEstimate: number,
|
|
67
|
+
options: DistributionPlotOptions = {},
|
|
68
|
+
): SVGSVGElement {
|
|
69
|
+
const opts = { ...defaultOpts, ...options };
|
|
70
|
+
const layout = buildLayout(opts.width, opts.height, !!opts.pointLabel);
|
|
71
|
+
const svg = createSvg(layout.width, layout.height);
|
|
72
|
+
if (!histogram?.length) return svg;
|
|
73
|
+
|
|
74
|
+
const { fill, stroke } = colors[opts.direction];
|
|
75
|
+
const { includeZero, equivMargin } = opts;
|
|
76
|
+
const scales = buildScales(
|
|
77
|
+
histogram,
|
|
78
|
+
ci,
|
|
79
|
+
layout,
|
|
80
|
+
includeZero,
|
|
81
|
+
equivMargin,
|
|
82
|
+
pointEstimate,
|
|
83
|
+
);
|
|
84
|
+
const { margin, plot } = layout;
|
|
85
|
+
const ptX = scales.x(pointEstimate);
|
|
86
|
+
|
|
87
|
+
drawTitles(svg, opts, margin, ptX);
|
|
88
|
+
|
|
89
|
+
if (equivMargin && includeZero)
|
|
90
|
+
drawMarginZone(svg, equivMargin, scales, layout);
|
|
91
|
+
|
|
92
|
+
const ciX = scales.x(ci[0]);
|
|
93
|
+
const ciRect = rect(ciX, margin.top, scales.x(ci[1]) - ciX, plot.h, { fill });
|
|
94
|
+
const strength = includeZero ? "ci-region-strong" : "ci-region";
|
|
95
|
+
ciRect.classList.add(strength, `ci-${opts.direction}`);
|
|
96
|
+
if (opts.ciReliable === false) {
|
|
97
|
+
ciRect.classList.add("ci-unreliable");
|
|
98
|
+
ciRect.setAttribute("filter", `url(#${ensureSketchFilter(svg)})`);
|
|
99
|
+
}
|
|
100
|
+
svg.appendChild(ciRect);
|
|
101
|
+
|
|
102
|
+
if (opts.smooth) drawSmoothedDist(svg, histogram, scales, stroke);
|
|
103
|
+
else drawHistogramBars(svg, histogram, scales, layout, stroke);
|
|
104
|
+
|
|
105
|
+
drawReferenceLine(svg, scales, layout, includeZero);
|
|
106
|
+
svg.appendChild(
|
|
107
|
+
line(ptX, margin.top, ptX, margin.top + plot.h, {
|
|
108
|
+
stroke,
|
|
109
|
+
strokeWidth: "2",
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
drawCILabels(svg, ci, scales, layout, opts);
|
|
114
|
+
return svg;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Convenience wrapper for DifferenceCI data */
|
|
118
|
+
export function createCIPlot(
|
|
119
|
+
ci: DifferenceCI,
|
|
120
|
+
options: Partial<DistributionPlotOptions> = {},
|
|
121
|
+
): SVGSVGElement {
|
|
122
|
+
if (!ci.histogram) return createSvg(0, 0);
|
|
123
|
+
return createDistributionPlot(ci.histogram, ci.ci, ci.percent, {
|
|
124
|
+
title: ci.label,
|
|
125
|
+
direction: ci.direction,
|
|
126
|
+
ciLevel: ci.ciLevel,
|
|
127
|
+
ciReliable: ci.ciReliable,
|
|
128
|
+
...options,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Use minimal margins when the chart is too small for standard spacing. */
|
|
133
|
+
function buildLayout(
|
|
134
|
+
width: number,
|
|
135
|
+
height: number,
|
|
136
|
+
hasPointLabel?: boolean,
|
|
137
|
+
): Layout {
|
|
138
|
+
const compact = height < defaultMargin.top + defaultMargin.bottom + 10;
|
|
139
|
+
const margin = compact
|
|
140
|
+
? { top: 4, right: 6, bottom: 4, left: 6 }
|
|
141
|
+
: { ...defaultMargin, top: hasPointLabel ? 30 : defaultMargin.top };
|
|
142
|
+
const plot = {
|
|
143
|
+
w: width - margin.left - margin.right,
|
|
144
|
+
h: height - margin.top - margin.bottom,
|
|
145
|
+
};
|
|
146
|
+
return { width, height, margin, plot };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildScales(
|
|
150
|
+
histogram: HistogramBin[],
|
|
151
|
+
ci: [number, number],
|
|
152
|
+
layout: Layout,
|
|
153
|
+
includeZero: boolean,
|
|
154
|
+
equivMargin?: number,
|
|
155
|
+
pointEstimate?: number,
|
|
156
|
+
): Scales {
|
|
157
|
+
const { margin, plot } = layout;
|
|
158
|
+
const xs = histogram.map(b => b.x);
|
|
159
|
+
const extra = includeZero ? [0] : [];
|
|
160
|
+
const marginBounds = equivMargin ? [-equivMargin, equivMargin] : [];
|
|
161
|
+
const ptBounds = pointEstimate != null ? [pointEstimate] : [];
|
|
162
|
+
const xMin = Math.min(...xs, ci[0], ...extra, ...marginBounds, ...ptBounds);
|
|
163
|
+
const xMax = Math.max(...xs, ci[1], ...extra, ...marginBounds, ...ptBounds);
|
|
164
|
+
const yMax = Math.max(...histogram.map(b => b.count));
|
|
165
|
+
const xRange = xMax - xMin || 1;
|
|
166
|
+
return {
|
|
167
|
+
x: (v: number) => margin.left + ((v - xMin) / xRange) * plot.w,
|
|
168
|
+
y: (v: number) => margin.top + plot.h - (v / yMax) * plot.h,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function drawTitles(
|
|
173
|
+
svg: SVGSVGElement,
|
|
174
|
+
opts: DistributionPlotOptions,
|
|
175
|
+
margin: Layout["margin"],
|
|
176
|
+
pointX: number,
|
|
177
|
+
): void {
|
|
178
|
+
if (opts.title)
|
|
179
|
+
svg.appendChild(
|
|
180
|
+
text(margin.left, 14, opts.title, "start", "13", "currentColor", "600"),
|
|
181
|
+
);
|
|
182
|
+
if (opts.pointLabel) {
|
|
183
|
+
const el = text(
|
|
184
|
+
pointX,
|
|
185
|
+
margin.top - 6,
|
|
186
|
+
opts.pointLabel,
|
|
187
|
+
"middle",
|
|
188
|
+
"15",
|
|
189
|
+
"currentColor",
|
|
190
|
+
"700",
|
|
191
|
+
);
|
|
192
|
+
svg.appendChild(el);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Draw equivalence margin zone: hatched band centered vertically */
|
|
197
|
+
function drawMarginZone(
|
|
198
|
+
svg: SVGSVGElement,
|
|
199
|
+
equivMargin: number,
|
|
200
|
+
scales: Scales,
|
|
201
|
+
layout: Layout,
|
|
202
|
+
): void {
|
|
203
|
+
const { margin, plot } = layout;
|
|
204
|
+
const x1 = scales.x(-equivMargin);
|
|
205
|
+
const x2 = scales.x(equivMargin);
|
|
206
|
+
const fill = `url(#${ensureHatchPattern(svg)})`;
|
|
207
|
+
const bandH = plot.h / 3;
|
|
208
|
+
const bandY = margin.top + (plot.h - bandH) / 2;
|
|
209
|
+
const zone = rect(x1, bandY, x2 - x1, bandH, { fill, strokeWidth: "1.5" });
|
|
210
|
+
zone.classList.add("margin-zone");
|
|
211
|
+
svg.appendChild(zone);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Draw a filled area + stroke path using gaussian-smoothed histogram data */
|
|
215
|
+
function drawSmoothedDist(
|
|
216
|
+
svg: SVGSVGElement,
|
|
217
|
+
histogram: HistogramBin[],
|
|
218
|
+
scales: Scales,
|
|
219
|
+
stroke: string,
|
|
220
|
+
): void {
|
|
221
|
+
const sorted = [...histogram].sort((a, b) => a.x - b.x);
|
|
222
|
+
const smoothed = gaussianSmooth(sorted, 2);
|
|
223
|
+
const pts = smoothed.map(b => `${scales.x(b.x)},${scales.y(b.count)}`);
|
|
224
|
+
const base = scales.y(0);
|
|
225
|
+
const startX = scales.x(smoothed[0].x);
|
|
226
|
+
const endX = scales.x(smoothed.at(-1)!.x);
|
|
227
|
+
const fillD = `M${startX},${base}L${pts.join("L")}L${endX},${base}Z`;
|
|
228
|
+
const fillPath = path(fillD, { fill: stroke });
|
|
229
|
+
fillPath.classList.add("dist-fill");
|
|
230
|
+
svg.appendChild(fillPath);
|
|
231
|
+
const strokePath = path(`M${pts.join("L")}`, {
|
|
232
|
+
stroke,
|
|
233
|
+
fill: "none",
|
|
234
|
+
strokeWidth: "1.5",
|
|
235
|
+
});
|
|
236
|
+
strokePath.classList.add("dist-stroke");
|
|
237
|
+
svg.appendChild(strokePath);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function drawHistogramBars(
|
|
241
|
+
svg: SVGSVGElement,
|
|
242
|
+
histogram: HistogramBin[],
|
|
243
|
+
scales: Scales,
|
|
244
|
+
layout: Layout,
|
|
245
|
+
stroke: string,
|
|
246
|
+
): void {
|
|
247
|
+
const sorted = [...histogram].sort((a, b) => a.x - b.x);
|
|
248
|
+
const binW = sorted.length > 1 ? sorted[1].x - sorted[0].x : 1;
|
|
249
|
+
const xRange = scales.x(sorted.at(-1)!.x) - scales.x(sorted[0].x) + binW;
|
|
250
|
+
const barW = (binW / xRange) * layout.plot.w * 0.9;
|
|
251
|
+
const base = scales.y(0);
|
|
252
|
+
const attrs = { fill: stroke, opacity: "0.6" };
|
|
253
|
+
for (const bin of sorted) {
|
|
254
|
+
const top = scales.y(bin.count);
|
|
255
|
+
svg.appendChild(
|
|
256
|
+
rect(scales.x(bin.x) - barW / 2, top, barW, base - top, attrs),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Draw zero reference line extending past plot area (comparison CIs only) */
|
|
262
|
+
function drawReferenceLine(
|
|
263
|
+
svg: SVGSVGElement,
|
|
264
|
+
scales: Scales,
|
|
265
|
+
layout: Layout,
|
|
266
|
+
includeZero: boolean,
|
|
267
|
+
): void {
|
|
268
|
+
const { margin, plot } = layout;
|
|
269
|
+
const zeroX = scales.x(0);
|
|
270
|
+
const inBounds = zeroX >= margin.left && zeroX <= layout.width - margin.right;
|
|
271
|
+
if (!includeZero || !inBounds) return;
|
|
272
|
+
|
|
273
|
+
svg.appendChild(
|
|
274
|
+
line(zeroX, margin.top - 4, zeroX, margin.top + plot.h + 4, {
|
|
275
|
+
stroke: "#000",
|
|
276
|
+
strokeWidth: "1",
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function drawCILabels(
|
|
282
|
+
svg: SVGSVGElement,
|
|
283
|
+
ci: [number, number],
|
|
284
|
+
scales: Scales,
|
|
285
|
+
layout: Layout,
|
|
286
|
+
opts: DistributionPlotOptions & { includeZero: boolean },
|
|
287
|
+
): void {
|
|
288
|
+
if (layout.margin.bottom < 15) return;
|
|
289
|
+
const labelY = layout.height - 4;
|
|
290
|
+
const loLabel = opts.ciLabels?.[0] ?? formatPct(ci[0], 0);
|
|
291
|
+
const hiLabel = opts.ciLabels?.[1] ?? formatPct(ci[1], 0);
|
|
292
|
+
const loX = scales.x(ci[0]);
|
|
293
|
+
const hiX = scales.x(ci[1]);
|
|
294
|
+
const minGap = Math.max(loLabel.length, hiLabel.length) * 6;
|
|
295
|
+
if (!opts.includeZero || hiX - loX >= minGap) {
|
|
296
|
+
svg.appendChild(text(loX, labelY, loLabel, "middle", "11"));
|
|
297
|
+
svg.appendChild(text(hiX, labelY, hiLabel, "middle", "11"));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Apply gaussian kernel smoothing to histogram bins */
|
|
302
|
+
function gaussianSmooth(bins: HistogramBin[], sigma: number): HistogramBin[] {
|
|
303
|
+
return bins.map((bin, i) => {
|
|
304
|
+
let sum = 0;
|
|
305
|
+
let wt = 0;
|
|
306
|
+
for (let j = 0; j < bins.length; j++) {
|
|
307
|
+
const w = Math.exp(-((i - j) ** 2) / (2 * sigma ** 2));
|
|
308
|
+
sum += bins[j].count * w;
|
|
309
|
+
wt += w;
|
|
310
|
+
}
|
|
311
|
+
return { x: bin.x, count: sum / wt };
|
|
312
|
+
});
|
|
313
|
+
}
|
|
@@ -1,34 +1,40 @@
|
|
|
1
1
|
import * as Plot from "@observablehq/plot";
|
|
2
2
|
import * as d3 from "d3";
|
|
3
3
|
import { buildLegend, type LegendItem } from "./LegendUtils.ts";
|
|
4
|
-
import type
|
|
4
|
+
import { getTimeUnit, plotLayout, type Sample } from "./PlotTypes.ts";
|
|
5
|
+
|
|
6
|
+
interface Bar {
|
|
7
|
+
benchmark: string;
|
|
8
|
+
count: number;
|
|
9
|
+
x1: number;
|
|
10
|
+
x2: number;
|
|
11
|
+
}
|
|
5
12
|
|
|
6
13
|
/** Create histogram + KDE plot for sample distribution */
|
|
7
14
|
export function createHistogramKde(
|
|
8
15
|
allSamples: Sample[],
|
|
9
16
|
benchmarkNames: string[],
|
|
10
17
|
): SVGSVGElement | HTMLElement {
|
|
18
|
+
const values = allSamples.map(d => d.value);
|
|
19
|
+
const { unitSuffix, convertValue, formatValue } = getTimeUnit(values);
|
|
20
|
+
const converted = allSamples.map(d => ({
|
|
21
|
+
...d,
|
|
22
|
+
value: convertValue(d.value),
|
|
23
|
+
}));
|
|
11
24
|
const { barData, binMin, binMax, yMax } = buildBarData(
|
|
12
|
-
|
|
25
|
+
converted,
|
|
13
26
|
benchmarkNames,
|
|
14
27
|
);
|
|
15
28
|
const { colorMap, legendItems } = buildColorData(benchmarkNames);
|
|
16
|
-
const xMax = binMax + (binMax - binMin) * 0.45; // extend for legend
|
|
17
29
|
|
|
18
30
|
return Plot.plot({
|
|
19
|
-
|
|
20
|
-
marginLeft: 70,
|
|
21
|
-
marginRight: 10,
|
|
22
|
-
marginBottom: 60,
|
|
23
|
-
width: 550,
|
|
24
|
-
height: 300,
|
|
25
|
-
style: { fontSize: "14px" },
|
|
31
|
+
...plotLayout,
|
|
26
32
|
x: {
|
|
27
|
-
label:
|
|
33
|
+
label: `Time (${unitSuffix})`,
|
|
28
34
|
labelAnchor: "center",
|
|
29
|
-
domain: [binMin,
|
|
35
|
+
domain: [binMin, binMax],
|
|
30
36
|
labelOffset: 45,
|
|
31
|
-
tickFormat:
|
|
37
|
+
tickFormat: formatValue,
|
|
32
38
|
ticks: 5,
|
|
33
39
|
},
|
|
34
40
|
y: {
|
|
@@ -43,58 +49,34 @@ export function createHistogramKde(
|
|
|
43
49
|
x1: "x1",
|
|
44
50
|
x2: "x2",
|
|
45
51
|
y: "count",
|
|
46
|
-
fill: (d:
|
|
52
|
+
fill: (d: Bar) => colorMap.get(d.benchmark),
|
|
47
53
|
fillOpacity: 0.6,
|
|
48
|
-
tip: true,
|
|
49
|
-
title: (d: (typeof barData)[0]) => `${d.benchmark}: ${d.count}`,
|
|
50
54
|
}),
|
|
51
55
|
Plot.ruleY([0]),
|
|
52
|
-
...buildLegend({ xMin: binMin, xMax, yMax }, legendItems),
|
|
56
|
+
...buildLegend({ xMin: binMin, xMax: binMax, yMax }, legendItems),
|
|
53
57
|
],
|
|
54
58
|
});
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
function buildColorData(benchmarkNames: string[]) {
|
|
58
|
-
const scheme = (d3 as any).schemeObservable10;
|
|
59
|
-
const colorMap = new Map(
|
|
60
|
-
benchmarkNames.map((name, i) => [name, scheme[i % 10]]),
|
|
61
|
-
);
|
|
62
|
-
const legendItems: LegendItem[] = benchmarkNames.map((name, i) => ({
|
|
63
|
-
color: scheme[i % 10],
|
|
64
|
-
label: name,
|
|
65
|
-
style: "vertical-bar",
|
|
66
|
-
}));
|
|
67
|
-
return { colorMap, legendItems };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
61
|
/** Bin samples into grouped histogram bars for each benchmark */
|
|
71
62
|
function buildBarData(allSamples: Sample[], benchmarkNames: string[]) {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const binMax = d3.quantile(sorted, 0.99)!;
|
|
63
|
+
const sortedValues = allSamples.map(d => d.value).sort((a, b) => a - b);
|
|
64
|
+
const binMin = d3.quantile(sortedValues, 0.01)!;
|
|
65
|
+
const binMax = d3.quantile(sortedValues, 0.99)!;
|
|
76
66
|
const binCount = 25;
|
|
77
67
|
const step = (binMax - binMin) / binCount;
|
|
78
68
|
const thresholds = d3.range(1, binCount).map(i => binMin + i * step);
|
|
79
|
-
const plotWidth = 550;
|
|
80
|
-
|
|
81
69
|
const bins = d3
|
|
82
70
|
.bin<Sample, number>()
|
|
83
71
|
.domain([binMin, binMax])
|
|
84
72
|
.thresholds(thresholds)
|
|
85
73
|
.value(d => d.value)(allSamples);
|
|
86
74
|
|
|
87
|
-
const barData: {
|
|
88
|
-
benchmark: string;
|
|
89
|
-
count: number;
|
|
90
|
-
x1: number;
|
|
91
|
-
x2: number;
|
|
92
|
-
}[] = [];
|
|
93
75
|
const n = benchmarkNames.length;
|
|
94
|
-
const unitsPerPx = (binMax - binMin) /
|
|
76
|
+
const unitsPerPx = (binMax - binMin) / plotLayout.width;
|
|
95
77
|
const groupGapPx = 8;
|
|
96
78
|
|
|
97
|
-
|
|
79
|
+
const barData: Bar[] = bins.flatMap(bin => {
|
|
98
80
|
const counts = new Map<string, number>();
|
|
99
81
|
for (const d of bin)
|
|
100
82
|
counts.set(d.benchmark, (counts.get(d.benchmark) || 0) + 1);
|
|
@@ -104,15 +86,28 @@ function buildBarData(allSamples: Sample[], benchmarkNames: string[]) {
|
|
|
104
86
|
const start = bin.x0! + groupGap / 2;
|
|
105
87
|
const w = (full - groupGap) / n;
|
|
106
88
|
|
|
107
|
-
benchmarkNames.
|
|
89
|
+
return benchmarkNames.map((benchmark, i) => {
|
|
108
90
|
const x1 = start + i * w;
|
|
109
91
|
const x2 = start + (i + 1) * w;
|
|
110
|
-
|
|
92
|
+
return { benchmark, count: counts.get(benchmark) || 0, x1, x2 };
|
|
111
93
|
});
|
|
112
|
-
}
|
|
94
|
+
});
|
|
113
95
|
|
|
114
96
|
const maxCount = d3.max(barData, d => d.count)! || 1;
|
|
115
97
|
const yMax = maxCount * 1.15;
|
|
116
98
|
|
|
117
99
|
return { barData, binMin, binMax, yMax };
|
|
118
100
|
}
|
|
101
|
+
|
|
102
|
+
/** Map benchmark names to colors and legend items using Observable 10 palette */
|
|
103
|
+
function buildColorData(benchmarkNames: string[]) {
|
|
104
|
+
const scheme = (d3 as any).schemeObservable10;
|
|
105
|
+
const color = (i: number) => scheme[i % 10];
|
|
106
|
+
const colorMap = new Map(benchmarkNames.map((name, i) => [name, color(i)]));
|
|
107
|
+
const legendItems: LegendItem[] = benchmarkNames.map((name, i) => ({
|
|
108
|
+
color: color(i),
|
|
109
|
+
label: name,
|
|
110
|
+
style: "vertical-bar",
|
|
111
|
+
}));
|
|
112
|
+
return { colorMap, legendItems };
|
|
113
|
+
}
|
|
@@ -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
|
+
}
|