/** * Benchmark baseline storage and comparison utilities. * Save benchmark results to disk and compare against baselines for regression detection. * * @module */ import { z } from 'zod'; import type { BenchmarkResult } from './benchmark_types.js'; import { type BenchmarkComparison } from './benchmark_stats.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. */ export declare const BenchmarkBaselineEntry: z.ZodObject<{ name: z.ZodString; mean_ns: z.ZodNumber; p50_ns: z.ZodNumber; std_dev_ns: z.ZodNumber; min_ns: z.ZodNumber; max_ns: z.ZodNumber; p75_ns: z.ZodNumber; p90_ns: z.ZodNumber; p95_ns: z.ZodNumber; p99_ns: z.ZodNumber; ops_per_second: z.ZodNumber; sample_size: z.ZodNumber; outlier_ratio: z.ZodNumber; budget: z.ZodObject<{ duration_ms: z.ZodNumber; warmup_iterations: z.ZodNumber; min_iterations: z.ZodNumber; max_iterations: z.ZodNumber; async_resolved: z.ZodBoolean; }, z.core.$strip>; }, z.core.$strip>; export type BenchmarkBaselineEntry = z.infer; /** * 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. */ export declare const BenchmarkBaseline: z.ZodObject<{ version: z.ZodNumber; timestamp: z.ZodString; git_commit: z.ZodNullable; git_branch: z.ZodNullable; node_version: z.ZodString; entries: z.ZodArray; }, z.core.$strip>>; metadata: z.ZodOptional>; }, z.core.$strip>; export type BenchmarkBaseline = z.infer; /** * Options for saving a baseline. */ export interface BenchmarkBaselineSaveOptions { /** Directory to store baselines (default: '.gro/benchmarks') */ path?: string; /** Git commit hash (auto-detected if not provided) */ git_commit?: string | null; /** Git branch name (auto-detected if not provided) */ git_branch?: string | null; /** * 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. */ metadata?: Record; } /** * Options for loading a baseline. */ export interface BenchmarkBaselineLoadOptions { /** Directory to load baseline from (default: '.gro/benchmarks') */ path?: string; } /** * Options for comparing against a baseline. */ export interface BenchmarkBaselineCompareOptions extends BenchmarkBaselineLoadOptions { /** * 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) */ regression_threshold?: number; /** * Number of days after which to warn about stale baseline. * Default: undefined (no staleness warning) */ staleness_warning_days?: number; /** * Minimum percentage difference to consider meaningful, as a ratio. * Passed through to `benchmark_stats_compare`. See `BenchmarkCompareOptions`. * Default: 0.10 (10%) */ min_percent_difference?: number; /** * 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 */ noise_warning_cv_threshold?: number; /** * 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 */ noise_warning_outlier_ratio_threshold?: number; } /** * Result of comparing current results against a baseline. */ export interface BenchmarkBaselineComparisonResult { /** Whether a baseline was found */ baseline_found: boolean; /** Timestamp of the baseline */ baseline_timestamp: string | null; /** Git commit of the baseline */ baseline_commit: string | null; /** Age of the baseline in days */ baseline_age_days: number | null; /** Whether the baseline is considered stale based on staleness_warning_days option */ baseline_stale: boolean; /** Node.js version recorded in the baseline (null if no baseline). */ baseline_node_version: string | null; /** Node.js version of the process that produced the current results. */ current_node_version: string; /** * 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). */ node_version_changed: boolean; /** * 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. */ baseline_metadata: Record | null; /** * 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. */ comparisons: Array; /** Tasks that regressed (slower with statistical significance), sorted by effect size (largest first) */ regressions: Array; /** Tasks that improved (faster with statistical significance), sorted by effect size (largest first) */ improvements: Array; /** Tasks with no significant change */ unchanged: Array; /** * 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. */ methodology_changed: Array; /** Tasks in current run but not in baseline */ new_tasks: Array; /** Tasks in baseline but not in current run */ removed_tasks: Array; } /** * Comparison result for a single task. */ export interface BenchmarkBaselineTaskComparison { name: string; baseline: BenchmarkBaselineEntry; current: BenchmarkBaselineEntry; /** * 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. */ comparison: BenchmarkComparison; /** * 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. */ methodology_changed: boolean; /** * 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. */ noise_warning: boolean; /** * 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. */ max_cv: number; /** * 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`. */ max_outlier_ratio: number; } /** * 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. */ export type BenchmarkBudgetDiffEntry = { field: 'duration_ms' | 'warmup_iterations' | 'min_iterations' | 'max_iterations'; baseline: number; current: number; } | { field: 'async_resolved'; baseline: boolean; current: boolean; }; /** * 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. */ export declare const benchmark_budget_diff: (baseline: BenchmarkBaselineEntry["budget"], current: BenchmarkBaselineEntry["budget"]) => Array; /** * 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. * * @throws Error if creating the baseline directory or writing the file fails * * @example * ```ts * const bench = new Benchmark(); * bench.add('test', () => fn()); * await bench.run(); * await benchmark_baseline_save(bench.results()); * ``` */ export declare const benchmark_baseline_save: (results: Array, options?: BenchmarkBaselineSaveOptions) => Promise; /** * Load the current baseline from disk. * * @returns the baseline, or null if not found or invalid * * @example * ```ts * const baseline = await benchmark_baseline_load(); * if (baseline) { * console.log(`Baseline from ${baseline.timestamp}`); * } * ``` */ export declare const benchmark_baseline_load: (options?: BenchmarkBaselineLoadOptions) => Promise; /** * Compare benchmark results against the stored baseline. * * @param options - comparison options including regression threshold and staleness warning * @returns comparison result with regressions, improvements, and unchanged tasks * * @example * ```ts * 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); * } * ``` */ export declare const benchmark_baseline_compare: (results: Array, options?: BenchmarkBaselineCompareOptions) => Promise; /** * Format a baseline comparison result as a human-readable string. * * @param result - comparison result from `benchmark_baseline_compare` */ export declare const benchmark_baseline_format: (result: BenchmarkBaselineComparisonResult) => string; /** * Format a baseline comparison result as JSON for programmatic consumption. * * @param result - comparison result from `benchmark_baseline_compare` */ export declare const benchmark_baseline_format_json: (result: BenchmarkBaselineComparisonResult, options?: { pretty?: boolean; }) => string; //# sourceMappingURL=benchmark_baseline.d.ts.map