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,216 @@
|
|
|
1
|
+
import { createHighlighterCore, type HighlighterCore } from "shiki/core";
|
|
2
|
+
import langCss from "shiki/dist/langs/css.mjs";
|
|
3
|
+
import langHtml from "shiki/dist/langs/html.mjs";
|
|
4
|
+
import langJs from "shiki/dist/langs/javascript.mjs";
|
|
5
|
+
import langTs from "shiki/dist/langs/typescript.mjs";
|
|
6
|
+
import themeDark from "shiki/dist/themes/github-dark.mjs";
|
|
7
|
+
import themeLight from "shiki/dist/themes/github-light.mjs";
|
|
8
|
+
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
|
|
9
|
+
import { useEffect, useRef, useState } from "preact/hooks";
|
|
10
|
+
import { filePathFromUrl, guessLang } from "../Helpers.ts";
|
|
11
|
+
import {
|
|
12
|
+
computeLineData,
|
|
13
|
+
formatGutterBytes,
|
|
14
|
+
formatGutterCount,
|
|
15
|
+
formatGutterTime,
|
|
16
|
+
} from "../LineData.ts";
|
|
17
|
+
import type {
|
|
18
|
+
ViewerCoverageData,
|
|
19
|
+
ViewerSpeedscopeFile,
|
|
20
|
+
} from "../Providers.ts";
|
|
21
|
+
import {
|
|
22
|
+
activeTabId,
|
|
23
|
+
provider,
|
|
24
|
+
sourceTabs,
|
|
25
|
+
type SourceTabState,
|
|
26
|
+
} from "../State.ts";
|
|
27
|
+
|
|
28
|
+
let highlighterPromise: Promise<HighlighterCore> | undefined;
|
|
29
|
+
|
|
30
|
+
/** Lazily create a shared Shiki highlighter with light/dark themes. */
|
|
31
|
+
function getHighlighter(): Promise<HighlighterCore> {
|
|
32
|
+
highlighterPromise ??= createHighlighterCore({
|
|
33
|
+
themes: [themeLight, themeDark],
|
|
34
|
+
langs: [langJs, langTs, langCss, langHtml],
|
|
35
|
+
engine: createJavaScriptRegexEngine(),
|
|
36
|
+
});
|
|
37
|
+
return highlighterPromise;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Open or focus a source tab, scrolling to the given line and column. */
|
|
41
|
+
export function openSourceTab(file: string, line: number, col: number): void {
|
|
42
|
+
const id = "src:" + file;
|
|
43
|
+
const tabs = sourceTabs.value;
|
|
44
|
+
const existing = tabs.find(t => t.id === id);
|
|
45
|
+
if (existing) {
|
|
46
|
+
sourceTabs.value = tabs.map(t =>
|
|
47
|
+
t.id === id ? { ...t, line, col, generation: t.generation + 1 } : t,
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
sourceTabs.value = [...tabs, { id, file, line, col, generation: 1 }];
|
|
51
|
+
}
|
|
52
|
+
activeTabId.value = id;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Fetches source, highlights with Shiki, then overlays profiling gutters and scrolls to target line. */
|
|
56
|
+
export function SourcePanel({ tab }: { tab: SourceTabState }): preact.JSX.Element {
|
|
57
|
+
const dataProvider = provider.value!;
|
|
58
|
+
const [html, setHtml] = useState<string | null>(null);
|
|
59
|
+
const [error, setError] = useState<string | null>(null);
|
|
60
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
61
|
+
const active = activeTabId.value === tab.id;
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
let stale = false;
|
|
65
|
+
setHtml(null);
|
|
66
|
+
setError(null);
|
|
67
|
+
|
|
68
|
+
(async () => {
|
|
69
|
+
try {
|
|
70
|
+
const code = await dataProvider.fetchSource(tab.file);
|
|
71
|
+
if (stale) return;
|
|
72
|
+
const highlighter = await getHighlighter();
|
|
73
|
+
if (stale) return;
|
|
74
|
+
const lang = guessLang(tab.file);
|
|
75
|
+
const themes = { light: "github-light", dark: "github-dark" };
|
|
76
|
+
setHtml(highlighter.codeToHtml(code, { lang, themes, defaultColor: false }));
|
|
77
|
+
} catch {
|
|
78
|
+
if (!stale) setError(tab.file);
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
stale = true;
|
|
84
|
+
};
|
|
85
|
+
}, [tab.file, tab.generation, dataProvider]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!html || !panelRef.current) return;
|
|
89
|
+
let stale = false;
|
|
90
|
+
|
|
91
|
+
Promise.all([
|
|
92
|
+
dataProvider.fetchProfileData("alloc"),
|
|
93
|
+
dataProvider.fetchProfileData("time"),
|
|
94
|
+
dataProvider.fetchCoverageData(),
|
|
95
|
+
]).then(([alloc, time, coverage]) => {
|
|
96
|
+
if (stale || !panelRef.current) return;
|
|
97
|
+
renderGutters(panelRef.current, tab.file, alloc, time, coverage);
|
|
98
|
+
|
|
99
|
+
if (tab.line) {
|
|
100
|
+
const lines = panelRef.current.querySelectorAll(".source-code .line");
|
|
101
|
+
const target = lines[tab.line - 1];
|
|
102
|
+
if (target) {
|
|
103
|
+
target.classList.add("highlighted");
|
|
104
|
+
target.scrollIntoView({ block: "center" });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return () => {
|
|
110
|
+
stale = true;
|
|
111
|
+
};
|
|
112
|
+
}, [html, tab.line, dataProvider, tab.file]);
|
|
113
|
+
|
|
114
|
+
const editorUri = dataProvider.config.editorUri;
|
|
115
|
+
const cls = `source-panel${active ? " active" : ""}`;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div class={cls} data-tab={tab.id} ref={panelRef}>
|
|
119
|
+
<SourceBody
|
|
120
|
+
file={tab.file}
|
|
121
|
+
line={tab.line}
|
|
122
|
+
col={tab.col}
|
|
123
|
+
html={html}
|
|
124
|
+
error={error}
|
|
125
|
+
editorUri={editorUri}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface SourceBodyProps {
|
|
132
|
+
file: string; line: number; col: number;
|
|
133
|
+
html: string | null; error: string | null; editorUri: string | null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Render loading/error placeholder or the highlighted source with header. */
|
|
137
|
+
function SourceBody({ file, line, col, html, error, editorUri }: SourceBodyProps) {
|
|
138
|
+
if (error) {
|
|
139
|
+
return (
|
|
140
|
+
<div class="source-placeholder">
|
|
141
|
+
<p>Source unavailable for {error}</p>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (!html) {
|
|
146
|
+
return (
|
|
147
|
+
<div class="source-placeholder">
|
|
148
|
+
<p>Loading source…</p>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return (
|
|
153
|
+
<>
|
|
154
|
+
<SourceHeader file={file} line={line} col={col} editorUri={editorUri} />
|
|
155
|
+
<div class="source-code" dangerouslySetInnerHTML={{ __html: html }} />
|
|
156
|
+
</>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** File path display with optional "Open in Editor" link. */
|
|
161
|
+
function SourceHeader({ file, line, col, editorUri }: {
|
|
162
|
+
file: string; line: number; col: number; editorUri: string | null;
|
|
163
|
+
}) {
|
|
164
|
+
const path = filePathFromUrl(file);
|
|
165
|
+
const href = editorUri ? `${editorUri}${path}:${line || 1}:${col || 1}` : null;
|
|
166
|
+
return (
|
|
167
|
+
<div class="source-header">
|
|
168
|
+
<span class="source-path">{file}</span>
|
|
169
|
+
{href && <a class="source-editor-link" href={href}>Open in Editor</a>}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Build a gutter span with optional heat-map styling (CSS custom property). */
|
|
175
|
+
function gutter(kind: string, text: string, heat?: number): string {
|
|
176
|
+
const style = heat ? ` style="--heat:${heat.toFixed(3)}"` : "";
|
|
177
|
+
const cls = heat ? ` heat` : "";
|
|
178
|
+
return `<span class="gutter gutter-${kind}${cls}"${style}>${text}</span>`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Inject call-count, alloc, and time gutters into highlighted source lines. */
|
|
182
|
+
function renderGutters(
|
|
183
|
+
panel: HTMLElement,
|
|
184
|
+
file: string,
|
|
185
|
+
allocProfile: ViewerSpeedscopeFile | null,
|
|
186
|
+
timeProfile: ViewerSpeedscopeFile | null,
|
|
187
|
+
coverage: ViewerCoverageData | null,
|
|
188
|
+
): void {
|
|
189
|
+
const { callCounts, allocBytes, selfTimeUs } = computeLineData(file, allocProfile, timeProfile, coverage);
|
|
190
|
+
const hasCounts = callCounts.size > 0, hasAlloc = allocBytes.size > 0, hasTime = selfTimeUs.size > 0;
|
|
191
|
+
if (!hasCounts && !hasAlloc && !hasTime) return;
|
|
192
|
+
|
|
193
|
+
const codeEl = panel.querySelector(".source-code") as HTMLElement;
|
|
194
|
+
if (!codeEl) return;
|
|
195
|
+
const maxAlloc = hasAlloc ? Math.max(...allocBytes.values()) : 0;
|
|
196
|
+
const maxTime = hasTime ? Math.max(...selfTimeUs.values()) : 0;
|
|
197
|
+
|
|
198
|
+
const heatAboveThreshold = (h: number) => (h > 0.01 ? h : undefined);
|
|
199
|
+
const lines = codeEl.querySelectorAll(".line");
|
|
200
|
+
for (let i = 0; i < lines.length; i++) {
|
|
201
|
+
const lineNum = i + 1;
|
|
202
|
+
const el = lines[i] as HTMLElement;
|
|
203
|
+
const counts = callCounts.get(lineNum);
|
|
204
|
+
const alloc = allocBytes.get(lineNum);
|
|
205
|
+
const time = selfTimeUs.get(lineNum);
|
|
206
|
+
const allocHeat = alloc && maxAlloc > 0 ? alloc / maxAlloc : 0;
|
|
207
|
+
const timeHeat = time && maxTime > 0 ? time / maxTime : 0;
|
|
208
|
+
let gutterHtml = "";
|
|
209
|
+
if (hasCounts) gutterHtml += gutter("count", formatGutterCount(counts));
|
|
210
|
+
if (hasAlloc) gutterHtml += gutter("alloc", formatGutterBytes(alloc), heatAboveThreshold(allocHeat));
|
|
211
|
+
if (hasTime) gutterHtml += gutter("time", formatGutterTime(time), heatAboveThreshold(timeHeat));
|
|
212
|
+
|
|
213
|
+
el.insertAdjacentHTML("afterbegin", gutterHtml);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
|
2
|
+
import { useLazyPlot } from "./LazyPlot.ts";
|
|
3
|
+
import type { GitVersion } from "../../report/GitUtils.ts";
|
|
4
|
+
import type { DifferenceCI } from "../../stats/StatisticalUtils.ts";
|
|
5
|
+
import { formatRelativeTime } from "../DateFormat.ts";
|
|
6
|
+
import { formatCount, formatDecimalBytes } from "../LineData.ts";
|
|
7
|
+
import type {
|
|
8
|
+
BenchmarkEntry,
|
|
9
|
+
BenchmarkGroup,
|
|
10
|
+
BootstrapCIData,
|
|
11
|
+
ReportData,
|
|
12
|
+
ViewerEntry,
|
|
13
|
+
ViewerRow,
|
|
14
|
+
ViewerSection,
|
|
15
|
+
} from "../ReportData.ts";
|
|
16
|
+
import { formatPct } from "../plots/PlotTypes.ts";
|
|
17
|
+
import { activeTabId, provider, reportData } from "../State.ts";
|
|
18
|
+
|
|
19
|
+
const skipArgs = new Set(["_", "$0", "view", "file"]);
|
|
20
|
+
|
|
21
|
+
/** Main summary view: fetches report data, shows CLI args header and collapsible benchmark groups. */
|
|
22
|
+
export function SummaryPanel() {
|
|
23
|
+
const dataProvider = provider.value!;
|
|
24
|
+
const data = reportData.value;
|
|
25
|
+
const [error, setError] = useState<string | null>(null);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
dataProvider.fetchReportData()
|
|
29
|
+
.then(result => (reportData.value = result as ReportData))
|
|
30
|
+
.catch(err => {
|
|
31
|
+
console.error("Report load failed:", err);
|
|
32
|
+
setError(String(err));
|
|
33
|
+
});
|
|
34
|
+
}, [dataProvider]);
|
|
35
|
+
|
|
36
|
+
if (error)
|
|
37
|
+
return <div class="empty-state"><p>Failed to load report data: {error}</p></div>;
|
|
38
|
+
if (!data)
|
|
39
|
+
return <div class="empty-state"><p>Loading report…</p></div>;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<ReportHeader metadata={data.metadata} />
|
|
44
|
+
{data.groups.map((group, i) => (
|
|
45
|
+
<CollapsibleGroup key={i} group={group} />
|
|
46
|
+
))}
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
declare const __BENCHFORGE_GIT_HASH__: string;
|
|
52
|
+
declare const __BENCHFORGE_GIT_DIRTY__: boolean;
|
|
53
|
+
declare const __BENCHFORGE_BUILD_DATE__: string;
|
|
54
|
+
|
|
55
|
+
/** Fallback for dev/unbundled builds where compile-time globals are absent. */
|
|
56
|
+
function safeGlobal<T>(v: T, fallback: T): T {
|
|
57
|
+
return typeof v !== "undefined" ? v : fallback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Assemble "benchforge <hash> <relative-date>" from compile-time globals. */
|
|
61
|
+
function benchforgeLabel(): string {
|
|
62
|
+
const hash = safeGlobal(__BENCHFORGE_GIT_HASH__, "dev");
|
|
63
|
+
const dirty = safeGlobal(__BENCHFORGE_GIT_DIRTY__, false);
|
|
64
|
+
const date = safeGlobal(__BENCHFORGE_BUILD_DATE__, "");
|
|
65
|
+
const label = `benchforge ${hash}${dirty ? "*" : ""}`;
|
|
66
|
+
return date ? `${label} ${formatRelativeTime(date)}` : label;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ReportHeader({ metadata }: { metadata: ReportData["metadata"] }) {
|
|
70
|
+
const { cliArgs, cliDefaults, currentVersion, baselineVersion } = metadata;
|
|
71
|
+
const versions = [
|
|
72
|
+
currentVersion && `Current: ${formatVersion(currentVersion)}`,
|
|
73
|
+
baselineVersion && `Baseline: ${formatVersion(baselineVersion)}`,
|
|
74
|
+
].filter(Boolean);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div class="report-header">
|
|
78
|
+
<div class="cli-args">{formatCliArgs(cliArgs, cliDefaults)}</div>
|
|
79
|
+
<div class="header-right">
|
|
80
|
+
<div class="metadata">{new Date().toLocaleString()}</div>
|
|
81
|
+
<div class="metadata benchforge-version">{benchforgeLabel()}</div>
|
|
82
|
+
{versions.length > 0 && (
|
|
83
|
+
<div class="version-info">{versions.join(" | ")}</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Expandable benchmark group with comparison badge and section panels. */
|
|
91
|
+
function CollapsibleGroup({ group }: { group: BenchmarkGroup }) {
|
|
92
|
+
const [open, setOpen] = useState(true);
|
|
93
|
+
const current = group.benchmarks?.[0];
|
|
94
|
+
if (!current) return <div class="error">No benchmark data for this group</div>;
|
|
95
|
+
|
|
96
|
+
const ci = current.comparisonCI;
|
|
97
|
+
return (
|
|
98
|
+
<div class="benchmark-group">
|
|
99
|
+
<div class="group-header" onClick={() => setOpen(o => !o)}>
|
|
100
|
+
<span class="group-toggle">{open ? "\u25be" : "\u25b8"}</span>
|
|
101
|
+
<h2>{group.name}</h2>
|
|
102
|
+
{ci && <ComparisonBadge ci={ci} />}
|
|
103
|
+
{group.warnings && (
|
|
104
|
+
<span class="batch-warnings">
|
|
105
|
+
{group.warnings.map(w => <span class="batch-warning">{w}</span>)}
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
{open && <GroupContent current={current} />}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function GroupContent({ current }: { current: BenchmarkEntry }) {
|
|
115
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (ref.current) alignRunColumns(ref.current);
|
|
118
|
+
});
|
|
119
|
+
return (
|
|
120
|
+
<div class="panel-grid" ref={ref}>
|
|
121
|
+
{current.sections?.map((s, i) => <SectionPanel key={i} section={s} />)}
|
|
122
|
+
<HeapPanel entry={current} />
|
|
123
|
+
<CoveragePanel entry={current} />
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function SectionPanel({ section }: { section: ViewerSection }) {
|
|
129
|
+
if (!section.rows.length) return null;
|
|
130
|
+
const range = useMemo(() => sectionEstimateRange(section), [section]);
|
|
131
|
+
const titleEl = section.tabLink
|
|
132
|
+
? <a class="panel-title-link" onClick={() => (activeTabId.value = section.tabLink!)}>{section.title}</a>
|
|
133
|
+
: <span>{section.title}</span>;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div class="section-panel">
|
|
137
|
+
<div class="panel-header">{titleEl}</div>
|
|
138
|
+
<div class="panel-body">
|
|
139
|
+
{section.rows.map((row, i) => <StatRow key={i} row={row} estimateRange={range} />)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Set CSS vars so run-name and run-value columns align across all sections. */
|
|
146
|
+
function alignRunColumns(panel: HTMLElement): void {
|
|
147
|
+
const maxW = (sel: string) =>
|
|
148
|
+
Math.max(0, ...[...panel.querySelectorAll<HTMLElement>(sel)].map(el => el.scrollWidth));
|
|
149
|
+
const maxName = maxW(".run-name");
|
|
150
|
+
const maxValue = maxW(".run-value");
|
|
151
|
+
if (maxName) panel.style.setProperty("--run-name-width", `${maxName}px`);
|
|
152
|
+
if (maxValue) panel.style.setProperty("--run-value-width", `${maxValue}px`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function StatRow({ row, estimateRange }: { row: ViewerRow; estimateRange?: [number, number] }) {
|
|
156
|
+
if (row.shared) {
|
|
157
|
+
return (
|
|
158
|
+
<div class="stat-row">
|
|
159
|
+
<div class="row-header">
|
|
160
|
+
<span class="row-label">{row.label}</span>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="run-entry">
|
|
163
|
+
<span class="run-name" />
|
|
164
|
+
<span class="run-value">{row.entries[0]?.value}</span>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div class={`stat-row${row.primary ? " primary-row" : ""}`}>
|
|
172
|
+
<div class="row-header">
|
|
173
|
+
<span class="row-label">{row.label}</span>
|
|
174
|
+
{row.comparisonCI && <ComparisonBadge ci={row.comparisonCI} compact />}
|
|
175
|
+
</div>
|
|
176
|
+
{row.entries.map((entry, i) => (
|
|
177
|
+
<RunEntry key={i} entry={entry} estimateRange={estimateRange} />
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Proportional horizontal offset range for aligning bootstrap CI plots. */
|
|
184
|
+
const maxCIShift = 80;
|
|
185
|
+
|
|
186
|
+
function RunEntry({ entry, estimateRange }: { entry: ViewerEntry; estimateRange?: [number, number] }) {
|
|
187
|
+
const ci = entry.bootstrapCI;
|
|
188
|
+
const [lo, hi] = estimateRange ?? [0, 0];
|
|
189
|
+
const shift = ci && hi > lo ? ((ci.estimate - lo) / (hi - lo)) * maxCIShift : undefined;
|
|
190
|
+
return (
|
|
191
|
+
<div class="run-entry">
|
|
192
|
+
<span class="run-name">{entry.runName}</span>
|
|
193
|
+
{ci
|
|
194
|
+
? <BootstrapCIMount ci={ci} label={entry.value} shift={shift} />
|
|
195
|
+
: <span class="run-value">{entry.value}</span>}
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function SharedStat({ label, value }: { label: string; value: string }) {
|
|
201
|
+
return (
|
|
202
|
+
<div class="stat-row shared-row">
|
|
203
|
+
<span class="row-label">{label}</span>
|
|
204
|
+
<span class="row-value">{value}</span>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function HeapPanel({ entry }: { entry: BenchmarkEntry }) {
|
|
210
|
+
const { heapSummary: heap, allocationSamples: allocSamples } = entry;
|
|
211
|
+
if (!heap && !allocSamples?.length) return null;
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div class="section-panel">
|
|
215
|
+
<div class="panel-header">
|
|
216
|
+
<a class="panel-title-link" onClick={() => (activeTabId.value = "flamechart")}>
|
|
217
|
+
heap allocation
|
|
218
|
+
</a>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="panel-body">
|
|
221
|
+
{heap && (
|
|
222
|
+
<>
|
|
223
|
+
<SharedStat label="total bytes" value={formatDecimalBytes(heap.totalBytes)} />
|
|
224
|
+
<SharedStat label="user bytes" value={formatDecimalBytes(heap.userBytes)} />
|
|
225
|
+
</>
|
|
226
|
+
)}
|
|
227
|
+
{allocSamples && allocSamples.length > 0 && (
|
|
228
|
+
<SharedStat label="alloc samples" value={allocSamples.length.toLocaleString()} />
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function CoveragePanel({ entry }: { entry: BenchmarkEntry }) {
|
|
236
|
+
const cov = entry.coverageSummary;
|
|
237
|
+
if (!cov) return null;
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div class="section-panel">
|
|
241
|
+
<div class="panel-header">
|
|
242
|
+
<span>calls</span>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="panel-body">
|
|
245
|
+
<SharedStat label="functions tracked" value={cov.functionCount.toLocaleString()} />
|
|
246
|
+
<SharedStat label="total calls" value={formatCount(cov.totalCalls)} />
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const directionLabels: Record<string, string> = {
|
|
253
|
+
faster: "Faster", slower: "Slower", uncertain: "Inconclusive", equivalent: "Equivalent",
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
function ComparisonBadge({ ci, compact }: { ci: DifferenceCI; compact?: boolean }) {
|
|
257
|
+
return (
|
|
258
|
+
<span class="comparison-badge">
|
|
259
|
+
<span class={`badge badge-${ci.direction}`}>
|
|
260
|
+
{compact ? formatPct(ci.percent) : directionLabels[ci.direction]}
|
|
261
|
+
</span>
|
|
262
|
+
{ci.histogram && <CIPlotMount ci={ci} compact={compact} />}
|
|
263
|
+
</span>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Lazy-imports CIPlot and renders a confidence interval chart inline. */
|
|
268
|
+
function CIPlotMount({ ci, compact }: { ci: DifferenceCI; compact?: boolean }) {
|
|
269
|
+
const ref = useLazyPlot(async () => {
|
|
270
|
+
const { createCIPlot } = await import("../plots/CIPlot.ts");
|
|
271
|
+
const equivMargin = (reportData.value?.metadata.cliArgs?.["equiv-margin"] as number) || undefined;
|
|
272
|
+
const opts = compact ? { width: 200, height: 70, title: "", equivMargin } : { equivMargin };
|
|
273
|
+
return createCIPlot(ci, opts);
|
|
274
|
+
}, [ci, compact], "CI plot");
|
|
275
|
+
return <div class="ci-plot-container" ref={ref} />;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Lazy-imports CIPlot and renders a bootstrap distribution sparkline inline. */
|
|
279
|
+
function BootstrapCIMount({ ci, label, shift }: {
|
|
280
|
+
ci: BootstrapCIData; label?: string; shift?: number;
|
|
281
|
+
}) {
|
|
282
|
+
const ref = useLazyPlot(async () => {
|
|
283
|
+
const { createDistributionPlot } = await import("../plots/CIPlot.ts");
|
|
284
|
+
const opts = {
|
|
285
|
+
width: 240, height: 80, title: "", direction: "uncertain" as const,
|
|
286
|
+
ciLabels: ci.ciLabels, includeZero: false, smooth: true, pointLabel: label,
|
|
287
|
+
ciLevel: ci.ciLevel, ciReliable: ci.ciReliable,
|
|
288
|
+
};
|
|
289
|
+
return createDistributionPlot(ci.histogram, ci.ci, ci.estimate, opts);
|
|
290
|
+
}, [ci, label], "Bootstrap CI plot");
|
|
291
|
+
const style = shift != null ? { marginLeft: `${Math.round(shift)}px` } : undefined;
|
|
292
|
+
return <div class="ci-plot-inline" style={style} ref={ref} />;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Compute min/max bootstrap estimates across a section for proportional positioning */
|
|
296
|
+
function sectionEstimateRange(section: ViewerSection): [number, number] | undefined {
|
|
297
|
+
const estimates = section.rows
|
|
298
|
+
.flatMap(row => row.entries)
|
|
299
|
+
.map(e => e.bootstrapCI?.estimate)
|
|
300
|
+
.filter((v): v is number => v != null);
|
|
301
|
+
if (estimates.length < 2) return undefined;
|
|
302
|
+
const min = Math.min(...estimates), max = Math.max(...estimates);
|
|
303
|
+
return max > min ? [min, max] : undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Format CLI args for display, filtering out defaults, internal keys, and camelCase aliases. */
|
|
307
|
+
function formatCliArgs(
|
|
308
|
+
args?: Record<string, unknown>,
|
|
309
|
+
defaults?: Record<string, unknown>,
|
|
310
|
+
): string {
|
|
311
|
+
if (!args) return "benchforge";
|
|
312
|
+
const isDisplayable = (key: string, value: unknown): boolean => {
|
|
313
|
+
if (skipArgs.has(key) || value === undefined || value === false) return false;
|
|
314
|
+
if (defaults?.[key] === value) return false;
|
|
315
|
+
// skip camelCase aliases (yargs generates both kebab-case and camelCase)
|
|
316
|
+
if (!key.includes("-") && key !== key.toLowerCase()) return false;
|
|
317
|
+
if (key === "convergence" && !args.adaptive) return false;
|
|
318
|
+
return true;
|
|
319
|
+
};
|
|
320
|
+
const flags = Object.entries(args)
|
|
321
|
+
.filter(([key, value]) => isDisplayable(key, value))
|
|
322
|
+
.map(([key, value]) => (value === true ? `--${key}` : `--${key} ${value}`));
|
|
323
|
+
return ["benchforge", ...flags].join(" ");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Format a git version as "hash (relative-date)", with dirty marker. */
|
|
327
|
+
function formatVersion(v: GitVersion): string {
|
|
328
|
+
if (!v || v.hash === "unknown") return "unknown";
|
|
329
|
+
const hash = v.dirty ? v.hash + "*" : v.hash;
|
|
330
|
+
if (!v.date) return hash;
|
|
331
|
+
return `${hash} (${formatRelativeTime(v.date)})`;
|
|
332
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useState } from "preact/hooks";
|
|
2
|
+
import type { DataProvider } from "../Providers.ts";
|
|
3
|
+
import {
|
|
4
|
+
activeTabId,
|
|
5
|
+
defaultTabId,
|
|
6
|
+
provider,
|
|
7
|
+
reportData,
|
|
8
|
+
samplesLoaded,
|
|
9
|
+
sourceTabs,
|
|
10
|
+
} from "../State.ts";
|
|
11
|
+
import { hasSufficientSamples } from "./SamplesPanel.tsx";
|
|
12
|
+
import { ThemeToggle } from "./ThemeToggle.tsx";
|
|
13
|
+
|
|
14
|
+
/** Top navigation bar with fixed tabs, dynamic source tabs, theme toggle, and archive download. */
|
|
15
|
+
export function TabBar() {
|
|
16
|
+
const dataProvider = provider.value!;
|
|
17
|
+
const { config } = dataProvider;
|
|
18
|
+
const data = reportData.value;
|
|
19
|
+
const samplesEnabled = !!data && hasSufficientSamples(data);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div class="tab-bar">
|
|
23
|
+
<TabButton tabId="summary" disabled={!config.hasReport}>
|
|
24
|
+
Summary
|
|
25
|
+
</TabButton>
|
|
26
|
+
<TabButton tabId="samples" disabled={!samplesEnabled} onActivate={() => (samplesLoaded.value = true)}>
|
|
27
|
+
Iterations
|
|
28
|
+
</TabButton>
|
|
29
|
+
<TabButton tabId="flamechart" disabled={!config.hasProfile}>
|
|
30
|
+
Allocation
|
|
31
|
+
</TabButton>
|
|
32
|
+
<TabButton tabId="time-flamechart" disabled={!config.hasTimeProfile}>
|
|
33
|
+
Timing
|
|
34
|
+
</TabButton>
|
|
35
|
+
|
|
36
|
+
{sourceTabs.value.map(st => (
|
|
37
|
+
<SourceTabBtn key={st.id} tabId={st.id} file={st.file} line={st.line} />
|
|
38
|
+
))}
|
|
39
|
+
|
|
40
|
+
<div class="tab-spacer" />
|
|
41
|
+
<ThemeToggle />
|
|
42
|
+
<ArchiveButton provider={dataProvider} />
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Fixed tab button that sets the active tab on click. */
|
|
48
|
+
function TabButton({ tabId, disabled, onActivate, children }: {
|
|
49
|
+
tabId: string; disabled: boolean; onActivate?: () => void; children: preact.ComponentChildren;
|
|
50
|
+
}) {
|
|
51
|
+
const active = activeTabId.value === tabId;
|
|
52
|
+
return (
|
|
53
|
+
<button
|
|
54
|
+
class={`tab${active ? " active" : ""}`}
|
|
55
|
+
data-tab={tabId}
|
|
56
|
+
id={`tab-${tabId}`}
|
|
57
|
+
disabled={disabled}
|
|
58
|
+
onClick={() => {
|
|
59
|
+
activeTabId.value = tabId;
|
|
60
|
+
onActivate?.();
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
</button>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Source-file tab with close button; clicking the close span removes the tab instead of activating it. */
|
|
69
|
+
function SourceTabBtn({ tabId, file, line }: { tabId: string; file: string; line: number }) {
|
|
70
|
+
const active = activeTabId.value === tabId;
|
|
71
|
+
const shortName = file.split("/").pop() || file;
|
|
72
|
+
const label = line ? `${shortName}:${line}` : shortName;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<button
|
|
76
|
+
class={`tab${active ? " active" : ""}`}
|
|
77
|
+
data-tab={tabId}
|
|
78
|
+
onClick={(e: MouseEvent) => {
|
|
79
|
+
if ((e.target as HTMLElement).closest(".tab-close")) {
|
|
80
|
+
closeSourceTab(tabId);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
activeTabId.value = tabId;
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{label}{" "}
|
|
87
|
+
<span class="tab-close" title="Close">
|
|
88
|
+
×
|
|
89
|
+
</span>
|
|
90
|
+
</button>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Remove a source tab and fall back to the best available fixed tab. */
|
|
95
|
+
function closeSourceTab(tabId: string): void {
|
|
96
|
+
sourceTabs.value = sourceTabs.value.filter(t => t.id !== tabId);
|
|
97
|
+
if (activeTabId.value === tabId) activeTabId.value = defaultTabId();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Download button that bundles all report data into a `.benchforge` archive. */
|
|
101
|
+
function ArchiveButton({ provider: dataProvider }: { provider: DataProvider }) {
|
|
102
|
+
const [archiving, setArchiving] = useState(false);
|
|
103
|
+
|
|
104
|
+
async function downloadArchive(): Promise<void> {
|
|
105
|
+
setArchiving(true);
|
|
106
|
+
try {
|
|
107
|
+
const { blob, filename } = await dataProvider.createArchive();
|
|
108
|
+
const url = URL.createObjectURL(blob);
|
|
109
|
+
const link = Object.assign(document.createElement("a"), { href: url, download: filename });
|
|
110
|
+
document.body.appendChild(link);
|
|
111
|
+
link.click();
|
|
112
|
+
link.remove();
|
|
113
|
+
URL.revokeObjectURL(url);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error("Archive failed:", err);
|
|
116
|
+
} finally {
|
|
117
|
+
setArchiving(false);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<button
|
|
123
|
+
class="tab archive-btn"
|
|
124
|
+
data-action="archive"
|
|
125
|
+
disabled={archiving}
|
|
126
|
+
onClick={downloadArchive}
|
|
127
|
+
>
|
|
128
|
+
{archiving ? "Archiving\u2026" : "Archive \u2193"}
|
|
129
|
+
</button>
|
|
130
|
+
);
|
|
131
|
+
}
|