use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
const SAMPLE_SCALE: u64 = 1_000_000;
#[derive(Debug)]
pub struct SamplingGate {
sampled_per_million: u64,
accumulator: AtomicU64,
}
impl SamplingGate {
pub fn new(sample_ratio: f64) -> Self {
Self {
sampled_per_million: sampled_per_million(sample_ratio),
accumulator: AtomicU64::new(0),
}
}
#[inline]
pub fn should_sample(&self) -> bool {
match self.sampled_per_million {
0 => false,
SAMPLE_SCALE => true,
sampled_per_million => {
let previous = self.accumulator.fetch_add(sampled_per_million, Ordering::Relaxed);
previous % SAMPLE_SCALE + sampled_per_million >= SAMPLE_SCALE
}
}
}
pub fn sampled_per_million(&self) -> u64 {
self.sampled_per_million
}
}
impl Default for SamplingGate {
fn default() -> Self {
Self::new(1.0)
}
}
fn sampled_per_million(sample_ratio: f64) -> u64 {
if sample_ratio.is_nan() || sample_ratio <= 0.0 {
return 0;
}
if sample_ratio >= 1.0 {
return SAMPLE_SCALE;
}
(sample_ratio * SAMPLE_SCALE as f64).ceil() as u64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn full_sample_ratio_always_samples() {
let gate = SamplingGate::new(1.0);
assert_eq!(gate.sampled_per_million(), SAMPLE_SCALE);
assert!((0..8).all(|_| gate.should_sample()));
}
#[test]
fn non_positive_or_invalid_ratio_never_samples() {
for ratio in [0.0, -1.0, f64::NAN, f64::NEG_INFINITY] {
let gate = SamplingGate::new(ratio);
assert_eq!(gate.sampled_per_million(), 0);
assert!((0..8).all(|_| !gate.should_sample()));
}
}
#[test]
fn fractional_ratio_is_evenly_spread() {
let one_quarter = SamplingGate::new(0.25);
let three_quarters = SamplingGate::new(0.75);
assert_eq!((0..8).filter(|_| one_quarter.should_sample()).count(), 2);
assert_eq!((0..8).filter(|_| three_quarters.should_sample()).count(), 6);
}
}