use std::any::Any;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Mutex, PoisonError};
use std::time::Duration;
use super::{TransactionObserver, TxnContext};
use crate::{binder::TransactionCode, Result};
pub const LATENCY_BUCKETS: usize = 25;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LatencyHistogram {
counts: [u64; LATENCY_BUCKETS],
}
impl Default for LatencyHistogram {
fn default() -> Self {
Self {
counts: [0; LATENCY_BUCKETS],
}
}
}
impl LatencyHistogram {
fn bucket(elapsed: Duration) -> usize {
let micros = elapsed.as_micros();
if micros == 0 {
return 0;
}
let bits = (u128::BITS - micros.leading_zeros()) as usize;
bits.min(LATENCY_BUCKETS - 1)
}
fn record(&mut self, elapsed: Duration) {
self.counts[Self::bucket(elapsed)] += 1;
}
pub fn counts(&self) -> &[u64; LATENCY_BUCKETS] {
&self.counts
}
pub fn upper_bound(i: usize) -> Option<Duration> {
(i < LATENCY_BUCKETS - 1).then(|| Duration::from_micros(1 << i))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct MethodStats {
pub descriptor: String,
pub code: TransactionCode,
pub method: Option<&'static str>,
pub calls: u64,
pub errors: u64,
pub total: Duration,
pub max: Duration,
pub latency: LatencyHistogram,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct StatsSnapshot {
pub methods: Vec<MethodStats>,
pub in_flight: usize,
pub max_in_flight: usize,
}
#[derive(Debug, Default)]
pub struct StatsObserver {
methods: Mutex<HashMap<String, HashMap<TransactionCode, MethodStats>>>,
in_flight: AtomicUsize,
max_in_flight: AtomicUsize,
}
impl StatsObserver {
pub fn new() -> Self {
Self::default()
}
pub fn snapshot(&self) -> StatsSnapshot {
let mut methods: Vec<MethodStats> = self
.methods
.lock()
.unwrap_or_else(PoisonError::into_inner)
.values()
.flat_map(|codes| codes.values().cloned())
.collect();
methods.sort_by(|a, b| (&a.descriptor, a.code).cmp(&(&b.descriptor, b.code)));
StatsSnapshot {
methods,
in_flight: self.in_flight.load(Ordering::Relaxed),
max_in_flight: self.max_in_flight.load(Ordering::Relaxed),
}
}
}
impl TransactionObserver for StatsObserver {
fn on_transact(&self, _ctx: &TxnContext<'_>) -> Option<Box<dyn Any + Send>> {
let now = self.in_flight.fetch_add(1, Ordering::Relaxed) + 1;
self.max_in_flight.fetch_max(now, Ordering::Relaxed);
None
}
fn on_reply(
&self,
ctx: &TxnContext<'_>,
_tag: Option<Box<dyn Any + Send>>,
result: &Result<()>,
elapsed: Duration,
) {
let mut n = self.in_flight.load(Ordering::Relaxed);
while let Some(next) = n.checked_sub(1) {
match self.in_flight.compare_exchange_weak(
n,
next,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(current) => n = current,
}
}
let mut methods = self.methods.lock().unwrap_or_else(PoisonError::into_inner);
let codes = match methods.get_mut(ctx.descriptor) {
Some(codes) => codes,
None => methods.entry(ctx.descriptor.to_owned()).or_default(),
};
let stats = codes.entry(ctx.code).or_insert_with(|| MethodStats {
descriptor: ctx.descriptor.to_owned(),
code: ctx.code,
method: ctx.method,
..Default::default()
});
stats.calls += 1;
stats.errors += u64::from(result.is_err());
stats.total += elapsed;
stats.max = stats.max.max(elapsed);
stats.latency.record(elapsed);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{StatusCode, TransportCaps};
fn ctx(descriptor: &str, code: TransactionCode) -> TxnContext<'_> {
TxnContext {
descriptor,
code,
method: Some("m"),
is_oneway: false,
calling_uid: 0,
calling_pid: 0,
transport: TransportCaps::KERNEL,
}
}
#[test]
fn histogram_buckets_are_powers_of_two_microseconds() {
let us = Duration::from_micros;
assert_eq!(LatencyHistogram::bucket(Duration::from_nanos(999)), 0);
assert_eq!(LatencyHistogram::bucket(us(1)), 1);
assert_eq!(LatencyHistogram::bucket(us(2)), 2);
assert_eq!(LatencyHistogram::bucket(us(3)), 2);
assert_eq!(LatencyHistogram::bucket(us(4)), 3);
assert_eq!(LatencyHistogram::bucket(us((1 << 23) - 1)), 23);
assert_eq!(LatencyHistogram::bucket(us(1 << 23)), 24);
assert_eq!(LatencyHistogram::bucket(Duration::MAX), 24);
for i in 1..LATENCY_BUCKETS - 1 {
let bound = LatencyHistogram::upper_bound(i).unwrap();
assert_eq!(LatencyHistogram::bucket(bound - Duration::from_nanos(1)), i);
assert_eq!(LatencyHistogram::bucket(bound), i + 1);
}
assert_eq!(LatencyHistogram::upper_bound(LATENCY_BUCKETS - 1), None);
}
#[test]
fn counts_per_method_and_in_flight() {
let stats = StatsObserver::new();
let (a, b) = (ctx("x.IA", 1), ctx("x.IB", 2));
stats.on_transact(&a);
stats.on_transact(&b);
assert_eq!(stats.snapshot().in_flight, 2);
stats.on_reply(&a, None, &Ok(()), Duration::from_micros(3));
stats.on_reply(
&b,
None,
&Err(StatusCode::DeadObject),
Duration::from_millis(1),
);
stats.on_transact(&a);
stats.on_reply(&a, None, &Ok(()), Duration::from_micros(5));
stats.on_reply(&a, None, &Ok(()), Duration::ZERO);
let snap = stats.snapshot();
assert_eq!((snap.in_flight, snap.max_in_flight), (0, 2));
let [ia, ib] = &snap.methods[..] else {
panic!("{:?}", snap.methods)
};
assert_eq!((ia.descriptor.as_str(), ia.code), ("x.IA", 1));
assert_eq!((ia.calls, ia.errors), (3, 0));
assert_eq!(ia.total, Duration::from_micros(8));
assert_eq!(ia.max, Duration::from_micros(5));
assert_eq!(ia.latency.counts().iter().sum::<u64>(), 3);
assert_eq!((ib.calls, ib.errors, ib.method), (1, 1, Some("m")));
}
}