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,214 @@
|
|
|
1
|
+
import { useMemo, useState } from "preact/hooks";
|
|
2
|
+
import type { BenchmarkGroup, ReportData } from "../ReportData.ts";
|
|
3
|
+
import {
|
|
4
|
+
batchCount,
|
|
5
|
+
filterToBatch,
|
|
6
|
+
type FlattenedData,
|
|
7
|
+
flattenSamples,
|
|
8
|
+
type PreparedBenchmark,
|
|
9
|
+
prepareBenchmarks,
|
|
10
|
+
} from "../plots/RenderPlots.ts";
|
|
11
|
+
import type { SeriesVisibility } from "../plots/SampleTimeSeries.ts";
|
|
12
|
+
import { reportData, samplesLoaded } from "../State.ts";
|
|
13
|
+
import { useLazyPlot } from "./LazyPlot.ts";
|
|
14
|
+
|
|
15
|
+
/** True when at least one benchmark group has multiple samples (enough to plot). */
|
|
16
|
+
export function hasSufficientSamples(data: ReportData): boolean {
|
|
17
|
+
return data.groups.some(groupHasSamples);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** True when any benchmark or baseline in the group has multiple samples. */
|
|
21
|
+
function groupHasSamples(group: BenchmarkGroup): boolean {
|
|
22
|
+
const multiSample = (b: { samples: unknown[] }) => b.samples.length > 1;
|
|
23
|
+
return group.benchmarks.some(multiSample) || (!!group.baseline && multiSample(group.baseline));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Time-series and histogram plots for each benchmark group. Lazy-loaded on first tab activation. */
|
|
27
|
+
export function SamplesPanel() {
|
|
28
|
+
const data = reportData.value;
|
|
29
|
+
if (!samplesLoaded.value || !data) return null;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
{data.groups.map((group, i) => (
|
|
34
|
+
<SamplesGroup key={i} group={group} index={i} />
|
|
35
|
+
))}
|
|
36
|
+
</>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Renders time-series and histogram plots for one benchmark group, with batch stepping and series toggles. */
|
|
41
|
+
function SamplesGroup({ group, index }: { group: BenchmarkGroup; index: number }) {
|
|
42
|
+
const hasSamples = groupHasSamples(group);
|
|
43
|
+
const benchmarks = useMemo(() => prepareBenchmarks(group), [group]);
|
|
44
|
+
const flat = useMemo(
|
|
45
|
+
() => hasSamples ? flattenSamples(benchmarks) : null,
|
|
46
|
+
[benchmarks, hasSamples],
|
|
47
|
+
);
|
|
48
|
+
const numBatches = hasSamples ? batchCount(benchmarks) : 0;
|
|
49
|
+
|
|
50
|
+
// batch === 0 means "All", 1..numBatches means specific batch
|
|
51
|
+
const [batch, setBatch] = useState(0);
|
|
52
|
+
const activeBatch = batch > numBatches ? 0 : batch;
|
|
53
|
+
|
|
54
|
+
const viewFlat = useMemo(
|
|
55
|
+
() => flat && activeBatch > 0 ? filterToBatch(flat, benchmarks, activeBatch - 1) : flat,
|
|
56
|
+
[flat, benchmarks, activeBatch],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const [visibility, setVisibility] = useState<SeriesVisibility>({
|
|
60
|
+
baseline: true,
|
|
61
|
+
heap: true,
|
|
62
|
+
baselineHeap: false,
|
|
63
|
+
rejected: true,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!group.benchmarks?.length) return null;
|
|
67
|
+
if (!hasSamples || !flat || !viewFlat) return (
|
|
68
|
+
<div>
|
|
69
|
+
<div class="group-header">
|
|
70
|
+
<h2>{group.name}</h2>
|
|
71
|
+
</div>
|
|
72
|
+
<p class="single-sample-notice">
|
|
73
|
+
Single sample collected — plots require multiple samples.
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const hasBaseline = !!group.baseline;
|
|
79
|
+
const hasHeap = flat.heapSeries.length > 0, hasBaselineHeap = flat.baselineHeapSeries.length > 0;
|
|
80
|
+
const hasRejected = flat.timeSeries.some(d => d.isRejected);
|
|
81
|
+
const totalPoints = viewFlat.timeSeries.length, sampled = totalPoints > 1000;
|
|
82
|
+
|
|
83
|
+
const toggle = (key: keyof SeriesVisibility) =>
|
|
84
|
+
setVisibility(v => ({ ...v, [key]: !v[key] }));
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div>
|
|
88
|
+
<div class="group-header">
|
|
89
|
+
<h2>{group.name}</h2>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="plot-grid">
|
|
92
|
+
<div class="plot-container">
|
|
93
|
+
<div class="plot-title">Time per Iteration</div>
|
|
94
|
+
<div class="plot-description">
|
|
95
|
+
{sampled
|
|
96
|
+
? `Sampled from ${totalPoints.toLocaleString()} iterations (showing ~1,000)`
|
|
97
|
+
: "Execution time for each iteration in collection order"}
|
|
98
|
+
</div>
|
|
99
|
+
<div class="plot-controls">
|
|
100
|
+
<SeriesToggles
|
|
101
|
+
hasBaseline={hasBaseline}
|
|
102
|
+
hasHeap={hasHeap}
|
|
103
|
+
hasBaselineHeap={hasBaselineHeap}
|
|
104
|
+
hasRejected={hasRejected}
|
|
105
|
+
visibility={visibility}
|
|
106
|
+
onToggle={toggle}
|
|
107
|
+
/>
|
|
108
|
+
{numBatches > 1 && (
|
|
109
|
+
<BatchStepper batch={activeBatch} total={numBatches} onChange={setBatch} />
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
<TimeSeriesPlot
|
|
113
|
+
benchmarks={benchmarks}
|
|
114
|
+
flat={viewFlat}
|
|
115
|
+
index={index}
|
|
116
|
+
visibility={visibility}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="plot-container">
|
|
120
|
+
<div class="plot-title">Time Distribution</div>
|
|
121
|
+
<div class="plot-description">
|
|
122
|
+
Frequency distribution of execution times
|
|
123
|
+
</div>
|
|
124
|
+
<HistogramPlot benchmarks={benchmarks} flat={viewFlat} index={index} />
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface ToggleProps {
|
|
132
|
+
hasBaseline: boolean;
|
|
133
|
+
hasHeap: boolean;
|
|
134
|
+
hasBaselineHeap: boolean;
|
|
135
|
+
hasRejected: boolean;
|
|
136
|
+
visibility: SeriesVisibility;
|
|
137
|
+
onToggle: (key: keyof SeriesVisibility) => void;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Pill button that toggles a boolean state with active/inactive styling. */
|
|
141
|
+
function TogglePill(
|
|
142
|
+
{ label, active, onClick }: { label: string; active: boolean; onClick: () => void },
|
|
143
|
+
) {
|
|
144
|
+
return (
|
|
145
|
+
<button class={`toggle-pill${active ? " active" : ""}`} onClick={onClick}>
|
|
146
|
+
{label}
|
|
147
|
+
</button>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Visibility toggles for optional series (baseline, heap, rejected). */
|
|
152
|
+
function SeriesToggles(props: ToggleProps) {
|
|
153
|
+
const { hasBaseline, hasHeap, hasBaselineHeap, hasRejected, visibility, onToggle } = props;
|
|
154
|
+
if (!hasBaseline && !hasHeap && !hasRejected) return null;
|
|
155
|
+
return (
|
|
156
|
+
<div class="series-toggles">
|
|
157
|
+
{hasBaseline && <TogglePill label="baseline" active={visibility.baseline} onClick={() => onToggle("baseline")} />}
|
|
158
|
+
{hasHeap && <TogglePill label="heap" active={visibility.heap} onClick={() => onToggle("heap")} />}
|
|
159
|
+
{hasBaselineHeap && <TogglePill label="heap (baseline)" active={visibility.baselineHeap} onClick={() => onToggle("baselineHeap")} />}
|
|
160
|
+
{hasRejected && <TogglePill label="rejected" active={visibility.rejected} onClick={() => onToggle("rejected")} />}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Prev/next stepper for cycling through batches or showing all. */
|
|
166
|
+
function BatchStepper({ batch, total, onChange }: {
|
|
167
|
+
batch: number; total: number; onChange: (batch: number) => void;
|
|
168
|
+
}) {
|
|
169
|
+
const prev = () => onChange(batch <= 0 ? total : batch - 1);
|
|
170
|
+
const next = () => onChange(batch >= total ? 0 : batch + 1);
|
|
171
|
+
const label = batch === 0 ? "All" : `Batch ${batch} of ${total}`;
|
|
172
|
+
return (
|
|
173
|
+
<div class="batch-stepper">
|
|
174
|
+
<button class="batch-btn" onClick={prev}>‹</button>
|
|
175
|
+
<span class="batch-label">{label}</span>
|
|
176
|
+
<button class="batch-btn" onClick={next}>›</button>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface PlotProps { benchmarks: PreparedBenchmark[]; flat: FlattenedData; index: number }
|
|
182
|
+
interface TimeSeriesPlotProps extends PlotProps { visibility: SeriesVisibility }
|
|
183
|
+
|
|
184
|
+
/** Lazy-imports and renders a time-series chart for one benchmark group. */
|
|
185
|
+
function TimeSeriesPlot({ flat, index, visibility }: TimeSeriesPlotProps) {
|
|
186
|
+
const ref = useLazyPlot(async () => {
|
|
187
|
+
if (flat.timeSeries.length === 0) return null;
|
|
188
|
+
const { createSampleTimeSeries } = await import("../plots/SampleTimeSeries.ts");
|
|
189
|
+
const { timeSeries, allGcEvents, allPausePoints, heapSeries, baselineHeapSeries } = flat;
|
|
190
|
+
return createSampleTimeSeries(
|
|
191
|
+
timeSeries, allGcEvents, allPausePoints, heapSeries, baselineHeapSeries, visibility,
|
|
192
|
+
);
|
|
193
|
+
}, [flat, visibility], "Time series plot");
|
|
194
|
+
return (
|
|
195
|
+
<div id={`sample-timeseries-${index}`} class="plot-area" ref={ref}>
|
|
196
|
+
<div class="loading">Loading time series...</div>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Lazy-imports and renders a histogram with KDE for one benchmark group. */
|
|
202
|
+
function HistogramPlot({ benchmarks, flat, index }: PlotProps) {
|
|
203
|
+
const names = benchmarks.map(b => b.name);
|
|
204
|
+
const ref = useLazyPlot(async () => {
|
|
205
|
+
if (flat.allSamples.length === 0) return null;
|
|
206
|
+
const { createHistogramKde } = await import("../plots/HistogramKde.ts");
|
|
207
|
+
return createHistogramKde(flat.allSamples, names);
|
|
208
|
+
}, [flat, benchmarks], "Histogram plot");
|
|
209
|
+
return (
|
|
210
|
+
<div id={`histogram-${index}`} class="plot-area" ref={ref}>
|
|
211
|
+
<div class="loading">Loading histogram...</div>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect } from "preact/hooks";
|
|
2
|
+
import { provider, sourceTabs } from "../State.ts";
|
|
3
|
+
import { openSourceTab } from "./SourcePanel.tsx";
|
|
4
|
+
import { TabBar } from "./TabBar.tsx";
|
|
5
|
+
import { TabContent } from "./TabContent.tsx";
|
|
6
|
+
|
|
7
|
+
/** Root viewer shell: listens for `open-source` messages, renders tab bar and content. */
|
|
8
|
+
export function Shell() {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
function onMessage(ev: MessageEvent): void {
|
|
11
|
+
if (ev.data?.type === "open-source") {
|
|
12
|
+
const { file, line, col } = ev.data;
|
|
13
|
+
if (file) openSourceTab(file, line, col);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
window.addEventListener("message", onMessage);
|
|
17
|
+
return () => window.removeEventListener("message", onMessage);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<TabBar />
|
|
23
|
+
<TabContent />
|
|
24
|
+
</>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -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
|
+
|