use grommet_core::Snapshot;
pub use hdrhistogram::Histogram;
use parking_lot::Mutex;
use std::cell::{Cell, RefCell};
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering::Relaxed;
const LOWEST_NANOS: u64 = 1;
const HIGHEST_NANOS: u64 = 60_000_000_000;
const SIGNIFICANT_FIGURES: u8 = 2;
pub fn histogram() -> Histogram<u64> {
Histogram::new_with_bounds(LOWEST_NANOS, HIGHEST_NANOS, SIGNIFICANT_FIGURES)
.expect("the bounds above are constant and valid")
}
#[derive(Debug)]
pub struct ShardHot<const CLASSES: usize = 2> {
pub panicked: Cell<u64>,
pub failed: Cell<u64>,
pub in_doubt: Cell<u64>,
pub coalesced: Cell<u64>,
pub evicted: Cell<u64>,
pub busy_nanos: Cell<u64>,
pub queue_wait_nanos: Cell<u64>,
pub started_by_class: [Cell<u64>; CLASSES],
pub completed_by_class: [Cell<u64>; CLASSES],
pub expired_by_class: [Cell<u64>; CLASSES],
pub inflight_age_max_nanos: Cell<u64>,
queue_wait: RefCell<Histogram<u64>>,
process: RefCell<Histogram<u64>>,
}
impl<const CLASSES: usize> Default for ShardHot<CLASSES> {
fn default() -> Self {
Self {
panicked: Cell::new(0),
failed: Cell::new(0),
in_doubt: Cell::new(0),
coalesced: Cell::new(0),
evicted: Cell::new(0),
busy_nanos: Cell::new(0),
queue_wait_nanos: Cell::new(0),
started_by_class: std::array::from_fn(|_| Cell::new(0)),
completed_by_class: std::array::from_fn(|_| Cell::new(0)),
expired_by_class: std::array::from_fn(|_| Cell::new(0)),
inflight_age_max_nanos: Cell::new(0),
queue_wait: RefCell::new(histogram()),
process: RefCell::new(histogram()),
}
}
}
impl<const CLASSES: usize> ShardHot<CLASSES> {
#[inline]
pub fn add(&self, counter: &Cell<u64>, value: u64) {
counter.set(counter.get().wrapping_add(value));
}
#[inline]
pub fn bump(&self, counter: &Cell<u64>) {
counter.set(counter.get().wrapping_add(1));
}
#[inline]
pub fn started(&self) -> u64 {
Self::total(&self.started_by_class)
}
#[inline]
pub fn completed(&self) -> u64 {
Self::total(&self.completed_by_class)
}
#[inline]
pub fn expired(&self) -> u64 {
Self::total(&self.expired_by_class)
}
fn total(counters: &[Cell<u64>; CLASSES]) -> u64 {
counters.iter().fold(0u64, |sum, counter| sum.wrapping_add(counter.get()))
}
#[inline]
pub fn record_queue_wait(&self, nanos: u64) {
self.queue_wait.borrow_mut().saturating_record(nanos);
}
#[inline]
pub fn record_process(&self, nanos: u64) {
self.process.borrow_mut().saturating_record(nanos);
}
}
#[derive(Debug)]
pub struct ShardStats<const CLASSES: usize = 2> {
pub started: AtomicU64,
pub completed: AtomicU64,
pub panicked: AtomicU64,
pub failed: AtomicU64,
pub in_doubt: AtomicU64,
pub coalesced: AtomicU64,
pub expired: AtomicU64,
pub evicted: AtomicU64,
pub busy_nanos: AtomicU64,
pub queue_wait_nanos: AtomicU64,
pub inflight: [AtomicU64; CLASSES],
pub ready: [AtomicU64; CLASSES],
pub pending: AtomicU64,
pub resident: AtomicU64,
pub evicting: AtomicU64,
pub eviction_backlog: AtomicU64,
pub queue_capacity: AtomicU64,
pub started_by_class: [AtomicU64; CLASSES],
pub completed_by_class: [AtomicU64; CLASSES],
pub expired_by_class: [AtomicU64; CLASSES],
pub inflight_age_max_nanos: AtomicU64,
queue_wait: Mutex<Histogram<u64>>,
process: Mutex<Histogram<u64>>,
}
impl<const CLASSES: usize> Default for ShardStats<CLASSES> {
fn default() -> Self {
Self {
started: AtomicU64::new(0),
completed: AtomicU64::new(0),
panicked: AtomicU64::new(0),
failed: AtomicU64::new(0),
in_doubt: AtomicU64::new(0),
coalesced: AtomicU64::new(0),
expired: AtomicU64::new(0),
evicted: AtomicU64::new(0),
busy_nanos: AtomicU64::new(0),
queue_wait_nanos: AtomicU64::new(0),
inflight: std::array::from_fn(|_| AtomicU64::new(0)),
ready: std::array::from_fn(|_| AtomicU64::new(0)),
pending: AtomicU64::new(0),
resident: AtomicU64::new(0),
evicting: AtomicU64::new(0),
eviction_backlog: AtomicU64::new(0),
queue_capacity: AtomicU64::new(0),
started_by_class: std::array::from_fn(|_| AtomicU64::new(0)),
completed_by_class: std::array::from_fn(|_| AtomicU64::new(0)),
expired_by_class: std::array::from_fn(|_| AtomicU64::new(0)),
inflight_age_max_nanos: AtomicU64::new(0),
queue_wait: Mutex::new(histogram()),
process: Mutex::new(histogram()),
}
}
}
impl<const CLASSES: usize> ShardStats<CLASSES> {
pub fn queue_wait_quantile(&self, quantile: f64) -> u64 {
self.queue_wait.lock().value_at_quantile(quantile)
}
pub fn process_quantile(&self, quantile: f64) -> u64 {
self.process.lock().value_at_quantile(quantile)
}
pub fn merge_queue_wait_into(&self, into: &mut Histogram<u64>) {
Self::merge(&self.queue_wait, into);
}
pub fn merge_process_into(&self, into: &mut Histogram<u64>) {
Self::merge(&self.process, into);
}
fn merge(from: &Mutex<Histogram<u64>>, into: &mut Histogram<u64>) {
into.add(&*from.lock()).expect("the destination must come from `metrics::histogram()`");
}
pub(crate) fn publish(&self, hot: &ShardHot<CLASSES>, snapshot: &Snapshot<CLASSES>) {
self.started.store(hot.started(), Relaxed);
self.completed.store(hot.completed(), Relaxed);
self.panicked.store(hot.panicked.get(), Relaxed);
self.failed.store(hot.failed.get(), Relaxed);
self.in_doubt.store(hot.in_doubt.get(), Relaxed);
self.coalesced.store(hot.coalesced.get(), Relaxed);
self.expired.store(hot.expired(), Relaxed);
self.evicted.store(hot.evicted.get(), Relaxed);
self.busy_nanos.store(hot.busy_nanos.get(), Relaxed);
self.queue_wait_nanos.store(hot.queue_wait_nanos.get(), Relaxed);
for class in 0..CLASSES {
self.inflight[class].store(snapshot.inflight[class] as u64, Relaxed);
self.ready[class].store(snapshot.ready[class] as u64, Relaxed);
}
self.pending.store(snapshot.pending as u64, Relaxed);
self.resident.store(snapshot.resident as u64, Relaxed);
self.evicting.store(snapshot.evicting as u64, Relaxed);
self.eviction_backlog.store(snapshot.eviction_backlog as u64, Relaxed);
self.queue_capacity.store(snapshot.queue_capacity as u64, Relaxed);
self.inflight_age_max_nanos.store(hot.inflight_age_max_nanos.get(), Relaxed);
for class in 0..CLASSES {
self.started_by_class[class].store(hot.started_by_class[class].get(), Relaxed);
self.completed_by_class[class].store(hot.completed_by_class[class].get(), Relaxed);
self.expired_by_class[class].store(hot.expired_by_class[class].get(), Relaxed);
}
Self::republish(&self.queue_wait, &hot.queue_wait);
Self::republish(&self.process, &hot.process);
}
fn republish(into: &Mutex<Histogram<u64>>, from: &RefCell<Histogram<u64>>) {
let mut into = into.lock();
into.clear();
into.add(&*from.borrow()).expect("both histograms were built with the same bounds");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[track_caller]
fn close(reported: u64, recorded: u64) {
let error = (reported as f64 - recorded as f64).abs() / recorded as f64;
assert!(
error <= 0.01,
"reported {reported} for {recorded}, off by {:.2}% and past the one percent \
the significant figures promise",
error * 100.0
);
}
fn snapshot() -> Snapshot<2> {
Snapshot::<2> {
inflight: [3, 1],
ready: [7, 2],
pending: 13,
resident: 5,
evicting: 1,
eviction_backlog: 4,
queue_capacity: 64,
}
}
#[test]
fn publishing_copies_hot_counters_and_scheduler_gauges() {
let hot = ShardHot::<2>::default();
hot.bump(&hot.started_by_class[0]);
hot.bump(&hot.started_by_class[0]);
hot.bump(&hot.panicked);
hot.add(&hot.busy_nanos, 900);
hot.add(&hot.queue_wait_nanos, 25);
hot.inflight_age_max_nanos.set(7_000);
let stats = ShardStats::<2>::default();
stats.publish(&hot, &snapshot());
assert_eq!(stats.started.load(Relaxed), 2);
assert_eq!(stats.panicked.load(Relaxed), 1);
assert_eq!(stats.busy_nanos.load(Relaxed), 900);
assert_eq!(stats.queue_wait_nanos.load(Relaxed), 25);
assert_eq!(stats.inflight_age_max_nanos.load(Relaxed), 7_000);
assert_eq!(stats.inflight[0].load(Relaxed), 3);
assert_eq!(stats.ready[1].load(Relaxed), 2);
assert_eq!(stats.pending.load(Relaxed), 13);
assert_eq!(stats.resident.load(Relaxed), 5);
assert_eq!(stats.evicting.load(Relaxed), 1);
assert_eq!(stats.eviction_backlog.load(Relaxed), 4);
assert_eq!(stats.queue_capacity.load(Relaxed), 64);
}
#[test]
fn a_total_is_the_sum_of_its_classes_rather_than_a_second_counter() {
let hot = ShardHot::<2>::default();
for _ in 0..5 {
hot.bump(&hot.started_by_class[0]);
}
for _ in 0..3 {
hot.bump(&hot.started_by_class[1]);
}
hot.bump(&hot.completed_by_class[1]);
hot.bump(&hot.expired_by_class[0]);
assert_eq!(hot.started(), 8);
assert_eq!(hot.completed(), 1);
assert_eq!(hot.expired(), 1);
let stats = ShardStats::<2>::default();
stats.publish(&hot, &snapshot());
assert_eq!(stats.started.load(Relaxed), 8);
assert_eq!(stats.started_by_class[0].load(Relaxed), 5);
assert_eq!(stats.started_by_class[1].load(Relaxed), 3);
assert_eq!(
stats.started.load(Relaxed),
stats.started_by_class.iter().map(|c| c.load(Relaxed)).sum::<u64>(),
"the published total must agree with the split it came from"
);
}
#[test]
fn known_latencies_produce_known_quantiles() {
let hot = ShardHot::<2>::default();
for _ in 0..99 {
hot.record_queue_wait(1_000);
}
hot.record_queue_wait(1_000_000_000);
hot.record_process(50_000);
let stats = ShardStats::<2>::default();
stats.publish(&hot, &snapshot());
let mut queue_wait = histogram();
stats.merge_queue_wait_into(&mut queue_wait);
assert_eq!(queue_wait.len(), 100);
close(queue_wait.value_at_quantile(0.5), 1_000);
close(queue_wait.value_at_quantile(0.99), 1_000);
close(queue_wait.value_at_quantile(1.0), 1_000_000_000);
assert!(queue_wait.value_at_quantile(0.5) < 10_000_000 / 1_000);
close(stats.process_quantile(0.5), 50_000);
}
#[test]
fn shards_merge_into_one_distribution() {
let (fast, slow) = (ShardHot::<2>::default(), ShardHot::<2>::default());
for _ in 0..90 {
fast.record_queue_wait(1_000);
}
for _ in 0..10 {
slow.record_queue_wait(50_000_000);
}
let (left, right) = (ShardStats::<2>::default(), ShardStats::<2>::default());
left.publish(&fast, &snapshot());
right.publish(&slow, &snapshot());
let mut all = histogram();
left.merge_queue_wait_into(&mut all);
right.merge_queue_wait_into(&mut all);
assert_eq!(all.len(), 100);
close(all.value_at_quantile(0.5), 1_000);
close(all.value_at_quantile(0.95), 50_000_000);
}
#[test]
fn publishing_replaces_rather_than_accumulates() {
let hot = ShardHot::<2>::default();
hot.record_queue_wait(5_000);
let stats = ShardStats::<2>::default();
stats.publish(&hot, &snapshot());
stats.publish(&hot, &snapshot());
close(stats.queue_wait_quantile(1.0), 5_000);
hot.record_queue_wait(9_000_000);
stats.publish(&hot, &snapshot());
close(stats.queue_wait_quantile(1.0), 9_000_000);
let mut merged = histogram();
stats.merge_queue_wait_into(&mut merged);
assert_eq!(merged.len(), 2, "and only the two that were recorded");
}
#[test]
fn recording_never_grows_the_histogram() {
let hot = ShardHot::<2>::default();
let cells = hot.queue_wait.borrow().distinct_values();
for nanos in [0, 1, HIGHEST_NANOS, HIGHEST_NANOS * 1_000, u64::MAX] {
hot.record_queue_wait(nanos);
}
assert_eq!(hot.queue_wait.borrow().distinct_values(), cells);
assert_eq!(hot.queue_wait.borrow().len(), 5, "every value was still counted");
}
#[test]
fn a_histogram_costs_about_thirty_kilobytes_per_shard() {
let cells = histogram().distinct_values();
assert_eq!(cells, 3_840);
assert_eq!(cells * std::mem::size_of::<u64>(), 30_720);
}
}