benchmark_baseline.ts

Benchmark baseline storage and comparison utilities. Save benchmark results to disk and compare against baselines for regression detection.

view source

Declarations
#

14 declarations

benchmark_baseline_compare
#

benchmark_baseline.ts view source

(results: BenchmarkResult[], options?: BenchmarkBaselineCompareOptions): Promise<BenchmarkBaselineComparisonResult> import {benchmark_baseline_compare} from '@fuzdev/fuz_util/benchmark_baseline.js';

Compare benchmark results against the stored baseline.

results

type BenchmarkResult[]

options

comparison options including regression threshold and staleness warning

default {}

returns

Promise<BenchmarkBaselineComparisonResult>

comparison result with regressions, improvements, and unchanged tasks

examples

const bench = new Benchmark(); bench.add('test', () => fn()); await bench.run(); const comparison = await benchmark_baseline_compare(bench.results(), { regression_threshold: 1.05, // Only flag regressions 5% or more slower staleness_warning_days: 7, // Warn if baseline is older than 7 days }); if (comparison.regressions.length > 0) { console.log('Performance regressions detected!'); for (const r of comparison.regressions) { console.log(` ${r.name}: ${r.comparison.speedup_ratio.toFixed(2)}x slower`); } process.exit(1); }

benchmark_baseline_format
#

benchmark_baseline_format_json
#

benchmark_baseline.ts view source

(result: BenchmarkBaselineComparisonResult, options?: { pretty?: boolean | undefined; }): string import {benchmark_baseline_format_json} from '@fuzdev/fuz_util/benchmark_baseline.js';

Format a baseline comparison result as JSON for programmatic consumption.

result

comparison result from benchmark_baseline_compare

options

type { pretty?: boolean | undefined; }
default {}

returns

string

benchmark_baseline_load
#

benchmark_baseline.ts view source

(options?: BenchmarkBaselineLoadOptions): Promise<{ version: number; timestamp: string; git_commit: string | null; git_branch: string | null; node_version: string; entries: { ...; }[]; metadata?: Record<...> | undefined; } | null> import {benchmark_baseline_load} from '@fuzdev/fuz_util/benchmark_baseline.js';

Load the current baseline from disk.

options

default {}

returns

Promise<{ version: number; timestamp: string; git_commit: string | null; git_branch: string | null; node_version: string; entries: { name: string; mean_ns: number; p50_ns: number; std_dev_ns: number; ... 9 more ...; budget: { ...; }; }[]; metadata?: Record<...> | undefined; } | null>

the baseline, or null if not found or invalid

examples

const baseline = await benchmark_baseline_load(); if (baseline) { console.log(`Baseline from ${baseline.timestamp}`); }

benchmark_baseline_save
#

benchmark_baseline.ts view source

(results: BenchmarkResult[], options?: BenchmarkBaselineSaveOptions): Promise<void> import {benchmark_baseline_save} from '@fuzdev/fuz_util/benchmark_baseline.js';

Save benchmark results as the current baseline.

Each entry's effective time-budget (duration_ms, warmup_iterations, min_iterations, max_iterations) is persisted alongside the stats so a later benchmark_baseline_compare can detect methodology drift — a budget mismatch between baseline and current routes the task to the methodology_changed bucket instead of producing false regressions / improvements. Suite-level config (timer, cooldown_ms) is *not* persisted; keep it stable across runs in the same baseline lineage.

results

type BenchmarkResult[]

options

default {}

returns

Promise<void>

throws

  • Error - if creating the baseline directory or writing the file fails

examples

const bench = new Benchmark(); bench.add('test', () => fn()); await bench.run(); await benchmark_baseline_save(bench.results());

benchmark_budget_diff
#

benchmark_baseline.ts view source

(baseline: { duration_ms: number; warmup_iterations: number; min_iterations: number; max_iterations: number; async_resolved: boolean; }, current: { duration_ms: number; warmup_iterations: number; min_iterations: number; max_iterations: number; async_resolved: boolean; }): BenchmarkBudgetDiffEntry[] import {benchmark_budget_diff} from '@fuzdev/fuz_util/benchmark_baseline.js';

Compute the per-field diff between two effective budgets. Returns an empty array if budgets are identical — the formatters use the length as a quick "methodology changed?" check without re-deriving the boolean.

baseline

type { duration_ms: number; warmup_iterations: number; min_iterations: number; max_iterations: number; async_resolved: boolean; }

current

type { duration_ms: number; warmup_iterations: number; min_iterations: number; max_iterations: number; async_resolved: boolean; }

returns

BenchmarkBudgetDiffEntry[]

BenchmarkBaseline
#

benchmark_baseline.ts view source

ZodObject<{ version: ZodNumber; timestamp: ZodString; git_commit: ZodNullable<ZodString>; git_branch: ZodNullable<ZodString>; node_version: ZodString; entries: ZodArray<...>; metadata: ZodOptional<...>; }, $strip> import type {BenchmarkBaseline} from '@fuzdev/fuz_util/benchmark_baseline.js';

Schema for the complete baseline file.

metadata is an opt-in passthrough bag for context that applies to the whole run but isn't part of the comparison math — corpus identity (file counts, total bytes), dependency versions, binary sizes, hardware notes, build flags. Pass it on save and read it back on load; it's not interpreted by benchmark_baseline_compare. The intended use is to let consumers attach "did this run measure the same thing as the baseline?" context (e.g. corpus shape) without forking the schema, then surface mismatches in their own reporting. fuz_util doesn't surface metadata in comparison output because it has no shape — consumers know what their metadata means and how to display the diff.

BenchmarkBaselineCompareOptions
#

benchmark_baseline.ts view source

BenchmarkBaselineCompareOptions import type {BenchmarkBaselineCompareOptions} from '@fuzdev/fuz_util/benchmark_baseline.js';

Options for comparing against a baseline.

inheritance

regression_threshold?

Minimum speedup ratio to consider a regression. For example, 1.05 means only flag regressions that are 5% or more slower. Default: 1.0 (any statistically significant slowdown is a regression)

type number

staleness_warning_days?

Number of days after which to warn about stale baseline. Default: undefined (no staleness warning)

type number

min_percent_difference?

Minimum percentage difference to consider meaningful, as a ratio. Passed through to benchmark_stats_compare. See BenchmarkCompareOptions. Default: 0.10 (10%)

type number

noise_warning_cv_threshold?

Coefficient of variation (std_dev / mean) at or above which a comparison is flagged with noise_warning: true. Doesn't affect bucketing — the task still routes to regressions / improvements / unchanged as the Welch math dictates — but tells the formatter (and any custom consumer) that the underlying measurement is noisy enough that a "significant" call should be read with skepticism. The default is calibrated for system-noise/thermal/background-load contamination, not microbenchmark floor effects; lower it (e.g. 0.15) for sub-microsecond functions where any cv signal matters, raise it for inherently noisy workloads where 0.3 fires on every run. Default: 0.30

type number

noise_warning_outlier_ratio_threshold?

Outlier ratio (fraction of iterations rejected by MAD outlier removal) at or above which the comparison is flagged with noise_warning: true, OR-gated with the cv check. Catches the case where the post-cleaning cv looks tight because outlier removal already deflated the spread — a third of the iterations being tail events is itself a noise signal, even if the surviving samples cluster cleanly. Set higher (e.g. 0.2) for benchmarks where outliers are expected (allocator-bound, async I/O), lower (e.g. 0.05) for tight CPU loops. Default: 0.10

type number

BenchmarkBaselineComparisonResult
#

benchmark_baseline.ts view source

BenchmarkBaselineComparisonResult import type {BenchmarkBaselineComparisonResult} from '@fuzdev/fuz_util/benchmark_baseline.js';

Result of comparing current results against a baseline.

baseline_found

Whether a baseline was found

type boolean

baseline_timestamp

Timestamp of the baseline

type string | null

baseline_commit

Git commit of the baseline

type string | null

baseline_age_days

Age of the baseline in days

type number | null

baseline_stale

Whether the baseline is considered stale based on staleness_warning_days option

type boolean

baseline_node_version

Node.js version recorded in the baseline (null if no baseline).

type string | null

current_node_version

Node.js version of the process that produced the current results.

type string

node_version_changed

True if the baseline's node version differs from the current process. Informational only — surfaced in the comparison header so readers can apply the caveat across all task comparisons; does not affect per-task classification (Node bumps affect every task uniformly).

type boolean

baseline_metadata

The metadata bag persisted in the baseline file, or null if the baseline didn't have one (or no baseline was found). Returned verbatim — fuz_util doesn't validate the shape or diff it against any "current run" metadata. If the consumer needs a diff, they pass options.metadata (current-run context) and walk the two records themselves.

type Record<string, unknown> | null

comparisons

Individual task comparisons for tasks where Welch's math is meaningful — i.e., budget unchanged between baseline and current. Methodology-changed tasks are NOT in here; they live in methodology_changed. Excluding them here keeps aggregate reads of c.comparison.faster / c.comparison.speedup_ratio / c.comparison.recommendation safe — those fields are still computed for methodology-changed rows but answer a counterfactual question (the math is correct over mismatched sample sizes) and would mislead consumers iterating this array.

type Array<BenchmarkBaselineTaskComparison>

regressions

Tasks that regressed (slower with statistical significance), sorted by effect size (largest first)

type Array<BenchmarkBaselineTaskComparison>

improvements

Tasks that improved (faster with statistical significance), sorted by effect size (largest first)

type Array<BenchmarkBaselineTaskComparison>

unchanged

Tasks with no significant change

type Array<BenchmarkBaselineTaskComparison>

methodology_changed

Tasks whose effective budget differs between baseline and current. Excluded from regressions/improvements/unchanged because the Welch comparison is contaminated by sample-size or warmup differences — the math is correct but answers a different question than the reader thinks. Re-save the baseline after intentional methodology changes to surface any genuine drift that was masked.

type Array<BenchmarkBaselineTaskComparison>

new_tasks

Tasks in current run but not in baseline

type Array<string>

removed_tasks

Tasks in baseline but not in current run

type Array<string>

BenchmarkBaselineEntry
#

benchmark_baseline.ts view source

ZodObject<{ name: ZodString; mean_ns: ZodNumber; p50_ns: ZodNumber; std_dev_ns: ZodNumber; min_ns: ZodNumber; max_ns: ZodNumber; ... 7 more ...; budget: ZodObject<...>; }, $strip> import type {BenchmarkBaselineEntry} from '@fuzdev/fuz_util/benchmark_baseline.js';

Schema for a single benchmark entry in the baseline.

outlier_ratio is persisted as a noise signal independent of the post-cleaning std_dev_ns/cv: outlier removal *deflates* std_dev (the cleaned set is by construction less variable than the raw set), so cv alone misses runs where a third of the iterations were tail events. benchmark_baseline_compare OR-gates noise_warning on this field so a noisy raw distribution gets flagged even when the cleaned cv looks tight.

BenchmarkBaselineLoadOptions
#

benchmark_baseline.ts view source

BenchmarkBaselineLoadOptions import type {BenchmarkBaselineLoadOptions} from '@fuzdev/fuz_util/benchmark_baseline.js';

Options for loading a baseline.

path?

Directory to load baseline from (default: '.gro/benchmarks')

type string

BenchmarkBaselineSaveOptions
#

benchmark_baseline.ts view source

BenchmarkBaselineSaveOptions import type {BenchmarkBaselineSaveOptions} from '@fuzdev/fuz_util/benchmark_baseline.js';

Options for saving a baseline.

path?

Directory to store baselines (default: '.gro/benchmarks')

type string

git_commit?

Git commit hash (auto-detected if not provided)

type string | null

git_branch?

Git branch name (auto-detected if not provided)

type string | null

metadata?

Opt-in passthrough for run-level context (corpus identity, dependency versions, binary sizes, hardware notes). Round-trips on _load and is accessible via BenchmarkBaselineComparisonResult.baseline_metadata, but is *not* interpreted by _compare — consumers decide what to do with mismatches. Keep values JSON-serializable; this is written verbatim to the baseline file.

type Record<string, unknown>

BenchmarkBaselineTaskComparison
#

benchmark_baseline.ts view source

BenchmarkBaselineTaskComparison import type {BenchmarkBaselineTaskComparison} from '@fuzdev/fuz_util/benchmark_baseline.js';

Comparison result for a single task.

name

type string

baseline

type BenchmarkBaselineEntry

current

type BenchmarkBaselineEntry

comparison

Welch comparison of baseline vs. current means.

When the row is in methodology_changed, every field of this object is contaminated by sample-size or warmup differences — not just recommendation. The Welch math runs end-to-end and populates faster, speedup_ratio, significant, p_value, percent_difference, effect_size, effect_magnitude, ci_overlap, and recommendation, but it's comparing distributions produced under different methodologies. Treat the result as diagnostic ("how dramatic is the contamination?"), not authoritative.

The built-in formatters (benchmark_baseline_format, benchmark_baseline_format_json) never render comparison.* for methodology-changed rows for this reason — they show only the budget diff. Custom consumers reading this field directly should check methodology_changed first.

type BenchmarkComparison

methodology_changed

True if the effective time budget differs between baseline and current. When set, the task is routed to methodology_changed instead of regressions/improvements/unchanged, and the comparison field's contents become unreliable per the doc above.

type boolean

noise_warning

True when measurement noise is high enough to undermine the significance call on this row. OR-gated across two signals: - max(baseline.cv, current.cv) >= noise_warning_cv_threshold where cv = std_dev_ns / mean_ns (post-outlier-removal). - `max(baseline.outlier_ratio, current.outlier_ratio) >= noise_warning_outlier_ratio_threshold`, since outlier removal deflates cv — a high outlier ratio is itself a noise signal even when the cleaned cv looks tight. Independent of methodology_changed. The Welch math still ran; this flag tells the reader to take comparison.significant with skepticism on this row. Cv is recomputed at comparison time from persisted mean_ns/std_dev_ns; outlier_ratio is persisted directly.

type boolean

max_cv

The larger of the two coefficients of variation across baseline and current, exposed so consumers can render the actual noise level. Recomputed at comparison time from persisted mean_ns and std_dev_ns — not a persisted field.

type number

max_outlier_ratio

The larger of the two outlier ratios across baseline and current. Read directly from the persisted entries. Exposed so consumers can render the actual outlier rate alongside noise_warning.

type number

BenchmarkBudgetDiffEntry
#

benchmark_baseline.ts view source

BenchmarkBudgetDiffEntry import type {BenchmarkBudgetDiffEntry} from '@fuzdev/fuz_util/benchmark_baseline.js';

Per-field diff entry produced by benchmark_budget_diff. The async_resolved entry carries booleans; every other entry carries numbers. Consumers reading baseline/current should narrow on field before using the value arithmetically.

Depends on
#