use std::fmt;
use std::future::Future;
use std::num::NonZeroU32;
use std::pin::Pin;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::task::{Context as TaskContext, Poll};
use std::time::Duration;
use pin_project_lite::pin_project;
use quanta::Clock;
use tracing::warn;
const DEFAULT_THRESHOLD: Duration = Duration::from_micros(100);
const DEFAULT_SAMPLE_EVERY: NonZeroU32 = NonZeroU32::new(64).expect("64 is not zero");
const WINDOW: u64 = 16;
const OVER: u64 = 1 << 63;
pub(crate) const BUCKET_BOUNDS_NS: [u64; 22] = [
1_000,
2_000,
5_000,
10_000,
20_000,
50_000,
100_000,
200_000,
500_000,
1_000_000,
2_000_000,
5_000_000,
10_000_000,
20_000_000,
50_000_000,
100_000_000,
200_000_000,
500_000_000,
1_000_000_000,
2_000_000_000,
5_000_000_000,
10_000_000_000,
];
const BUCKETS: usize = BUCKET_BOUNDS_NS.len() + 1;
#[derive(Clone)]
pub struct PollDiagnostics {
threshold: Duration,
sample_every: NonZeroU32,
registry: Arc<Registry>,
}
struct Registry {
clock: Clock,
subscriptions: Mutex<Vec<Arc<PollStats>>>,
}
impl PollDiagnostics {
#[must_use]
pub fn new() -> Self {
Self {
threshold: DEFAULT_THRESHOLD,
sample_every: DEFAULT_SAMPLE_EVERY,
registry: Arc::new(Registry {
clock: Clock::new(),
subscriptions: Mutex::default(),
}),
}
}
#[must_use]
pub const fn threshold(mut self, threshold: Duration) -> Self {
self.threshold = threshold;
self
}
#[must_use]
pub const fn sample_every(mut self, every: NonZeroU32) -> Self {
self.sample_every = every;
self
}
#[must_use]
pub fn report(&self, subscription: &str) -> Option<PollReport> {
self.lock()
.iter()
.find(|stats| &*stats.name == subscription)
.map(|stats| stats.report())
}
#[must_use]
pub fn reports(&self) -> Vec<PollReport> {
self.lock().iter().map(|stats| stats.report()).collect()
}
#[cfg_attr(not(feature = "metrics"), allow(dead_code))]
pub(crate) fn stats(&self) -> Vec<Arc<PollStats>> {
self.lock().clone()
}
pub(crate) fn register(&self, subscription: &str) -> PollSampler {
let mut subscriptions = self.lock();
let known = subscriptions
.iter()
.find(|stats| &*stats.name == subscription)
.map(Arc::clone);
let stats = known.unwrap_or_else(|| {
let stats = Arc::new(PollStats::new(
subscription,
self.threshold,
self.registry.clock.clone(),
));
subscriptions.push(Arc::clone(&stats));
stats
});
drop(subscriptions);
PollSampler::new(stats, self.sample_every)
}
fn lock(&self) -> MutexGuard<'_, Vec<Arc<PollStats>>> {
self.registry
.subscriptions
.lock()
.unwrap_or_else(PoisonError::into_inner)
}
}
impl Default for PollDiagnostics {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for PollDiagnostics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PollDiagnostics")
.field("threshold", &self.threshold)
.field("sample_every", &self.sample_every)
.field("subscriptions", &self.lock().len())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PollReport {
subscription: Arc<str>,
samples: u64,
average: Duration,
p99: Duration,
}
impl PollReport {
#[must_use]
pub fn subscription(&self) -> &str {
&self.subscription
}
#[must_use]
pub const fn samples(&self) -> u64 {
self.samples
}
#[must_use]
pub const fn average(&self) -> Duration {
self.average
}
#[must_use]
pub const fn p99(&self) -> Duration {
self.p99
}
}
pub(crate) struct PollStats {
name: Arc<str>,
threshold_ns: u64,
clock: Clock,
samples: AtomicU64,
sum_ns: AtomicU64,
average_ns: AtomicU64,
buckets: [AtomicU64; BUCKETS],
}
impl PollStats {
fn new(name: &str, threshold: Duration, clock: Clock) -> Self {
Self {
name: Arc::from(name),
threshold_ns: saturating_nanos(threshold),
clock,
samples: AtomicU64::new(0),
sum_ns: AtomicU64::new(0),
average_ns: AtomicU64::new(0),
buckets: std::array::from_fn(|_| AtomicU64::new(0)),
}
}
#[cfg_attr(not(feature = "metrics"), allow(dead_code))]
pub(crate) fn name(&self) -> &str {
&self.name
}
pub(crate) fn histogram(&self) -> (u64, u64, [u64; BUCKETS]) {
let buckets = std::array::from_fn(|i| self.buckets[i].load(Ordering::Relaxed));
(
buckets.iter().sum(),
self.sum_ns.load(Ordering::Relaxed),
buckets,
)
}
pub(crate) fn report(&self) -> PollReport {
let (samples, _, buckets) = self.histogram();
PollReport {
subscription: Arc::clone(&self.name),
samples,
average: Duration::from_nanos(self.average_ns.load(Ordering::Relaxed) & !OVER),
p99: Duration::from_nanos(quantile_ns(&buckets, 0.99)),
}
}
#[cold]
#[inline(never)]
fn start(&self) -> u64 {
self.clock.raw()
}
#[cold]
#[inline(never)]
fn finish(&self, start: u64) {
let end = self.clock.raw();
self.record(self.clock.delta_as_nanos(start, end));
}
fn record(&self, ns: u64) {
let taken = self.samples.fetch_add(1, Ordering::Relaxed) + 1;
self.sum_ns.fetch_add(ns, Ordering::Relaxed);
self.buckets[bucket_of(ns)].fetch_add(1, Ordering::Relaxed);
let weight = taken.min(WINDOW);
let ns = ns.min(!OVER);
let step = |state: u64| {
let average = state & !OVER;
let average = if ns >= average {
average + (ns - average) / weight
} else {
average - (average - ns) / weight
};
let over = if average <= self.threshold_ns {
0
} else if taken >= WINDOW {
OVER
} else {
state & OVER
};
Some(average | over)
};
let previous = self
.average_ns
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, step)
.unwrap_or_else(|current| current);
let current = step(previous).unwrap_or(previous);
if previous & OVER == 0 && current & OVER != 0 {
self.warn(current & !OVER);
}
}
#[cold]
fn warn(&self, average_ns: u64) {
let (_, _, buckets) = self.histogram();
warn!(
target: "ruststream::dispatch",
subscription = %self.name,
average = ?Duration::from_nanos(average_ns),
p99 = ?Duration::from_nanos(quantile_ns(&buckets, 0.99)),
threshold = ?Duration::from_nanos(self.threshold_ns),
"the handler computes between awaits for longer than the threshold, holding up the \
app runtime's threads; mount the subscription with threads(n) to give it threads \
of its own",
);
}
}
pub(crate) struct PollSampler {
stats: Arc<PollStats>,
every: u32,
countdown: AtomicU32,
}
impl PollSampler {
fn new(stats: Arc<PollStats>, every: NonZeroU32) -> Self {
Self {
stats,
every: every.get(),
countdown: AtomicU32::new(every.get()),
}
}
pub(crate) fn for_thread(&self) -> Self {
Self {
stats: Arc::clone(&self.stats),
every: self.every,
countdown: AtomicU32::new(self.every),
}
}
pub(crate) const fn observe<F>(&self, future: F) -> Sampled<'_, F> {
Sampled {
future,
sampler: self,
}
}
#[inline]
fn tick(&self) -> bool {
let left = self.countdown.load(Ordering::Relaxed);
if left > 1 {
self.countdown.store(left - 1, Ordering::Relaxed);
false
} else {
self.countdown.store(self.every, Ordering::Relaxed);
true
}
}
}
impl fmt::Debug for PollSampler {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PollSampler")
.field("subscription", &self.stats.name)
.field("every", &self.every)
.finish_non_exhaustive()
}
}
pin_project! {
pub(crate) struct Sampled<'a, F> {
#[pin]
future: F,
sampler: &'a PollSampler,
}
}
impl<F: Future> Future for Sampled<'_, F> {
type Output = F::Output;
#[inline]
fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
let this = self.project();
let stats = &*this.sampler.stats;
let start = if this.sampler.tick() {
Some(stats.start())
} else {
None
};
let polled = this.future.poll(cx);
if let Some(start) = start {
stats.finish(start);
}
polled
}
}
fn saturating_nanos(duration: Duration) -> u64 {
u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
}
fn bucket_of(ns: u64) -> usize {
BUCKET_BOUNDS_NS.partition_point(|&bound| bound < ns)
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
fn quantile_ns(buckets: &[u64; BUCKETS], q: f64) -> u64 {
let total: u64 = buckets.iter().sum();
if total == 0 {
return 0;
}
let rank = (total as f64 * q).ceil().max(1.0);
let mut below = 0u64;
for (i, &count) in buckets.iter().enumerate() {
if count > 0 && (below + count) as f64 >= rank {
let lower = if i == 0 { 0 } else { BUCKET_BOUNDS_NS[i - 1] };
let Some(&upper) = BUCKET_BOUNDS_NS.get(i) else {
return lower;
};
let within = (rank - below as f64) / count as f64;
return lower + ((upper - lower) as f64 * within) as u64;
}
below += count;
}
BUCKET_BOUNDS_NS[BUCKET_BOUNDS_NS.len() - 1]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_defaults_are_a_hundred_microseconds_and_one_poll_in_64() {
let diagnostics = PollDiagnostics::new();
assert_eq!(diagnostics.threshold, Duration::from_micros(100));
assert_eq!(diagnostics.sample_every.get(), 64);
assert_eq!(
format!("{diagnostics:?}"),
format!("{:?}", PollDiagnostics::default())
);
}
#[test]
fn a_sampler_times_every_nth_poll() {
let sampler = PollDiagnostics::new()
.sample_every(NonZeroU32::new(3).expect("three"))
.register("s");
let timed: Vec<bool> = (0..7).map(|_| sampler.tick()).collect();
assert_eq!(timed, [false, false, true, false, false, true, false]);
let own = sampler.for_thread();
assert!(!own.tick());
assert!(format!("{sampler:?}").contains("\"s\""));
}
#[test]
fn a_name_registered_twice_shares_its_statistics() {
let diagnostics = PollDiagnostics::new();
let first = diagnostics.register("s");
let second = diagnostics.register("s");
first.stats.record(1_500);
second.stats.record(1_500);
assert_eq!(diagnostics.reports().len(), 1);
assert_eq!(diagnostics.report("s").map(|r| r.samples()), Some(2));
}
#[test]
fn the_average_moves_over_the_window() {
let stats = PollStats::new("s", Duration::from_secs(1), Clock::new());
stats.record(1_000);
assert_eq!(stats.report().average(), Duration::from_nanos(1_000));
stats.record(3_000);
assert_eq!(stats.report().average(), Duration::from_nanos(2_000));
for _ in 0..1_000 {
stats.record(10_000);
}
assert!(stats.report().average() > Duration::from_nanos(9_900));
stats.record(0);
assert_eq!(stats.report().average(), Duration::from_nanos(9_361));
}
#[test]
fn a_crossing_is_held_in_the_average_word_and_hidden_from_the_report() {
let stats = PollStats::new("s", Duration::from_nanos(500), Clock::new());
let over = || stats.average_ns.load(Ordering::Relaxed) & OVER != 0;
for _ in 1..WINDOW {
stats.record(1_000);
}
assert!(!over(), "the average warned before it was warmed up");
stats.record(1_000);
assert!(over());
assert_eq!(stats.report().average(), Duration::from_nanos(1_000));
for _ in 0..100 {
stats.record(0);
}
assert!(
!over(),
"the average fell under the threshold and stayed flagged"
);
assert!(stats.report().average() < Duration::from_nanos(500));
}
#[test]
fn the_quantile_is_read_off_the_buckets() {
let mut buckets = [0u64; BUCKETS];
assert_eq!(quantile_ns(&buckets, 0.99), 0);
buckets[bucket_of(70_000)] = 100;
assert_eq!(bucket_of(70_000), 6);
assert_eq!(quantile_ns(&buckets, 0.99), 99_500);
assert_eq!(quantile_ns(&buckets, 0.5), 75_000);
let mut long = [0u64; BUCKETS];
long[bucket_of(u64::MAX)] = 1;
assert_eq!(bucket_of(u64::MAX), BUCKETS - 1);
assert_eq!(quantile_ns(&long, 0.99), 10_000_000_000);
assert_eq!(bucket_of(1_000), 0);
assert_eq!(bucket_of(1_001), 1);
assert_eq!(saturating_nanos(Duration::MAX), u64::MAX);
}
}