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,639 @@
|
|
|
1
|
+
import { C as swapDirection, S as subsample, _ as prepareBlocks, a as computeInterval, c as defaultConfidence, d as maxBootstrapInput, f as maxOf, g as percentile, h as minOf, l as flipCI, o as computeStat, r as bootstrapSamples, s as createResample, t as average, u as isBootstrappable, v as quickSelect, x as statKindToFn, y as resampleInto } from "./StatisticalUtils-BD92crgM.mjs";
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import open from "open";
|
|
8
|
+
import sirv from "sirv";
|
|
9
|
+
//#region src/profiling/node/ResolvedProfile.ts
|
|
10
|
+
/** Convert a V8 0-indexed CallFrame to a display-ready 1-indexed ResolvedFrame */
|
|
11
|
+
function resolveCallFrame(cf) {
|
|
12
|
+
return {
|
|
13
|
+
name: cf.functionName || "(anonymous)",
|
|
14
|
+
url: cf.url || "",
|
|
15
|
+
line: cf.lineNumber + 1,
|
|
16
|
+
col: cf.columnNumber != null ? cf.columnNumber + 1 : void 0
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Walk a HeapProfile tree once, producing a fully resolved intermediate form */
|
|
20
|
+
function resolveProfile(profile) {
|
|
21
|
+
const nodes = [];
|
|
22
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
23
|
+
let totalBytes = 0;
|
|
24
|
+
function walk(node, parentStack) {
|
|
25
|
+
const frame = resolveCallFrame(node.callFrame);
|
|
26
|
+
const stack = [...parentStack, frame];
|
|
27
|
+
const resolved = {
|
|
28
|
+
frame,
|
|
29
|
+
stack,
|
|
30
|
+
selfSize: node.selfSize,
|
|
31
|
+
nodeId: node.id
|
|
32
|
+
};
|
|
33
|
+
nodes.push(resolved);
|
|
34
|
+
nodeMap.set(node.id, resolved);
|
|
35
|
+
totalBytes += node.selfSize;
|
|
36
|
+
for (const child of node.children ?? []) walk(child, stack);
|
|
37
|
+
}
|
|
38
|
+
walk(profile.head, []);
|
|
39
|
+
return {
|
|
40
|
+
nodes,
|
|
41
|
+
nodeMap,
|
|
42
|
+
allocationNodes: nodes.filter((n) => n.selfSize > 0).sort((a, b) => b.selfSize - a.selfSize),
|
|
43
|
+
sortedSamples: profile.samples ? [...profile.samples].sort((a, b) => a.ordinal - b.ordinal) : void 0,
|
|
44
|
+
totalBytes
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/stats/BootstrapDifference.ts
|
|
49
|
+
/** Shared-resample difference CI: one resample pair per iteration, all stats computed.
|
|
50
|
+
* @return DifferenceCI[] in same order as input stats. */
|
|
51
|
+
function multiSampleDifferenceCI(a, b, stats, options = {}) {
|
|
52
|
+
const { resamples = bootstrapSamples, confidence: conf = defaultConfidence } = options;
|
|
53
|
+
const subA = subsample(a, maxBootstrapInput);
|
|
54
|
+
const subB = subsample(b, maxBootstrapInput);
|
|
55
|
+
const bufA = new Array(subA.length);
|
|
56
|
+
const bufB = new Array(subB.length);
|
|
57
|
+
const ops = buildDiffOps(stats, subA.length, subB.length);
|
|
58
|
+
const allDiffs = ops.map(() => new Array(resamples));
|
|
59
|
+
const baseVals = ops.map((op) => op.pointEstimate(a));
|
|
60
|
+
const currVals = ops.map((op) => op.pointEstimate(b));
|
|
61
|
+
const observedPcts = ops.map((_, j) => (currVals[j] - baseVals[j]) / baseVals[j] * 100);
|
|
62
|
+
for (let i = 0; i < resamples; i++) {
|
|
63
|
+
resampleInto(subA, bufA);
|
|
64
|
+
resampleInto(subB, bufB);
|
|
65
|
+
for (let j = 0; j < ops.length; j++) {
|
|
66
|
+
const base = ops[j].computeA(bufA);
|
|
67
|
+
const curr = ops[j].computeB(bufB);
|
|
68
|
+
allDiffs[j][i] = (curr - base) / base * 100;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const capped = subA !== a || subB !== b;
|
|
72
|
+
const results = new Array(stats.length);
|
|
73
|
+
for (const op of ops) {
|
|
74
|
+
const j = op.execIndex;
|
|
75
|
+
const ci = computeInterval(allDiffs[j], conf);
|
|
76
|
+
results[op.origIndex] = {
|
|
77
|
+
percent: observedPcts[j],
|
|
78
|
+
ci,
|
|
79
|
+
direction: classifyDirection(ci, observedPcts[j], options.equivMargin),
|
|
80
|
+
histogram: binValues(allDiffs[j]),
|
|
81
|
+
ciLevel: "sample",
|
|
82
|
+
...capped && { subsampled: Math.max(a.length, b.length) }
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
/** Difference CIs for multiple stats, dispatching block vs sample automatically.
|
|
88
|
+
* Returns undefined for non-bootstrappable stats (min/max). */
|
|
89
|
+
function diffCIs(a, aOffsets, b, bOffsets, stats, options = {}) {
|
|
90
|
+
const bsStats = stats.filter(isBootstrappable);
|
|
91
|
+
if (bsStats.length === 0) return stats.map(() => void 0);
|
|
92
|
+
const bsResults = (aOffsets?.length ?? 0) >= 2 && (bOffsets?.length ?? 0) >= 2 ? bsStats.map((s) => blockDifferenceCI(a, aOffsets, b, statKindToFn(s), {
|
|
93
|
+
...options,
|
|
94
|
+
blocksB: bOffsets
|
|
95
|
+
})) : multiSampleDifferenceCI(a, b, bsStats, options);
|
|
96
|
+
const results = new Array(stats.length);
|
|
97
|
+
let bi = 0;
|
|
98
|
+
for (let i = 0; i < stats.length; i++) results[i] = isBootstrappable(stats[i]) ? bsResults[bi++] : void 0;
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
/** @return block bootstrap CI for percentage difference between baseline (a) and current (b).
|
|
102
|
+
* Tukey-trims outlier batches, then resamples per-block statFn values. Requires 2+ blocks. */
|
|
103
|
+
function blockDifferenceCI(a, blocksA, b, statFn, options = {}) {
|
|
104
|
+
const { resamples = bootstrapSamples, confidence: conf = defaultConfidence } = options;
|
|
105
|
+
const bB = options.blocksB ?? blocksA;
|
|
106
|
+
const noTrim = options.noBatchTrim;
|
|
107
|
+
const sideA = prepareBlocks(a, blocksA, statFn, noTrim);
|
|
108
|
+
const sideB = prepareBlocks(b, bB, statFn, noTrim);
|
|
109
|
+
const baseVal = statFn(sideA.filtered);
|
|
110
|
+
const observedPct = (statFn(sideB.filtered) - baseVal) / baseVal * 100;
|
|
111
|
+
const drawA = () => average(createResample(sideA.blockVals));
|
|
112
|
+
const drawB = () => average(createResample(sideB.blockVals));
|
|
113
|
+
const diffs = Array.from({ length: resamples }, () => {
|
|
114
|
+
const base = drawA();
|
|
115
|
+
return (drawB() - base) / base * 100;
|
|
116
|
+
});
|
|
117
|
+
const ci = computeInterval(diffs, conf);
|
|
118
|
+
return {
|
|
119
|
+
percent: observedPct,
|
|
120
|
+
ci,
|
|
121
|
+
direction: classifyDirection(ci, observedPct, options.equivMargin),
|
|
122
|
+
histogram: binValues(diffs),
|
|
123
|
+
trimmed: [sideA.trimCount, sideB.trimCount],
|
|
124
|
+
ciLevel: "block"
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** @return binned CI with histogram from a BootstrapResult */
|
|
128
|
+
function binBootstrapResult(result) {
|
|
129
|
+
const { estimate, ci, samples } = result;
|
|
130
|
+
return {
|
|
131
|
+
estimate,
|
|
132
|
+
ci,
|
|
133
|
+
histogram: binValues(samples)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** @return CI direction, with optional equivalence margin (in percent) */
|
|
137
|
+
function classifyDirection(ci, observed, margin) {
|
|
138
|
+
if (margin != null && margin > 0 && ci[0] >= -margin && ci[1] <= margin) return "equivalent";
|
|
139
|
+
if (ci[0] > 0 || ci[1] < 0) return observed < 0 ? "faster" : "slower";
|
|
140
|
+
return "uncertain";
|
|
141
|
+
}
|
|
142
|
+
/** @return values binned into histogram for compact visualization */
|
|
143
|
+
function binValues(values, binCount = 30) {
|
|
144
|
+
let min = values[0];
|
|
145
|
+
let max = values[0];
|
|
146
|
+
for (let i = 1; i < values.length; i++) {
|
|
147
|
+
if (values[i] < min) min = values[i];
|
|
148
|
+
if (values[i] > max) max = values[i];
|
|
149
|
+
}
|
|
150
|
+
if (min === max) return [{
|
|
151
|
+
x: min,
|
|
152
|
+
count: values.length
|
|
153
|
+
}];
|
|
154
|
+
const step = (max - min) / binCount;
|
|
155
|
+
const counts = new Array(binCount).fill(0);
|
|
156
|
+
for (const v of values) {
|
|
157
|
+
const bin = Math.min(Math.floor((v - min) / step), binCount - 1);
|
|
158
|
+
counts[bin]++;
|
|
159
|
+
}
|
|
160
|
+
return counts.map((count, i) => ({
|
|
161
|
+
x: min + (i + .5) * step,
|
|
162
|
+
count
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
/** Build diff operations: mean/min/max first (non-destructive), then percentiles ascending.
|
|
166
|
+
* Each side (A, B) gets its own quickSelect k values since sample sizes may differ. */
|
|
167
|
+
function buildDiffOps(stats, nA, nB) {
|
|
168
|
+
const uniform = (order, i, fn) => ({
|
|
169
|
+
order,
|
|
170
|
+
origIndex: i,
|
|
171
|
+
execIndex: 0,
|
|
172
|
+
computeA: fn,
|
|
173
|
+
computeB: fn,
|
|
174
|
+
pointEstimate: fn
|
|
175
|
+
});
|
|
176
|
+
const entries = stats.map((s, i) => {
|
|
177
|
+
if (s === "mean") return uniform(-3, i, average);
|
|
178
|
+
if (s === "min") return uniform(-2, i, minOf);
|
|
179
|
+
if (s === "max") return uniform(-1, i, maxOf);
|
|
180
|
+
const p = s.percentile;
|
|
181
|
+
const kA = Math.max(0, Math.ceil(nA * p) - 1);
|
|
182
|
+
const kB = Math.max(0, Math.ceil(nB * p) - 1);
|
|
183
|
+
return {
|
|
184
|
+
order: p,
|
|
185
|
+
origIndex: i,
|
|
186
|
+
execIndex: 0,
|
|
187
|
+
computeA: (buf) => quickSelect(buf, kA),
|
|
188
|
+
computeB: (buf) => quickSelect(buf, kB),
|
|
189
|
+
pointEstimate: (v) => percentile(v, p)
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
entries.sort((a, b) => a.order - b.order);
|
|
193
|
+
for (let i = 0; i < entries.length; i++) entries[i].execIndex = i;
|
|
194
|
+
return entries;
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/report/BenchmarkReport.ts
|
|
198
|
+
/** Compute column values for a section from results + metadata.
|
|
199
|
+
* statKind columns: computeStat(samples, kind), then toDisplay.
|
|
200
|
+
* value columns: call the accessor directly. */
|
|
201
|
+
function computeColumnValues(section, results, metadata) {
|
|
202
|
+
return Object.fromEntries(section.columns.map((col) => {
|
|
203
|
+
const key = col.key ?? col.title;
|
|
204
|
+
if (col.value) return [key, col.value(results, metadata)];
|
|
205
|
+
if (col.statKind) {
|
|
206
|
+
const raw = computeStat(results.samples, col.statKind);
|
|
207
|
+
return [key, col.toDisplay ? col.toDisplay(raw, metadata) : raw];
|
|
208
|
+
}
|
|
209
|
+
return [key, void 0];
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
/** Run each section's computeColumnValues and merge into one record */
|
|
213
|
+
function extractSectionValues(measuredResults, sections, metadata) {
|
|
214
|
+
const entries = sections.flatMap((s) => Object.entries(computeColumnValues(s, measuredResults, metadata)));
|
|
215
|
+
return Object.fromEntries(entries);
|
|
216
|
+
}
|
|
217
|
+
/** All reports in a group, including the baseline if present */
|
|
218
|
+
function groupReports(group) {
|
|
219
|
+
return group.baseline ? [...group.reports, group.baseline] : group.reports;
|
|
220
|
+
}
|
|
221
|
+
/** True if any result in the groups has the specified field with a defined value */
|
|
222
|
+
function hasField(groups, field) {
|
|
223
|
+
return groups.some((group) => groupReports(group).some(({ measuredResults }) => measuredResults[field] !== void 0));
|
|
224
|
+
}
|
|
225
|
+
/** @return true if the first comparable column in sections has higherIsBetter set */
|
|
226
|
+
function isHigherIsBetter(sections) {
|
|
227
|
+
return sections.flatMap((s) => s.columns).find((c) => c.comparable)?.higherIsBetter ?? false;
|
|
228
|
+
}
|
|
229
|
+
/** @return the first comparable column with a statKind across all sections */
|
|
230
|
+
function findPrimaryColumn(sections) {
|
|
231
|
+
if (!sections) return void 0;
|
|
232
|
+
return sections.flatMap((s) => s.columns).find((c) => c.comparable && c.statKind);
|
|
233
|
+
}
|
|
234
|
+
/** Bootstrap difference CI for a column, using batch structure when available */
|
|
235
|
+
function computeDiffCI(baseline, current, statKind, comparison, higherIsBetter) {
|
|
236
|
+
if (!baseline?.samples?.length || !current.samples?.length) return void 0;
|
|
237
|
+
const { equivMargin, noBatchTrim } = comparison ?? {};
|
|
238
|
+
const rawCIs = diffCIs(baseline.samples, baseline.batchOffsets, current.samples, current.batchOffsets, [statKind], {
|
|
239
|
+
equivMargin,
|
|
240
|
+
noBatchTrim
|
|
241
|
+
});
|
|
242
|
+
if (!rawCIs[0]) return void 0;
|
|
243
|
+
return higherIsBetter ? swapDirection(flipCI(rawCIs[0])) : rawCIs[0];
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/export/SpeedscopeTypes.ts
|
|
247
|
+
/** Create an empty FrameContext for building speedscope profiles. */
|
|
248
|
+
function frameContext() {
|
|
249
|
+
return {
|
|
250
|
+
frames: [],
|
|
251
|
+
index: /* @__PURE__ */ new Map()
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/** Wrap profiles in a SpeedscopeFile envelope */
|
|
255
|
+
function speedscopeFile(ctx, profiles) {
|
|
256
|
+
return {
|
|
257
|
+
$schema: "https://www.speedscope.app/file-format-schema.json",
|
|
258
|
+
shared: { frames: ctx.frames },
|
|
259
|
+
profiles,
|
|
260
|
+
exporter: "benchforge"
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/** Intern a call frame, returning its index in the shared frames array.
|
|
264
|
+
* All values should be 1-indexed (caller converts from V8's 0-indexed if needed). */
|
|
265
|
+
function internFrame(name, url, line, col, ctx) {
|
|
266
|
+
const key = `${name}\0${url}\0${line}\0${col}`;
|
|
267
|
+
const existing = ctx.index.get(key);
|
|
268
|
+
if (existing !== void 0) return existing;
|
|
269
|
+
const idx = ctx.frames.length;
|
|
270
|
+
const entry = { name: displayName(name, url, line) };
|
|
271
|
+
if (url) entry.file = url;
|
|
272
|
+
if (line > 0) entry.line = line;
|
|
273
|
+
if (col != null) entry.col = col;
|
|
274
|
+
ctx.frames.push(entry);
|
|
275
|
+
ctx.index.set(key, idx);
|
|
276
|
+
return idx;
|
|
277
|
+
}
|
|
278
|
+
/** Display name for a frame: named functions use their name, anonymous get a location hint */
|
|
279
|
+
function displayName(name, url, line) {
|
|
280
|
+
if (name !== "(anonymous)") return name;
|
|
281
|
+
const file = url?.split("/").pop();
|
|
282
|
+
return file ? `(anonymous ${file}:${line})` : "(anonymous)";
|
|
283
|
+
}
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/export/AllocExport.ts
|
|
286
|
+
/** Heap profile export to Speedscope format. */
|
|
287
|
+
/** Export heap profiles to speedscope JSON. Returns output path, or undefined if no profiles. */
|
|
288
|
+
function exportSpeedscope(groups, outputPath) {
|
|
289
|
+
const file = buildSpeedscopeFile(groups);
|
|
290
|
+
if (!file) {
|
|
291
|
+
console.log("No heap profiles to export.");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const absPath = resolve(outputPath);
|
|
295
|
+
writeFileSync(absPath, JSON.stringify(file));
|
|
296
|
+
console.log(`Speedscope profile exported to: ${outputPath}`);
|
|
297
|
+
return absPath;
|
|
298
|
+
}
|
|
299
|
+
/** Convert a single HeapProfile to speedscope format. */
|
|
300
|
+
function heapProfileToSpeedscope(name, profile) {
|
|
301
|
+
const ctx = frameContext();
|
|
302
|
+
return speedscopeFile(ctx, [buildProfile(name, resolveProfile(profile), ctx)]);
|
|
303
|
+
}
|
|
304
|
+
/** Build SpeedscopeFile from report groups. Returns undefined if no profiles found. */
|
|
305
|
+
function buildSpeedscopeFile(groups) {
|
|
306
|
+
const ctx = frameContext();
|
|
307
|
+
const profiles = [];
|
|
308
|
+
for (const group of groups) for (const report of groupReports(group)) {
|
|
309
|
+
const { heapProfile } = report.measuredResults;
|
|
310
|
+
if (!heapProfile) continue;
|
|
311
|
+
const resolved = resolveProfile(heapProfile);
|
|
312
|
+
profiles.push(buildProfile(report.name, resolved, ctx));
|
|
313
|
+
}
|
|
314
|
+
if (profiles.length === 0) return void 0;
|
|
315
|
+
return speedscopeFile(ctx, profiles);
|
|
316
|
+
}
|
|
317
|
+
/** Build a single speedscope profile from a resolved heap profile. */
|
|
318
|
+
function buildProfile(name, resolved, ctx) {
|
|
319
|
+
const intern = (f) => internFrame(f.name, f.url, f.line, f.col, ctx);
|
|
320
|
+
const nodeStacks = new Map(resolved.nodes.map((node) => [node.nodeId, node.stack.map(intern)]));
|
|
321
|
+
if (!resolved.sortedSamples?.length) {
|
|
322
|
+
console.error(`Speedscope export: no samples in heap profile for "${name}", skipping`);
|
|
323
|
+
return emptyProfile(name);
|
|
324
|
+
}
|
|
325
|
+
const samples = [];
|
|
326
|
+
const weights = [];
|
|
327
|
+
for (const sample of resolved.sortedSamples) {
|
|
328
|
+
const stack = nodeStacks.get(sample.nodeId);
|
|
329
|
+
if (stack) {
|
|
330
|
+
samples.push(stack);
|
|
331
|
+
weights.push(sample.size);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
type: "sampled",
|
|
336
|
+
name,
|
|
337
|
+
unit: "bytes",
|
|
338
|
+
startValue: 0,
|
|
339
|
+
endValue: weights.reduce((sum, w) => sum + w, 0),
|
|
340
|
+
samples,
|
|
341
|
+
weights
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/** Placeholder profile with no samples (used when heap data is missing). */
|
|
345
|
+
function emptyProfile(name) {
|
|
346
|
+
return {
|
|
347
|
+
type: "sampled",
|
|
348
|
+
name,
|
|
349
|
+
unit: "bytes",
|
|
350
|
+
startValue: 0,
|
|
351
|
+
endValue: 0,
|
|
352
|
+
samples: [],
|
|
353
|
+
weights: []
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/export/ArchiveFormat.ts
|
|
358
|
+
/** Migrate a parsed archive from older schema versions to current. */
|
|
359
|
+
function migrateArchive(raw) {
|
|
360
|
+
if ((raw.schema ?? 0) <= 1 && "profile" in raw && !("allocProfile" in raw)) {
|
|
361
|
+
raw.allocProfile = raw.profile;
|
|
362
|
+
delete raw.profile;
|
|
363
|
+
}
|
|
364
|
+
return raw;
|
|
365
|
+
}
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/export/ArchiveExport.ts
|
|
368
|
+
/** .benchforge archive creation, source collection, and archive filename derivation. */
|
|
369
|
+
/** Build a .benchforge archive file. Returns output path, or undefined if nothing to archive. */
|
|
370
|
+
async function archiveBenchmark(options) {
|
|
371
|
+
const { groups, reportData, timeProfileData, coverageData, outputPath } = options;
|
|
372
|
+
const allocProfile = buildSpeedscopeFile(groups) ?? void 0;
|
|
373
|
+
const timeProfile = timeProfileData ? JSON.parse(timeProfileData) : void 0;
|
|
374
|
+
if (!allocProfile && !timeProfile && !reportData) {
|
|
375
|
+
console.log("No data to archive.");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const allFrames = collectProfileFrames(allocProfile, timeProfile);
|
|
379
|
+
const sources = allFrames.length ? await collectSources(allFrames) : {};
|
|
380
|
+
const { archive, timestamp } = buildArchiveObject({
|
|
381
|
+
allocProfile,
|
|
382
|
+
timeProfile,
|
|
383
|
+
coverage: coverageData ? JSON.parse(coverageData) : void 0,
|
|
384
|
+
report: reportData,
|
|
385
|
+
sources
|
|
386
|
+
});
|
|
387
|
+
const filename = outputPath || defaultArchiveName(allocProfile, timestamp);
|
|
388
|
+
const absPath = resolve(filename);
|
|
389
|
+
writeFileSync(absPath, JSON.stringify(archive));
|
|
390
|
+
console.log(`Archive written to: ${filename}`);
|
|
391
|
+
return absPath;
|
|
392
|
+
}
|
|
393
|
+
function buildArchiveObject(input) {
|
|
394
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
395
|
+
return {
|
|
396
|
+
archive: {
|
|
397
|
+
schema: 2,
|
|
398
|
+
allocProfile: input.allocProfile,
|
|
399
|
+
timeProfile: input.timeProfile,
|
|
400
|
+
coverage: input.coverage,
|
|
401
|
+
report: input.report,
|
|
402
|
+
sources: input.sources,
|
|
403
|
+
metadata: {
|
|
404
|
+
timestamp,
|
|
405
|
+
benchforgeVersion: process.env.npm_package_version || "unknown"
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
timestamp
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function collectProfileFrames(allocProfile, timeProfile) {
|
|
412
|
+
const heapFrames = allocProfile?.shared?.frames ?? [];
|
|
413
|
+
const timeFrames = timeProfile?.shared?.frames ?? [];
|
|
414
|
+
return [...heapFrames, ...timeFrames];
|
|
415
|
+
}
|
|
416
|
+
/** Fetch source code for all unique file URLs in profile frames. */
|
|
417
|
+
async function collectSources(frames, cache) {
|
|
418
|
+
const urls = new Set(frames.map((f) => f.file).filter((u) => !!u));
|
|
419
|
+
const sources = {};
|
|
420
|
+
for (const url of urls) {
|
|
421
|
+
const cached = cache?.get(url);
|
|
422
|
+
const text = cached ?? await fetchSource(url);
|
|
423
|
+
if (text === void 0) continue;
|
|
424
|
+
sources[url] = text;
|
|
425
|
+
if (!cached) cache?.set(url, text);
|
|
426
|
+
}
|
|
427
|
+
return sources;
|
|
428
|
+
}
|
|
429
|
+
/** Derive archive filename from profile (or generic fallback). */
|
|
430
|
+
function defaultArchiveName(profile, timestamp) {
|
|
431
|
+
return profile ? archiveFileName(profile, timestamp) : `benchforge-${timestamp}.benchforge`;
|
|
432
|
+
}
|
|
433
|
+
/** Fetch source text from a file:// or http(s):// URL. */
|
|
434
|
+
async function fetchSource(url) {
|
|
435
|
+
try {
|
|
436
|
+
if (url.startsWith("file://")) return await readFile(fileURLToPath(url), "utf-8");
|
|
437
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(5e3) });
|
|
438
|
+
if (!resp.ok) return void 0;
|
|
439
|
+
return await resp.text();
|
|
440
|
+
} catch {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/** Derive an archive filename from the profile name (sanitizes URLs to safe filenames). */
|
|
445
|
+
function archiveFileName(file, timestamp) {
|
|
446
|
+
return `${(file.profiles[0]?.name || "profile").replace(/^https?:\/\//, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "profile"}-${timestamp}.benchforge`;
|
|
447
|
+
}
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/cli/ViewerServer.ts
|
|
450
|
+
/** Start the viewer HTTP server and open in browser. */
|
|
451
|
+
async function startViewerServer(options) {
|
|
452
|
+
const port = options.port ?? 3939;
|
|
453
|
+
const bound = await tryListen(createServer(createRequestHandler(options, new Map(Object.entries(options.sources ?? {})), sirv(join(packageRoot(), "dist/viewer"), { single: true }))), port);
|
|
454
|
+
const url = `http://localhost:${bound.port}`;
|
|
455
|
+
if (options.open !== false) await open(url);
|
|
456
|
+
console.log(`Viewer: ${url}`);
|
|
457
|
+
const close = () => {
|
|
458
|
+
bound.server.closeAllConnections();
|
|
459
|
+
bound.server.close();
|
|
460
|
+
};
|
|
461
|
+
return {
|
|
462
|
+
server: bound.server,
|
|
463
|
+
port: bound.port,
|
|
464
|
+
close
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
/** Open a .benchforge archive in the viewer. */
|
|
468
|
+
async function viewArchive(filePath) {
|
|
469
|
+
const content = await readFile(resolve(filePath), "utf-8");
|
|
470
|
+
const raw = JSON.parse(content);
|
|
471
|
+
const schema = raw.schema ?? 0;
|
|
472
|
+
if (schema > 2) {
|
|
473
|
+
const msg = `Archive schema version ${schema} is newer than supported (2).`;
|
|
474
|
+
console.error(`${msg} Please update benchforge to view this archive.`);
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
const archive = migrateArchive(raw);
|
|
478
|
+
const sources = archive.sources;
|
|
479
|
+
const { close } = await startViewerServer({
|
|
480
|
+
profileData: optionalJson(archive.allocProfile),
|
|
481
|
+
timeProfileData: optionalJson(archive.timeProfile),
|
|
482
|
+
coverageData: optionalJson(archive.coverage),
|
|
483
|
+
reportData: optionalJson(archive.report),
|
|
484
|
+
sources
|
|
485
|
+
});
|
|
486
|
+
await waitForCtrlC();
|
|
487
|
+
close();
|
|
488
|
+
}
|
|
489
|
+
/** Serialize a value to JSON if truthy, otherwise return undefined. */
|
|
490
|
+
function optionalJson(v) {
|
|
491
|
+
return v ? JSON.stringify(v) : void 0;
|
|
492
|
+
}
|
|
493
|
+
/** Wait for Ctrl+C (SIGINT) before resolving. */
|
|
494
|
+
function waitForCtrlC() {
|
|
495
|
+
return new Promise((resolve) => {
|
|
496
|
+
console.log("\nPress Ctrl+C to exit");
|
|
497
|
+
process.once("SIGINT", () => {
|
|
498
|
+
console.log();
|
|
499
|
+
resolve();
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/** Resolve the package root (dev: src/cli/ ==> up 2, dist: dist/ ==> up 1). */
|
|
504
|
+
function packageRoot() {
|
|
505
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
506
|
+
if (basename(thisDir) === "cli") return join(thisDir, "../..");
|
|
507
|
+
return join(thisDir, "..");
|
|
508
|
+
}
|
|
509
|
+
/** Build HTTP request handler with API routes and static asset fallback. */
|
|
510
|
+
function createRequestHandler(ctx, sourceCache, assets) {
|
|
511
|
+
const routes = {
|
|
512
|
+
"/api/config": (res) => {
|
|
513
|
+
const config = {
|
|
514
|
+
editorUri: ctx.editorUri || null,
|
|
515
|
+
hasReport: !!ctx.reportData,
|
|
516
|
+
hasProfile: !!ctx.profileData,
|
|
517
|
+
hasTimeProfile: !!ctx.timeProfileData,
|
|
518
|
+
hasCoverage: !!ctx.coverageData
|
|
519
|
+
};
|
|
520
|
+
res.setHeader("Content-Type", "application/json");
|
|
521
|
+
res.end(JSON.stringify(config));
|
|
522
|
+
},
|
|
523
|
+
"/api/report-data": (res) => sendJson(res, ctx.reportData, "report data"),
|
|
524
|
+
"/api/coverage": (res) => sendJson(res, ctx.coverageData, "coverage data"),
|
|
525
|
+
"/api/profile": (res) => sendJson(res, ctx.profileData, "profile data", true),
|
|
526
|
+
"/api/profile/alloc": (res) => sendJson(res, ctx.profileData, "profile data", true),
|
|
527
|
+
"/api/profile/time": (res) => sendJson(res, ctx.timeProfileData, "time profile data", true),
|
|
528
|
+
"/api/source": (res, query) => handleSourceRequest(res, query, sourceCache),
|
|
529
|
+
"/api/archive": (res, _q, method) => {
|
|
530
|
+
if (method !== "POST") {
|
|
531
|
+
res.statusCode = 405;
|
|
532
|
+
res.end("Method not allowed");
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
return handleArchiveRequest(res, ctx, sourceCache);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
return async (req, res) => {
|
|
539
|
+
const url = req.url || "/";
|
|
540
|
+
const qIdx = url.indexOf("?");
|
|
541
|
+
const pathname = qIdx >= 0 ? url.slice(0, qIdx) : url;
|
|
542
|
+
const query = qIdx >= 0 ? url.slice(qIdx + 1) : "";
|
|
543
|
+
const handler = routes[pathname];
|
|
544
|
+
if (handler) {
|
|
545
|
+
await handler(res, query, req.method || "GET");
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
assets(req, res, () => {
|
|
549
|
+
res.statusCode = 404;
|
|
550
|
+
res.end("Not found");
|
|
551
|
+
});
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
/** Listen on port, retrying on next port if EADDRINUSE. */
|
|
555
|
+
function tryListen(server, port, maxRetries = 10) {
|
|
556
|
+
return new Promise((resolve, reject) => {
|
|
557
|
+
let attempt = 0;
|
|
558
|
+
const listen = (p) => {
|
|
559
|
+
server.once("error", (err) => {
|
|
560
|
+
if (err.code === "EADDRINUSE" && attempt < maxRetries) {
|
|
561
|
+
attempt++;
|
|
562
|
+
listen(p + 1);
|
|
563
|
+
} else reject(err);
|
|
564
|
+
});
|
|
565
|
+
server.listen(p, () => {
|
|
566
|
+
server.removeAllListeners("error");
|
|
567
|
+
const addr = server.address();
|
|
568
|
+
resolve({
|
|
569
|
+
server,
|
|
570
|
+
port: typeof addr === "object" && addr ? addr.port : p
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
listen(port);
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
/** Send pre-serialized JSON or 404 if data is absent. */
|
|
578
|
+
function sendJson(res, data, label, cors = false) {
|
|
579
|
+
if (!data) {
|
|
580
|
+
res.statusCode = 404;
|
|
581
|
+
res.end(`No ${label}`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
res.setHeader("Content-Type", "application/json");
|
|
585
|
+
if (cors) res.setHeader("Access-Control-Allow-Origin", "*");
|
|
586
|
+
res.end(data);
|
|
587
|
+
}
|
|
588
|
+
/** Fetch source text by URL query param, caching for subsequent requests. */
|
|
589
|
+
async function handleSourceRequest(res, query, cache) {
|
|
590
|
+
const sourceUrl = new URLSearchParams(query).get("url");
|
|
591
|
+
if (!sourceUrl) {
|
|
592
|
+
res.statusCode = 400;
|
|
593
|
+
res.end("Missing url parameter");
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
let source = cache.get(sourceUrl);
|
|
598
|
+
if (source === void 0) {
|
|
599
|
+
source = await fetchSource(sourceUrl);
|
|
600
|
+
if (source === void 0) throw new Error("not found");
|
|
601
|
+
cache.set(sourceUrl, source);
|
|
602
|
+
}
|
|
603
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
604
|
+
res.end(source);
|
|
605
|
+
} catch {
|
|
606
|
+
res.statusCode = 404;
|
|
607
|
+
res.end("Source unavailable");
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/** Build a .benchforge archive from current session data and send as download. */
|
|
611
|
+
async function handleArchiveRequest(res, ctx, sourceCache) {
|
|
612
|
+
try {
|
|
613
|
+
const parse = (s) => s ? JSON.parse(s) : void 0;
|
|
614
|
+
const profile = parse(ctx.profileData);
|
|
615
|
+
const timeProfile = parse(ctx.timeProfileData);
|
|
616
|
+
const coverage = parse(ctx.coverageData);
|
|
617
|
+
const report = parse(ctx.reportData);
|
|
618
|
+
const allFrames = collectProfileFrames(profile, timeProfile);
|
|
619
|
+
const { archive, timestamp } = buildArchiveObject({
|
|
620
|
+
allocProfile: profile,
|
|
621
|
+
timeProfile,
|
|
622
|
+
coverage,
|
|
623
|
+
report,
|
|
624
|
+
sources: allFrames.length ? await collectSources(allFrames, sourceCache) : Object.fromEntries(sourceCache)
|
|
625
|
+
});
|
|
626
|
+
const body = JSON.stringify(archive);
|
|
627
|
+
const filename = defaultArchiveName(profile, timestamp);
|
|
628
|
+
res.setHeader("Content-Type", "application/json");
|
|
629
|
+
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
|
630
|
+
res.end(body);
|
|
631
|
+
} catch {
|
|
632
|
+
res.statusCode = 500;
|
|
633
|
+
res.end("Archive failed");
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
//#endregion
|
|
637
|
+
export { resolveProfile as C, resolveCallFrame as S, groupReports as _, archiveBenchmark as a, binBootstrapResult as b, exportSpeedscope as c, internFrame as d, speedscopeFile as f, findPrimaryColumn as g, extractSectionValues as h, waitForCtrlC as i, heapProfileToSpeedscope as l, computeDiffCI as m, startViewerServer as n, collectSources as o, computeColumnValues as p, viewArchive as r, buildSpeedscopeFile as s, optionalJson as t, frameContext as u, hasField as v, diffCIs as x, isHigherIsBetter as y };
|
|
638
|
+
|
|
639
|
+
//# sourceMappingURL=ViewerServer-BJhdnxlN.mjs.map
|