benchforge 0.1.11 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +99 -294
- package/bin/benchforge +1 -2
- package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
- package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
- package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
- package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
- package/dist/{BenchRunner-BzyUfiyB.d.mts → BenchRunner-DglX1NOn.d.mts} +119 -66
- package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
- package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
- package/dist/Formatters-BWj3d4sv.mjs +95 -0
- package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
- package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
- package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
- package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
- package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
- package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
- package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
- package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
- package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
- package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
- package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
- package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
- package/dist/bin/benchforge.mjs +4 -5
- package/dist/bin/benchforge.mjs.map +1 -1
- package/dist/index.d.mts +711 -558
- package/dist/index.mjs +98 -3
- package/dist/index.mjs.map +1 -0
- package/dist/runners/WorkerScript.d.mts +12 -4
- package/dist/runners/WorkerScript.mjs +77 -105
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
- package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
- package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
- package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
- package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
- package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
- package/dist/viewer/index.html +19 -0
- package/dist/viewer/speedscope/LICENSE +21 -0
- package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
- package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
- package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
- package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
- package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
- package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
- package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
- package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
- package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
- package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
- package/dist/viewer/speedscope/file-format-schema.json +274 -0
- package/dist/viewer/speedscope/index.html +19 -0
- package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
- package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
- package/dist/viewer/speedscope/release.txt +3 -0
- package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
- package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
- package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
- package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
- package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
- package/package.json +52 -27
- package/src/bin/benchforge.ts +2 -2
- package/src/cli/AnalyzeArchive.ts +232 -0
- package/src/cli/BrowserBench.ts +322 -0
- package/src/cli/CliArgs.ts +164 -51
- package/src/cli/CliExport.ts +179 -0
- package/src/cli/CliOptions.ts +147 -0
- package/src/cli/CliReport.ts +197 -0
- package/src/cli/FilterBenchmarks.ts +18 -30
- package/src/cli/RunBenchCLI.ts +132 -866
- package/src/cli/SuiteRunner.ts +160 -0
- package/src/cli/ViewerServer.ts +282 -0
- package/src/export/AllocExport.ts +121 -0
- package/src/export/ArchiveExport.ts +146 -0
- package/src/export/ArchiveFormat.ts +50 -0
- package/src/export/CoverageExport.ts +148 -0
- package/src/export/EditorUri.ts +10 -0
- package/src/export/PerfettoExport.ts +64 -99
- package/src/export/SpeedscopeTypes.ts +98 -0
- package/src/export/TimeExport.ts +115 -0
- package/src/index.ts +86 -67
- package/src/matrix/BenchMatrix.ts +230 -0
- package/src/matrix/CaseLoader.ts +8 -6
- package/src/matrix/MatrixDirRunner.ts +153 -0
- package/src/matrix/MatrixFilter.ts +49 -47
- package/src/matrix/MatrixInlineRunner.ts +50 -0
- package/src/matrix/MatrixReport.ts +90 -250
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/profiling/browser/BenchLoop.ts +51 -0
- package/src/profiling/browser/BrowserCDP.ts +133 -0
- package/src/profiling/browser/BrowserGcStats.ts +33 -0
- package/src/profiling/browser/BrowserProfiler.ts +160 -0
- package/src/profiling/browser/CdpClient.ts +82 -0
- package/src/profiling/browser/CdpPage.ts +138 -0
- package/src/profiling/browser/ChromeLauncher.ts +158 -0
- package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
- package/src/profiling/browser/PageLoadMode.ts +61 -0
- package/src/profiling/node/CoverageSampler.ts +27 -0
- package/src/profiling/node/CoverageTypes.ts +23 -0
- package/src/profiling/node/HeapSampleReport.ts +261 -0
- package/src/{heap-sample → profiling/node}/HeapSampler.ts +1 -2
- package/src/{heap-sample → profiling/node}/ResolvedProfile.ts +18 -9
- package/src/profiling/node/TimeSampler.ts +57 -0
- package/src/report/BenchmarkReport.ts +146 -0
- package/src/report/Colors.ts +9 -0
- package/src/report/Formatters.ts +110 -0
- package/src/report/GcSections.ts +151 -0
- package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
- package/src/report/HtmlReport.ts +223 -0
- package/src/report/ParseStats.ts +73 -0
- package/src/report/StandardSections.ts +147 -0
- package/src/report/ViewerSections.ts +286 -0
- package/src/report/text/TableReport.ts +253 -0
- package/src/report/text/TextReport.ts +123 -0
- package/src/runners/AdaptiveWrapper.ts +116 -236
- package/src/runners/BenchRunner.ts +20 -15
- package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
- package/src/runners/CreateRunner.ts +5 -7
- package/src/runners/GcStats.ts +47 -50
- package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
- package/src/runners/MergeBatches.ts +123 -0
- package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
- package/src/runners/RunnerOrchestrator.ts +127 -243
- package/src/runners/RunnerUtils.ts +75 -1
- package/src/runners/SampleStats.ts +100 -0
- package/src/runners/TimingRunner.ts +244 -0
- package/src/runners/TimingUtils.ts +3 -2
- package/src/runners/WorkerScript.ts +135 -151
- package/src/stats/BootstrapDifference.ts +282 -0
- package/src/{PermutationTest.ts → stats/PermutationTest.ts} +8 -17
- package/src/stats/StatisticalUtils.ts +445 -0
- package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
- package/src/test/AdaptiveRunner.test.ts +39 -41
- package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +2 -2
- package/src/{tests → test}/BenchMatrix.test.ts +19 -16
- package/src/test/BenchmarkReport.test.ts +63 -13
- package/src/test/BrowserBench.e2e.test.ts +186 -17
- package/src/test/BrowserBench.test.ts +10 -5
- package/src/test/BuildTimeSection.test.ts +130 -0
- package/src/test/CapSamples.test.ts +82 -0
- package/src/test/CoverageExport.test.ts +115 -0
- package/src/test/CoverageSampler.test.ts +33 -0
- package/src/test/HeapAttribution.test.ts +14 -14
- package/src/{tests → test}/MatrixFilter.test.ts +1 -1
- package/src/{tests → test}/MatrixReport.test.ts +1 -1
- package/src/test/PermutationTest.test.ts +1 -1
- package/src/{tests → test}/RealDataValidation.test.ts +6 -6
- package/src/test/RunBenchCLI.test.ts +39 -38
- package/src/test/RunnerOrchestrator.test.ts +12 -12
- package/src/test/StatisticalUtils.test.ts +48 -12
- package/src/{table-util/test → test}/TableReport.test.ts +2 -2
- package/src/test/TestUtils.ts +12 -7
- package/src/test/TimeExport.test.ts +139 -0
- package/src/test/TimeSampler.test.ts +37 -0
- package/src/test/ViewerLive.e2e.test.ts +159 -0
- package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
- package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
- package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
- package/src/test/fixtures/cases/asyncCases.ts +9 -0
- package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
- package/src/test/fixtures/cases/variants/product.ts +2 -0
- package/src/test/fixtures/cases/variants/sum.ts +2 -0
- package/src/test/fixtures/discover/fast.ts +1 -0
- package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
- package/src/test/fixtures/invalid/bad.ts +1 -0
- package/src/test/fixtures/loader/fast.ts +1 -0
- package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
- package/src/test/fixtures/loader/stateful.ts +2 -0
- package/src/test/fixtures/stateful/stateful.ts +2 -0
- package/src/test/fixtures/variants/extra.ts +1 -0
- package/src/test/fixtures/variants/impl.ts +1 -0
- package/src/test/fixtures/worker/fast.ts +1 -0
- package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
- package/src/viewer/DateFormat.ts +30 -0
- package/src/viewer/Helpers.ts +23 -0
- package/src/viewer/LineData.ts +120 -0
- package/src/viewer/Providers.ts +191 -0
- package/src/viewer/ReportData.ts +123 -0
- package/src/viewer/State.ts +49 -0
- package/src/viewer/Theme.ts +15 -0
- package/src/viewer/components/App.tsx +73 -0
- package/src/viewer/components/DropZone.tsx +71 -0
- package/src/viewer/components/LazyPlot.ts +33 -0
- package/src/viewer/components/SamplesPanel.tsx +214 -0
- package/src/viewer/components/Shell.tsx +26 -0
- package/src/viewer/components/SourcePanel.tsx +216 -0
- package/src/viewer/components/SummaryPanel.tsx +332 -0
- package/src/viewer/components/TabBar.tsx +131 -0
- package/src/viewer/components/TabContent.tsx +46 -0
- package/src/viewer/components/ThemeToggle.tsx +50 -0
- package/src/viewer/index.html +20 -0
- package/src/viewer/main.tsx +4 -0
- package/src/viewer/plots/CIPlot.ts +313 -0
- package/src/{html/browser → viewer/plots}/HistogramKde.ts +33 -38
- package/src/viewer/plots/LegendUtils.ts +134 -0
- package/src/viewer/plots/PlotTypes.ts +85 -0
- package/src/viewer/plots/RenderPlots.ts +230 -0
- package/src/viewer/plots/SampleTimeSeries.ts +306 -0
- package/src/viewer/plots/SvgHelpers.ts +136 -0
- package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
- package/src/viewer/report.css +427 -0
- package/src/viewer/shell.css +357 -0
- package/src/viewer/tsconfig.json +11 -0
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs +0 -202
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +0 -1
- package/dist/GcStats-wX7Xyblu.mjs +0 -77
- package/dist/GcStats-wX7Xyblu.mjs.map +0 -1
- package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
- package/dist/TimingUtils-DwOwkc8G.mjs +0 -597
- package/dist/TimingUtils-DwOwkc8G.mjs.map +0 -1
- package/dist/browser/index.js +0 -914
- package/dist/src-B-DDaCa9.mjs +0 -3108
- package/dist/src-B-DDaCa9.mjs.map +0 -1
- package/src/BenchMatrix.ts +0 -380
- package/src/BenchmarkReport.ts +0 -161
- package/src/HtmlDataPrep.ts +0 -148
- package/src/StandardSections.ts +0 -261
- package/src/StatisticalUtils.ts +0 -175
- package/src/TypeUtil.ts +0 -8
- package/src/browser/BrowserGcStats.ts +0 -44
- package/src/browser/BrowserHeapSampler.ts +0 -271
- package/src/export/JsonExport.ts +0 -103
- package/src/export/JsonFormat.ts +0 -91
- package/src/export/SpeedscopeExport.ts +0 -202
- package/src/heap-sample/HeapSampleReport.ts +0 -269
- package/src/html/HtmlReport.ts +0 -131
- package/src/html/HtmlTemplate.ts +0 -284
- package/src/html/Types.ts +0 -88
- package/src/html/browser/CIPlot.ts +0 -287
- package/src/html/browser/LegendUtils.ts +0 -163
- package/src/html/browser/RenderPlots.ts +0 -263
- package/src/html/browser/SampleTimeSeries.ts +0 -389
- package/src/html/browser/Types.ts +0 -96
- package/src/html/browser/index.ts +0 -1
- package/src/html/index.ts +0 -17
- package/src/runners/BasicRunner.ts +0 -364
- package/src/table-util/ConvergenceFormatters.ts +0 -19
- package/src/table-util/Formatters.ts +0 -157
- package/src/table-util/README.md +0 -70
- package/src/table-util/TableReport.ts +0 -293
- package/src/tests/fixtures/cases/asyncCases.ts +0 -7
- package/src/tests/fixtures/cases/variants/product.ts +0 -2
- package/src/tests/fixtures/cases/variants/sum.ts +0 -2
- package/src/tests/fixtures/discover/fast.ts +0 -1
- package/src/tests/fixtures/invalid/bad.ts +0 -1
- package/src/tests/fixtures/loader/fast.ts +0 -1
- package/src/tests/fixtures/loader/stateful.ts +0 -2
- package/src/tests/fixtures/stateful/stateful.ts +0 -2
- package/src/tests/fixtures/variants/extra.ts +0 -1
- package/src/tests/fixtures/variants/impl.ts +0 -1
- package/src/tests/fixtures/worker/fast.ts +0 -1
- /package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
- /package/src/{table-util/test → test}/TableValueExtractor.ts +0 -0
|
@@ -0,0 +1,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;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CIDirection,
|
|
3
|
+
type DifferenceCI,
|
|
4
|
+
flipCI,
|
|
5
|
+
} from "../stats/StatisticalUtils.ts";
|
|
6
|
+
import colors from "./Colors.ts";
|
|
7
|
+
|
|
8
|
+
const { red, green } = colors;
|
|
9
|
+
|
|
10
|
+
const lowConfidence = 80;
|
|
11
|
+
|
|
12
|
+
/** Format time in milliseconds with appropriate units */
|
|
13
|
+
export function timeMs(ms: unknown): string | null {
|
|
14
|
+
if (typeof ms !== "number") return null;
|
|
15
|
+
if (ms < 0.001) return `${(ms * 1000000).toFixed(0)}ns`;
|
|
16
|
+
if (ms < 0.01) return `${(ms * 1000).toFixed(1)}μs`;
|
|
17
|
+
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
|
|
18
|
+
if (ms >= 10) return `${ms.toFixed(0)}ms`;
|
|
19
|
+
return `${ms.toFixed(2)}ms`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Format integer with thousand separators */
|
|
23
|
+
export function integer(x: unknown): string | null {
|
|
24
|
+
if (typeof x !== "number") return null;
|
|
25
|
+
return new Intl.NumberFormat("en-US").format(Math.round(x));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Format fraction as percentage (0.473 → 47.3%) */
|
|
29
|
+
export function percent(fraction: unknown, precision = 1): string | null {
|
|
30
|
+
if (typeof fraction !== "number") return null;
|
|
31
|
+
return `${Math.abs(fraction * 100).toFixed(precision)}%`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Format percentage difference between two values */
|
|
35
|
+
export function diffPercent(main: unknown, base: unknown): string {
|
|
36
|
+
if (typeof main !== "number" || typeof base !== "number") return " ";
|
|
37
|
+
const diff = main - base;
|
|
38
|
+
return coloredPercent(diff, base);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Format bytes with appropriate units. Use `space: true` for `1.5 KB` style. */
|
|
42
|
+
export function formatBytes(
|
|
43
|
+
bytes: unknown,
|
|
44
|
+
opts?: { space?: boolean },
|
|
45
|
+
): string | null {
|
|
46
|
+
if (typeof bytes !== "number") return null;
|
|
47
|
+
const s = opts?.space ? " " : "";
|
|
48
|
+
const [kb, mb, gb] = [1024, 1024 ** 2, 1024 ** 3];
|
|
49
|
+
if (bytes < kb) return `${bytes.toFixed(0)}${s}B`;
|
|
50
|
+
if (bytes < mb) return `${(bytes / kb).toFixed(1)}${s}KB`;
|
|
51
|
+
if (bytes < gb) return `${(bytes / mb).toFixed(1)}${s}MB`;
|
|
52
|
+
return `${(bytes / gb).toFixed(1)}${s}GB`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Format percentage difference with confidence interval.
|
|
56
|
+
* When higherIsBetter is true, flips the CI so positive = improvement. */
|
|
57
|
+
export function formatDiffWithCI(
|
|
58
|
+
value: unknown,
|
|
59
|
+
higherIsBetter?: boolean,
|
|
60
|
+
): string | null {
|
|
61
|
+
if (!isDifferenceCI(value)) return null;
|
|
62
|
+
const ci = higherIsBetter ? flipCI(value) : value;
|
|
63
|
+
const suffix = value.ciLevel === "sample" ? " *" : "";
|
|
64
|
+
const text = diffCIText(ci.percent, ci.ci) + suffix;
|
|
65
|
+
return colorByDirection(text, ci.direction);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @return truncated string with ellipsis if over maxLen */
|
|
69
|
+
export function truncate(str: string, maxLen = 30): string {
|
|
70
|
+
return str.length > maxLen ? str.slice(0, maxLen - 3) + "..." : str;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @return signed percentage string (e.g. "+1.2%", "-3.4%") */
|
|
74
|
+
export function formatSignedPercent(v: number): string {
|
|
75
|
+
const sign = v >= 0 ? "+" : "";
|
|
76
|
+
return `${sign}${v.toFixed(1)}%`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @return convergence percentage with color for low values */
|
|
80
|
+
export function formatConvergence(v: unknown): string {
|
|
81
|
+
if (typeof v !== "number") return "—";
|
|
82
|
+
const pct = `${Math.round(v)}%`;
|
|
83
|
+
return v < lowConfidence ? red(pct) : pct;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Format fraction as colored +/- percentage (positive = green, negative = red) */
|
|
87
|
+
function coloredPercent(numerator: number, denominator: number): string {
|
|
88
|
+
const fraction = numerator / denominator;
|
|
89
|
+
if (!Number.isFinite(fraction)) return " ";
|
|
90
|
+
const sign = fraction >= 0 ? "+" : "-";
|
|
91
|
+
const percentStr = `${sign}${percent(fraction)}`;
|
|
92
|
+
return fraction >= 0 ? green(percentStr) : red(percentStr);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isDifferenceCI(x: unknown): x is DifferenceCI {
|
|
96
|
+
return typeof x === "object" && x !== null && "ci" in x && "direction" in x;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** @return formatted "pct [lo, hi]" text for a diff with CI */
|
|
100
|
+
function diffCIText(pct: number, ci: [number, number]): string {
|
|
101
|
+
const [lo, hi] = ci.map(formatSignedPercent);
|
|
102
|
+
return `${formatSignedPercent(pct)} [${lo}, ${hi}]`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @return text colored green for faster/equivalent, red for slower */
|
|
106
|
+
function colorByDirection(text: string, direction: CIDirection): string {
|
|
107
|
+
if (direction === "faster" || direction === "equivalent") return green(text);
|
|
108
|
+
if (direction === "slower") return red(text);
|
|
109
|
+
return text;
|
|
110
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { NavTiming } from "../profiling/browser/BrowserProfiler.ts";
|
|
2
|
+
import type { MeasuredResults } from "../runners/MeasuredResults.ts";
|
|
3
|
+
import { average, median, percentile } from "../stats/StatisticalUtils.ts";
|
|
4
|
+
import type { ReportSection } from "./BenchmarkReport.ts";
|
|
5
|
+
import { formatBytes, integer, percent, timeMs } from "./Formatters.ts";
|
|
6
|
+
|
|
7
|
+
/** Report section: GC time as fraction of total benchmark time. */
|
|
8
|
+
export const gcSection: ReportSection = {
|
|
9
|
+
title: "gc",
|
|
10
|
+
columns: [
|
|
11
|
+
{
|
|
12
|
+
key: "gc",
|
|
13
|
+
title: "mean",
|
|
14
|
+
formatter: percent,
|
|
15
|
+
comparable: true,
|
|
16
|
+
value: (r: MeasuredResults) => {
|
|
17
|
+
const { nodeGcTime, time, samples } = r;
|
|
18
|
+
if (!nodeGcTime || !time?.avg) return undefined;
|
|
19
|
+
const totalBenchTime = time.avg * samples.length;
|
|
20
|
+
if (totalBenchTime <= 0) return undefined;
|
|
21
|
+
const gcFraction = nodeGcTime.inRun / totalBenchTime;
|
|
22
|
+
return gcFraction <= 1 ? gcFraction : undefined;
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Report section: detailed GC stats from --trace-gc-nvp. */
|
|
29
|
+
export const gcStatsSection: ReportSection = {
|
|
30
|
+
title: "gc",
|
|
31
|
+
columns: [
|
|
32
|
+
{
|
|
33
|
+
key: "allocPerIter",
|
|
34
|
+
title: "alloc/iter",
|
|
35
|
+
formatter: formatBytes,
|
|
36
|
+
comparable: true,
|
|
37
|
+
value: (r: MeasuredResults) => {
|
|
38
|
+
const { gcStats, samples } = r;
|
|
39
|
+
if (!gcStats) return undefined;
|
|
40
|
+
const alloc = gcStats.totalAllocated;
|
|
41
|
+
return alloc != null ? alloc / (samples.length || 1) : undefined;
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "collected",
|
|
46
|
+
title: "collected",
|
|
47
|
+
formatter: formatBytes,
|
|
48
|
+
comparable: true,
|
|
49
|
+
value: (r: MeasuredResults) => r.gcStats?.totalCollected || undefined,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: "scavenges",
|
|
53
|
+
title: "scav",
|
|
54
|
+
formatter: integer,
|
|
55
|
+
comparable: true,
|
|
56
|
+
value: (r: MeasuredResults) => r.gcStats?.scavenges,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "fullGCs",
|
|
60
|
+
title: "full",
|
|
61
|
+
formatter: integer,
|
|
62
|
+
comparable: true,
|
|
63
|
+
value: (r: MeasuredResults) => r.gcStats?.markCompacts,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: "promoPercent",
|
|
67
|
+
title: "promo%",
|
|
68
|
+
formatter: percent,
|
|
69
|
+
comparable: true,
|
|
70
|
+
value: (r: MeasuredResults) => {
|
|
71
|
+
const gs = r.gcStats;
|
|
72
|
+
if (!gs) return undefined;
|
|
73
|
+
const alloc = gs.totalAllocated;
|
|
74
|
+
return alloc && alloc > 0 ? (gs.totalPromoted ?? 0) / alloc : undefined;
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: "pausePerIter",
|
|
79
|
+
title: "pause/iter",
|
|
80
|
+
formatter: timeMs,
|
|
81
|
+
comparable: true,
|
|
82
|
+
value: (r: MeasuredResults) => {
|
|
83
|
+
const gs = r.gcStats;
|
|
84
|
+
return gs ? gs.gcPauseTime / (r.samples.length || 1) : undefined;
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Report section: browser GC stats from CDP tracing (subset of gcStatsSection). */
|
|
91
|
+
export const browserGcStatsSection: ReportSection = {
|
|
92
|
+
title: "gc",
|
|
93
|
+
columns: [
|
|
94
|
+
gcStatsSection.columns.find(c => c.key === "collected")!,
|
|
95
|
+
gcStatsSection.columns.find(c => c.key === "scavenges")!,
|
|
96
|
+
gcStatsSection.columns.find(c => c.key === "fullGCs")!,
|
|
97
|
+
{
|
|
98
|
+
key: "pausePerIter",
|
|
99
|
+
title: "pause",
|
|
100
|
+
formatter: timeMs,
|
|
101
|
+
comparable: true,
|
|
102
|
+
value: gcStatsSection.columns.find(c => c.key === "pausePerIter")!.value!,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Report sections: page-load stats (mean/p50/p99) across multiple iterations. */
|
|
108
|
+
export const pageLoadStatsSections: ReportSection[] = [
|
|
109
|
+
pageLoadSection("DCL", n => n.domContentLoaded || undefined),
|
|
110
|
+
pageLoadSection("load", n => n.loadEvent || undefined),
|
|
111
|
+
pageLoadSection("LCP", n => n.lcp),
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
/** @return GC stats sections if enabled by CLI flags */
|
|
115
|
+
export function gcSections(args: { "gc-stats"?: boolean }): ReportSection[] {
|
|
116
|
+
return args["gc-stats"] ? [gcStatsSection] : [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Build a page-load section with mean/p50/p99 columns from NavTiming data */
|
|
120
|
+
function pageLoadSection(
|
|
121
|
+
title: string,
|
|
122
|
+
extract: (n: NavTiming) => number | undefined,
|
|
123
|
+
): ReportSection {
|
|
124
|
+
const vals = (r: MeasuredResults) => navValues(r.navTimings, extract);
|
|
125
|
+
const col = (suffix: string, stat: (v: number[]) => number) => ({
|
|
126
|
+
key: `${title.toLowerCase()}${suffix}`,
|
|
127
|
+
title: suffix.toLowerCase(),
|
|
128
|
+
formatter: timeMs,
|
|
129
|
+
value: (r: MeasuredResults) => {
|
|
130
|
+
const v = vals(r);
|
|
131
|
+
return v.length ? stat(v) : undefined;
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
title,
|
|
136
|
+
columns: [
|
|
137
|
+
col("Mean", average),
|
|
138
|
+
col("P50", median),
|
|
139
|
+
col("P99", v => percentile(v, 0.99)),
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Extract one field from all NavTimings, filtering undefineds. */
|
|
145
|
+
function navValues(
|
|
146
|
+
navs: NavTiming[] | undefined,
|
|
147
|
+
fn: (n: NavTiming) => number | undefined,
|
|
148
|
+
): number[] {
|
|
149
|
+
if (!navs?.length) return [];
|
|
150
|
+
return navs.map(fn).filter((v): v is number => v != null);
|
|
151
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import
|
|
5
|
-
import { formatDateWithTimezone } from "./html/index.ts";
|
|
4
|
+
import { formatDateWithTimezone } from "../viewer/DateFormat.ts";
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
export
|
|
6
|
+
/** Git commit hash, date, and dirty status for version tracking */
|
|
7
|
+
export interface GitVersion {
|
|
8
|
+
hash: string;
|
|
9
|
+
date: string;
|
|
10
|
+
dirty?: boolean;
|
|
11
|
+
}
|
|
9
12
|
|
|
10
13
|
/** Get current git version info. For dirty repos, uses most recent modified file date. */
|
|
11
14
|
export function getCurrentGitVersion(): GitVersion | undefined {
|
|
@@ -14,7 +17,6 @@ export function getCurrentGitVersion(): GitVersion | undefined {
|
|
|
14
17
|
const hash = exec("git rev-parse --short HEAD");
|
|
15
18
|
const commitDate = exec("git log -1 --format=%aI");
|
|
16
19
|
const dirty = exec("git status --porcelain").length > 0;
|
|
17
|
-
|
|
18
20
|
const date = dirty
|
|
19
21
|
? (getMostRecentModifiedDate(".") ?? commitDate)
|
|
20
22
|
: commitDate;
|
|
@@ -54,24 +56,21 @@ export function getMostRecentModifiedDate(dir: string): string | undefined {
|
|
|
54
56
|
encoding: "utf-8",
|
|
55
57
|
cwd: dir,
|
|
56
58
|
});
|
|
57
|
-
const
|
|
59
|
+
const files = raw
|
|
58
60
|
.trim()
|
|
59
61
|
.split("\n")
|
|
60
|
-
.filter(
|
|
61
|
-
.map(
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map(l => l.slice(3));
|
|
64
|
+
if (!files.length) return undefined;
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
let mostRecent = 0;
|
|
66
|
-
for (const file of modifiedFiles) {
|
|
66
|
+
const mtime = (f: string): number => {
|
|
67
67
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
return statSync(join(dir, f)).mtimeMs;
|
|
69
|
+
} catch {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const mostRecent = Math.max(0, ...files.map(mtime));
|
|
75
74
|
return mostRecent > 0 ? new Date(mostRecent).toISOString() : undefined;
|
|
76
75
|
} catch {
|
|
77
76
|
return undefined;
|