1use std::{
13 collections::HashMap,
14 sync::{
15 Arc,
16 atomic::{AtomicU64, Ordering},
17 },
18 time::Instant,
19};
20
21use parking_lot::{Mutex, MutexGuard, RwLock};
22use prometheus_filtered::{
23 MetricLevel, Opts, Registry,
24 core::{Collector, Desc},
25 prometheus::{IntGauge, IntGaugeVec, proto::MetricFamily},
26};
27
28use crate::quantile_gauge::{WINDOW_SLOT, WINDOW_SLOTS};
29
30struct PeakWindow {
34 start: Instant,
35 current_slot: AtomicU64,
37 slots: [AtomicU64; WINDOW_SLOTS],
40 rotation: Mutex<()>,
41}
42
43impl PeakWindow {
44 fn new(now: Instant) -> Self {
45 Self {
46 start: now,
47 current_slot: AtomicU64::new(0),
48 slots: std::array::from_fn(|_| AtomicU64::new(0)),
49 rotation: Mutex::new(()),
50 }
51 }
52
53 fn slot_number(&self, now: Instant) -> u64 {
54 (now.saturating_duration_since(self.start).as_nanos() / WINDOW_SLOT.as_nanos()) as u64
55 }
56
57 fn observe_at(&self, value: u64, now: Instant) {
60 let value_slot = self.slot_number(now);
61 let current_slot = self.current_slot.load(Ordering::Acquire);
62 if value_slot == current_slot {
63 let slot_max = self.slot_max(value_slot);
64 if value > slot_max.load(Ordering::Relaxed) {
65 slot_max.fetch_max(value, Ordering::Relaxed);
66 }
67 return;
68 }
69 let rotation = self.rotation.lock();
70 self.advance_to(&rotation, value_slot);
71 let current_slot = self.current_slot.load(Ordering::Relaxed);
72 let still_in_window = value_slot + WINDOW_SLOTS as u64 > current_slot;
73 if still_in_window {
74 self.slot_max(value_slot)
75 .fetch_max(value, Ordering::Relaxed);
76 }
77 }
78
79 fn slot_max(&self, slot: u64) -> &AtomicU64 {
80 &self.slots[slot as usize % WINDOW_SLOTS]
81 }
82
83 fn advance_to(&self, _rotation: &MutexGuard<'_, ()>, target_slot: u64) {
85 let current_slot = self.current_slot.load(Ordering::Relaxed);
86 if target_slot <= current_slot {
87 return;
88 }
89 let first_to_clear =
90 (current_slot + 1).max(target_slot.saturating_sub(WINDOW_SLOTS as u64 - 1));
91 for slot in first_to_clear..=target_slot {
92 self.slot_max(slot).store(0, Ordering::Relaxed);
93 }
94 self.current_slot.store(target_slot, Ordering::Release);
95 }
96
97 fn max_at(&self, now: Instant) -> u64 {
98 {
99 let rotation = self.rotation.lock();
100 self.advance_to(&rotation, self.slot_number(now));
101 }
102 self.slots
103 .iter()
104 .map(|slot| slot.load(Ordering::Relaxed))
105 .max()
106 .unwrap_or(0)
107 }
108
109 fn observe(&self, value: u64) {
110 self.observe_at(value, Instant::now());
111 }
112
113 fn max(&self) -> u64 {
114 self.max_at(Instant::now())
115 }
116}
117
118#[derive(Clone)]
131pub struct PeakGauge {
132 gauge: IntGauge,
133 window: Arc<PeakWindow>,
134 floor: Option<IntGauge>,
136}
137
138impl PeakGauge {
139 pub fn register(
143 name: &str,
144 help: &str,
145 module: &str,
146 registry: &Registry,
147 level: MetricLevel,
148 ) -> Self {
149 Self::register_with_floor(name, help, module, registry, level, None)
150 }
151
152 fn register_with_floor(
155 name: &str,
156 help: &str,
157 module: &str,
158 registry: &Registry,
159 level: MetricLevel,
160 floor: Option<IntGauge>,
161 ) -> Self {
162 let gauge = IntGauge::with_opts(Opts::new(name, help)).expect("valid gauge options");
163 let this = Self {
164 gauge,
165 window: Arc::new(PeakWindow::new(Instant::now())),
166 floor,
167 };
168 registry
169 .register_filtered(name, module, level, this)
170 .expect("peak gauge registers without collision")
171 }
172
173 pub fn observe(&self, value: u64) {
174 self.window.observe(value);
175 }
176}
177
178impl Collector for PeakGauge {
179 fn desc(&self) -> Vec<&Desc> {
180 self.gauge.desc()
181 }
182
183 fn collect(&self) -> Vec<MetricFamily> {
184 let current = self
185 .floor
186 .as_ref()
187 .map_or(0, |gauge| gauge.get().max(0) as u64);
188 self.gauge
189 .set(i64::try_from(self.window.max().max(current)).unwrap_or(i64::MAX));
190 self.gauge.collect()
191 }
192}
193
194#[derive(Clone)]
198pub struct PeakGaugeVec {
199 gauge: IntGaugeVec,
200 windows: Arc<RwLock<HashMap<String, Arc<PeakWindow>>>>,
201}
202
203impl PeakGaugeVec {
204 pub fn register(
208 name: &str,
209 help: &str,
210 label: &str,
211 module: &str,
212 registry: &Registry,
213 level: MetricLevel,
214 ) -> Self {
215 let gauge = IntGaugeVec::new(Opts::new(name, help), &[label]).expect("valid gauge options");
216 let this = Self {
217 gauge,
218 windows: Arc::new(RwLock::new(HashMap::new())),
219 };
220 registry
221 .register_filtered(name, module, level, this)
222 .expect("peak gauge registers without collision")
223 }
224
225 pub fn with_label_values(&self, label_values: &[&str]) -> PeakGauge {
232 let [label_value] = label_values else {
233 panic!("a peak gauge vector has one label");
234 };
235 PeakGauge {
236 gauge: self.gauge.with_label_values(label_values),
237 window: self.window(label_value),
238 floor: None,
239 }
240 }
241
242 fn window(&self, label_value: &str) -> Arc<PeakWindow> {
243 if let Some(window) = self.windows.read().get(label_value) {
244 return window.clone();
245 }
246 self.windows
247 .write()
248 .entry(label_value.to_owned())
249 .or_insert_with(|| Arc::new(PeakWindow::new(Instant::now())))
250 .clone()
251 }
252}
253
254impl Collector for PeakGaugeVec {
255 fn desc(&self) -> Vec<&Desc> {
256 self.gauge.desc()
257 }
258
259 fn collect(&self) -> Vec<MetricFamily> {
260 let windows: Vec<_> = self
261 .windows
262 .read()
263 .iter()
264 .map(|(label, window)| (label.clone(), window.clone()))
265 .collect();
266 for (label_value, window) in windows {
267 self.gauge
268 .with_label_values(&[label_value.as_str()])
269 .set(i64::try_from(window.max()).unwrap_or(i64::MAX));
270 }
271 self.gauge.collect()
272 }
273}
274
275#[derive(Clone)]
281pub struct IntGaugeWithPeakGauge {
282 gauge: IntGauge,
283 peak: PeakGauge,
284}
285
286impl IntGaugeWithPeakGauge {
287 pub fn register(
293 (name, help): (&str, &str),
294 (peak_name, peak_help): (&str, &str),
295 module: &str,
296 registry: &Registry,
297 level: MetricLevel,
298 ) -> Self {
299 let gauge = IntGauge::with_opts(Opts::new(name, help)).expect("valid gauge options");
300 let gauge = registry
301 .register_filtered(name, module, level, gauge)
302 .expect("gauge registers without collision");
303 let peak = PeakGauge::register_with_floor(
304 peak_name,
305 peak_help,
306 module,
307 registry,
308 level,
309 Some(gauge.clone()),
310 );
311 Self { gauge, peak }
312 }
313
314 pub fn inc(&self) {
316 self.gauge.inc();
317 self.peak.observe(self.gauge.get().max(0) as u64);
318 }
319
320 pub fn dec(&self) {
322 self.gauge.dec();
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use std::time::Duration;
329
330 use super::*;
331 use crate::test_utils::MetricsReader;
332
333 #[test]
334 fn peak_is_zero_before_any_observation() {
335 let window = PeakWindow::new(Instant::now());
336 assert_eq!(window.max_at(Instant::now()), 0);
337 }
338
339 #[test]
340 fn observing_zero_leaves_the_peak_at_zero() {
341 let t0 = Instant::now();
342 let window = PeakWindow::new(t0);
343 window.observe_at(0, t0);
344 assert_eq!(window.max_at(t0), 0);
345 }
346
347 #[test]
348 fn peak_is_exact() {
349 let t0 = Instant::now();
350 for value in [32768, 1500, 1_000_003, u64::MAX / 2] {
351 let window = PeakWindow::new(t0);
352 window.observe_at(value, t0);
353 assert_eq!(window.max_at(t0), value);
354 }
355 }
356
357 #[test]
358 fn peak_is_the_highest_value_across_slots() {
359 let t0 = Instant::now();
360 let window = PeakWindow::new(t0);
361 window.observe_at(3, t0);
362 window.observe_at(70, t0 + WINDOW_SLOT);
363 window.observe_at(5, t0 + WINDOW_SLOT * 2);
364 assert_eq!(window.max_at(t0 + WINDOW_SLOT * 2), 70);
365 }
366
367 #[test]
368 fn peak_drops_values_older_than_the_window() {
369 let t0 = Instant::now();
370 let window = PeakWindow::new(t0);
371 window.observe_at(90, t0);
372 assert_eq!(
374 window.max_at(t0 + WINDOW_SLOT * (WINDOW_SLOTS as u32 - 1)),
375 90
376 );
377 assert_eq!(window.max_at(t0 + WINDOW_SLOT * WINDOW_SLOTS as u32), 0);
378 window.observe_at(2, t0 + WINDOW_SLOT * WINDOW_SLOTS as u32);
379 assert_eq!(window.max_at(t0 + WINDOW_SLOT * WINDOW_SLOTS as u32), 2);
380 }
381
382 #[test]
383 fn peak_forgets_everything_after_a_gap_longer_than_the_window() {
384 let t0 = Instant::now();
385 let window = PeakWindow::new(t0);
386 for i in 0..WINDOW_SLOTS as u32 {
387 window.observe_at(100 + i as u64, t0 + WINDOW_SLOT * i);
388 }
389 assert_eq!(
390 window.max_at(t0 + WINDOW_SLOT * (WINDOW_SLOTS as u32 - 1)),
391 111
392 );
393 assert_eq!(window.max_at(t0 + WINDOW_SLOT * 100), 0);
394 }
395
396 #[test]
397 fn peak_accepts_a_late_value_for_a_slot_still_in_the_window() {
398 let t0 = Instant::now();
399 let window = PeakWindow::new(t0);
400 window.observe_at(1, t0 + WINDOW_SLOT * 3);
401 window.observe_at(9, t0 + WINDOW_SLOT);
402 assert_eq!(window.max_at(t0 + WINDOW_SLOT * 3), 9);
403 }
404
405 #[test]
406 fn concurrent_observations_end_with_the_highest_value() {
407 let window = Arc::new(PeakWindow::new(Instant::now()));
408 let threads: Vec<_> = (0..8u64)
409 .map(|t| {
410 let window = window.clone();
411 std::thread::spawn(move || {
412 for i in 0..10_000u64 {
413 window.observe(t * 10_000 + i);
414 }
415 })
416 })
417 .collect();
418 for thread in threads {
419 thread.join().unwrap();
420 }
421 assert_eq!(window.max(), 79_999);
422 }
423
424 #[test]
425 fn observe_at_uses_the_given_time() {
426 let t0 = Instant::now();
427 let window = PeakWindow::new(t0);
428 window.observe_at(9, t0);
429 let later = t0 + WINDOW_SLOT * (WINDOW_SLOTS as u32 + 5);
430 window.observe_at(4, later);
431 assert_eq!(window.max_at(later), 4, "the earlier value left the window");
432 }
433
434 #[test]
435 fn a_late_value_at_the_edge_of_the_window_is_dropped() {
436 let t0 = Instant::now();
437 let window = PeakWindow::new(t0);
438 let edge = t0 + WINDOW_SLOT * WINDOW_SLOTS as u32;
439 window.observe_at(1, edge);
440 window.observe_at(9, t0);
441 assert_eq!(window.max_at(edge), 1);
442 }
443
444 #[test]
445 fn observing_in_the_current_slot_takes_no_lock() {
446 let t0 = Instant::now();
447 let window = Arc::new(PeakWindow::new(t0));
448 let _held = window.rotation.lock();
449 let (sender, receiver) = std::sync::mpsc::channel();
450 let observer = window.clone();
451 std::thread::spawn(move || {
452 observer.observe_at(1, t0);
453 sender.send(()).unwrap();
454 });
455 receiver
456 .recv_timeout(Duration::from_secs(5))
457 .expect("the observation waited for the rotation lock");
458 }
459
460 #[test]
461 fn peak_gauge_reports_the_exact_peak() {
462 let registry = Registry::new();
463 let gauge = PeakGauge::register(
464 "test_peak_gauge",
465 "help",
466 module_path!(),
467 ®istry,
468 MetricLevel::Warn,
469 );
470 gauge.observe(32768);
471 gauge.observe(1500);
472 let families = registry.gather();
473 let family = families
474 .iter()
475 .find(|f| f.name() == "test_peak_gauge")
476 .unwrap();
477 assert_eq!(family.get_metric()[0].get_gauge().value(), 32768.0);
478 }
479
480 #[test]
481 fn int_gauge_with_peak_gauge_reports_the_peak_and_the_gauge() {
482 let registry = Registry::new();
483 let gauge = IntGaugeWithPeakGauge::register(
484 ("test_gauge", "help"),
485 ("test_gauge_peak", "help"),
486 module_path!(),
487 ®istry,
488 MetricLevel::Warn,
489 );
490 let reader = MetricsReader::new(®istry);
491 for _ in 0..3 {
492 gauge.inc();
493 }
494 gauge.dec();
495 gauge.dec();
496 assert_eq!(reader.value("test_gauge", &[]), 1.0);
497 assert_eq!(reader.value("test_gauge_peak", &[]), 3.0);
498 }
499
500 #[test]
501 fn the_peak_of_a_gauge_is_never_below_the_gauge() {
502 let registry = Registry::new();
503 let gauge = IntGaugeWithPeakGauge::register(
504 ("test_gauge", "help"),
505 ("test_gauge_peak", "help"),
506 module_path!(),
507 ®istry,
508 MetricLevel::Warn,
509 );
510 gauge.gauge.set(5);
512 assert_eq!(
513 MetricsReader::new(®istry).value("test_gauge_peak", &[]),
514 5.0
515 );
516 }
517
518 #[test]
519 fn peak_with_a_floor_reports_the_peak_and_never_less_than_the_gauge() {
520 let registry = Registry::new();
521 let gauge = IntGauge::new("test_gauge", "help").unwrap();
522 let peak = PeakGauge::register_with_floor(
523 "test_gauge_peak",
524 "help",
525 module_path!(),
526 ®istry,
527 MetricLevel::Warn,
528 Some(gauge.clone()),
529 );
530 let reader = MetricsReader::new(®istry);
531 for _ in 0..3 {
532 gauge.inc();
533 peak.observe(gauge.get() as u64);
534 }
535 gauge.dec();
536 gauge.dec();
537 assert_eq!(reader.value("test_gauge_peak", &[]), 3.0);
538
539 peak.window
540 .observe_at(0, Instant::now() + WINDOW_SLOT * (WINDOW_SLOTS as u32 + 1));
541 assert_eq!(
542 reader.value("test_gauge_peak", &[]),
543 1.0,
544 "the window is empty, so the peak is the current gauge"
545 );
546 }
547
548 #[test]
549 fn peak_gauge_vec_reports_the_peak_per_label_value() {
550 let registry = Registry::new();
551 let gauge = PeakGaugeVec::register(
552 "test_gauge_vec_peak",
553 "help",
554 "kind",
555 module_path!(),
556 ®istry,
557 MetricLevel::Warn,
558 );
559 let (a, b) = (
560 gauge.with_label_values(&["a"]),
561 gauge.with_label_values(&["b"]),
562 );
563 a.observe(2);
564 a.observe(1);
565 b.observe(7);
566 let reader = MetricsReader::new(®istry);
567 assert_eq!(reader.value("test_gauge_vec_peak", &[("kind", "a")]), 2.0);
568 assert_eq!(reader.value("test_gauge_vec_peak", &[("kind", "b")]), 7.0);
569 assert_eq!(
570 gauge.with_label_values(&["a"]).window.max(),
571 2,
572 "a second peak of the label value shares the window"
573 );
574 }
575
576 #[test]
577 fn threads_that_get_the_same_peak_share_one_window() {
578 let registry = Registry::new();
579 let gauge = PeakGaugeVec::register(
580 "test_gauge_vec_threads",
581 "help",
582 "kind",
583 module_path!(),
584 ®istry,
585 MetricLevel::Warn,
586 );
587 for round in 0..200 {
588 let label = round.to_string();
589 let barrier = std::sync::Barrier::new(8);
590 std::thread::scope(|scope| {
591 for value in 1..=8 {
592 let (gauge, barrier, label) = (&gauge, &barrier, label.as_str());
593 scope.spawn(move || {
594 barrier.wait();
595 gauge.with_label_values(&[label]).observe(value);
596 });
597 }
598 });
599 assert_eq!(
600 gauge.with_label_values(&[&label]).window.max(),
601 8,
602 "round {round}"
603 );
604 }
605 }
606}