use crate::config::DistributionConfig;
use crate::util::splitmix64;
pub const DEFAULT_HISTOGRAM_BUCKETS: &[f64] = &[
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
];
#[derive(Debug, Clone)]
pub enum Distribution {
Exponential {
rate: f64,
},
Normal {
mean: f64,
stddev: f64,
},
Uniform {
min: f64,
max: f64,
},
}
pub(crate) fn to_distribution(config: &DistributionConfig) -> Distribution {
match config {
DistributionConfig::Exponential { rate } => Distribution::Exponential { rate: *rate },
DistributionConfig::Normal { mean, stddev } => Distribution::Normal {
mean: *mean,
stddev: *stddev,
},
DistributionConfig::Uniform { min, max } => Distribution::Uniform {
min: *min,
max: *max,
},
}
}
#[derive(Debug, Clone)]
pub struct HistogramSample {
pub bucket_counts: Vec<u64>,
pub count: u64,
pub sum: f64,
}
pub struct HistogramGenerator {
buckets: Vec<f64>,
bucket_counts: Vec<u64>,
count: u64,
sum: f64,
distribution: Distribution,
observations_per_tick: u64,
mean_shift_per_sec: f64,
seed: u64,
rate: f64,
tick_counter: u64,
}
impl HistogramGenerator {
pub fn new(
buckets: Vec<f64>,
distribution: Distribution,
observations_per_tick: u64,
mean_shift_per_sec: f64,
seed: u64,
rate: f64,
) -> Self {
let bucket_counts = vec![0u64; buckets.len()];
Self {
buckets,
bucket_counts,
count: 0,
sum: 0.0,
distribution,
observations_per_tick,
mean_shift_per_sec,
seed,
rate,
tick_counter: 0,
}
}
pub fn buckets(&self) -> &[f64] {
&self.buckets
}
pub fn observe(&mut self, tick: u64) -> HistogramSample {
let elapsed_secs = tick as f64 / self.rate;
let shift = self.mean_shift_per_sec * elapsed_secs;
for i in 0..self.observations_per_tick {
let rng_input = self
.seed
.wrapping_mul(0x517c_c1b7_2722_0a95)
.wrapping_add(self.tick_counter)
.wrapping_mul(0x6c62_272e_07bb_0142)
.wrapping_add(i);
let value = self.sample_distribution(rng_input, shift);
self.count += 1;
self.sum += value;
for (j, &bound) in self.buckets.iter().enumerate() {
if value <= bound {
self.bucket_counts[j] += 1;
}
}
}
self.tick_counter += 1;
HistogramSample {
bucket_counts: self.bucket_counts.clone(),
count: self.count,
sum: self.sum,
}
}
fn sample_distribution(&self, rng_input: u64, shift: f64) -> f64 {
match &self.distribution {
Distribution::Exponential { rate } => {
let u = uniform_01(rng_input);
let u_clamped = u.min(1.0 - f64::EPSILON);
let value = -(1.0 - u_clamped).ln() / rate;
value + shift
}
Distribution::Normal { mean, stddev } => {
let u1 = uniform_01(rng_input);
let u2 = uniform_01(splitmix64(rng_input.wrapping_add(1)));
let u1_clamped = u1.max(f64::EPSILON);
let z = (-2.0 * u1_clamped.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
(mean + shift) + stddev * z
}
Distribution::Uniform { min, max } => {
let u = uniform_01(rng_input);
min + u * (max - min) + shift
}
}
}
}
fn uniform_01(input: u64) -> f64 {
let hash = splitmix64(input);
(hash >> 11) as f64 / (1u64 << 53) as f64
}
#[cfg(test)]
mod tests {
use super::*;
fn exponential_gen(buckets: Vec<f64>, rate: f64, seed: u64) -> HistogramGenerator {
HistogramGenerator::new(
buckets,
Distribution::Exponential { rate },
100, 0.0, seed,
10.0, )
}
#[test]
fn bucket_counts_never_decrease_across_ticks() {
let mut gen = exponential_gen(DEFAULT_HISTOGRAM_BUCKETS.to_vec(), 10.0, 42);
let mut prev = gen.observe(0);
for tick in 1..20 {
let curr = gen.observe(tick);
for (i, (&prev_count, &curr_count)) in prev
.bucket_counts
.iter()
.zip(curr.bucket_counts.iter())
.enumerate()
{
assert!(
curr_count >= prev_count,
"bucket {i} decreased: {prev_count} -> {curr_count} at tick {tick}"
);
}
assert!(curr.count >= prev.count, "count decreased at tick {tick}");
prev = curr;
}
}
#[test]
fn count_equals_observations_times_ticks() {
let mut gen = HistogramGenerator::new(
DEFAULT_HISTOGRAM_BUCKETS.to_vec(),
Distribution::Exponential { rate: 5.0 },
50,
0.0,
0,
10.0,
);
for tick in 0..10 {
let sample = gen.observe(tick);
assert_eq!(
sample.count,
50 * (tick + 1),
"count must equal observations_per_tick * ticks at tick {tick}"
);
}
}
#[test]
fn largest_bucket_captures_most_observations() {
let mut gen = exponential_gen(vec![0.1, 0.5, 1.0, 5.0, 100.0], 10.0, 42);
let sample = gen.observe(0);
assert!(
sample.bucket_counts[4] >= 95,
"largest bucket should capture most of 100 observations, got {}",
sample.bucket_counts[4]
);
}
#[test]
fn same_seed_produces_identical_output() {
let mut gen_a = exponential_gen(DEFAULT_HISTOGRAM_BUCKETS.to_vec(), 10.0, 42);
let mut gen_b = exponential_gen(DEFAULT_HISTOGRAM_BUCKETS.to_vec(), 10.0, 42);
for tick in 0..10 {
let a = gen_a.observe(tick);
let b = gen_b.observe(tick);
assert_eq!(
a.bucket_counts, b.bucket_counts,
"bucket counts must match at tick {tick}"
);
assert_eq!(a.count, b.count, "count must match at tick {tick}");
assert_eq!(a.sum, b.sum, "sum must match at tick {tick}");
}
}
#[test]
fn different_seeds_produce_different_output() {
let mut gen_a = exponential_gen(DEFAULT_HISTOGRAM_BUCKETS.to_vec(), 10.0, 1);
let mut gen_b = exponential_gen(DEFAULT_HISTOGRAM_BUCKETS.to_vec(), 10.0, 2);
let a = gen_a.observe(0);
let b = gen_b.observe(0);
assert_ne!(
a.bucket_counts, b.bucket_counts,
"different seeds should produce different bucket counts"
);
}
#[test]
fn default_buckets_are_sorted_and_positive() {
for window in DEFAULT_HISTOGRAM_BUCKETS.windows(2) {
assert!(
window[0] < window[1],
"default buckets must be strictly sorted: {} >= {}",
window[0],
window[1]
);
}
for &b in DEFAULT_HISTOGRAM_BUCKETS {
assert!(b > 0.0, "default bucket {b} must be positive");
}
}
#[test]
fn default_buckets_have_expected_count() {
assert_eq!(
DEFAULT_HISTOGRAM_BUCKETS.len(),
11,
"Prometheus default has 11 buckets"
);
}
#[test]
fn exponential_values_are_non_negative() {
let mut gen = exponential_gen(vec![0.1, 1.0, 10.0], 5.0, 99);
let sample = gen.observe(0);
assert!(
sample.sum >= 0.0,
"exponential sum must be non-negative, got {}",
sample.sum
);
}
#[test]
fn normal_distribution_centers_around_mean() {
let mut gen = HistogramGenerator::new(
vec![0.0, 0.05, 0.1, 0.15, 0.2, 0.5, 1.0],
Distribution::Normal {
mean: 0.1,
stddev: 0.02,
},
10000,
0.0,
42,
10.0,
);
let sample = gen.observe(0);
let average = sample.sum / sample.count as f64;
assert!(
(average - 0.1).abs() < 0.01,
"normal mean should be ~0.1, got {average}"
);
}
#[test]
fn uniform_distribution_within_range() {
let mut gen = HistogramGenerator::new(
vec![1.0, 2.0, 3.0, 4.0, 5.0],
Distribution::Uniform { min: 1.0, max: 5.0 },
10000,
0.0,
42,
10.0,
);
let sample = gen.observe(0);
let average = sample.sum / sample.count as f64;
assert!(
(average - 3.0).abs() < 0.1,
"uniform mean should be ~3.0, got {average}"
);
}
#[test]
fn mean_shift_increases_values_over_time() {
let mut gen = HistogramGenerator::new(
vec![0.1, 0.5, 1.0, 5.0, 10.0, 50.0],
Distribution::Exponential { rate: 10.0 },
1000,
1.0, 42,
1.0, );
let sample_early = gen.observe(0);
let avg_early = sample_early.sum / 1000.0;
for tick in 1..10 {
gen.observe(tick);
}
let sample_late = gen.observe(10);
let avg_late = (sample_late.sum - sample_early.sum - {
let mut sum = 0.0;
let mut gen2 = HistogramGenerator::new(
vec![0.1, 0.5, 1.0, 5.0, 10.0, 50.0],
Distribution::Exponential { rate: 10.0 },
1000,
1.0,
42,
1.0,
);
for t in 0..10 {
let s = gen2.observe(t);
sum = s.sum;
}
sum - sample_early.sum
}) / 1000.0;
assert!(
avg_late > avg_early,
"late average ({avg_late}) should be higher than early ({avg_early}) due to mean shift"
);
}
#[test]
fn single_bucket_works() {
let mut gen = HistogramGenerator::new(
vec![1.0],
Distribution::Uniform { min: 0.0, max: 2.0 },
100,
0.0,
42,
10.0,
);
let sample = gen.observe(0);
assert_eq!(sample.bucket_counts.len(), 1);
assert_eq!(sample.count, 100);
assert!(
sample.bucket_counts[0] > 20,
"single bucket should capture some observations, got {}",
sample.bucket_counts[0]
);
}
#[test]
fn sum_accumulates_across_ticks() {
let mut gen = HistogramGenerator::new(
vec![1.0],
Distribution::Uniform { min: 0.0, max: 1.0 },
100,
0.0,
42,
10.0,
);
let s1 = gen.observe(0);
let s2 = gen.observe(1);
assert!(
s2.sum > s1.sum,
"sum must increase across ticks: {} -> {}",
s1.sum,
s2.sum
);
}
#[test]
fn uniform_01_in_range() {
for i in 0..10_000u64 {
let v = uniform_01(i);
assert!(
(0.0..1.0).contains(&v),
"uniform_01({i}) = {v} is outside [0, 1)"
);
}
}
#[test]
fn histogram_generator_is_send() {
fn assert_send<T: Send>() {}
assert_send::<HistogramGenerator>();
}
#[test]
fn histogram_sample_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<HistogramSample>();
}
}