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,261 @@
|
|
|
1
|
+
import colors from "../../report/Colors.ts";
|
|
2
|
+
import { formatBytes } from "../../report/Formatters.ts";
|
|
3
|
+
import type { HeapProfile, HeapSample } from "./HeapSampler.ts";
|
|
4
|
+
import {
|
|
5
|
+
type ResolvedFrame,
|
|
6
|
+
type ResolvedProfile,
|
|
7
|
+
resolveProfile,
|
|
8
|
+
} from "./ResolvedProfile.ts";
|
|
9
|
+
|
|
10
|
+
/** An allocation site with byte totals, call stack, and optional raw samples */
|
|
11
|
+
export interface HeapSite {
|
|
12
|
+
name: string;
|
|
13
|
+
url: string;
|
|
14
|
+
/** 1-indexed */
|
|
15
|
+
line: number;
|
|
16
|
+
col?: number;
|
|
17
|
+
bytes: number;
|
|
18
|
+
/** Call stack from root to this frame */
|
|
19
|
+
stack?: ResolvedFrame[];
|
|
20
|
+
/** Individual allocation samples at this site */
|
|
21
|
+
samples?: HeapSample[];
|
|
22
|
+
/** Distinct caller paths with byte weights (populated by {@link aggregateSites}) */
|
|
23
|
+
callers?: { stack: ResolvedFrame[]; bytes: number }[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Predicate that returns true for user code (vs. runtime internals) */
|
|
27
|
+
export type UserCodeFilter = (site: ResolvedFrame) => boolean;
|
|
28
|
+
|
|
29
|
+
/** Options for {@link formatHeapReport} */
|
|
30
|
+
export interface HeapReportOptions {
|
|
31
|
+
/** Max sites to display */
|
|
32
|
+
topN: number;
|
|
33
|
+
/** Caller stack frames to show per site (default 3) */
|
|
34
|
+
stackDepth?: number;
|
|
35
|
+
/** Multi-line format with file paths (default false) */
|
|
36
|
+
verbose?: boolean;
|
|
37
|
+
/** Dump every raw sample */
|
|
38
|
+
raw?: boolean;
|
|
39
|
+
/** Filter to user code only, hiding runtime internals */
|
|
40
|
+
userOnly?: boolean;
|
|
41
|
+
/** Predicate for user vs internal code (default {@link isNodeUserCode}) */
|
|
42
|
+
isUserCode?: UserCodeFilter;
|
|
43
|
+
/** Total bytes across all nodes (before filtering) */
|
|
44
|
+
totalAll?: number;
|
|
45
|
+
/** Total bytes for user code only */
|
|
46
|
+
totalUserCode?: number;
|
|
47
|
+
/** Number of samples taken */
|
|
48
|
+
sampleCount?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Sum selfSize across all nodes in profile (before any filtering) */
|
|
52
|
+
export function totalProfileBytes(profile: HeapProfile): number {
|
|
53
|
+
return resolveProfile(profile).totalBytes;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Flatten resolved profile into sorted list of allocation sites with call stacks.
|
|
57
|
+
* When raw samples are available, attaches them to corresponding sites. */
|
|
58
|
+
export function flattenProfile(resolved: ResolvedProfile): HeapSite[] {
|
|
59
|
+
const sites: HeapSite[] = [];
|
|
60
|
+
const nodeIdToSites = new Map<number, HeapSite[]>();
|
|
61
|
+
|
|
62
|
+
for (const node of resolved.allocationNodes) {
|
|
63
|
+
const site: HeapSite = {
|
|
64
|
+
...node.frame,
|
|
65
|
+
bytes: node.selfSize,
|
|
66
|
+
stack: node.stack,
|
|
67
|
+
};
|
|
68
|
+
sites.push(site);
|
|
69
|
+
const bucket = nodeIdToSites.get(node.nodeId) ?? [];
|
|
70
|
+
if (!bucket.length) nodeIdToSites.set(node.nodeId, bucket);
|
|
71
|
+
bucket.push(site);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const sample of resolved.sortedSamples ?? []) {
|
|
75
|
+
const matchingSites = nodeIdToSites.get(sample.nodeId);
|
|
76
|
+
if (!matchingSites) continue;
|
|
77
|
+
for (const site of matchingSites) {
|
|
78
|
+
if (!site.samples) site.samples = [];
|
|
79
|
+
site.samples.push(sample);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return sites.sort((a, b) => b.bytes - a.bytes);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Return true if the call frame is user code (excludes node: and internal/ URLs) */
|
|
87
|
+
export function isNodeUserCode(site: ResolvedFrame): boolean {
|
|
88
|
+
const { url } = site;
|
|
89
|
+
return (
|
|
90
|
+
!!url &&
|
|
91
|
+
!url.startsWith("node:") &&
|
|
92
|
+
!url.includes("(native)") &&
|
|
93
|
+
!url.includes("internal/")
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Return true if the call frame is user code (excludes chrome-extension:// and devtools:// URLs) */
|
|
98
|
+
export function isBrowserUserCode(site: ResolvedFrame): boolean {
|
|
99
|
+
const { url } = site;
|
|
100
|
+
return (
|
|
101
|
+
!!url &&
|
|
102
|
+
!url.startsWith("chrome-extension://") &&
|
|
103
|
+
!url.startsWith("devtools://") &&
|
|
104
|
+
!url.includes("(native)")
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Return only sites matching a user-code predicate (default: {@link isNodeUserCode}) */
|
|
109
|
+
export function filterSites(
|
|
110
|
+
sites: HeapSite[],
|
|
111
|
+
isUser: UserCodeFilter = isNodeUserCode,
|
|
112
|
+
): HeapSite[] {
|
|
113
|
+
return sites.filter(isUser);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Aggregate sites by location (combine same file:line:col).
|
|
117
|
+
* Tracks distinct caller stacks with byte weights when merging. */
|
|
118
|
+
export function aggregateSites(sites: HeapSite[]): HeapSite[] {
|
|
119
|
+
const byLocation = new Map<string, HeapSite>();
|
|
120
|
+
|
|
121
|
+
for (const site of sites) {
|
|
122
|
+
// When column is unknown, include name to avoid merging distinct sites
|
|
123
|
+
const colKey = site.col != null ? `${site.col}` : `?:${site.name}`;
|
|
124
|
+
const key = `${site.url}:${site.line}:${colKey}`;
|
|
125
|
+
const existing = byLocation.get(key);
|
|
126
|
+
if (existing) {
|
|
127
|
+
existing.bytes += site.bytes;
|
|
128
|
+
addCaller(existing, site);
|
|
129
|
+
} else {
|
|
130
|
+
const callers = site.stack
|
|
131
|
+
? [{ stack: site.stack, bytes: site.bytes }]
|
|
132
|
+
: undefined;
|
|
133
|
+
byLocation.set(key, { ...site, callers });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const site of byLocation.values()) {
|
|
138
|
+
if (!site.callers || site.callers.length <= 1) continue;
|
|
139
|
+
site.callers.sort((a, b) => b.bytes - a.bytes);
|
|
140
|
+
site.stack = site.callers[0].stack;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return [...byLocation.values()].sort((a, b) => b.bytes - a.bytes);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Format heap report for console output */
|
|
147
|
+
export function formatHeapReport(
|
|
148
|
+
sites: HeapSite[],
|
|
149
|
+
options: HeapReportOptions,
|
|
150
|
+
): string {
|
|
151
|
+
const { topN, stackDepth = 3, verbose = false } = options;
|
|
152
|
+
const { totalAll, totalUserCode, sampleCount, isUserCode } = options;
|
|
153
|
+
const isUser = isUserCode ?? isNodeUserCode;
|
|
154
|
+
const formatSite = verbose ? formatVerboseSite : formatCompactSite;
|
|
155
|
+
const lines: string[] = [];
|
|
156
|
+
lines.push(`Heap allocation sites (top ${topN}, garbage included):`);
|
|
157
|
+
|
|
158
|
+
for (const site of sites.slice(0, topN)) {
|
|
159
|
+
formatSite(lines, site, stackDepth, isUser);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
lines.push("");
|
|
163
|
+
if (totalAll !== undefined)
|
|
164
|
+
lines.push(`Total (all): ${fmtBytes(totalAll)}`);
|
|
165
|
+
if (totalUserCode !== undefined)
|
|
166
|
+
lines.push(`Total (user-code): ${fmtBytes(totalUserCode)}`);
|
|
167
|
+
if (sampleCount !== undefined)
|
|
168
|
+
lines.push(`Samples: ${sampleCount.toLocaleString()}`);
|
|
169
|
+
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Sum bytes across all sites */
|
|
174
|
+
export function totalBytes(sites: HeapSite[]): number {
|
|
175
|
+
return sites.reduce((sum, s) => sum + s.bytes, 0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Format every raw sample as one line, ordered by ordinal (time).
|
|
179
|
+
* Output is tab-separated for easy piping/grep/diff. */
|
|
180
|
+
export function formatRawSamples(resolved: ResolvedProfile): string {
|
|
181
|
+
const { sortedSamples, nodeMap } = resolved;
|
|
182
|
+
if (!sortedSamples || sortedSamples.length === 0)
|
|
183
|
+
return "No raw samples available.";
|
|
184
|
+
|
|
185
|
+
const header = "ordinal\tsize\tfunction\tlocation";
|
|
186
|
+
const rows = sortedSamples.map(s => {
|
|
187
|
+
const frame = nodeMap.get(s.nodeId)?.frame;
|
|
188
|
+
const fn = frame?.name || "(unknown)";
|
|
189
|
+
const url = frame?.url || "";
|
|
190
|
+
const loc = url ? fmtLoc(url, frame!.line, frame!.col) : "(unknown)";
|
|
191
|
+
return `${s.ordinal}\t${s.size}\t${fn}\t${loc}`;
|
|
192
|
+
});
|
|
193
|
+
return [header, ...rows].join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Add a caller stack to an aggregated site, merging if the same path exists */
|
|
197
|
+
function addCaller(existing: HeapSite, site: HeapSite): void {
|
|
198
|
+
if (!site.stack) return;
|
|
199
|
+
existing.callers ??= [];
|
|
200
|
+
const key = callerKey(site.stack);
|
|
201
|
+
const match = existing.callers.find(c => callerKey(c.stack) === key);
|
|
202
|
+
if (match) match.bytes += site.bytes;
|
|
203
|
+
else existing.callers.push({ stack: site.stack, bytes: site.bytes });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Verbose multi-line format with file:// paths and line numbers */
|
|
207
|
+
function formatVerboseSite(
|
|
208
|
+
lines: string[],
|
|
209
|
+
site: HeapSite,
|
|
210
|
+
stackDepth: number,
|
|
211
|
+
isUser: UserCodeFilter,
|
|
212
|
+
): void {
|
|
213
|
+
const bytes = fmtBytes(site.bytes).padStart(10);
|
|
214
|
+
const loc = site.url ? fmtLoc(site.url, site.line, site.col) : "(unknown)";
|
|
215
|
+
const style = isUser(site) ? (s: string) => s : colors.dim;
|
|
216
|
+
lines.push(style(`${bytes} ${site.name} ${loc}`));
|
|
217
|
+
|
|
218
|
+
const userCallers = callerFrames(site, stackDepth).filter(
|
|
219
|
+
f => f.url && isUser(f),
|
|
220
|
+
);
|
|
221
|
+
for (const frame of userCallers) {
|
|
222
|
+
const loc = fmtLoc(frame.url, frame.line, frame.col);
|
|
223
|
+
lines.push(style(` <- ${frame.name} ${loc}`));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Compact single-line format: `49 MB fn1 <- fn2 <- fn3` */
|
|
228
|
+
function formatCompactSite(
|
|
229
|
+
lines: string[],
|
|
230
|
+
site: HeapSite,
|
|
231
|
+
stackDepth: number,
|
|
232
|
+
isUser: UserCodeFilter,
|
|
233
|
+
): void {
|
|
234
|
+
const bytes = fmtBytes(site.bytes).padStart(10);
|
|
235
|
+
const callers = callerFrames(site, stackDepth)
|
|
236
|
+
.filter(f => f.url && isUser(f))
|
|
237
|
+
.map(f => f.name);
|
|
238
|
+
const line = `${bytes} ${[site.name, ...callers].join(" <- ")}`;
|
|
239
|
+
lines.push(isUser(site) ? line : colors.dim(line));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Format bytes with a space separator, falling back to raw bytes */
|
|
243
|
+
function fmtBytes(bytes: number): string {
|
|
244
|
+
return formatBytes(bytes, { space: true }) ?? `${bytes} B`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Format location, omitting column when unknown */
|
|
248
|
+
function fmtLoc(url: string, line: number, col?: number): string {
|
|
249
|
+
return col != null ? `${url}:${line}:${col}` : `${url}:${line}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Serialize a call stack for dedup comparison */
|
|
253
|
+
function callerKey(stack: ResolvedFrame[]): string {
|
|
254
|
+
return stack.map(f => `${f.url}:${f.line}:${f.col}`).join("|");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Get caller frames (parent stack excluding self, reversed, truncated) */
|
|
258
|
+
function callerFrames(site: HeapSite, depth: number): ResolvedFrame[] {
|
|
259
|
+
if (!site.stack || site.stack.length <= 1) return [];
|
|
260
|
+
return site.stack.slice(0, -1).reverse().slice(0, depth);
|
|
261
|
+
}
|
|
@@ -1,26 +1,68 @@
|
|
|
1
1
|
import { Session } from "node:inspector/promises";
|
|
2
2
|
|
|
3
3
|
export interface HeapSampleOptions {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/** Bytes between samples (default 32768) */
|
|
5
|
+
samplingInterval?: number;
|
|
6
|
+
|
|
7
|
+
/** Max stack frames (default 64) */
|
|
8
|
+
stackDepth?: number;
|
|
9
|
+
|
|
10
|
+
/** Keep objects collected by minor GC (default true) */
|
|
11
|
+
includeMinorGC?: boolean;
|
|
12
|
+
|
|
13
|
+
/** Keep objects collected by major GC (default true) */
|
|
14
|
+
includeMajorGC?: boolean;
|
|
8
15
|
}
|
|
9
16
|
|
|
17
|
+
/** V8 call frame location within a profiled script */
|
|
18
|
+
export interface CallFrame {
|
|
19
|
+
/** Function name (empty string for anonymous) */
|
|
20
|
+
functionName: string;
|
|
21
|
+
|
|
22
|
+
/** Script URL or file path */
|
|
23
|
+
url: string;
|
|
24
|
+
|
|
25
|
+
/** Zero-based line number */
|
|
26
|
+
lineNumber: number;
|
|
27
|
+
|
|
28
|
+
/** Zero-based column number */
|
|
29
|
+
columnNumber?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Node in the V8 sampling heap profile tree */
|
|
10
33
|
export interface ProfileNode {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
columnNumber?: number;
|
|
16
|
-
};
|
|
34
|
+
/** Call site for this allocation node */
|
|
35
|
+
callFrame: CallFrame;
|
|
36
|
+
|
|
37
|
+
/** Bytes allocated directly at this node (not children) */
|
|
17
38
|
selfSize: number;
|
|
39
|
+
|
|
40
|
+
/** Unique node ID, links to {@link HeapSample.nodeId} */
|
|
41
|
+
id: number;
|
|
42
|
+
|
|
43
|
+
/** Child nodes in the call tree */
|
|
18
44
|
children?: ProfileNode[];
|
|
19
45
|
}
|
|
20
46
|
|
|
47
|
+
/** Individual heap allocation sample from V8's SamplingHeapProfiler */
|
|
48
|
+
export interface HeapSample {
|
|
49
|
+
/** Links to {@link ProfileNode.id} for stack lookup */
|
|
50
|
+
nodeId: number;
|
|
51
|
+
|
|
52
|
+
/** Allocation size in bytes */
|
|
53
|
+
size: number;
|
|
54
|
+
|
|
55
|
+
/** Monotonically increasing, gives temporal ordering */
|
|
56
|
+
ordinal: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** V8 sampling heap profile tree with optional per-allocation samples */
|
|
21
60
|
export interface HeapProfile {
|
|
61
|
+
/** Root of the profile call tree */
|
|
22
62
|
head: ProfileNode;
|
|
23
|
-
|
|
63
|
+
|
|
64
|
+
/** Per-allocation samples, if collected */
|
|
65
|
+
samples?: HeapSample[];
|
|
24
66
|
}
|
|
25
67
|
|
|
26
68
|
const defaultOptions: Required<HeapSampleOptions> = {
|
|
@@ -61,7 +103,6 @@ async function startSampling(
|
|
|
61
103
|
includeObjectsCollectedByMinorGC: opts.includeMinorGC,
|
|
62
104
|
includeObjectsCollectedByMajorGC: opts.includeMajorGC,
|
|
63
105
|
};
|
|
64
|
-
|
|
65
106
|
try {
|
|
66
107
|
await session.post("HeapProfiler.startSampling", params);
|
|
67
108
|
} catch {
|
|
@@ -72,7 +113,8 @@ async function startSampling(
|
|
|
72
113
|
}
|
|
73
114
|
}
|
|
74
115
|
|
|
116
|
+
/** Stop heap sampling and return the profile, casting past incomplete @types/node typings */
|
|
75
117
|
async function stopSampling(session: Session): Promise<HeapProfile> {
|
|
76
118
|
const { profile } = await session.post("HeapProfiler.stopSampling");
|
|
77
|
-
return profile as HeapProfile;
|
|
119
|
+
return profile as unknown as HeapProfile;
|
|
78
120
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CallFrame,
|
|
3
|
+
HeapProfile,
|
|
4
|
+
HeapSample,
|
|
5
|
+
ProfileNode,
|
|
6
|
+
} from "./HeapSampler.ts";
|
|
7
|
+
|
|
8
|
+
/** A call frame with display-ready 1-indexed source positions */
|
|
9
|
+
export interface ResolvedFrame {
|
|
10
|
+
/** Function name, "(anonymous)" when empty */
|
|
11
|
+
name: string;
|
|
12
|
+
|
|
13
|
+
/** Script URL or file path, "" when unknown */
|
|
14
|
+
url: string;
|
|
15
|
+
|
|
16
|
+
/** 1-indexed line number */
|
|
17
|
+
line: number;
|
|
18
|
+
|
|
19
|
+
/** 1-indexed column number (undefined when unknown) */
|
|
20
|
+
col?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A profile node with its resolved call stack from root to this node */
|
|
24
|
+
export interface ResolvedNode {
|
|
25
|
+
/** The call frame at this node */
|
|
26
|
+
frame: ResolvedFrame;
|
|
27
|
+
|
|
28
|
+
/** Call stack from root to this node (inclusive) */
|
|
29
|
+
stack: ResolvedFrame[];
|
|
30
|
+
|
|
31
|
+
/** Bytes allocated directly at this node */
|
|
32
|
+
selfSize: number;
|
|
33
|
+
|
|
34
|
+
/** V8 node ID, used to match {@link HeapSample.nodeId} */
|
|
35
|
+
nodeId: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Pre-resolved heap profile: single tree walk produces all derived data */
|
|
39
|
+
export interface ResolvedProfile {
|
|
40
|
+
/** All nodes from the profile tree, flattened */
|
|
41
|
+
nodes: ResolvedNode[];
|
|
42
|
+
|
|
43
|
+
/** nodeId -> ResolvedNode lookup */
|
|
44
|
+
nodeMap: Map<number, ResolvedNode>;
|
|
45
|
+
|
|
46
|
+
/** Nodes with selfSize > 0, sorted by selfSize descending */
|
|
47
|
+
allocationNodes: ResolvedNode[];
|
|
48
|
+
|
|
49
|
+
/** Samples sorted by ordinal (temporal order), if available */
|
|
50
|
+
sortedSamples: HeapSample[] | undefined;
|
|
51
|
+
|
|
52
|
+
/** Total bytes across all nodes (sum of selfSize) */
|
|
53
|
+
totalBytes: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Convert a V8 0-indexed CallFrame to a display-ready 1-indexed ResolvedFrame */
|
|
57
|
+
export function resolveCallFrame(cf: CallFrame): ResolvedFrame {
|
|
58
|
+
return {
|
|
59
|
+
name: cf.functionName || "(anonymous)",
|
|
60
|
+
url: cf.url || "",
|
|
61
|
+
line: cf.lineNumber + 1,
|
|
62
|
+
col: cf.columnNumber != null ? cf.columnNumber + 1 : undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Walk a HeapProfile tree once, producing a fully resolved intermediate form */
|
|
67
|
+
export function resolveProfile(profile: HeapProfile): ResolvedProfile {
|
|
68
|
+
const nodes: ResolvedNode[] = [];
|
|
69
|
+
const nodeMap = new Map<number, ResolvedNode>();
|
|
70
|
+
let totalBytes = 0;
|
|
71
|
+
|
|
72
|
+
function walk(node: ProfileNode, parentStack: ResolvedFrame[]): void {
|
|
73
|
+
const frame = resolveCallFrame(node.callFrame);
|
|
74
|
+
const stack = [...parentStack, frame];
|
|
75
|
+
const resolved: ResolvedNode = {
|
|
76
|
+
frame,
|
|
77
|
+
stack,
|
|
78
|
+
selfSize: node.selfSize,
|
|
79
|
+
nodeId: node.id,
|
|
80
|
+
};
|
|
81
|
+
nodes.push(resolved);
|
|
82
|
+
nodeMap.set(node.id, resolved);
|
|
83
|
+
totalBytes += node.selfSize;
|
|
84
|
+
for (const child of node.children ?? []) walk(child, stack);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
walk(profile.head, []);
|
|
88
|
+
|
|
89
|
+
const allocationNodes = nodes
|
|
90
|
+
.filter(n => n.selfSize > 0)
|
|
91
|
+
.sort((a, b) => b.selfSize - a.selfSize);
|
|
92
|
+
|
|
93
|
+
const sortedSamples = profile.samples
|
|
94
|
+
? [...profile.samples].sort((a, b) => a.ordinal - b.ordinal)
|
|
95
|
+
: undefined;
|
|
96
|
+
|
|
97
|
+
return { nodes, nodeMap, allocationNodes, sortedSamples, totalBytes };
|
|
98
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Session } from "node:inspector/promises";
|
|
2
|
+
import type { CallFrame } from "./HeapSampler.ts";
|
|
3
|
+
|
|
4
|
+
/** V8 CPU profile node (flat array element, not tree) */
|
|
5
|
+
export interface TimeProfileNode {
|
|
6
|
+
id: number;
|
|
7
|
+
callFrame: CallFrame;
|
|
8
|
+
hitCount?: number;
|
|
9
|
+
/** Child node IDs */
|
|
10
|
+
children?: number[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** V8 CPU profile returned by Profiler.stop */
|
|
14
|
+
export interface TimeProfile {
|
|
15
|
+
nodes: TimeProfileNode[];
|
|
16
|
+
/** Microseconds */
|
|
17
|
+
startTime: number;
|
|
18
|
+
/** Microseconds */
|
|
19
|
+
endTime: number;
|
|
20
|
+
/** Node IDs sampled at each tick */
|
|
21
|
+
samples?: number[];
|
|
22
|
+
/** Microseconds between samples */
|
|
23
|
+
timeDeltas?: number[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TimeProfileOptions {
|
|
27
|
+
/** Sampling interval in microseconds (default 1000 = 1ms) */
|
|
28
|
+
interval?: number;
|
|
29
|
+
/** External session to use (shares Profiler domain, caller manages enable/disable) */
|
|
30
|
+
session?: Session;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Run a function while sampling CPU time, return profile */
|
|
34
|
+
export async function withTimeProfiling<T>(
|
|
35
|
+
options: TimeProfileOptions,
|
|
36
|
+
fn: () => Promise<T> | T,
|
|
37
|
+
): Promise<{ result: T; profile: TimeProfile }> {
|
|
38
|
+
const { interval } = options;
|
|
39
|
+
const ownSession = !options.session;
|
|
40
|
+
const session = options.session ?? new Session();
|
|
41
|
+
if (ownSession) session.connect();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (ownSession) await session.post("Profiler.enable");
|
|
45
|
+
if (interval)
|
|
46
|
+
await session.post("Profiler.setSamplingInterval", { interval });
|
|
47
|
+
await session.post("Profiler.start");
|
|
48
|
+
const result = await fn();
|
|
49
|
+
const { profile } = await session.post("Profiler.stop");
|
|
50
|
+
return { result, profile: profile as unknown as TimeProfile };
|
|
51
|
+
} finally {
|
|
52
|
+
if (ownSession) {
|
|
53
|
+
await session.post("Profiler.disable");
|
|
54
|
+
session.disconnect();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { MeasuredResults } from "../runners/MeasuredResults.ts";
|
|
2
|
+
import { diffCIs } from "../stats/BootstrapDifference.ts";
|
|
3
|
+
import {
|
|
4
|
+
computeStat,
|
|
5
|
+
type DifferenceCI,
|
|
6
|
+
flipCI,
|
|
7
|
+
type StatKind,
|
|
8
|
+
swapDirection,
|
|
9
|
+
} from "../stats/StatisticalUtils.ts";
|
|
10
|
+
|
|
11
|
+
import type { AnyColumn } from "./text/TableReport.ts";
|
|
12
|
+
|
|
13
|
+
/** Options that affect baseline comparison statistics */
|
|
14
|
+
export interface ComparisonOptions {
|
|
15
|
+
/** Equivalence margin in percent (0 to disable) */
|
|
16
|
+
equivMargin?: number;
|
|
17
|
+
/** Disable Tukey trimming of outlier batches */
|
|
18
|
+
noBatchTrim?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Benchmark results with optional baseline for comparison */
|
|
22
|
+
export interface ReportGroup {
|
|
23
|
+
name: string;
|
|
24
|
+
reports: BenchmarkReport[];
|
|
25
|
+
baseline?: BenchmarkReport;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Results from a single benchmark run */
|
|
29
|
+
export interface BenchmarkReport {
|
|
30
|
+
name: string;
|
|
31
|
+
measuredResults: MeasuredResults;
|
|
32
|
+
metadata?: UnknownRecord;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A titled group of related columns (one per report section) */
|
|
36
|
+
export interface ReportSection {
|
|
37
|
+
title: string;
|
|
38
|
+
columns: ReportColumn[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A table column with optional comparison behavior */
|
|
42
|
+
export type ReportColumn = AnyColumn<Record<string, unknown>> & {
|
|
43
|
+
/** Add diff column after this column when baseline exists */
|
|
44
|
+
comparable?: boolean;
|
|
45
|
+
/** Set true for throughput metrics where higher values are better (e.g., lines/sec) */
|
|
46
|
+
higherIsBetter?: boolean;
|
|
47
|
+
/** Stat descriptor: framework computes value from samples via computeStat */
|
|
48
|
+
statKind?: StatKind;
|
|
49
|
+
/** Accessor for non-sample data (e.g., run count, metadata fields) */
|
|
50
|
+
value?: (results: MeasuredResults, metadata?: UnknownRecord) => unknown;
|
|
51
|
+
/** Convert a timing-domain value to display domain (e.g., ms to lines/sec) */
|
|
52
|
+
toDisplay?: (
|
|
53
|
+
timingValue: number,
|
|
54
|
+
metadata?: Record<string, unknown>,
|
|
55
|
+
) => number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type UnknownRecord = Record<string, unknown>;
|
|
59
|
+
|
|
60
|
+
/** Compute column values for a section from results + metadata.
|
|
61
|
+
* statKind columns: computeStat(samples, kind), then toDisplay.
|
|
62
|
+
* value columns: call the accessor directly. */
|
|
63
|
+
export function computeColumnValues(
|
|
64
|
+
section: ReportSection,
|
|
65
|
+
results: MeasuredResults,
|
|
66
|
+
metadata?: UnknownRecord,
|
|
67
|
+
): UnknownRecord {
|
|
68
|
+
return Object.fromEntries(
|
|
69
|
+
section.columns.map(col => {
|
|
70
|
+
const key = col.key ?? col.title;
|
|
71
|
+
if (col.value) return [key, col.value(results, metadata)];
|
|
72
|
+
if (col.statKind) {
|
|
73
|
+
const raw = computeStat(results.samples, col.statKind);
|
|
74
|
+
return [key, col.toDisplay ? col.toDisplay(raw, metadata) : raw];
|
|
75
|
+
}
|
|
76
|
+
return [key, undefined];
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Run each section's computeColumnValues and merge into one record */
|
|
82
|
+
export function extractSectionValues(
|
|
83
|
+
measuredResults: MeasuredResults,
|
|
84
|
+
sections: ReadonlyArray<ReportSection>,
|
|
85
|
+
metadata?: UnknownRecord,
|
|
86
|
+
): UnknownRecord {
|
|
87
|
+
const entries = sections.flatMap(s =>
|
|
88
|
+
Object.entries(computeColumnValues(s, measuredResults, metadata)),
|
|
89
|
+
);
|
|
90
|
+
return Object.fromEntries(entries);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** All reports in a group, including the baseline if present */
|
|
94
|
+
export function groupReports(group: ReportGroup): BenchmarkReport[] {
|
|
95
|
+
return group.baseline ? [...group.reports, group.baseline] : group.reports;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** True if any result in the groups has the specified field with a defined value */
|
|
99
|
+
export function hasField(
|
|
100
|
+
groups: ReportGroup[],
|
|
101
|
+
field: keyof MeasuredResults,
|
|
102
|
+
): boolean {
|
|
103
|
+
return groups.some(group =>
|
|
104
|
+
groupReports(group).some(
|
|
105
|
+
({ measuredResults }) => measuredResults[field] !== undefined,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @return true if the first comparable column in sections has higherIsBetter set */
|
|
111
|
+
export function isHigherIsBetter(sections: ReportSection[]): boolean {
|
|
112
|
+
return (
|
|
113
|
+
sections.flatMap(s => s.columns).find(c => c.comparable)?.higherIsBetter ??
|
|
114
|
+
false
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @return the first comparable column with a statKind across all sections */
|
|
119
|
+
export function findPrimaryColumn(
|
|
120
|
+
sections?: ReportSection[],
|
|
121
|
+
): ReportColumn | undefined {
|
|
122
|
+
if (!sections) return undefined;
|
|
123
|
+
return sections.flatMap(s => s.columns).find(c => c.comparable && c.statKind);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Bootstrap difference CI for a column, using batch structure when available */
|
|
127
|
+
export function computeDiffCI(
|
|
128
|
+
baseline: MeasuredResults | undefined,
|
|
129
|
+
current: MeasuredResults,
|
|
130
|
+
statKind: StatKind,
|
|
131
|
+
comparison?: ComparisonOptions,
|
|
132
|
+
higherIsBetter?: boolean,
|
|
133
|
+
): DifferenceCI | undefined {
|
|
134
|
+
if (!baseline?.samples?.length || !current.samples?.length) return undefined;
|
|
135
|
+
const { equivMargin, noBatchTrim } = comparison ?? {};
|
|
136
|
+
const rawCIs = diffCIs(
|
|
137
|
+
baseline.samples,
|
|
138
|
+
baseline.batchOffsets,
|
|
139
|
+
current.samples,
|
|
140
|
+
current.batchOffsets,
|
|
141
|
+
[statKind],
|
|
142
|
+
{ equivMargin, noBatchTrim },
|
|
143
|
+
);
|
|
144
|
+
if (!rawCIs[0]) return undefined;
|
|
145
|
+
return higherIsBetter ? swapDirection(flipCI(rawCIs[0])) : rawCIs[0];
|
|
146
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import pico from "picocolors";
|
|
2
|
+
import type { Colors } from "picocolors/types";
|
|
3
|
+
|
|
4
|
+
const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
|
|
5
|
+
|
|
6
|
+
/** Picocolors instance that disables color in test environments */
|
|
7
|
+
const colors: Colors = pico.createColors(!isTest);
|
|
8
|
+
|
|
9
|
+
export default colors;
|