benchforge 0.1.8 → 0.1.11
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/README.md +69 -42
- package/dist/{BenchRunner-CSKN9zPy.d.mts → BenchRunner-BzyUfiyB.d.mts} +32 -8
- package/dist/{BrowserHeapSampler-DCeL42RE.mjs → BrowserHeapSampler-B6asLKWQ.mjs} +57 -57
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +1 -0
- package/dist/{GcStats-ByEovUi1.mjs → GcStats-wX7Xyblu.mjs} +15 -15
- package/dist/GcStats-wX7Xyblu.mjs.map +1 -0
- package/dist/HeapSampler-B8dtKHn1.mjs.map +1 -1
- package/dist/{TimingUtils-ClclVQ7E.mjs → TimingUtils-DwOwkc8G.mjs} +225 -225
- package/dist/TimingUtils-DwOwkc8G.mjs.map +1 -0
- package/dist/bin/benchforge.mjs +1 -1
- package/dist/browser/index.js +210 -210
- package/dist/index.d.mts +106 -48
- package/dist/index.mjs +3 -3
- package/dist/runners/WorkerScript.d.mts +1 -1
- package/dist/runners/WorkerScript.mjs +66 -66
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/{src-HfimYuW_.mjs → src-B-DDaCa9.mjs} +1250 -991
- package/dist/src-B-DDaCa9.mjs.map +1 -0
- package/package.json +4 -3
- package/src/BenchMatrix.ts +125 -125
- package/src/BenchmarkReport.ts +50 -45
- package/src/HtmlDataPrep.ts +21 -21
- package/src/PermutationTest.ts +24 -24
- package/src/StandardSections.ts +45 -45
- package/src/StatisticalUtils.ts +60 -61
- package/src/browser/BrowserGcStats.ts +5 -5
- package/src/browser/BrowserHeapSampler.ts +63 -63
- package/src/cli/CliArgs.ts +20 -6
- package/src/cli/FilterBenchmarks.ts +5 -5
- package/src/cli/RunBenchCLI.ts +533 -476
- package/src/export/JsonExport.ts +10 -10
- package/src/export/PerfettoExport.ts +74 -74
- package/src/export/SpeedscopeExport.ts +202 -0
- package/src/heap-sample/HeapSampleReport.ts +143 -70
- package/src/heap-sample/HeapSampler.ts +55 -12
- package/src/heap-sample/ResolvedProfile.ts +89 -0
- package/src/html/HtmlReport.ts +33 -33
- package/src/html/HtmlTemplate.ts +67 -67
- package/src/html/browser/CIPlot.ts +50 -50
- package/src/html/browser/HistogramKde.ts +13 -13
- package/src/html/browser/LegendUtils.ts +48 -48
- package/src/html/browser/RenderPlots.ts +98 -98
- package/src/html/browser/SampleTimeSeries.ts +79 -79
- package/src/index.ts +6 -0
- package/src/matrix/MatrixFilter.ts +6 -6
- package/src/matrix/MatrixReport.ts +96 -96
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/runners/AdaptiveWrapper.ts +151 -151
- package/src/runners/BasicRunner.ts +175 -175
- package/src/runners/BenchRunner.ts +8 -8
- package/src/runners/GcStats.ts +22 -22
- package/src/runners/RunnerOrchestrator.ts +168 -168
- package/src/runners/WorkerScript.ts +96 -96
- package/src/table-util/Formatters.ts +41 -36
- package/src/table-util/TableReport.ts +122 -122
- package/src/table-util/test/TableValueExtractor.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +7 -39
- package/src/test/HeapAttribution.test.ts +51 -0
- package/src/test/RunBenchCLI.test.ts +36 -11
- package/src/test/TestUtils.ts +24 -24
- package/src/test/fixtures/fn-export-bench.ts +3 -0
- package/src/test/fixtures/suite-export-bench.ts +16 -0
- package/src/tests/BenchMatrix.test.ts +12 -12
- package/src/tests/MatrixFilter.test.ts +15 -15
- package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
- package/dist/GcStats-ByEovUi1.mjs.map +0 -1
- package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
- package/dist/src-HfimYuW_.mjs.map +0 -1
package/src/BenchMatrix.ts
CHANGED
|
@@ -74,13 +74,6 @@ export interface MatrixResults {
|
|
|
74
74
|
variants: VariantResult[];
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/** @return true if variant is a StatefulVariant (has setup + run) */
|
|
78
|
-
export function isStatefulVariant<T, S>(
|
|
79
|
-
v: Variant<T, S>,
|
|
80
|
-
): v is StatefulVariant<T, S> {
|
|
81
|
-
return typeof v === "object" && "setup" in v && "run" in v;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
77
|
/** Options for runMatrix */
|
|
85
78
|
export interface RunMatrixOptions {
|
|
86
79
|
iterations?: number;
|
|
@@ -103,6 +96,22 @@ export interface RunMatrixOptions {
|
|
|
103
96
|
heapDepth?: number;
|
|
104
97
|
}
|
|
105
98
|
|
|
99
|
+
/** Context for running matrix benchmarks in worker mode */
|
|
100
|
+
interface DirMatrixContext<T> {
|
|
101
|
+
matrix: BenchMatrix<T>;
|
|
102
|
+
casesModule?: import("./matrix/CaseLoader.ts").CasesModule<T>;
|
|
103
|
+
baselineIds: string[];
|
|
104
|
+
caseIds: string[];
|
|
105
|
+
runnerOpts: RunnerOptions;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @return true if variant is a StatefulVariant (has setup + run) */
|
|
109
|
+
export function isStatefulVariant<T, S>(
|
|
110
|
+
v: Variant<T, S>,
|
|
111
|
+
): v is StatefulVariant<T, S> {
|
|
112
|
+
return typeof v === "object" && "setup" in v && "run" in v;
|
|
113
|
+
}
|
|
114
|
+
|
|
106
115
|
/** Run a BenchMatrix with inline variants or variantDir */
|
|
107
116
|
export async function runMatrix<T>(
|
|
108
117
|
matrix: BenchMatrix<T>,
|
|
@@ -127,36 +136,24 @@ function validateBaseline<T>(matrix: BenchMatrix<T>): void {
|
|
|
127
136
|
if (matrix.baselineDir && matrix.baselineVariant) throw new Error(msg);
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
maxIterations: options.iterations,
|
|
133
|
-
maxTime: options.maxTime ?? 1000,
|
|
134
|
-
warmup: options.warmup ?? 0,
|
|
135
|
-
collect: options.collect,
|
|
136
|
-
cpuCounters: options.cpuCounters,
|
|
137
|
-
traceOpt: options.traceOpt,
|
|
138
|
-
noSettle: options.noSettle,
|
|
139
|
-
pauseFirst: options.pauseFirst,
|
|
140
|
-
pauseInterval: options.pauseInterval,
|
|
141
|
-
pauseDuration: options.pauseDuration,
|
|
142
|
-
gcStats: options.gcStats,
|
|
143
|
-
heapSample: options.heapSample,
|
|
144
|
-
heapInterval: options.heapInterval,
|
|
145
|
-
heapDepth: options.heapDepth,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Load cases module and resolve filtered case IDs */
|
|
150
|
-
async function resolveCases<T>(
|
|
139
|
+
/** Run matrix with variantDir (worker mode for memory isolation) */
|
|
140
|
+
async function runMatrixWithDir<T>(
|
|
151
141
|
matrix: BenchMatrix<T>,
|
|
152
142
|
options: RunMatrixOptions,
|
|
153
|
-
) {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
143
|
+
): Promise<MatrixResults> {
|
|
144
|
+
const allVariantIds = await discoverVariants(matrix.variantDir!);
|
|
145
|
+
if (allVariantIds.length === 0) {
|
|
146
|
+
throw new Error(`No variants found in ${matrix.variantDir}`);
|
|
147
|
+
}
|
|
148
|
+
const variantIds = options.filteredVariants ?? allVariantIds;
|
|
149
|
+
|
|
150
|
+
const ctx = await createDirContext(matrix, options);
|
|
151
|
+
const variants = await runDirVariants(variantIds, ctx);
|
|
152
|
+
|
|
153
|
+
if (matrix.baselineVariant) {
|
|
154
|
+
applyBaselineVariant(variants, matrix.baselineVariant);
|
|
155
|
+
}
|
|
156
|
+
return { name: matrix.name, variants };
|
|
160
157
|
}
|
|
161
158
|
|
|
162
159
|
/** Run matrix with inline variants (non-worker mode) */
|
|
@@ -205,35 +202,6 @@ async function runMatrixInline<T>(
|
|
|
205
202
|
return { name: matrix.name, variants };
|
|
206
203
|
}
|
|
207
204
|
|
|
208
|
-
/** Context for running matrix benchmarks in worker mode */
|
|
209
|
-
interface DirMatrixContext<T> {
|
|
210
|
-
matrix: BenchMatrix<T>;
|
|
211
|
-
casesModule?: import("./matrix/CaseLoader.ts").CasesModule<T>;
|
|
212
|
-
baselineIds: string[];
|
|
213
|
-
caseIds: string[];
|
|
214
|
-
runnerOpts: RunnerOptions;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** Run matrix with variantDir (worker mode for memory isolation) */
|
|
218
|
-
async function runMatrixWithDir<T>(
|
|
219
|
-
matrix: BenchMatrix<T>,
|
|
220
|
-
options: RunMatrixOptions,
|
|
221
|
-
): Promise<MatrixResults> {
|
|
222
|
-
const allVariantIds = await discoverVariants(matrix.variantDir!);
|
|
223
|
-
if (allVariantIds.length === 0) {
|
|
224
|
-
throw new Error(`No variants found in ${matrix.variantDir}`);
|
|
225
|
-
}
|
|
226
|
-
const variantIds = options.filteredVariants ?? allVariantIds;
|
|
227
|
-
|
|
228
|
-
const ctx = await createDirContext(matrix, options);
|
|
229
|
-
const variants = await runDirVariants(variantIds, ctx);
|
|
230
|
-
|
|
231
|
-
if (matrix.baselineVariant) {
|
|
232
|
-
applyBaselineVariant(variants, matrix.baselineVariant);
|
|
233
|
-
}
|
|
234
|
-
return { name: matrix.name, variants };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
205
|
/** Create context for directory-based matrix execution */
|
|
238
206
|
async function createDirContext<T>(
|
|
239
207
|
matrix: BenchMatrix<T>,
|
|
@@ -260,6 +228,89 @@ async function runDirVariants<T>(
|
|
|
260
228
|
return variants;
|
|
261
229
|
}
|
|
262
230
|
|
|
231
|
+
/** Apply baselineVariant comparison - one variant is the reference for all others */
|
|
232
|
+
function applyBaselineVariant(
|
|
233
|
+
variants: VariantResult[],
|
|
234
|
+
baselineVariantId: string,
|
|
235
|
+
): void {
|
|
236
|
+
const baselineVariant = variants.find(v => v.id === baselineVariantId);
|
|
237
|
+
if (!baselineVariant) return;
|
|
238
|
+
|
|
239
|
+
const baselineByCase = new Map<string, MeasuredResults>();
|
|
240
|
+
for (const c of baselineVariant.cases) {
|
|
241
|
+
baselineByCase.set(c.caseId, c.measured);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const variant of variants) {
|
|
245
|
+
if (variant.id === baselineVariantId) continue;
|
|
246
|
+
for (const caseResult of variant.cases) {
|
|
247
|
+
const baseline = baselineByCase.get(caseResult.caseId);
|
|
248
|
+
if (baseline) {
|
|
249
|
+
caseResult.baseline = baseline;
|
|
250
|
+
caseResult.deltaPercent = computeDeltaPercent(
|
|
251
|
+
baseline,
|
|
252
|
+
caseResult.measured,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Load cases module and resolve filtered case IDs */
|
|
260
|
+
async function resolveCases<T>(
|
|
261
|
+
matrix: BenchMatrix<T>,
|
|
262
|
+
options: RunMatrixOptions,
|
|
263
|
+
) {
|
|
264
|
+
const casesModule = matrix.casesModule
|
|
265
|
+
? await loadCasesModule<T>(matrix.casesModule)
|
|
266
|
+
: undefined;
|
|
267
|
+
const allCaseIds = casesModule?.cases ?? matrix.cases ?? ["default"];
|
|
268
|
+
const caseIds = options.filteredCases ?? allCaseIds;
|
|
269
|
+
return { casesModule, caseIds };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function buildRunnerOptions(options: RunMatrixOptions): RunnerOptions {
|
|
273
|
+
return {
|
|
274
|
+
maxIterations: options.iterations,
|
|
275
|
+
maxTime: options.maxTime ?? 1000,
|
|
276
|
+
warmup: options.warmup ?? 0,
|
|
277
|
+
collect: options.collect,
|
|
278
|
+
cpuCounters: options.cpuCounters,
|
|
279
|
+
traceOpt: options.traceOpt,
|
|
280
|
+
noSettle: options.noSettle,
|
|
281
|
+
pauseFirst: options.pauseFirst,
|
|
282
|
+
pauseInterval: options.pauseInterval,
|
|
283
|
+
pauseDuration: options.pauseDuration,
|
|
284
|
+
gcStats: options.gcStats,
|
|
285
|
+
heapSample: options.heapSample,
|
|
286
|
+
heapInterval: options.heapInterval,
|
|
287
|
+
heapDepth: options.heapDepth,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Run a single variant with case data */
|
|
292
|
+
async function runVariant<T>(
|
|
293
|
+
variant: AnyVariant<T>,
|
|
294
|
+
caseData: T,
|
|
295
|
+
name: string,
|
|
296
|
+
runner: BasicRunner,
|
|
297
|
+
options: RunnerOptions,
|
|
298
|
+
): Promise<MeasuredResults> {
|
|
299
|
+
if (isStatefulVariant(variant)) {
|
|
300
|
+
const state = await variant.setup(caseData);
|
|
301
|
+
const [result] = await runner.runBench(
|
|
302
|
+
{ name, fn: () => variant.run(state) },
|
|
303
|
+
options,
|
|
304
|
+
);
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
const [result] = await runner.runBench(
|
|
308
|
+
{ name, fn: () => variant(caseData) },
|
|
309
|
+
options,
|
|
310
|
+
);
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
|
|
263
314
|
/** Run all cases for a single variant */
|
|
264
315
|
async function runDirVariantCases<T>(
|
|
265
316
|
variantId: string,
|
|
@@ -296,6 +347,16 @@ async function runDirVariantCases<T>(
|
|
|
296
347
|
return cases;
|
|
297
348
|
}
|
|
298
349
|
|
|
350
|
+
/** Compute delta percentage: (current - baseline) / baseline * 100 */
|
|
351
|
+
function computeDeltaPercent(
|
|
352
|
+
baseline: MeasuredResults,
|
|
353
|
+
current: MeasuredResults,
|
|
354
|
+
): number {
|
|
355
|
+
const baseAvg = average(baseline.samples);
|
|
356
|
+
if (baseAvg === 0) return 0;
|
|
357
|
+
return ((average(current.samples) - baseAvg) / baseAvg) * 100;
|
|
358
|
+
}
|
|
359
|
+
|
|
299
360
|
/** Run baseline variant if it exists in baselineDir */
|
|
300
361
|
async function runBaselineIfExists<T>(
|
|
301
362
|
variantId: string,
|
|
@@ -317,64 +378,3 @@ async function runBaselineIfExists<T>(
|
|
|
317
378
|
});
|
|
318
379
|
return measured;
|
|
319
380
|
}
|
|
320
|
-
|
|
321
|
-
/** Compute delta percentage: (current - baseline) / baseline * 100 */
|
|
322
|
-
function computeDeltaPercent(
|
|
323
|
-
baseline: MeasuredResults,
|
|
324
|
-
current: MeasuredResults,
|
|
325
|
-
): number {
|
|
326
|
-
const baseAvg = average(baseline.samples);
|
|
327
|
-
if (baseAvg === 0) return 0;
|
|
328
|
-
return ((average(current.samples) - baseAvg) / baseAvg) * 100;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** Apply baselineVariant comparison - one variant is the reference for all others */
|
|
332
|
-
function applyBaselineVariant(
|
|
333
|
-
variants: VariantResult[],
|
|
334
|
-
baselineVariantId: string,
|
|
335
|
-
): void {
|
|
336
|
-
const baselineVariant = variants.find(v => v.id === baselineVariantId);
|
|
337
|
-
if (!baselineVariant) return;
|
|
338
|
-
|
|
339
|
-
const baselineByCase = new Map<string, MeasuredResults>();
|
|
340
|
-
for (const c of baselineVariant.cases) {
|
|
341
|
-
baselineByCase.set(c.caseId, c.measured);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
for (const variant of variants) {
|
|
345
|
-
if (variant.id === baselineVariantId) continue;
|
|
346
|
-
for (const caseResult of variant.cases) {
|
|
347
|
-
const baseline = baselineByCase.get(caseResult.caseId);
|
|
348
|
-
if (baseline) {
|
|
349
|
-
caseResult.baseline = baseline;
|
|
350
|
-
caseResult.deltaPercent = computeDeltaPercent(
|
|
351
|
-
baseline,
|
|
352
|
-
caseResult.measured,
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** Run a single variant with case data */
|
|
360
|
-
async function runVariant<T>(
|
|
361
|
-
variant: AnyVariant<T>,
|
|
362
|
-
caseData: T,
|
|
363
|
-
name: string,
|
|
364
|
-
runner: BasicRunner,
|
|
365
|
-
options: RunnerOptions,
|
|
366
|
-
): Promise<MeasuredResults> {
|
|
367
|
-
if (isStatefulVariant(variant)) {
|
|
368
|
-
const state = await variant.setup(caseData);
|
|
369
|
-
const [result] = await runner.runBench(
|
|
370
|
-
{ name, fn: () => variant.run(state) },
|
|
371
|
-
options,
|
|
372
|
-
);
|
|
373
|
-
return result;
|
|
374
|
-
}
|
|
375
|
-
const [result] = await runner.runBench(
|
|
376
|
-
{ name, fn: () => variant(caseData) },
|
|
377
|
-
options,
|
|
378
|
-
);
|
|
379
|
-
return result;
|
|
380
|
-
}
|
package/src/BenchmarkReport.ts
CHANGED
|
@@ -58,6 +58,11 @@ interface ReportRowBase {
|
|
|
58
58
|
type ReportRowData<S extends ReadonlyArray<ResultsMapper<any>>> =
|
|
59
59
|
ReportRowBase & UnionToIntersection<SectionStats<S[number]>>;
|
|
60
60
|
|
|
61
|
+
/** All reports in a group, including the baseline if present */
|
|
62
|
+
export function groupReports(group: ReportGroup): BenchmarkReport[] {
|
|
63
|
+
return group.baseline ? [...group.reports, group.baseline] : group.reports;
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
/** @return formatted table report with optional baseline comparisons */
|
|
62
67
|
export function reportResults<S extends ReadonlyArray<ResultsMapper<any>>>(
|
|
63
68
|
groups: ReportGroup[],
|
|
@@ -68,6 +73,41 @@ export function reportResults<S extends ReadonlyArray<ResultsMapper<any>>>(
|
|
|
68
73
|
return buildTable(createColumnGroups(sections, hasBaseline), results);
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
/** @return rows with stats from sections */
|
|
77
|
+
export function valuesForReports<S extends ReadonlyArray<ResultsMapper<any>>>(
|
|
78
|
+
reports: BenchmarkReport[],
|
|
79
|
+
sections: S,
|
|
80
|
+
): ReportRowData<S>[] {
|
|
81
|
+
return reports.map(report => ({
|
|
82
|
+
name: truncate(report.name),
|
|
83
|
+
...extractReportValues(report, sections),
|
|
84
|
+
})) as ReportRowData<S>[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @return groups with single CI column after first comparable field */
|
|
88
|
+
export function injectDiffColumns<T>(
|
|
89
|
+
reportGroups: ReportColumnGroup<T>[],
|
|
90
|
+
): ColumnGroup<T>[] {
|
|
91
|
+
let ciAdded = false;
|
|
92
|
+
|
|
93
|
+
return reportGroups.map(group => ({
|
|
94
|
+
groupTitle: group.groupTitle,
|
|
95
|
+
columns: group.columns.flatMap(col => {
|
|
96
|
+
if (col.comparable && !ciAdded) {
|
|
97
|
+
ciAdded = true;
|
|
98
|
+
const fmt = col.higherIsBetter
|
|
99
|
+
? formatDiffWithCIHigherIsBetter
|
|
100
|
+
: formatDiffWithCI;
|
|
101
|
+
return [
|
|
102
|
+
col,
|
|
103
|
+
{ title: "Δ% CI", key: "diffCI" as keyof T, formatter: fmt },
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
return [col];
|
|
107
|
+
}),
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
71
111
|
/** @return values for report group */
|
|
72
112
|
function resultGroupValues<S extends ReadonlyArray<ResultsMapper<any>>>(
|
|
73
113
|
group: ReportGroup,
|
|
@@ -95,29 +135,6 @@ function resultGroupValues<S extends ReadonlyArray<ResultsMapper<any>>>(
|
|
|
95
135
|
return { results, baseline: baselineRow };
|
|
96
136
|
}
|
|
97
137
|
|
|
98
|
-
/** @return rows with stats from sections */
|
|
99
|
-
export function valuesForReports<S extends ReadonlyArray<ResultsMapper<any>>>(
|
|
100
|
-
reports: BenchmarkReport[],
|
|
101
|
-
sections: S,
|
|
102
|
-
): ReportRowData<S>[] {
|
|
103
|
-
return reports.map(report => ({
|
|
104
|
-
name: truncate(report.name),
|
|
105
|
-
...extractReportValues(report, sections),
|
|
106
|
-
})) as ReportRowData<S>[];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** @return merged statistics from all sections */
|
|
110
|
-
function extractReportValues(
|
|
111
|
-
report: BenchmarkReport,
|
|
112
|
-
sections: ReadonlyArray<ResultsMapper<any>>,
|
|
113
|
-
): UnknownRecord {
|
|
114
|
-
const { measuredResults, metadata } = report;
|
|
115
|
-
const entries = sections.flatMap(s =>
|
|
116
|
-
Object.entries(s.extract(measuredResults, metadata)),
|
|
117
|
-
);
|
|
118
|
-
return Object.fromEntries(entries);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
138
|
/** @return column groups with diff columns if baseline exists */
|
|
122
139
|
function createColumnGroups<S extends ReadonlyArray<ResultsMapper<any>>>(
|
|
123
140
|
sections: S,
|
|
@@ -131,26 +148,14 @@ function createColumnGroups<S extends ReadonlyArray<ResultsMapper<any>>>(
|
|
|
131
148
|
return [nameColumn, ...(hasBaseline ? injectDiffColumns(groups) : groups)];
|
|
132
149
|
}
|
|
133
150
|
|
|
134
|
-
/** @return
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
ciAdded = true;
|
|
145
|
-
const fmt = col.higherIsBetter
|
|
146
|
-
? formatDiffWithCIHigherIsBetter
|
|
147
|
-
: formatDiffWithCI;
|
|
148
|
-
return [
|
|
149
|
-
col,
|
|
150
|
-
{ title: "Δ% CI", key: "diffCI" as keyof T, formatter: fmt },
|
|
151
|
-
];
|
|
152
|
-
}
|
|
153
|
-
return [col];
|
|
154
|
-
}),
|
|
155
|
-
}));
|
|
151
|
+
/** @return merged statistics from all sections */
|
|
152
|
+
function extractReportValues(
|
|
153
|
+
report: BenchmarkReport,
|
|
154
|
+
sections: ReadonlyArray<ResultsMapper<any>>,
|
|
155
|
+
): UnknownRecord {
|
|
156
|
+
const { measuredResults, metadata } = report;
|
|
157
|
+
const entries = sections.flatMap(s =>
|
|
158
|
+
Object.entries(s.extract(measuredResults, metadata)),
|
|
159
|
+
);
|
|
160
|
+
return Object.fromEntries(entries);
|
|
156
161
|
}
|
package/src/HtmlDataPrep.ts
CHANGED
|
@@ -20,21 +20,11 @@ export interface PrepareHtmlOptions {
|
|
|
20
20
|
baselineVersion?: GitVersion;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Flip CI percent for metrics where higher is better (e.g., lines/sec) */
|
|
30
|
-
function flipCI(ci: DifferenceCI): DifferenceCI {
|
|
31
|
-
return {
|
|
32
|
-
percent: -ci.percent,
|
|
33
|
-
ci: [-ci.ci[1], -ci.ci[0]],
|
|
34
|
-
direction: ci.direction,
|
|
35
|
-
histogram: ci.histogram?.map(bin => ({ x: -bin.x, count: bin.count })),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
23
|
+
type ColumnLike = {
|
|
24
|
+
key: string;
|
|
25
|
+
title: string;
|
|
26
|
+
formatter?: (v: unknown) => string | null;
|
|
27
|
+
};
|
|
38
28
|
|
|
39
29
|
/** Prepare ReportData from benchmark results for HTML rendering */
|
|
40
30
|
export function prepareHtmlData(
|
|
@@ -58,6 +48,12 @@ export function prepareHtmlData(
|
|
|
58
48
|
};
|
|
59
49
|
}
|
|
60
50
|
|
|
51
|
+
/** Find higherIsBetter from first comparable column in sections */
|
|
52
|
+
function findHigherIsBetter(sections?: ResultsMapper[]): boolean {
|
|
53
|
+
const cols = sections?.flatMap(s => s.columns().flatMap(g => g.columns));
|
|
54
|
+
return cols?.find(c => c.comparable)?.higherIsBetter ?? false;
|
|
55
|
+
}
|
|
56
|
+
|
|
61
57
|
/** @return group data with bootstrap CI comparisons against baseline */
|
|
62
58
|
function prepareGroupData(
|
|
63
59
|
group: ReportGroup,
|
|
@@ -107,6 +103,16 @@ function prepareBenchmarkData(
|
|
|
107
103
|
};
|
|
108
104
|
}
|
|
109
105
|
|
|
106
|
+
/** Flip CI percent for metrics where higher is better (e.g., lines/sec) */
|
|
107
|
+
function flipCI(ci: DifferenceCI): DifferenceCI {
|
|
108
|
+
return {
|
|
109
|
+
percent: -ci.percent,
|
|
110
|
+
ci: [-ci.ci[1], -ci.ci[0]],
|
|
111
|
+
direction: ci.direction,
|
|
112
|
+
histogram: ci.histogram?.map(bin => ({ x: -bin.x, count: bin.count })),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
110
116
|
/** @return formatted stats from all sections for tooltip display */
|
|
111
117
|
function extractSectionStats(
|
|
112
118
|
report: { measuredResults: any; metadata?: Record<string, unknown> },
|
|
@@ -128,12 +134,6 @@ function formatGroupStats(
|
|
|
128
134
|
.filter((s): s is FormattedStat => s !== undefined);
|
|
129
135
|
}
|
|
130
136
|
|
|
131
|
-
type ColumnLike = {
|
|
132
|
-
key: string;
|
|
133
|
-
title: string;
|
|
134
|
-
formatter?: (v: unknown) => string | null;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
137
|
/** @return formatted stat for a single column, or undefined if empty/placeholder */
|
|
138
138
|
function formatColumnStat(
|
|
139
139
|
values: Record<string, unknown>,
|
package/src/PermutationTest.ts
CHANGED
|
@@ -10,11 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
import { average, percentile } from "./StatisticalUtils.ts";
|
|
12
12
|
|
|
13
|
-
const significanceThreshold = 0.05;
|
|
14
|
-
const strongSignificance = 0.001;
|
|
15
|
-
const goodSignificance = 0.01;
|
|
16
|
-
const defaultBootstrapSamples = 10000;
|
|
17
|
-
|
|
18
13
|
/** Statistical comparison between baseline and current benchmark samples */
|
|
19
14
|
export interface ComparisonResult {
|
|
20
15
|
baselineMedian: number;
|
|
@@ -39,6 +34,11 @@ export interface ComparisonResult {
|
|
|
39
34
|
};
|
|
40
35
|
}
|
|
41
36
|
|
|
37
|
+
const significanceThreshold = 0.05;
|
|
38
|
+
const strongSignificance = 0.001;
|
|
39
|
+
const goodSignificance = 0.01;
|
|
40
|
+
const defaultBootstrapSamples = 10000;
|
|
41
|
+
|
|
42
42
|
/** @return statistical comparison between baseline and current samples */
|
|
43
43
|
export function compareWithBaseline(
|
|
44
44
|
baseline: number[],
|
|
@@ -63,25 +63,6 @@ export function compareWithBaseline(
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/** @return change statistics for a current vs baseline comparison */
|
|
67
|
-
function changeStats(current: number, base: number, pValue: number) {
|
|
68
|
-
return {
|
|
69
|
-
absolute: current - base,
|
|
70
|
-
percent: ((current - base) / base) * 100,
|
|
71
|
-
pValue,
|
|
72
|
-
significant: pValue < significanceThreshold,
|
|
73
|
-
significance: getSignificance(pValue),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** @return significance level based on p-value thresholds */
|
|
78
|
-
function getSignificance(pValue: number): "strong" | "good" | "weak" | "none" {
|
|
79
|
-
if (pValue < strongSignificance) return "strong";
|
|
80
|
-
if (pValue < goodSignificance) return "good";
|
|
81
|
-
if (pValue < significanceThreshold) return "weak";
|
|
82
|
-
return "none";
|
|
83
|
-
}
|
|
84
|
-
|
|
85
66
|
/** @return p-value from permutation test for difference in statistics */
|
|
86
67
|
function bootstrapDifferenceTest(
|
|
87
68
|
sample1: number[],
|
|
@@ -101,6 +82,17 @@ function bootstrapDifferenceTest(
|
|
|
101
82
|
return moreExtreme / defaultBootstrapSamples;
|
|
102
83
|
}
|
|
103
84
|
|
|
85
|
+
/** @return change statistics for a current vs baseline comparison */
|
|
86
|
+
function changeStats(current: number, base: number, pValue: number) {
|
|
87
|
+
return {
|
|
88
|
+
absolute: current - base,
|
|
89
|
+
percent: ((current - base) / base) * 100,
|
|
90
|
+
pValue,
|
|
91
|
+
significant: pValue < significanceThreshold,
|
|
92
|
+
significance: getSignificance(pValue),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
104
96
|
/** @return randomly shuffled samples split at n1 (Fisher-Yates shuffle) */
|
|
105
97
|
function shuffleAndSplit(combined: number[], n1: number) {
|
|
106
98
|
const shuffled = [...combined];
|
|
@@ -113,3 +105,11 @@ function shuffleAndSplit(combined: number[], n1: number) {
|
|
|
113
105
|
resample2: shuffled.slice(n1),
|
|
114
106
|
};
|
|
115
107
|
}
|
|
108
|
+
|
|
109
|
+
/** @return significance level based on p-value thresholds */
|
|
110
|
+
function getSignificance(pValue: number): "strong" | "good" | "weak" | "none" {
|
|
111
|
+
if (pValue < strongSignificance) return "strong";
|
|
112
|
+
if (pValue < goodSignificance) return "good";
|
|
113
|
+
if (pValue < significanceThreshold) return "weak";
|
|
114
|
+
return "none";
|
|
115
|
+
}
|
package/src/StandardSections.ts
CHANGED
|
@@ -15,6 +15,40 @@ export interface TimeStats {
|
|
|
15
15
|
p99?: number;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface GcSectionStats {
|
|
19
|
+
gc?: number; // GC time as fraction of total bench time
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface GcStatsInfo {
|
|
23
|
+
allocPerIter?: number;
|
|
24
|
+
collected?: number;
|
|
25
|
+
scavenges?: number;
|
|
26
|
+
fullGCs?: number;
|
|
27
|
+
promoPercent?: number;
|
|
28
|
+
pausePerIter?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CpuStats {
|
|
32
|
+
cpuCacheMiss?: number;
|
|
33
|
+
cpuStall?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RunStats {
|
|
37
|
+
runs?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AdaptiveStats {
|
|
41
|
+
median?: number;
|
|
42
|
+
mean?: number;
|
|
43
|
+
p99?: number;
|
|
44
|
+
convergence?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface OptStats {
|
|
48
|
+
tiers?: string; // tier distribution summary
|
|
49
|
+
deopt?: number; // deopt count
|
|
50
|
+
}
|
|
51
|
+
|
|
18
52
|
/** Section: mean, p50, p99 timing */
|
|
19
53
|
export const timeSection: ResultsMapper<TimeStats> = {
|
|
20
54
|
extract: (results: MeasuredResults) => ({
|
|
@@ -34,10 +68,6 @@ export const timeSection: ResultsMapper<TimeStats> = {
|
|
|
34
68
|
],
|
|
35
69
|
};
|
|
36
70
|
|
|
37
|
-
export interface GcSectionStats {
|
|
38
|
-
gc?: number; // GC time as fraction of total bench time
|
|
39
|
-
}
|
|
40
|
-
|
|
41
71
|
/** Section: GC time as fraction of total benchmark time (Node performance hooks) */
|
|
42
72
|
export const gcSection: ResultsMapper<GcSectionStats> = {
|
|
43
73
|
extract: (results: MeasuredResults) => {
|
|
@@ -59,15 +89,6 @@ export const gcSection: ResultsMapper<GcSectionStats> = {
|
|
|
59
89
|
],
|
|
60
90
|
};
|
|
61
91
|
|
|
62
|
-
export interface GcStatsInfo {
|
|
63
|
-
allocPerIter?: number;
|
|
64
|
-
collected?: number;
|
|
65
|
-
scavenges?: number;
|
|
66
|
-
fullGCs?: number;
|
|
67
|
-
promoPercent?: number;
|
|
68
|
-
pausePerIter?: number;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
92
|
/** Section: detailed GC stats from --trace-gc-nvp (allocation, promotion, pauses) */
|
|
72
93
|
export const gcStatsSection: ResultsMapper<GcStatsInfo> = {
|
|
73
94
|
extract: (results: MeasuredResults) => {
|
|
@@ -120,11 +141,6 @@ export const browserGcStatsSection: ResultsMapper<GcStatsInfo> = {
|
|
|
120
141
|
],
|
|
121
142
|
};
|
|
122
143
|
|
|
123
|
-
export interface CpuStats {
|
|
124
|
-
cpuCacheMiss?: number;
|
|
125
|
-
cpuStall?: number;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
144
|
/** Section: CPU L1 cache miss rate and stall rate (requires @mitata/counters) */
|
|
129
145
|
export const cpuSection: ResultsMapper<CpuStats> = {
|
|
130
146
|
extract: (results: MeasuredResults) => ({
|
|
@@ -142,10 +158,6 @@ export const cpuSection: ResultsMapper<CpuStats> = {
|
|
|
142
158
|
],
|
|
143
159
|
};
|
|
144
160
|
|
|
145
|
-
export interface RunStats {
|
|
146
|
-
runs?: number;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
161
|
/** Section: number of sample iterations */
|
|
150
162
|
export const runsSection: ResultsMapper<RunStats> = {
|
|
151
163
|
extract: (results: MeasuredResults) => ({
|
|
@@ -177,13 +189,6 @@ export const totalTimeSection: ResultsMapper<{ totalTime?: number }> = {
|
|
|
177
189
|
],
|
|
178
190
|
};
|
|
179
191
|
|
|
180
|
-
export interface AdaptiveStats {
|
|
181
|
-
median?: number;
|
|
182
|
-
mean?: number;
|
|
183
|
-
p99?: number;
|
|
184
|
-
convergence?: number;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
192
|
/** Section: median, mean, p99, and convergence for adaptive mode */
|
|
188
193
|
export const adaptiveSection: ResultsMapper<AdaptiveStats> = {
|
|
189
194
|
extract: (results: MeasuredResults) => ({
|
|
@@ -209,22 +214,6 @@ export const adaptiveSection: ResultsMapper<AdaptiveStats> = {
|
|
|
209
214
|
],
|
|
210
215
|
};
|
|
211
216
|
|
|
212
|
-
/** Build generic sections based on CLI flags */
|
|
213
|
-
export function buildGenericSections(args: {
|
|
214
|
-
"gc-stats"?: boolean;
|
|
215
|
-
"heap-sample"?: boolean;
|
|
216
|
-
}): ResultsMapper[] {
|
|
217
|
-
const sections: ResultsMapper[] = [];
|
|
218
|
-
if (args["gc-stats"]) sections.push(gcStatsSection);
|
|
219
|
-
sections.push(runsSection);
|
|
220
|
-
return sections;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export interface OptStats {
|
|
224
|
-
tiers?: string; // tier distribution summary
|
|
225
|
-
deopt?: number; // deopt count
|
|
226
|
-
}
|
|
227
|
-
|
|
228
217
|
/** Section: V8 optimization tier distribution and deopt count */
|
|
229
218
|
export const optSection: ResultsMapper<OptStats> = {
|
|
230
219
|
extract: (results: MeasuredResults) => {
|
|
@@ -259,3 +248,14 @@ export const optSection: ResultsMapper<OptStats> = {
|
|
|
259
248
|
},
|
|
260
249
|
],
|
|
261
250
|
};
|
|
251
|
+
|
|
252
|
+
/** Build generic sections based on CLI flags */
|
|
253
|
+
export function buildGenericSections(args: {
|
|
254
|
+
"gc-stats"?: boolean;
|
|
255
|
+
"heap-sample"?: boolean;
|
|
256
|
+
}): ResultsMapper[] {
|
|
257
|
+
const sections: ResultsMapper[] = [];
|
|
258
|
+
if (args["gc-stats"]) sections.push(gcStatsSection);
|
|
259
|
+
sections.push(runsSection);
|
|
260
|
+
return sections;
|
|
261
|
+
}
|