use std::cell::Cell;
use std::collections::HashMap;
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Sampler {
Never,
OneIn(u64),
}
impl Sampler {
pub(crate) fn from_rate(rate: f64) -> Option<Self> {
if !rate.is_finite() || !(0.0..=1.0).contains(&rate) {
return None;
}
if rate == 0.0 {
return Some(Sampler::Never);
}
let k = (1.0 / rate).round() as u64;
if k <= 1 {
None
} else {
Some(Sampler::OneIn(k))
}
}
pub(crate) fn effective_rate(&self) -> f64 {
match self {
Sampler::Never => 0.0,
Sampler::OneIn(k) => 1.0 / *k as f64,
}
}
#[inline]
pub(crate) fn sample_id(&self, id: u64) -> bool {
match self {
Sampler::Never => false,
Sampler::OneIn(k) => id.is_multiple_of(*k),
}
}
#[inline]
fn sample_counter(&self, counter: &Cell<u64>) -> bool {
let n = counter.get();
counter.set(n.wrapping_add(1));
self.sample_id(n)
}
}
pub(crate) struct ResourceSampling {
slot: OnceLock<Option<Sampler>>,
}
impl ResourceSampling {
const fn new() -> Self {
Self {
slot: OnceLock::new(),
}
}
fn set(&self, sampler: Option<Sampler>) {
let _ = self.slot.set(sampler);
}
#[inline]
pub(crate) fn sampler(&self) -> Option<Sampler> {
self.slot.get().copied().flatten()
}
}
pub(crate) static FUNCTIONS_SAMPLING: ResourceSampling = ResourceSampling::new();
pub(crate) static MUTEXES_SAMPLING: ResourceSampling = ResourceSampling::new();
pub(crate) static RW_LOCKS_SAMPLING: ResourceSampling = ResourceSampling::new();
pub(crate) static FUTURES_SAMPLING: ResourceSampling = ResourceSampling::new();
pub(crate) static CHANNELS_SAMPLING: ResourceSampling = ResourceSampling::new();
thread_local! {
static FUNCTIONS_COUNTER: Cell<u64> = const { Cell::new(0) };
static MUTEXES_COUNTER: Cell<u64> = const { Cell::new(0) };
static RW_LOCKS_COUNTER: Cell<u64> = const { Cell::new(0) };
static FUTURES_COUNTER: Cell<u64> = const { Cell::new(0) };
}
#[inline]
fn should_time(
sampling: &ResourceSampling,
counter: &'static std::thread::LocalKey<Cell<u64>>,
) -> bool {
match sampling.sampler() {
None => true,
Some(Sampler::Never) => false,
Some(sampler) => counter
.try_with(|c| sampler.sample_counter(c))
.unwrap_or(false),
}
}
#[inline]
fn untime(sampling: &ResourceSampling, counter: &'static std::thread::LocalKey<Cell<u64>>) {
if matches!(sampling.sampler(), Some(Sampler::OneIn(_))) {
let _ = counter.try_with(|c| c.set(c.get().wrapping_sub(1)));
}
}
#[inline]
pub(crate) fn functions_should_time() -> bool {
should_time(&FUNCTIONS_SAMPLING, &FUNCTIONS_COUNTER)
}
#[inline]
pub(crate) fn mutexes_should_time() -> bool {
should_time(&MUTEXES_SAMPLING, &MUTEXES_COUNTER)
}
#[inline]
pub(crate) fn mutexes_untime() {
untime(&MUTEXES_SAMPLING, &MUTEXES_COUNTER)
}
#[inline]
pub(crate) fn rw_locks_should_time() -> bool {
should_time(&RW_LOCKS_SAMPLING, &RW_LOCKS_COUNTER)
}
#[inline]
pub(crate) fn rw_locks_untime() {
untime(&RW_LOCKS_SAMPLING, &RW_LOCKS_COUNTER)
}
#[inline]
pub(crate) fn futures_should_time() -> bool {
should_time(&FUTURES_SAMPLING, &FUTURES_COUNTER)
}
#[inline]
pub(crate) fn channels_should_time(msg_id: u64) -> bool {
match CHANNELS_SAMPLING.sampler() {
None => true,
Some(sampler) => sampler.sample_id(msg_id),
}
}
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct TimeSamplingConfig {
pub(crate) global: Option<f64>,
pub(crate) functions: Option<f64>,
pub(crate) mutexes: Option<f64>,
pub(crate) rw_locks: Option<f64>,
pub(crate) futures: Option<f64>,
pub(crate) channels: Option<f64>,
}
fn parse_rate_env(name: &str) -> Option<f64> {
let rate: f64 = std::env::var(name).ok()?.parse().ok()?;
(rate.is_finite() && (0.0..=1.0).contains(&rate)).then_some(rate)
}
pub(crate) fn init_time_sampling_rate(config: &TimeSamplingConfig) {
let global_env = parse_rate_env("HOTPATH_TIME_SAMPLING_RATE");
let resolve = |env_name: &str, builder_rate: Option<f64>| {
parse_rate_env(env_name)
.or(global_env)
.or(builder_rate)
.or(config.global)
.and_then(Sampler::from_rate)
};
FUNCTIONS_SAMPLING.set(resolve(
"HOTPATH_FUNCTIONS_TIME_SAMPLING_RATE",
config.functions,
));
MUTEXES_SAMPLING.set(resolve(
"HOTPATH_MUTEXES_TIME_SAMPLING_RATE",
config.mutexes,
));
RW_LOCKS_SAMPLING.set(resolve(
"HOTPATH_RW_LOCKS_TIME_SAMPLING_RATE",
config.rw_locks,
));
FUTURES_SAMPLING.set(resolve(
"HOTPATH_FUTURES_TIME_SAMPLING_RATE",
config.futures,
));
CHANNELS_SAMPLING.set(resolve(
"HOTPATH_CHANNELS_TIME_SAMPLING_RATE",
config.channels,
));
}
pub(crate) fn active_rates() -> Option<HashMap<String, f64>> {
let mut rates = HashMap::new();
for (name, sampling) in [
("functions", &FUNCTIONS_SAMPLING),
("mutexes", &MUTEXES_SAMPLING),
("rw_locks", &RW_LOCKS_SAMPLING),
("futures", &FUTURES_SAMPLING),
("channels", &CHANNELS_SAMPLING),
] {
if let Some(sampler) = sampling.sampler() {
rates.insert(name.to_string(), sampler.effective_rate());
}
}
(!rates.is_empty()).then_some(rates)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_rate_bounds() {
assert_eq!(Sampler::from_rate(1.0), None);
assert_eq!(Sampler::from_rate(0.0), Some(Sampler::Never));
assert_eq!(Sampler::from_rate(0.5), Some(Sampler::OneIn(2)));
assert_eq!(Sampler::from_rate(0.1), Some(Sampler::OneIn(10)));
assert_eq!(Sampler::from_rate(0.001), Some(Sampler::OneIn(1000)));
assert_eq!(Sampler::from_rate(0.3), Some(Sampler::OneIn(3)));
assert_eq!(Sampler::from_rate(-0.1), None);
assert_eq!(Sampler::from_rate(1.5), None);
assert_eq!(Sampler::from_rate(f64::NAN), None);
assert_eq!(Sampler::from_rate(0.9), None);
}
#[test]
fn sample_id_keeps_one_in_k() {
let s = Sampler::OneIn(10);
let kept = (0..100).filter(|&i| s.sample_id(i)).count();
assert_eq!(kept, 10);
assert!(s.sample_id(0));
assert!(!Sampler::Never.sample_id(0));
}
#[test]
fn counter_sampling_is_deterministic() {
let s = Sampler::OneIn(3);
let c = Cell::new(0);
let pattern: Vec<bool> = (0..6).map(|_| s.sample_counter(&c)).collect();
assert_eq!(pattern, [true, false, false, true, false, false]);
}
}