use statrs::distribution::{ContinuousCDF, StudentsT};
use statrs::statistics::Statistics;
pub(crate) const ZERO_MEAN_EPS: f64 = 1e-9;
pub(crate) const NOISE_ALPHA: f64 = 0.05;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SideSummary {
pub n: usize,
pub mean: f64,
pub min: f64,
pub max: f64,
pub spread_pct: f64,
pub var: f64,
pub sample_mean: f64,
}
impl SideSummary {
pub fn of(samples: &[f64]) -> SideSummary {
let n = samples.len();
if n == 0 {
return SideSummary {
n: 0,
mean: 0.0,
min: 0.0,
max: 0.0,
spread_pct: 0.0,
var: f64::NAN,
sample_mean: 0.0,
};
}
let mean = samples.iter().sum::<f64>() / n as f64;
let min = samples.iter().copied().fold(f64::INFINITY, f64::min);
let max = samples.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let var = samples.variance();
let spread_pct = if max == min {
0.0
} else if mean.abs() < ZERO_MEAN_EPS {
f64::INFINITY
} else {
(max - min) / mean.abs() * 100.0
};
SideSummary {
n,
mean,
min,
max,
spread_pct,
var,
sample_mean: mean,
}
}
pub fn with_pooled_mean(mut self, pooled_mean: f64) -> SideSummary {
self.mean = pooled_mean;
self.spread_pct = if self.max == self.min {
0.0
} else if pooled_mean.abs() < ZERO_MEAN_EPS {
f64::INFINITY
} else {
(self.max - self.min) / pooled_mean.abs() * 100.0
};
self
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NoiseVerdict {
pub a: SideSummary,
pub b: SideSummary,
pub separated: bool,
pub insufficient_samples: bool,
pub high_spread: bool,
}
pub fn noise_verdict(
a_samples: &[f64],
b_samples: &[f64],
spread_threshold_pct: f64,
) -> NoiseVerdict {
noise_verdict_from(
SideSummary::of(a_samples),
SideSummary::of(b_samples),
spread_threshold_pct,
)
}
pub fn noise_verdict_from(
a: SideSummary,
b: SideSummary,
spread_threshold_pct: f64,
) -> NoiseVerdict {
let insufficient_samples = a.n < 2 || b.n < 2;
let disjoint_bands = a.max < b.min || b.max < a.min;
let welch_separated = if insufficient_samples {
false
} else {
let sa = a.var / a.n as f64;
let sb = b.var / b.n as f64;
let se2 = sa + sb;
if se2.is_finite() && se2 > 0.0 {
let dof = (se2 * se2) / (sa * sa / (a.n as f64 - 1.0) + sb * sb / (b.n as f64 - 1.0));
if dof.is_finite() && dof > 0.0 {
let t = (a.sample_mean - b.sample_mean) / se2.sqrt();
let p = 2.0 * StudentsT::new(0.0, 1.0, dof).unwrap().sf(t.abs());
p < NOISE_ALPHA
} else {
false
}
} else {
false
}
};
let separated = welch_separated || disjoint_bands;
let high_spread = a.spread_pct > spread_threshold_pct || b.spread_pct > spread_threshold_pct;
NoiseVerdict {
a,
b,
separated,
insufficient_samples,
high_spread,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(x: f64, y: f64) -> bool {
(x - y).abs() < 1e-9
}
#[test]
fn side_summary_empty_field_by_field() {
let s = SideSummary::of(&[]);
assert_eq!(
(s.n, s.mean, s.min, s.max, s.spread_pct),
(0, 0.0, 0.0, 0.0, 0.0)
);
assert!(s.var.is_nan(), "empty side has undefined variance");
}
#[test]
fn side_summary_single_has_zero_spread_and_nan_var() {
let s = SideSummary::of(&[42.0]);
assert_eq!((s.n, s.mean, s.min, s.max), (1, 42.0, 42.0, 42.0));
assert_eq!(s.spread_pct, 0.0);
assert!(s.var.is_nan(), "single-sample side has undefined variance");
}
#[test]
fn noise_verdict_flags_side_with_fewer_than_two_samples() {
let v = noise_verdict(&[100.0], &[130.0, 130.0, 130.0], 5.0);
assert_eq!(v.a.n, 1);
assert!(
v.insufficient_samples,
"a side with <2 realized samples must be flagged insufficient",
);
let v = noise_verdict(&[100.0, 100.0, 100.0], &[130.0], 5.0);
assert_eq!(v.b.n, 1);
assert!(
v.insufficient_samples,
"a degenerate B side is also insufficient",
);
let v = noise_verdict(&[100.0, 100.0], &[130.0, 130.0], 5.0);
assert!(
v.separated && !v.insufficient_samples,
"n>=2 sides with disjoint bands stay separated + gateable",
);
}
#[test]
fn side_summary_mean_min_max_spread_var() {
let s = SideSummary::of(&[90.0, 110.0, 100.0]);
assert!(approx(s.mean, 100.0));
assert_eq!((s.min, s.max), (90.0, 110.0));
assert!(approx(s.spread_pct, 20.0));
assert!(approx(s.var, 100.0), "Bessel N-1 variance");
}
#[test]
fn side_summary_identical_runs_have_zero_spread_and_var() {
let s = SideSummary::of(&[7.0, 7.0, 7.0]);
assert_eq!(s.spread_pct, 0.0, "identical runs => no spread");
assert_eq!(s.var, 0.0, "identical runs => zero variance");
}
#[test]
fn side_summary_near_zero_mean_with_variance_is_infinite_spread() {
let s = SideSummary::of(&[-1.0, 1.0]);
assert!(approx(s.mean, 0.0));
assert_eq!(s.spread_pct, f64::INFINITY);
assert!(approx(s.var, 2.0));
}
#[test]
fn with_pooled_mean_keeps_sample_mean_and_var_for_a_coherent_welch_arm() {
let s = SideSummary::of(&[10.0, 20.0, 30.0]); assert!(approx(s.mean, 20.0) && approx(s.sample_mean, 20.0));
let p = s.with_pooled_mean(18.18);
assert!(
approx(p.mean, 18.18),
"reported mean is the pooled centroid"
);
assert!(
approx(p.sample_mean, 20.0),
"Welch mean stays the raw sample mean"
);
assert!(
approx(p.var, 100.0),
"variance stays the raw-sample variance"
);
assert_eq!((p.min, p.max), (10.0, 30.0), "band stays raw");
}
#[test]
fn verdict_overlapping_bands_equal_means_not_separated() {
let v = noise_verdict(&[90.0, 110.0, 100.0], &[95.0, 105.0, 100.0], 100.0);
assert!(
!v.separated,
"overlapping bands, equal means => not separated"
);
}
#[test]
fn verdict_disjoint_bands_separated_higher() {
let v = noise_verdict(&[90.0, 110.0], &[150.0, 150.0], 100.0);
assert!(v.separated);
assert!(v.b.mean > v.a.mean);
}
#[test]
fn verdict_disjoint_bands_separated_lower() {
let v = noise_verdict(&[90.0, 110.0], &[50.0, 50.0], 100.0);
assert!(v.separated);
assert!(v.b.mean < v.a.mean);
}
#[test]
fn verdict_touching_bands_weak_welch_not_separated() {
let v = noise_verdict(&[90.0, 110.0], &[110.0, 110.0], 100.0);
assert!(!v.separated, "touching (non-disjoint) bands + weak Welch");
}
#[test]
fn verdict_welch_separates_overlapping_bands() {
let a = [96.0, 98.0, 100.0, 102.0, 104.0];
let b = [102.0, 104.0, 106.0, 108.0, 110.0];
let v = noise_verdict(&a, &b, 100.0);
assert!(
v.a.max >= v.b.min,
"bands must overlap so this exercises the Welch arm, not the floor",
);
assert!(
v.separated,
"Welch t-test must separate overlapping-band sides"
);
}
#[test]
fn verdict_zero_variance_disjoint_no_panic() {
let v = noise_verdict(&[100.0, 100.0, 100.0], &[130.0, 130.0, 130.0], 5.0);
assert_eq!(v.a.var, 0.0);
assert_eq!(v.b.var, 0.0);
assert!(
v.separated,
"disjoint bands separate even when Welch is skipped"
);
assert!(!v.insufficient_samples);
}
#[test]
fn verdict_high_spread_is_advisory_not_suppressing() {
let v = noise_verdict(&[90.0, 110.0, 100.0], &[300.0, 300.0, 300.0], 5.0);
assert!(v.high_spread, "A's 20% spread exceeds the 5% threshold");
assert!(
v.separated,
"high_spread is advisory and must not suppress a separated verdict",
);
}
#[test]
fn verdict_spread_flag_is_strict_threshold() {
let v = noise_verdict(&[99.5, 100.5], &[99.5, 100.5], 1.0);
assert!(approx(v.a.spread_pct, 1.0));
assert!(
!v.high_spread,
"spread == threshold is not over it (strict >)"
);
}
#[test]
fn schbench_noise_adjust_demonstration() {
use crate::workload::{SchbenchConfig, run_standalone};
const RUN_SECS: u64 = 2; const N: usize = 3;
let p50 = |cfg: &SchbenchConfig| run_standalone(cfg, RUN_SECS).request_pcts_us[1] as f64;
let samples = |cfg: &SchbenchConfig| (0..N).map(|_| p50(cfg)).collect::<Vec<f64>>();
let light = SchbenchConfig::default()
.worker_threads(2)
.sleep_usec(0)
.operations(5);
let heavy = SchbenchConfig::default()
.worker_threads(2)
.sleep_usec(0)
.operations(50);
let a = samples(&light);
let b_same = samples(&light); let b_heavy = samples(&heavy);
let same = noise_verdict(&a, &b_same, 5.0);
let diff = noise_verdict(&a, &b_heavy, 5.0);
eprintln!(
"\nnoise-adjust schbench demo (request p50 us, N={N}, {RUN_SECS}s/run):\n \
A = {:.0} [{:.0}-{:.0}] spread {:.2}%\n \
A vs A : B {:.0} [{:.0}-{:.0}] spread {:.2}% -> separated={} high_spread={}\n \
A vs B' : B {:.0} [{:.0}-{:.0}] spread {:.2}% -> separated={} high_spread={}",
same.a.mean,
same.a.min,
same.a.max,
same.a.spread_pct,
same.b.mean,
same.b.min,
same.b.max,
same.b.spread_pct,
same.separated,
same.high_spread,
diff.b.mean,
diff.b.min,
diff.b.max,
diff.b.spread_pct,
diff.separated,
diff.high_spread,
);
assert!(
diff.separated && diff.b.mean > a.iter().copied().fold(f64::NEG_INFINITY, f64::max),
"10x operations must read as a separated increase clearing A's band: A={:?} B={:?}",
diff.a,
diff.b,
);
}
}