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,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
coefficientOfVariation,
|
|
3
|
+
median,
|
|
4
|
+
medianAbsoluteDeviation,
|
|
5
|
+
percentile,
|
|
6
|
+
} from "../stats/StatisticalUtils.ts";
|
|
7
|
+
import {
|
|
8
|
+
type MeasuredResults,
|
|
9
|
+
type OptStatusInfo,
|
|
10
|
+
optStatusNames,
|
|
11
|
+
} from "./MeasuredResults.ts";
|
|
12
|
+
|
|
13
|
+
/** Compute percentiles, CV, MAD, and outlier rate from timing samples. */
|
|
14
|
+
export function computeStats(samples: number[]): MeasuredResults["time"] {
|
|
15
|
+
let min = Number.POSITIVE_INFINITY;
|
|
16
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
17
|
+
let sum = 0;
|
|
18
|
+
for (const s of samples) {
|
|
19
|
+
if (s < min) min = s;
|
|
20
|
+
if (s > max) max = s;
|
|
21
|
+
sum += s;
|
|
22
|
+
}
|
|
23
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
24
|
+
const pct = (p: number) =>
|
|
25
|
+
sorted[Math.max(0, Math.ceil(sorted.length * p) - 1)];
|
|
26
|
+
return {
|
|
27
|
+
min,
|
|
28
|
+
max,
|
|
29
|
+
avg: sum / samples.length,
|
|
30
|
+
p25: pct(0.25),
|
|
31
|
+
p50: pct(0.5),
|
|
32
|
+
p75: pct(0.75),
|
|
33
|
+
p95: pct(0.95),
|
|
34
|
+
p99: pct(0.99),
|
|
35
|
+
p999: pct(0.999),
|
|
36
|
+
cv: coefficientOfVariation(samples),
|
|
37
|
+
mad: medianAbsoluteDeviation(samples),
|
|
38
|
+
outlierRate: outlierImpactRatio(samples),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Measure outlier impact as proportion of excess time above 1.5*IQR threshold. */
|
|
43
|
+
export function outlierImpactRatio(samples: number[]): number {
|
|
44
|
+
if (samples.length === 0) return 0;
|
|
45
|
+
const med = median(samples);
|
|
46
|
+
const q75 = percentile(samples, 0.75);
|
|
47
|
+
const threshold = med + 1.5 * (q75 - med);
|
|
48
|
+
|
|
49
|
+
let excessTime = 0;
|
|
50
|
+
for (const sample of samples) {
|
|
51
|
+
if (sample > threshold) excessTime += sample - med;
|
|
52
|
+
}
|
|
53
|
+
const total = samples.reduce((a, b) => a + b, 0);
|
|
54
|
+
return total > 0 ? excessTime / total : 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Group samples by V8 optimization tier and count deopts. */
|
|
58
|
+
export function analyzeOptStatus(
|
|
59
|
+
samples: number[],
|
|
60
|
+
statuses: number[],
|
|
61
|
+
): OptStatusInfo | undefined {
|
|
62
|
+
if (statuses.length === 0 || statuses[0] === undefined) return undefined;
|
|
63
|
+
|
|
64
|
+
const byStatus = new Map<number, number[]>();
|
|
65
|
+
let deoptCount = 0;
|
|
66
|
+
for (let i = 0; i < samples.length; i++) {
|
|
67
|
+
const status = statuses[i];
|
|
68
|
+
if (status === undefined) continue;
|
|
69
|
+
if (status & 8) deoptCount++; // deopt flag (bit 3)
|
|
70
|
+
const group = byStatus.get(status);
|
|
71
|
+
if (group) group.push(samples[i]);
|
|
72
|
+
else byStatus.set(status, [samples[i]]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const entries = [...byStatus].map(([status, times]) => {
|
|
76
|
+
const name = optStatusNames[status] || `status=${status}`;
|
|
77
|
+
return [name, { count: times.length, medianMs: median(times) }] as const;
|
|
78
|
+
});
|
|
79
|
+
return { byTier: Object.fromEntries(entries), deoptCount };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @return runtime gc() function, or a no-op if --expose-gc wasn't passed. */
|
|
83
|
+
export function gcFunction(): () => void {
|
|
84
|
+
const gc = globalThis.gc ?? (globalThis as any).__gc;
|
|
85
|
+
if (gc) return gc;
|
|
86
|
+
console.warn("gc() not available, run node/bun with --expose-gc");
|
|
87
|
+
return () => {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @return function that reads V8 optimization status via %GetOptimizationStatus. */
|
|
91
|
+
export function createOptStatusGetter(): ((fn: unknown) => number) | undefined {
|
|
92
|
+
try {
|
|
93
|
+
// %GetOptimizationStatus returns a bitmask
|
|
94
|
+
const fn = new Function("f", "return %GetOptimizationStatus(f)");
|
|
95
|
+
fn(() => {});
|
|
96
|
+
return fn as (fn: unknown) => number;
|
|
97
|
+
} catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { getHeapStatistics } from "node:v8";
|
|
2
|
+
import type { BenchmarkSpec } from "./BenchmarkSpec.ts";
|
|
3
|
+
import type { BenchRunner, RunnerOptions } from "./BenchRunner.ts";
|
|
4
|
+
import { executeBenchmark } from "./BenchRunner.ts";
|
|
5
|
+
import type {
|
|
6
|
+
MeasuredResults,
|
|
7
|
+
OptStatusInfo,
|
|
8
|
+
PausePoint,
|
|
9
|
+
} from "./MeasuredResults.ts";
|
|
10
|
+
import {
|
|
11
|
+
analyzeOptStatus,
|
|
12
|
+
computeStats,
|
|
13
|
+
createOptStatusGetter,
|
|
14
|
+
gcFunction,
|
|
15
|
+
} from "./SampleStats.ts";
|
|
16
|
+
|
|
17
|
+
type CollectParams<T = unknown> = {
|
|
18
|
+
benchmark: BenchmarkSpec<T>;
|
|
19
|
+
maxTime: number;
|
|
20
|
+
maxIterations: number;
|
|
21
|
+
warmup: number;
|
|
22
|
+
params?: T;
|
|
23
|
+
skipWarmup?: boolean;
|
|
24
|
+
traceOpt?: boolean;
|
|
25
|
+
pauseWarmup?: number;
|
|
26
|
+
pauseFirst?: number;
|
|
27
|
+
pauseInterval?: number;
|
|
28
|
+
pauseDuration?: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type CollectResult = {
|
|
32
|
+
samples: number[];
|
|
33
|
+
warmupSamples: number[];
|
|
34
|
+
heapGrowth: number;
|
|
35
|
+
heapSamples: number[];
|
|
36
|
+
startTime: number;
|
|
37
|
+
optStatus?: OptStatusInfo;
|
|
38
|
+
optSamples?: number[];
|
|
39
|
+
pausePoints: PausePoint[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type SampleArrays = {
|
|
43
|
+
samples: number[];
|
|
44
|
+
heapSamples: number[];
|
|
45
|
+
optStatuses: number[];
|
|
46
|
+
pausePoints: PausePoint[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const defaultCollectOptions = {
|
|
50
|
+
maxTime: 5000,
|
|
51
|
+
maxIterations: 1000000,
|
|
52
|
+
warmup: 0,
|
|
53
|
+
traceOpt: false,
|
|
54
|
+
pauseWarmup: 0,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Timing-based runner that collects samples within time/iteration limits.
|
|
59
|
+
* Handles warmup, heap tracking, V8 optimization tracing, and periodic pauses.
|
|
60
|
+
*/
|
|
61
|
+
export class TimingRunner implements BenchRunner {
|
|
62
|
+
async runBench<T = unknown>(
|
|
63
|
+
benchmark: BenchmarkSpec<T>,
|
|
64
|
+
options: RunnerOptions,
|
|
65
|
+
params?: T,
|
|
66
|
+
): Promise<MeasuredResults[]> {
|
|
67
|
+
const opts = { ...defaultCollectOptions, ...(options as any) };
|
|
68
|
+
const collected = await collectSamples({ benchmark, params, ...opts });
|
|
69
|
+
return [buildMeasuredResults(benchmark.name, collected)];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Collect timing samples with warmup, heap tracking, and optional V8 opt tracing. */
|
|
74
|
+
async function collectSamples<T>(
|
|
75
|
+
config: CollectParams<T>,
|
|
76
|
+
): Promise<CollectResult> {
|
|
77
|
+
if (!config.maxIterations && !config.maxTime) {
|
|
78
|
+
throw new Error(`At least one of maxIterations or maxTime must be set`);
|
|
79
|
+
}
|
|
80
|
+
const warmupSamples = config.skipWarmup ? [] : await runWarmup(config);
|
|
81
|
+
const heapBefore = process.memoryUsage().heapUsed;
|
|
82
|
+
const { samples, heapSamples, optStatuses, pausePoints, startTime } =
|
|
83
|
+
await runSampleLoop(config);
|
|
84
|
+
if (samples.length === 0)
|
|
85
|
+
throw new Error(
|
|
86
|
+
`No samples collected for benchmark: ${config.benchmark.name}`,
|
|
87
|
+
);
|
|
88
|
+
const heapAfter = process.memoryUsage().heapUsed;
|
|
89
|
+
const heapGrowth =
|
|
90
|
+
Math.max(0, heapAfter - heapBefore) / 1024 / samples.length;
|
|
91
|
+
const optStatus = config.traceOpt
|
|
92
|
+
? analyzeOptStatus(samples, optStatuses)
|
|
93
|
+
: undefined;
|
|
94
|
+
const optSamples =
|
|
95
|
+
config.traceOpt && optStatuses.length > 0 ? optStatuses : undefined;
|
|
96
|
+
return {
|
|
97
|
+
samples,
|
|
98
|
+
warmupSamples,
|
|
99
|
+
heapGrowth,
|
|
100
|
+
heapSamples,
|
|
101
|
+
startTime,
|
|
102
|
+
optStatus,
|
|
103
|
+
optSamples,
|
|
104
|
+
pausePoints,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Assemble CollectResult into a MeasuredResults record. */
|
|
109
|
+
function buildMeasuredResults(
|
|
110
|
+
name: string,
|
|
111
|
+
collected: CollectResult,
|
|
112
|
+
): MeasuredResults {
|
|
113
|
+
const { samples, warmupSamples, heapSamples } = collected;
|
|
114
|
+
const { optStatus, optSamples, pausePoints, heapGrowth, startTime } =
|
|
115
|
+
collected;
|
|
116
|
+
const time = computeStats(samples);
|
|
117
|
+
const heapSize = { avg: heapGrowth, min: heapGrowth, max: heapGrowth };
|
|
118
|
+
return {
|
|
119
|
+
name,
|
|
120
|
+
samples,
|
|
121
|
+
warmupSamples,
|
|
122
|
+
heapSamples,
|
|
123
|
+
time,
|
|
124
|
+
heapSize,
|
|
125
|
+
startTime,
|
|
126
|
+
optStatus,
|
|
127
|
+
optSamples,
|
|
128
|
+
pausePoints,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Run warmup iterations with gc + settle time for V8 optimization. Returns warmup timings.
|
|
134
|
+
*
|
|
135
|
+
* V8 has 4 compilation tiers: Ignition (interpreter) ==> Sparkplug (baseline) ==>
|
|
136
|
+
* Maglev (mid-tier optimizer) ==> TurboFan (full optimizer). Tiering thresholds:
|
|
137
|
+
* - Ignition ==> Sparkplug: 8 invocations
|
|
138
|
+
* - Sparkplug ==> Maglev: 500 invocations
|
|
139
|
+
* - Maglev ==> TurboFan: 6000 invocations
|
|
140
|
+
*
|
|
141
|
+
* Optimization compilation happens on background threads and requires idle time
|
|
142
|
+
* on the main thread to complete. Without sufficient warmup + settle time,
|
|
143
|
+
* benchmarks exhibit bimodal timing: slow Sparkplug samples (~30% slower) mixed
|
|
144
|
+
* with fast optimized samples.
|
|
145
|
+
*
|
|
146
|
+
* The warmup iterations trigger the optimization decision, then settle time
|
|
147
|
+
* provides idle time for background compilation to finish before measurement.
|
|
148
|
+
*
|
|
149
|
+
* @see https://v8.dev/blog/sparkplug
|
|
150
|
+
* @see https://v8.dev/blog/maglev
|
|
151
|
+
* @see https://v8.dev/blog/background-compilation
|
|
152
|
+
*/
|
|
153
|
+
async function runWarmup<T>(config: CollectParams<T>): Promise<number[]> {
|
|
154
|
+
const gc = gcFunction();
|
|
155
|
+
const samples = new Array<number>(config.warmup);
|
|
156
|
+
for (let i = 0; i < config.warmup; i++) {
|
|
157
|
+
const start = performance.now();
|
|
158
|
+
executeBenchmark(config.benchmark, config.params);
|
|
159
|
+
samples[i] = performance.now() - start;
|
|
160
|
+
}
|
|
161
|
+
gc();
|
|
162
|
+
if (config.pauseWarmup) {
|
|
163
|
+
await new Promise(r => setTimeout(r, config.pauseWarmup));
|
|
164
|
+
gc();
|
|
165
|
+
}
|
|
166
|
+
return samples;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Collect timing samples with optional periodic pauses for V8 background compilation to complete. */
|
|
170
|
+
async function runSampleLoop<T>(
|
|
171
|
+
config: CollectParams<T>,
|
|
172
|
+
): Promise<SampleArrays & { startTime: number }> {
|
|
173
|
+
const { maxTime, maxIterations, pauseFirst } = config;
|
|
174
|
+
const { pauseInterval = 0, pauseDuration = 100 } = config;
|
|
175
|
+
const getOptStatus = config.traceOpt ? createOptStatusGetter() : undefined;
|
|
176
|
+
const trackOpt = !!getOptStatus;
|
|
177
|
+
const estimated = maxIterations || Math.ceil(maxTime / 0.1);
|
|
178
|
+
const arrays = createSampleArrays(estimated, trackOpt);
|
|
179
|
+
|
|
180
|
+
let count = 0;
|
|
181
|
+
let elapsed = 0;
|
|
182
|
+
let totalPauseTime = 0;
|
|
183
|
+
const startTime = Number(process.hrtime.bigint() / 1000n);
|
|
184
|
+
const loopStart = performance.now();
|
|
185
|
+
|
|
186
|
+
while (
|
|
187
|
+
(!maxIterations || count < maxIterations) &&
|
|
188
|
+
(!maxTime || elapsed < maxTime)
|
|
189
|
+
) {
|
|
190
|
+
const start = performance.now();
|
|
191
|
+
executeBenchmark(config.benchmark, config.params);
|
|
192
|
+
const end = performance.now();
|
|
193
|
+
arrays.samples[count] = end - start;
|
|
194
|
+
arrays.heapSamples[count] = getHeapStatistics().used_heap_size;
|
|
195
|
+
if (getOptStatus)
|
|
196
|
+
arrays.optStatuses[count] = getOptStatus(config.benchmark.fn);
|
|
197
|
+
count++;
|
|
198
|
+
|
|
199
|
+
if (shouldPause(count, pauseFirst, pauseInterval)) {
|
|
200
|
+
const sampleIndex = count - 1;
|
|
201
|
+
arrays.pausePoints.push({ sampleIndex, durationMs: pauseDuration });
|
|
202
|
+
const pauseStart = performance.now();
|
|
203
|
+
await new Promise(r => setTimeout(r, pauseDuration));
|
|
204
|
+
totalPauseTime += performance.now() - pauseStart;
|
|
205
|
+
}
|
|
206
|
+
elapsed = performance.now() - loopStart - totalPauseTime;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
trimArrays(arrays, count, trackOpt);
|
|
210
|
+
return { ...arrays, startTime };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Pre-allocate sample arrays to reduce GC pressure during measurement. */
|
|
214
|
+
function createSampleArrays(n: number, trackOpt: boolean): SampleArrays {
|
|
215
|
+
const arr = () => new Array<number>(n);
|
|
216
|
+
return {
|
|
217
|
+
samples: arr(),
|
|
218
|
+
heapSamples: arr(),
|
|
219
|
+
optStatuses: trackOpt ? arr() : [],
|
|
220
|
+
pausePoints: [],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** @return true if this iteration should pause for V8 background compilation. */
|
|
225
|
+
function shouldPause(
|
|
226
|
+
iter: number,
|
|
227
|
+
first: number | undefined,
|
|
228
|
+
interval: number,
|
|
229
|
+
): boolean {
|
|
230
|
+
if (first !== undefined && iter === first) return true;
|
|
231
|
+
if (interval <= 0) return false;
|
|
232
|
+
if (first === undefined) return iter % interval === 0;
|
|
233
|
+
return (iter - first) % interval === 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Trim pre-allocated arrays to the actual sample count. */
|
|
237
|
+
function trimArrays(
|
|
238
|
+
arrays: SampleArrays,
|
|
239
|
+
count: number,
|
|
240
|
+
trackOpt: boolean,
|
|
241
|
+
): void {
|
|
242
|
+
arrays.samples.length = arrays.heapSamples.length = count;
|
|
243
|
+
if (trackOpt) arrays.optStatuses.length = count;
|
|
244
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
/** Toggle for worker process timing logs (manual, not exposed as CLI flag) */
|
|
1
2
|
export const debugWorkerTiming = false;
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
+
/** Current time in ms, or 0 when debug timing is off (zero-cost no-op) */
|
|
4
5
|
export function getPerfNow(): number {
|
|
5
6
|
return debugWorkerTiming ? performance.now() : 0;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
+
/** Elapsed ms between marks, or 0 when debug timing is off */
|
|
9
10
|
export function getElapsed(startMark: number, endMark?: number): number {
|
|
10
11
|
if (!debugWorkerTiming) return 0;
|
|
11
12
|
const end = endMark ?? performance.now();
|