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,46 @@
|
|
|
1
|
+
import type { ViewerConfig } from "../Providers.ts";
|
|
2
|
+
import { activeTabId, provider, sourceTabs } from "../State.ts";
|
|
3
|
+
import { SamplesPanel } from "./SamplesPanel.tsx";
|
|
4
|
+
import { SourcePanel } from "./SourcePanel.tsx";
|
|
5
|
+
import { SummaryPanel } from "./SummaryPanel.tsx";
|
|
6
|
+
|
|
7
|
+
/** Renders all tab panels, showing only the active one via CSS class toggling. */
|
|
8
|
+
export function TabContent() {
|
|
9
|
+
const dataProvider = provider.value!;
|
|
10
|
+
const { config } = dataProvider;
|
|
11
|
+
const tabId = activeTabId.value;
|
|
12
|
+
const panelClass = (id: string) => `report-panel${tabId === id ? " active" : ""}`;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div class="tab-content">
|
|
16
|
+
<div id="summary-panel" class={panelClass("summary")}>
|
|
17
|
+
{config.hasReport && <SummaryPanel />}
|
|
18
|
+
</div>
|
|
19
|
+
<div id="samples-panel" class={panelClass("samples")}>
|
|
20
|
+
<SamplesPanel />
|
|
21
|
+
</div>
|
|
22
|
+
<iframe
|
|
23
|
+
id="speedscope-iframe"
|
|
24
|
+
src={iframeSrc(dataProvider.profileUrl("alloc"), config)}
|
|
25
|
+
style={{ display: tabId === "flamechart" ? "block" : "none" }}
|
|
26
|
+
/>
|
|
27
|
+
<iframe
|
|
28
|
+
id="time-speedscope-iframe"
|
|
29
|
+
src={iframeSrc(dataProvider.profileUrl("time"), config)}
|
|
30
|
+
style={{ display: tabId === "time-flamechart" ? "block" : "none" }}
|
|
31
|
+
/>
|
|
32
|
+
{sourceTabs.value.map(st => (
|
|
33
|
+
<SourcePanel key={st.id} tab={st} />
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Build a Speedscope iframe hash-URL with optional editor URI. */
|
|
40
|
+
function iframeSrc(url: string | null, config: ViewerConfig): string {
|
|
41
|
+
if (!url) return "";
|
|
42
|
+
const parts = ["profileURL=" + encodeURIComponent(url)];
|
|
43
|
+
if (config.editorUri)
|
|
44
|
+
parts.push("editorUri=" + encodeURIComponent(config.editorUri));
|
|
45
|
+
return "speedscope/#" + parts.join("&");
|
|
46
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { themePreference } from "../State.ts";
|
|
2
|
+
import { setTheme } from "../Theme.ts";
|
|
3
|
+
|
|
4
|
+
/** Light/dark mode buttons. Clicking the active mode reverts to system default. */
|
|
5
|
+
export function ThemeToggle(): preact.JSX.Element {
|
|
6
|
+
const pref = themePreference.value;
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div class="theme-toggle">
|
|
10
|
+
<button
|
|
11
|
+
class={`theme-btn${pref === "light" ? " active" : ""}`}
|
|
12
|
+
title="Light mode"
|
|
13
|
+
onClick={() => setTheme(pref === "light" ? "system" : "light")}
|
|
14
|
+
>
|
|
15
|
+
<Sun />
|
|
16
|
+
</button>
|
|
17
|
+
<button
|
|
18
|
+
class={`theme-btn${pref === "dark" ? " active" : ""}`}
|
|
19
|
+
title="Dark mode"
|
|
20
|
+
onClick={() => setTheme(pref === "dark" ? "system" : "dark")}
|
|
21
|
+
>
|
|
22
|
+
<Moon />
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Sun() {
|
|
29
|
+
return (
|
|
30
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
31
|
+
<circle cx="12" cy="12" r="5" />
|
|
32
|
+
<line x1="12" y1="1" x2="12" y2="3" />
|
|
33
|
+
<line x1="12" y1="21" x2="12" y2="23" />
|
|
34
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
35
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
36
|
+
<line x1="1" y1="12" x2="3" y2="12" />
|
|
37
|
+
<line x1="21" y1="12" x2="23" y2="12" />
|
|
38
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
39
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
40
|
+
</svg>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Moon() {
|
|
45
|
+
return (
|
|
46
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
47
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
48
|
+
</svg>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Benchforge Viewer</title>
|
|
7
|
+
<script>
|
|
8
|
+
var m = document.cookie.match(/(?:^|; )theme=(light|dark)/);
|
|
9
|
+
if (m) document.documentElement.dataset.theme = m[1];
|
|
10
|
+
</script>
|
|
11
|
+
<link rel="stylesheet" href="./shell.css">
|
|
12
|
+
<link rel="stylesheet" href="./report.css">
|
|
13
|
+
</head>
|
|
14
|
+
|
|
15
|
+
<body>
|
|
16
|
+
<div id="app"></div>
|
|
17
|
+
|
|
18
|
+
<script type="module" src="./main.tsx"></script>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
@@ -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,45 +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
61
|
/** Bin samples into grouped histogram bars for each benchmark */
|
|
58
62
|
function buildBarData(allSamples: Sample[], benchmarkNames: string[]) {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
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)!;
|
|
63
66
|
const binCount = 25;
|
|
64
67
|
const step = (binMax - binMin) / binCount;
|
|
65
68
|
const thresholds = d3.range(1, binCount).map(i => binMin + i * step);
|
|
66
|
-
const plotWidth = 550;
|
|
67
|
-
|
|
68
69
|
const bins = d3
|
|
69
70
|
.bin<Sample, number>()
|
|
70
71
|
.domain([binMin, binMax])
|
|
71
72
|
.thresholds(thresholds)
|
|
72
73
|
.value(d => d.value)(allSamples);
|
|
73
74
|
|
|
74
|
-
const barData: {
|
|
75
|
-
benchmark: string;
|
|
76
|
-
count: number;
|
|
77
|
-
x1: number;
|
|
78
|
-
x2: number;
|
|
79
|
-
}[] = [];
|
|
80
75
|
const n = benchmarkNames.length;
|
|
81
|
-
const unitsPerPx = (binMax - binMin) /
|
|
76
|
+
const unitsPerPx = (binMax - binMin) / plotLayout.width;
|
|
82
77
|
const groupGapPx = 8;
|
|
83
78
|
|
|
84
|
-
|
|
79
|
+
const barData: Bar[] = bins.flatMap(bin => {
|
|
85
80
|
const counts = new Map<string, number>();
|
|
86
81
|
for (const d of bin)
|
|
87
82
|
counts.set(d.benchmark, (counts.get(d.benchmark) || 0) + 1);
|
|
@@ -91,12 +86,12 @@ function buildBarData(allSamples: Sample[], benchmarkNames: string[]) {
|
|
|
91
86
|
const start = bin.x0! + groupGap / 2;
|
|
92
87
|
const w = (full - groupGap) / n;
|
|
93
88
|
|
|
94
|
-
benchmarkNames.
|
|
89
|
+
return benchmarkNames.map((benchmark, i) => {
|
|
95
90
|
const x1 = start + i * w;
|
|
96
91
|
const x2 = start + (i + 1) * w;
|
|
97
|
-
|
|
92
|
+
return { benchmark, count: counts.get(benchmark) || 0, x1, x2 };
|
|
98
93
|
});
|
|
99
|
-
}
|
|
94
|
+
});
|
|
100
95
|
|
|
101
96
|
const maxCount = d3.max(barData, d => d.count)! || 1;
|
|
102
97
|
const yMax = maxCount * 1.15;
|
|
@@ -104,13 +99,13 @@ function buildBarData(allSamples: Sample[], benchmarkNames: string[]) {
|
|
|
104
99
|
return { barData, binMin, binMax, yMax };
|
|
105
100
|
}
|
|
106
101
|
|
|
102
|
+
/** Map benchmark names to colors and legend items using Observable 10 palette */
|
|
107
103
|
function buildColorData(benchmarkNames: string[]) {
|
|
108
104
|
const scheme = (d3 as any).schemeObservable10;
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
);
|
|
105
|
+
const color = (i: number) => scheme[i % 10];
|
|
106
|
+
const colorMap = new Map(benchmarkNames.map((name, i) => [name, color(i)]));
|
|
112
107
|
const legendItems: LegendItem[] = benchmarkNames.map((name, i) => ({
|
|
113
|
-
color:
|
|
108
|
+
color: color(i),
|
|
114
109
|
label: name,
|
|
115
110
|
style: "vertical-bar",
|
|
116
111
|
}));
|