commonware_runtime/telemetry/metrics/
histogram.rs1use super::{raw, Histogram, MetricsExt as _};
4use crate::{Clock, Metrics};
5use std::{sync::Arc, time::SystemTime};
6
7pub trait HistogramExt {
9 fn observe_between(&self, start: SystemTime, end: SystemTime);
13}
14
15impl HistogramExt for raw::Histogram {
16 fn observe_between(&self, start: SystemTime, end: SystemTime) {
17 let duration = end
18 .duration_since(start)
19 .map_or(0.0, |duration| duration.as_secs_f64());
20 self.observe(duration);
21 }
22}
23
24pub struct Buckets;
28
29impl Buckets {
30 pub const NETWORK: [f64; 13] = [
35 0.010, 0.020, 0.050, 0.100, 0.200, 0.500, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0, 300.0,
36 ];
37
38 pub const LOCAL: [f64; 12] = [
43 3e-6, 1e-5, 3e-5, 1e-4, 3e-4, 0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1.0,
44 ];
45
46 pub const CRYPTOGRAPHY: [f64; 16] = [
51 3e-6, 1e-5, 3e-5, 1e-4, 3e-4, 0.001, 0.002, 0.003, 0.005, 0.01, 0.015, 0.02, 0.025, 0.03,
52 0.1, 0.2,
53 ];
54}
55
56#[derive(Clone)]
58pub struct Timed {
59 histogram: Histogram,
61}
62
63impl Timed {
64 pub const fn new(histogram: Histogram) -> Self {
66 Self { histogram }
67 }
68
69 pub fn timer<C: Clock>(&self, clock: &C) -> Timer {
71 let start = clock.current();
72 Timer {
73 histogram: self.histogram.clone(),
74 start,
75 }
76 }
77
78 pub fn time_some<C: Clock, T, F: FnOnce() -> Option<T>>(&self, clock: &C, f: F) -> Option<T> {
80 let start = clock.current();
81 let result = f();
82 if result.is_some() {
83 self.histogram.observe_between(start, clock.current());
84 }
85 result
86 }
87}
88
89pub struct Timer {
91 histogram: Histogram,
93
94 start: SystemTime,
96}
97
98impl Timer {
99 pub fn observe<C: Clock>(self, clock: &C) {
101 self.histogram.observe_between(self.start, clock.current());
102 }
103}
104
105pub struct ScopedTimer<C: Clock> {
112 timer: Option<Timer>,
113 clock: Arc<C>,
114}
115
116impl<C: Clock> ScopedTimer<C> {
117 pub fn cancel(mut self) {
119 self.timer = None;
120 }
121}
122
123impl<C: Clock> Drop for ScopedTimer<C> {
124 fn drop(&mut self) {
125 if let Some(timer) = self.timer.take() {
126 timer.observe(self.clock.as_ref());
127 }
128 }
129}
130
131impl Timed {
132 pub fn scoped<C: Clock>(&self, clock: &Arc<C>) -> ScopedTimer<C> {
134 ScopedTimer {
135 timer: Some(self.timer(clock.as_ref())),
136 clock: clock.clone(),
137 }
138 }
139}
140
141pub fn duration_histogram<M: Metrics>(
143 context: &M,
144 name: &'static str,
145 help: &'static str,
146) -> Histogram {
147 context.histogram(name, help, Buckets::LOCAL)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::{deterministic, Runner as _, Supervisor as _};
154 use std::time::Duration;
155
156 #[test]
157 fn duration_records_all_calls() {
158 deterministic::Runner::default().start(|context| async move {
159 let histogram = duration_histogram(&context, "test_duration", "test duration");
160 let timed = Timed::new(histogram);
161 let clock = Arc::new(context.child("timer"));
162
163 {
164 let _timer = timed.scoped(&clock);
165 context.sleep(Duration::from_millis(1)).await;
166 let result: Result<(), ()> = Ok(());
167 assert!(result.is_ok());
168 }
169
170 {
171 let _timer = timed.scoped(&clock);
172 context.sleep(Duration::from_millis(1)).await;
173 let result: Result<(), ()> = Err(());
174 assert!(result.is_err());
175 }
176
177 {
178 let _timer = timed.scoped(&clock);
179 context.sleep(Duration::from_millis(1)).await;
180 }
181
182 let metrics = context.encode();
183 assert!(
184 metrics.contains("test_duration_count 3"),
185 "unexpected metrics: {metrics}"
186 );
187 });
188 }
189}