1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
8use std::sync::{Arc, OnceLock};
9use std::time::{Duration, Instant};
10
11use futures_timer::Delay;
12
13pub use crate::pool::PoolMetrics;
14pub use crate::system::PingRecord;
15
16pub const HISTOGRAM_BUCKETS: usize = 16;
18
19const FIRST_BUCKET_US: u64 = 16;
21
22#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
33pub struct Histogram {
34 buckets: [u64; HISTOGRAM_BUCKETS],
35}
36
37impl Histogram {
38 pub fn buckets(&self) -> &[u64; HISTOGRAM_BUCKETS] {
40 &self.buckets
41 }
42
43 pub fn count(&self) -> u64 {
45 self.buckets.iter().fold(0, |acc, c| acc.wrapping_add(*c))
46 }
47
48 pub fn bucket_range(idx: usize) -> (Duration, Option<Duration>) {
57 assert!(idx < HISTOGRAM_BUCKETS, "bucket index out of range");
58 let upper = |i: usize| Duration::from_micros(FIRST_BUCKET_US << i);
59 let lower = if idx == 0 { Duration::ZERO } else { upper(idx - 1) };
60 if idx == HISTOGRAM_BUCKETS - 1 {
61 (lower, None)
62 } else {
63 (lower, Some(upper(idx)))
64 }
65 }
66
67 #[must_use]
72 pub fn delta(&self, prev: &Histogram) -> Histogram {
73 let mut buckets = self.buckets;
74 for (b, p) in buckets.iter_mut().zip(prev.buckets.iter()) {
75 *b = b.wrapping_sub(*p);
76 }
77 Histogram { buckets }
78 }
79
80 #[must_use]
83 pub fn merge(&self, other: &Histogram) -> Histogram {
84 let mut buckets = self.buckets;
85 for (b, o) in buckets.iter_mut().zip(other.buckets.iter()) {
86 *b = b.wrapping_add(*o);
87 }
88 Histogram { buckets }
89 }
90}
91
92#[derive(Debug, Default)]
94pub(crate) struct AtomicHistogram {
95 buckets: [AtomicU64; HISTOGRAM_BUCKETS],
96}
97
98impl AtomicHistogram {
99 fn bucket(d: Duration) -> usize {
100 if d < Duration::from_micros(FIRST_BUCKET_US) {
101 0
102 } else {
103 let us = u64::try_from(d.as_micros()).unwrap_or(u64::MAX);
104 let idx = (63 - us.leading_zeros() as usize) - 3;
106 idx.min(HISTOGRAM_BUCKETS - 1)
107 }
108 }
109
110 pub(crate) fn record(&self, d: Duration) {
112 let b = &self.buckets[Self::bucket(d)];
113 b.store(b.load(Ordering::Relaxed).wrapping_add(1), Ordering::Relaxed);
114 }
115
116 pub(crate) fn snapshot(&self) -> Histogram {
117 let mut buckets = [0; HISTOGRAM_BUCKETS];
118 for (b, a) in buckets.iter_mut().zip(self.buckets.iter()) {
119 *b = a.load(Ordering::Relaxed);
120 }
121 Histogram { buckets }
122 }
123}
124
125#[derive(Copy, Clone, Debug, PartialEq, Eq)]
133#[non_exhaustive]
134pub struct LatencyMetrics {
135 pub interval: Duration,
137 pub last: Duration,
139 pub histogram: Histogram,
141 pub last_sample: Option<Instant>,
145 pub stalls: u64,
151 pub stall_duration: Duration,
156 pub stalled: bool,
158}
159
160#[derive(Debug, PartialEq, Eq)]
162pub(crate) enum Stall {
163 None,
164 Started(Duration),
166 Recovered(Duration),
168}
169
170#[derive(Debug, Default)]
171pub(crate) struct LatencyStats {
172 interval: AtomicU64,
174 last: AtomicU64,
175 last_sample: AtomicU64,
177 base: OnceLock<Instant>,
178 histogram: AtomicHistogram,
179 stalls: AtomicU64,
181 stall_duration: AtomicU64,
182 stalled: AtomicBool,
183 stalled_at: AtomicU64,
184}
185
186fn as_nanos(d: Duration) -> u64 {
187 u64::try_from(d.as_nanos()).unwrap_or(u64::MAX)
188}
189
190impl LatencyStats {
191 fn record(&self, lateness: Duration, now: Instant) {
192 let base = *self.base.get_or_init(|| now);
193 self.histogram.record(lateness);
194 self.last.store(as_nanos(lateness), Ordering::Relaxed);
195 self.last_sample
196 .store(as_nanos(now - base).saturating_add(1), Ordering::Relaxed);
197 }
198
199 pub(crate) fn metrics(&self) -> Option<LatencyMetrics> {
200 let interval = self.interval.load(Ordering::Relaxed);
201 if interval == 0 {
202 return None;
203 }
204 let last_sample = match self.last_sample.load(Ordering::Relaxed) {
205 0 => None,
206 n => self
207 .base
208 .get()
209 .map(|base| *base + Duration::from_nanos(n - 1)),
210 };
211 Some(LatencyMetrics {
212 interval: Duration::from_nanos(interval),
213 last: Duration::from_nanos(self.last.load(Ordering::Relaxed)),
214 histogram: self.histogram.snapshot(),
215 last_sample,
216 stalls: self.stalls.load(Ordering::Relaxed),
217 stall_duration: Duration::from_nanos(self.stall_duration.load(Ordering::Relaxed)),
218 stalled: self.stalled.load(Ordering::Relaxed),
219 })
220 }
221
222 pub(crate) fn check(&self, now: Instant, threshold: Duration) -> Stall {
226 let interval = Duration::from_nanos(self.interval.load(Ordering::Relaxed));
227 let Some(base) = self.base.get() else {
228 return Stall::None;
229 };
230 if interval.is_zero() {
231 return Stall::None;
232 }
233
234 let last = self.last_sample.load(Ordering::Relaxed).saturating_sub(1);
236 if self.stalled.load(Ordering::Relaxed) {
237 let prev = self.stalled_at.load(Ordering::Relaxed);
238 if last == prev {
239 return Stall::None;
240 }
241 let gap = Duration::from_nanos(last.saturating_sub(prev));
242 let stall = gap.saturating_sub(interval);
243 self.stalled.store(false, Ordering::Relaxed);
244 self.stall_duration.store(
245 self.stall_duration
246 .load(Ordering::Relaxed)
247 .saturating_add(as_nanos(stall)),
248 Ordering::Relaxed,
249 );
250 Stall::Recovered(stall)
251 } else {
252 let age = now.saturating_duration_since(*base + Duration::from_nanos(last));
253 if age <= interval + threshold {
254 return Stall::None;
255 }
256 self.stalled_at.store(last, Ordering::Relaxed);
257 self.stalled.store(true, Ordering::Relaxed);
258 self.stalls.store(
259 self.stalls.load(Ordering::Relaxed).wrapping_add(1),
260 Ordering::Relaxed,
261 );
262 Stall::Started(age)
263 }
264 }
265}
266
267pub(crate) fn start_latency_sampler(stats: &Arc<LatencyStats>, interval_ms: usize) {
269 if interval_ms == 0 {
270 return;
271 }
272 let interval = Duration::from_millis(interval_ms as u64);
273 stats.base.get_or_init(Instant::now);
274 stats.interval.store(as_nanos(interval), Ordering::Relaxed);
275
276 let stats = stats.clone();
277 crate::spawn_detached(async move {
278 loop {
279 let deadline = Instant::now() + interval;
280 Delay::new(interval).await;
281 let now = Instant::now();
282 stats.record(now.saturating_duration_since(deadline), now);
283 }
284 });
285}
286
287#[cfg(test)]
288#[path = "../tests/unit/metrics.rs"]
289mod tests;