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,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
|
+
}
|
|
@@ -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>
|