use serde::Serialize;
use std::{
collections::BTreeMap,
sync::OnceLock,
sync::atomic::{AtomicBool, AtomicU64, Ordering},
time::Instant,
};
const LIMITS_NS: [u64; 27] = [
1_000,
2_000,
3_000,
5_000,
7_500,
10_000,
15_000,
20_000,
25_000,
30_000,
35_000,
40_000,
50_000,
60_000,
75_000,
100_000,
125_000,
150_000,
200_000,
300_000,
500_000,
1_000_000,
2_000_000,
5_000_000,
10_000_000,
100_000_000,
u64::MAX,
];
#[derive(Default)]
pub(super) struct Stage {
total: AtomicU64,
max: AtomicU64,
buckets: [AtomicU64; LIMITS_NS.len()],
}
#[derive(Serialize)]
pub struct StageDistribution {
pub total_ns: u64,
pub max_ns: u64,
pub histogram_ns: Vec<(u64, u64)>,
}
pub(super) fn now_ns() -> u64 {
static START: OnceLock<Instant> = OnceLock::new();
(START.get_or_init(Instant::now).elapsed().as_nanos() as u64).max(1)
}
impl Stage {
pub(super) fn observe(&self, start: Option<Instant>) {
let Some(start) = start else { return };
self.record(start.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);
}
pub(super) fn between(&self, start: Option<Instant>, end: Option<Instant>) {
if let (Some(s), Some(e)) = (start, end) {
self.record(e.saturating_duration_since(s).as_nanos() as u64);
}
}
pub(super) fn since_ns(&self, start: u64) {
if start != 0 {
self.record(now_ns().saturating_sub(start));
}
}
fn record(&self, ns: u64) {
self.total.fetch_add(ns, Ordering::Relaxed);
self.max.fetch_max(ns, Ordering::Relaxed);
self.buckets[LIMITS_NS.partition_point(|&top| top < ns)].fetch_add(1, Ordering::Relaxed);
}
fn snapshot(&self) -> StageDistribution {
StageDistribution {
total_ns: self.total.load(Ordering::Relaxed),
max_ns: self.max.load(Ordering::Relaxed),
histogram_ns: LIMITS_NS
.iter()
.zip(&self.buckets)
.map(|(&limit, count)| (limit, count.load(Ordering::Relaxed)))
.collect(),
}
}
}
pub fn stage_summary(
before: &BTreeMap<&'static str, StageDistribution>,
after: &BTreeMap<&'static str, StageDistribution>,
) -> serde_json::Value {
let mut out = serde_json::Map::new();
for (name, a) in after {
let b = before.get(name);
let hist: Vec<(u64, u64)> = a
.histogram_ns
.iter()
.map(|&(limit, n)| {
let was = b
.and_then(|b| b.histogram_ns.iter().find(|(l, _)| *l == limit))
.map_or(0, |&(_, n)| n);
(limit, n.saturating_sub(was))
})
.collect();
let count: u64 = hist.iter().map(|(_, n)| n).sum();
if count == 0 {
continue;
}
let total = a.total_ns.saturating_sub(b.map_or(0, |b| b.total_ns));
let bound = |q: f64| {
let need = (q * count as f64).ceil() as u64;
let mut seen = 0;
for &(limit, n) in &hist {
seen += n;
if seen >= need.max(1) {
return if limit == u64::MAX {
f64::INFINITY
} else {
limit as f64 / 1000.0
};
}
}
f64::INFINITY
};
out.insert(
name.to_string(),
serde_json::json!({
"count": count,
"mean_us": (total as f64 / count as f64 / 10.0).round() / 100.0,
"p50_bound_us": bound(0.5),
"p99_bound_us": bound(0.99),
}),
);
}
serde_json::Value::Object(out)
}
#[derive(Default)]
pub(super) struct Timings {
enabled: AtomicBool,
pub app_queue: Stage,
pub protocol_append: Stage,
pub protocol_sync: Stage,
pub quorum: Stage,
pub materialize: Stage,
pub outbound_queue: Stage,
pub inbound_queue: Stage,
pub hint: Stage,
pub stage_to_take: Stage,
pub take_to_sent: Stage,
pub sent_to_ack: Stage,
pub ack_to_release: Stage,
pub release_to_resume: Stage,
pub resume_to_stage: Stage,
pub prepare_to_write: Stage,
pub durable_to_ack: Stage,
pub prepare_to_ack: Stage,
pub entries_per_prepare: Stage,
pub sections_per_entry: Stage,
pub io_tick: Stage,
pub io_lock_hold: Stage,
pub tick_protocol: Stage,
pub tick_process: Stage,
pub tick_checkpoints: Stage,
pub checkpoint_publish: Stage,
pub checkpoint_reclaim: Stage,
pub checkpoint_capture: Stage,
pub checkpoint_install: Stage,
pub generation_write: Stage,
pub generation_switch: Stage,
pub install_check: Stage,
pub install_replica: Stage,
}
impl Timings {
pub(super) fn enable(&self) {
self.enabled.store(true, Ordering::Relaxed);
}
pub(super) fn enabled(&self) -> bool {
self.enabled.load(Ordering::Relaxed)
}
pub(super) fn start(&self) -> Option<Instant> {
self.enabled().then(Instant::now)
}
pub(super) fn stamp(&self) -> u64 {
if self.enabled() { now_ns() } else { 0 }
}
pub(super) fn count(&self, stage: &Stage, n: usize) {
if self.enabled() {
stage.record(n as u64 * 1000);
}
}
pub(super) fn snapshot(&self) -> BTreeMap<&'static str, StageDistribution> {
[
("application_proposal_queue", &self.app_queue),
("protocol_log_append", &self.protocol_append),
("protocol_log_sync", &self.protocol_sync),
("proposal_to_quorum", &self.quorum),
("committed_batch_materialization", &self.materialize),
("outbound_frame_queue", &self.outbound_queue),
("inbound_frame_queue", &self.inbound_queue),
("applied_watermark_sync", &self.hint),
("shard_stage_to_take", &self.stage_to_take),
("take_to_prepare_sent", &self.take_to_sent),
("prepare_sent_to_ack_read", &self.sent_to_ack),
("ack_read_to_release", &self.ack_to_release),
("release_to_shard_resume", &self.release_to_resume),
("shard_resume_to_stage", &self.resume_to_stage),
("prepare_read_to_write", &self.prepare_to_write),
("durable_to_ack_sent", &self.durable_to_ack),
("prepare_read_to_ack_sent", &self.prepare_to_ack),
("entries_per_prepare_count_us", &self.entries_per_prepare),
("sections_per_entry_count_us", &self.sections_per_entry),
("io_thread_tick", &self.io_tick),
("io_thread_lock_hold", &self.io_lock_hold),
("io_tick_protocol", &self.tick_protocol),
("io_tick_process", &self.tick_process),
("io_tick_checkpoints", &self.tick_checkpoints),
("checkpoint_publish", &self.checkpoint_publish),
("checkpoint_reclaim", &self.checkpoint_reclaim),
("checkpoint_capture", &self.checkpoint_capture),
("checkpoint_install", &self.checkpoint_install),
("generation_write", &self.generation_write),
("generation_switch", &self.generation_switch),
("install_check", &self.install_check),
("install_replica", &self.install_replica),
]
.into_iter()
.map(|(name, stage)| (name, stage.snapshot()))
.collect()
}
}