Skip to content

Commit b277a75

Browse files
regexidentberkus
authored andcommitted
Fix panic with uniform iteration durations in benchmarks
When all benchmark iterations report identical durations, criterion.rs would panic during chart generation with "assertion failed: !(range.0.is_nan() || range.1.is_nan())". This occurred because the Silverman bandwidth calculation produced zero when standard deviation was zero, leading to division by zero in the KDE sweep code and ultimately NaN values in chart coordinates. This fix adds a fallback bandwidth when standard deviation is zero, using 1% of the data range (or 1% of the absolute value if all values are identical, or a minimal epsilon for all-zero data). This is semantically correct because KDE assumes data has spread; when variance is zero, we provide minimal artificial bandwidth to create a narrow distribution centered on the actual value. The fix is scale-invariant and handles all edge cases while preserving existing behavior for benchmarks with variance. Fixes #873
1 parent 828af14 commit b277a75

2 files changed

Lines changed: 35 additions & 6 deletions

File tree

src/stats/univariate/kde/mod.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,29 @@ impl Bandwidth {
7979
fn estimate<A: Float>(self, sample: &Sample<A>) -> A {
8080
match self {
8181
Bandwidth::Silverman => {
82-
let factor = A::cast(4. / 3.);
83-
let exponent = A::cast(1. / 5.);
84-
let n = A::cast(sample.len());
85-
let sigma = sample.std_dev(None);
86-
87-
sigma * (factor / n).powf(exponent)
82+
let zero = A::cast(0.0);
83+
let range = sample.max() - sample.min();
84+
85+
// When std_dev is zero (all values identical), bandwidth calculation
86+
// would produce zero, leading to division by zero in KDE sweep.
87+
// Use a small fallback bandwidth relative to the data range.
88+
if range == zero {
89+
// All values are identical. Use 1% of the value magnitude, or a
90+
// minimal epsilon if the value is also zero.
91+
let val = sample.mean();
92+
if val.abs() > zero {
93+
val.abs() * A::cast(0.01)
94+
} else {
95+
A::cast(1e-10) // Minimal epsilon for zero values
96+
}
97+
} else {
98+
// Non-constant data: use Silverman's rule of thumb
99+
let sigma = sample.std_dev(None);
100+
let factor = A::cast(4. / 3.);
101+
let exponent = A::cast(1. / 5.);
102+
let n = A::cast(sample.len());
103+
sigma * (factor / n).powf(exponent)
104+
}
88105
}
89106
}
90107
}

tests/criterion_tests.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,18 @@ fn test_criterion_doesnt_panic_if_measured_time_is_zero() {
465465
});
466466
}
467467

468+
#[test]
469+
fn test_criterion_doesnt_panic_with_uniform_durations() {
470+
// Reproduces issue #873: iter_custom returning uniform durations
471+
// (iters milliseconds) causes panic during chart generation due to
472+
// zero variance leading to zero bandwidth in KDE calculation.
473+
let dir = temp_dir();
474+
let mut c = short_benchmark(&dir);
475+
c.bench_function("uniform_durations", |bencher| {
476+
bencher.iter_custom(|iters| Duration::from_millis(iters));
477+
});
478+
}
479+
468480
mod macros {
469481
use super::{criterion_group, criterion_main, Criterion};
470482

0 commit comments

Comments
 (0)