commonware_runtime/telemetry/metrics/
histogram.rs1use super::{raw, Histogram, MetricsExt as _};
4use crate::{Clock, Metrics};
5use std::{future::Future, 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 register<M: Metrics>(context: &M, name: &'static str, help: &'static str) -> Self {
71 Self::new(duration_histogram(context, name, help))
72 }
73
74 pub fn timer<C: Clock>(&self, clock: &C) -> Timer {
76 let start = clock.current();
77 Timer {
78 histogram: self.histogram.clone(),
79 start,
80 }
81 }
82
83 pub async fn time_some<C: Clock, T>(
85 &self,
86 clock: &C,
87 op: impl Future<Output = Option<T>>,
88 ) -> Option<T> {
89 let start = clock.current();
90 let result = op.await;
91 if result.is_some() {
92 self.histogram.observe_between(start, clock.current());
93 }
94 result
95 }
96}
97
98pub struct Timer {
100 histogram: Histogram,
102
103 start: SystemTime,
105}
106
107impl Timer {
108 pub fn observe<C: Clock>(self, clock: &C) {
110 self.histogram.observe_between(self.start, clock.current());
111 }
112}
113
114pub struct ScopedTimer<C: Clock> {
121 timer: Option<Timer>,
122 clock: Arc<C>,
123}
124
125impl<C: Clock> ScopedTimer<C> {
126 pub fn cancel(mut self) {
128 self.timer = None;
129 }
130}
131
132impl<C: Clock> Drop for ScopedTimer<C> {
133 fn drop(&mut self) {
134 if let Some(timer) = self.timer.take() {
135 timer.observe(self.clock.as_ref());
136 }
137 }
138}
139
140impl Timed {
141 pub fn scoped<C: Clock>(&self, clock: &Arc<C>) -> ScopedTimer<C> {
143 ScopedTimer {
144 timer: Some(self.timer(clock.as_ref())),
145 clock: clock.clone(),
146 }
147 }
148}
149
150pub fn duration_histogram<M: Metrics>(
152 context: &M,
153 name: &'static str,
154 help: &'static str,
155) -> Histogram {
156 context.histogram(name, help, Buckets::LOCAL)
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crate::{deterministic, Runner as _, Supervisor as _};
163 use std::time::Duration;
164
165 #[test]
166 fn duration_records_all_calls() {
167 deterministic::Runner::default().start(|context| async move {
168 let histogram = duration_histogram(&context, "test_duration", "test duration");
169 let timed = Timed::new(histogram);
170 let clock = Arc::new(context.child("timer"));
171
172 {
173 let _timer = timed.scoped(&clock);
174 context.sleep(Duration::from_millis(1)).await;
175 let result: Result<(), ()> = Ok(());
176 assert!(result.is_ok());
177 }
178
179 {
180 let _timer = timed.scoped(&clock);
181 context.sleep(Duration::from_millis(1)).await;
182 let result: Result<(), ()> = Err(());
183 assert!(result.is_err());
184 }
185
186 {
187 let _timer = timed.scoped(&clock);
188 context.sleep(Duration::from_millis(1)).await;
189 }
190
191 let metrics = context.encode();
192 assert!(
193 metrics.contains("test_duration_count 3"),
194 "unexpected metrics: {metrics}"
195 );
196 });
197 }
198}