400105e2e2
Tasks 7-8 of the bench-harness plan plus the orchestrator's sizing pass — this completes the phase-1 harness (closes #251). Driver: run [--surface|--quick|--reps|--out] / pin over five surfaces, report-only comparison (drift NOTICE at >=10%, never a failure), fingerprint mismatch exits 1 with a baseline-vs-measured block, infra errors exit 2, debug builds refuse to measure. The Bench project-facts line and the crate README (discipline: quiet box, warmup+median, deliberate re-pins) land with it. Workload sizing, measured then corrected: the release engine outran the plan's guesses by ~100x, leaving sub-second child walls where spawn jitter reaches the 10% threshold and flaps the report. Full-size workloads now land in whole seconds — engine 10M bars (~0.7s), ingest 24 months x 20 fresh drains (~0.4s), campaigns on a 24-month archive with a ~22-month window and a 5x5 member grid (sweep ~1.4s, heavy ~5.5s), fixed-cost as in-rep medians over spawn batches (20x help, 10x run). Quick mode keeps the small E2E-fixture shape. The campaign fingerprint folds all three realization layers — sweep winner, gate survivor ordinals, bootstrap trade counts (pooled or per-survivor sum) — so a wrong-result regression in any stage fails the bench, not just a wrong sweep winner. Baselines pinned on this host (commit-stamped), verified by a follow-up run: five fingerprints OK at <=2% drift under load. Verification: cargo test --workspace 1379 green (bench tests all quick-mode; the release-binary E2Es are #[ignore]d behind -- --ignored per the suite-wallclock discipline), clippy -D warnings clean, doc build clean.
165 lines
5.5 KiB
Rust
165 lines
5.5 KiB
Rust
//! Baseline documents and the report-only comparison core. Pure functions —
|
|
//! everything here is unit-tested without running a single benchmark.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::BTreeMap;
|
|
|
|
/// One committed baseline: `baselines/<surface>.json`.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct BaselineDoc {
|
|
pub surface: String,
|
|
/// Flat metric map, e.g. `bars_per_s`, `wall_s`, `peak_rss_mb`.
|
|
pub metrics: BTreeMap<String, f64>,
|
|
/// Opaque determinism fingerprint; compared for byte equality.
|
|
pub fingerprint: String,
|
|
pub reps: u32,
|
|
pub host: HostInfo,
|
|
pub profile: String,
|
|
pub commit: String,
|
|
pub date: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct HostInfo {
|
|
pub hostname: String,
|
|
pub nproc: u32,
|
|
}
|
|
|
|
/// One measurement result, produced by a surface run.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Measured {
|
|
pub surface: String,
|
|
pub metrics: BTreeMap<String, f64>,
|
|
pub fingerprint: String,
|
|
}
|
|
|
|
/// Timing drift at or beyond this fraction is reported as a NOTICE line.
|
|
pub const NOTICE_THRESHOLD: f64 = 0.10;
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Compared {
|
|
/// Per-metric relative delta, `(name, (measured - baseline) / baseline)`.
|
|
/// Metrics missing on either side are skipped.
|
|
pub deltas: Vec<(String, f64)>,
|
|
/// Metric names whose |delta| >= NOTICE_THRESHOLD.
|
|
pub notices: Vec<String>,
|
|
pub fingerprint_changed: bool,
|
|
pub host_mismatch: bool,
|
|
}
|
|
|
|
pub fn compare(baseline: &BaselineDoc, measured: &Measured, host: &HostInfo) -> Compared {
|
|
let mut deltas = Vec::new();
|
|
let mut notices = Vec::new();
|
|
for (name, base) in &baseline.metrics {
|
|
let Some(now) = measured.metrics.get(name) else { continue };
|
|
if *base == 0.0 {
|
|
continue; // no meaningful relative delta
|
|
}
|
|
let d = (now - base) / base;
|
|
if d.abs() >= NOTICE_THRESHOLD {
|
|
notices.push(name.clone());
|
|
}
|
|
deltas.push((name.clone(), d));
|
|
}
|
|
Compared {
|
|
deltas,
|
|
notices,
|
|
fingerprint_changed: baseline.fingerprint != measured.fingerprint,
|
|
host_mismatch: baseline.host != *host,
|
|
}
|
|
}
|
|
|
|
/// The process exit code for a full run: 1 if ANY surface's fingerprint
|
|
/// changed, else 0. (Infra errors exit 2 before this is ever consulted;
|
|
/// timing drift is report-only by design — the #251 cadence decision.)
|
|
pub fn exit_code(compared: &[Option<Compared>]) -> i32 {
|
|
if compared.iter().flatten().any(|c| c.fingerprint_changed) { 1 } else { 0 }
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn doc(metrics: &[(&str, f64)], fp: &str) -> BaselineDoc {
|
|
BaselineDoc {
|
|
surface: "s".into(),
|
|
metrics: metrics.iter().map(|(k, v)| (k.to_string(), *v)).collect(),
|
|
fingerprint: fp.into(),
|
|
reps: 3,
|
|
host: HostInfo { hostname: "h".into(), nproc: 24 },
|
|
profile: "release".into(),
|
|
commit: "abc".into(),
|
|
date: "2026-07-17".into(),
|
|
}
|
|
}
|
|
|
|
fn measured(metrics: &[(&str, f64)], fp: &str) -> Measured {
|
|
Measured {
|
|
surface: "s".into(),
|
|
metrics: metrics.iter().map(|(k, v)| (k.to_string(), *v)).collect(),
|
|
fingerprint: fp.into(),
|
|
}
|
|
}
|
|
|
|
fn host() -> HostInfo {
|
|
HostInfo { hostname: "h".into(), nproc: 24 }
|
|
}
|
|
|
|
#[test]
|
|
fn compare_reports_relative_drift_per_metric() {
|
|
let c = compare(&doc(&[("wall_s", 10.0)], "f"), &measured(&[("wall_s", 10.5)], "f"), &host());
|
|
assert_eq!(c.deltas.len(), 1);
|
|
assert_eq!(c.deltas[0].0, "wall_s");
|
|
assert!((c.deltas[0].1 - 0.05).abs() < 1e-12, "5% drift, got {}", c.deltas[0].1);
|
|
assert!(c.notices.is_empty(), "5% is below the 10% notice bar");
|
|
assert!(!c.fingerprint_changed);
|
|
assert!(!c.host_mismatch);
|
|
}
|
|
|
|
#[test]
|
|
fn compare_flags_notice_at_ten_percent() {
|
|
let c = compare(&doc(&[("wall_s", 10.0)], "f"), &measured(&[("wall_s", 11.0)], "f"), &host());
|
|
assert_eq!(c.notices, vec!["wall_s".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn compare_detects_fingerprint_change() {
|
|
let c = compare(&doc(&[], "old"), &measured(&[], "new"), &host());
|
|
assert!(c.fingerprint_changed);
|
|
}
|
|
|
|
#[test]
|
|
fn compare_flags_host_mismatch_informationally() {
|
|
let other = HostInfo { hostname: "elsewhere".into(), nproc: 8 };
|
|
let c = compare(&doc(&[], "f"), &measured(&[], "f"), &other);
|
|
assert!(c.host_mismatch);
|
|
assert!(!c.fingerprint_changed);
|
|
}
|
|
|
|
#[test]
|
|
fn compare_skips_metrics_missing_on_either_side() {
|
|
let c = compare(
|
|
&doc(&[("only_base", 1.0)], "f"),
|
|
&measured(&[("only_measured", 2.0)], "f"),
|
|
&host(),
|
|
);
|
|
assert!(c.deltas.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn exit_code_is_one_only_on_fingerprint_change() {
|
|
let clean = compare(&doc(&[], "f"), &measured(&[], "f"), &host());
|
|
let changed = compare(&doc(&[], "f"), &measured(&[], "g"), &host());
|
|
assert_eq!(exit_code(&[Some(clean.clone()), None]), 0);
|
|
assert_eq!(exit_code(&[Some(clean), Some(changed)]), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn baseline_doc_round_trips_through_json() {
|
|
let d = doc(&[("bars_per_s", 812345.0)], "last=0.1 n=5");
|
|
let json = serde_json::to_string_pretty(&d).expect("serialize");
|
|
let back: BaselineDoc = serde_json::from_str(&json).expect("parse");
|
|
assert_eq!(d, back);
|
|
}
|
|
}
|