1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
2use std::sync::{Arc, OnceLock};
3use std::time::{Duration, Instant};
4
5use futures_timer::Delay;
6
7pub const HISTOGRAM_BUCKETS: usize = 16;
9
10const FIRST_BUCKET_US: u64 = 16;
12
13#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
22pub struct Histogram {
23 buckets: [u64; HISTOGRAM_BUCKETS],
24}
25
26impl Histogram {
27 pub fn buckets(&self) -> &[u64; HISTOGRAM_BUCKETS] {
29 &self.buckets
30 }
31
32 pub fn count(&self) -> u64 {
34 self.buckets.iter().fold(0, |acc, c| acc.wrapping_add(*c))
35 }
36
37 pub fn bucket_range(idx: usize) -> (Duration, Option<Duration>) {
46 assert!(idx < HISTOGRAM_BUCKETS, "bucket index out of range");
47 let upper = |i: usize| Duration::from_micros(FIRST_BUCKET_US << i);
48 let lower = if idx == 0 { Duration::ZERO } else { upper(idx - 1) };
49 if idx == HISTOGRAM_BUCKETS - 1 {
50 (lower, None)
51 } else {
52 (lower, Some(upper(idx)))
53 }
54 }
55
56 #[must_use]
58 pub fn delta(&self, prev: &Histogram) -> Histogram {
59 let mut buckets = self.buckets;
60 for (b, p) in buckets.iter_mut().zip(prev.buckets.iter()) {
61 *b = b.wrapping_sub(*p);
62 }
63 Histogram { buckets }
64 }
65
66 #[must_use]
69 pub fn merge(&self, other: &Histogram) -> Histogram {
70 let mut buckets = self.buckets;
71 for (b, o) in buckets.iter_mut().zip(other.buckets.iter()) {
72 *b = b.wrapping_add(*o);
73 }
74 Histogram { buckets }
75 }
76}
77
78#[derive(Debug, Default)]
80pub(crate) struct AtomicHistogram {
81 buckets: [AtomicU64; HISTOGRAM_BUCKETS],
82}
83
84impl AtomicHistogram {
85 fn bucket(d: Duration) -> usize {
86 let us = u64::try_from(d.as_micros()).unwrap_or(u64::MAX);
87 if us < FIRST_BUCKET_US {
88 0
89 } else {
90 let idx = (63 - us.leading_zeros() as usize) - 3;
92 idx.min(HISTOGRAM_BUCKETS - 1)
93 }
94 }
95
96 pub(crate) fn record(&self, d: Duration) {
98 let b = &self.buckets[Self::bucket(d)];
99 b.store(b.load(Ordering::Relaxed).wrapping_add(1), Ordering::Relaxed);
100 }
101
102 pub(crate) fn snapshot(&self) -> Histogram {
103 let mut buckets = [0; HISTOGRAM_BUCKETS];
104 for (b, a) in buckets.iter_mut().zip(self.buckets.iter()) {
105 *b = a.load(Ordering::Relaxed);
106 }
107 Histogram { buckets }
108 }
109}
110
111#[derive(Copy, Clone, Debug, PartialEq, Eq)]
117#[non_exhaustive]
118pub struct LatencyMetrics {
119 pub interval: Duration,
121 pub last: Duration,
123 pub histogram: Histogram,
125 pub last_sample: Option<Instant>,
129 pub stalls: u64,
135 pub stall_duration: Duration,
137 pub stalled: bool,
139}
140
141#[derive(Debug, PartialEq, Eq)]
143pub(crate) enum Stall {
144 None,
145 Started(Duration),
147 Recovered(Duration),
149}
150
151#[derive(Debug, Default)]
152pub(crate) struct LatencyStats {
153 interval: AtomicU64,
155 last: AtomicU64,
156 last_sample: AtomicU64,
158 base: OnceLock<Instant>,
159 histogram: AtomicHistogram,
160 stalls: AtomicU64,
162 stall_duration: AtomicU64,
163 stalled: AtomicBool,
164 stalled_at: AtomicU64,
165}
166
167fn as_nanos(d: Duration) -> u64 {
168 u64::try_from(d.as_nanos()).unwrap_or(u64::MAX)
169}
170
171impl LatencyStats {
172 fn record(&self, lateness: Duration, now: Instant) {
173 let base = *self.base.get_or_init(|| now);
174 self.histogram.record(lateness);
175 self.last.store(as_nanos(lateness), Ordering::Relaxed);
176 self.last_sample
177 .store(as_nanos(now - base).saturating_add(1), Ordering::Relaxed);
178 }
179
180 pub(crate) fn metrics(&self) -> Option<LatencyMetrics> {
181 let interval = self.interval.load(Ordering::Relaxed);
182 if interval == 0 {
183 return None;
184 }
185 let last_sample = match self.last_sample.load(Ordering::Relaxed) {
186 0 => None,
187 n => self
188 .base
189 .get()
190 .map(|base| *base + Duration::from_nanos(n - 1)),
191 };
192 Some(LatencyMetrics {
193 interval: Duration::from_nanos(interval),
194 last: Duration::from_nanos(self.last.load(Ordering::Relaxed)),
195 histogram: self.histogram.snapshot(),
196 last_sample,
197 stalls: self.stalls.load(Ordering::Relaxed),
198 stall_duration: Duration::from_nanos(self.stall_duration.load(Ordering::Relaxed)),
199 stalled: self.stalled.load(Ordering::Relaxed),
200 })
201 }
202
203 pub(crate) fn check(&self, now: Instant, threshold: Duration) -> Stall {
207 let interval = Duration::from_nanos(self.interval.load(Ordering::Relaxed));
208 let Some(base) = self.base.get() else {
209 return Stall::None;
210 };
211 if interval.is_zero() {
212 return Stall::None;
213 }
214
215 let last = self.last_sample.load(Ordering::Relaxed).saturating_sub(1);
217 if self.stalled.load(Ordering::Relaxed) {
218 let prev = self.stalled_at.load(Ordering::Relaxed);
219 if last == prev {
220 return Stall::None;
221 }
222 let gap = Duration::from_nanos(last.saturating_sub(prev));
223 let stall = gap.saturating_sub(interval);
224 self.stalled.store(false, Ordering::Relaxed);
225 self.stall_duration.store(
226 self.stall_duration
227 .load(Ordering::Relaxed)
228 .saturating_add(as_nanos(stall)),
229 Ordering::Relaxed,
230 );
231 Stall::Recovered(stall)
232 } else {
233 let age = now.saturating_duration_since(*base + Duration::from_nanos(last));
234 if age <= interval + threshold {
235 return Stall::None;
236 }
237 self.stalled_at.store(last, Ordering::Relaxed);
238 self.stalled.store(true, Ordering::Relaxed);
239 self.stalls.store(
240 self.stalls.load(Ordering::Relaxed).wrapping_add(1),
241 Ordering::Relaxed,
242 );
243 Stall::Started(age)
244 }
245 }
246}
247
248pub(crate) fn start_latency_sampler(stats: &Arc<LatencyStats>, interval_ms: usize) {
250 if interval_ms == 0 {
251 return;
252 }
253 let interval = Duration::from_millis(interval_ms as u64);
254 stats.base.get_or_init(Instant::now);
255 stats.interval.store(as_nanos(interval), Ordering::Relaxed);
256
257 let stats = stats.clone();
258 crate::spawn_detached(async move {
259 loop {
260 let deadline = Instant::now() + interval;
261 Delay::new(interval).await;
262 let now = Instant::now();
263 stats.record(now.saturating_duration_since(deadline), now);
264 }
265 });
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn histogram_buckets() {
274 let bucket = |us| AtomicHistogram::bucket(Duration::from_micros(us));
275 assert_eq!(bucket(0), 0);
276 assert_eq!(bucket(15), 0);
277 assert_eq!(bucket(16), 1);
278 assert_eq!(bucket(31), 1);
279 assert_eq!(bucket(32), 2);
280 assert_eq!(bucket(262_143), 14);
281 assert_eq!(bucket(262_144), 15);
282 assert_eq!(AtomicHistogram::bucket(Duration::MAX), 15);
283
284 for i in 0..HISTOGRAM_BUCKETS {
285 let (lower, upper) = Histogram::bucket_range(i);
286 assert_eq!(AtomicHistogram::bucket(lower), i);
287 if let Some(upper) = upper {
288 assert_eq!(AtomicHistogram::bucket(upper), i + 1);
289 assert_eq!(
290 AtomicHistogram::bucket(upper.checked_sub(Duration::from_nanos(1)).unwrap()),
291 i
292 );
293 }
294 }
295 assert_eq!(Histogram::bucket_range(15).1, None);
296 }
297
298 #[test]
299 fn histogram_delta() {
300 let h = AtomicHistogram::default();
301 h.record(Duration::from_micros(1));
302 let prev = h.snapshot();
303 h.record(Duration::from_micros(1));
304 h.record(Duration::from_millis(1));
305 let cur = h.snapshot();
306 assert_eq!(cur.count(), 3);
307
308 let delta = cur.delta(&prev);
309 assert_eq!(delta.count(), 2);
310 assert_eq!(delta.buckets()[0], 1);
311 assert_eq!(
312 delta.buckets()[AtomicHistogram::bucket(Duration::from_millis(1))],
313 1
314 );
315 assert_eq!(prev.merge(&delta), cur);
316 }
317
318 #[test]
319 fn latency_stats() {
320 let stats = LatencyStats::default();
321 assert_eq!(stats.metrics(), None);
322
323 stats.interval.store(100, Ordering::Relaxed);
324 let m = stats.metrics().unwrap();
325 assert_eq!((m.last_sample, m.histogram.count()), (None, 0));
326
327 let now = Instant::now();
328 stats.record(Duration::from_millis(2), now);
329 let m = stats.metrics().unwrap();
330 assert_eq!(m.interval, Duration::from_nanos(100));
331 assert_eq!(m.last, Duration::from_millis(2));
332 assert_eq!(m.last_sample, Some(now));
333 assert_eq!(m.histogram.count(), 1);
334 }
335
336 #[test]
337 fn stall_check() {
338 let ms = Duration::from_millis;
339 let threshold = ms(20);
340 let stats = LatencyStats::default();
341 let t0 = Instant::now();
342 assert_eq!(stats.check(t0 + ms(100), threshold), Stall::None);
343
344 stats.base.set(t0).unwrap();
345 stats.interval.store(as_nanos(ms(10)), Ordering::Relaxed);
346 assert_eq!(stats.check(t0 + ms(30), threshold), Stall::None);
347 assert_eq!(stats.check(t0 + ms(31), threshold), Stall::Started(ms(31)));
348 assert_eq!(stats.check(t0 + ms(50), threshold), Stall::None);
349 let m = stats.metrics().unwrap();
350 assert_eq!((m.stalls, m.stall_duration, m.stalled), (1, ms(0), true));
351
352 stats.record(ms(90), t0 + ms(100));
353 assert_eq!(
354 stats.check(t0 + ms(101), threshold),
355 Stall::Recovered(ms(90))
356 );
357 assert_eq!(stats.check(t0 + ms(120), threshold), Stall::None);
358 let m = stats.metrics().unwrap();
359 assert_eq!((m.stalls, m.stall_duration, m.stalled), (1, ms(90), false));
360
361 assert_eq!(stats.check(t0 + ms(131), threshold), Stall::Started(ms(31)));
362 assert_eq!(stats.metrics().unwrap().stalls, 2);
363 }
364}