Skip to main content

tokio_metrics/runtime/
poll_time_histogram.rs

1use std::time::Duration;
2
3/// A histogram of task poll durations, pairing each bucket's count with its
4/// time range from the runtime configuration.
5///
6/// This type is returned as part of [`RuntimeMetrics`][super::RuntimeMetrics]
7/// when the runtime has poll time histograms enabled via
8/// [`enable_metrics_poll_time_histogram`][tokio::runtime::Builder::enable_metrics_poll_time_histogram].
9///
10/// Each bucket contains the [`Duration`] range configured for that bucket and
11/// the count of task polls that fell into that range during the sampling
12/// interval.
13#[derive(Debug, Clone, Default)]
14#[non_exhaustive]
15pub struct PollTimeHistogram {
16    buckets: Vec<HistogramBucket>,
17}
18
19impl PollTimeHistogram {
20    // Only used to populate the histogram, which requires `tokio_unstable`.
21    #[cfg_attr(not(tokio_unstable), allow(dead_code))]
22    pub(crate) fn new(buckets: Vec<HistogramBucket>) -> Self {
23        Self { buckets }
24    }
25
26    /// Returns the histogram buckets.
27    pub fn buckets(&self) -> &[HistogramBucket] {
28        &self.buckets
29    }
30
31    // Only used to populate the histogram, which requires `tokio_unstable`.
32    #[cfg_attr(not(tokio_unstable), allow(dead_code))]
33    pub(crate) fn buckets_mut(&mut self) -> &mut [HistogramBucket] {
34        &mut self.buckets
35    }
36
37    /// Returns just the bucket counts as a `Vec<u64>`.
38    pub fn as_counts(&self) -> Vec<u64> {
39        self.buckets.iter().map(|b| b.count).collect()
40    }
41}
42
43/// A single bucket in a [`PollTimeHistogram`].
44#[derive(Debug, Clone, Copy, Default)]
45#[non_exhaustive]
46pub struct HistogramBucket {
47    range_start: Duration,
48    range_end: Duration,
49    count: u64,
50}
51
52impl HistogramBucket {
53    // Only used to populate the histogram, which requires `tokio_unstable`.
54    #[cfg_attr(not(tokio_unstable), allow(dead_code))]
55    pub(crate) fn new(range_start: Duration, range_end: Duration, count: u64) -> Self {
56        Self { range_start, range_end, count }
57    }
58
59    /// The start of the time range for this bucket (inclusive).
60    pub fn range_start(&self) -> Duration {
61        self.range_start
62    }
63
64    /// The end of the time range for this bucket (exclusive).
65    pub fn range_end(&self) -> Duration {
66        self.range_end
67    }
68
69    /// Returns the poll count for this bucket during the interval.
70    pub fn count(&self) -> u64 {
71        self.count
72    }
73
74    /// Adds to the count of this bucket.
75    // Only used to populate the histogram, which requires `tokio_unstable`.
76    #[cfg_attr(not(tokio_unstable), allow(dead_code))]
77    pub(crate) fn add_count(&mut self, delta: u64) {
78        self.count = self.count.saturating_add(delta);
79    }
80}
81
82#[cfg(feature = "metrique-integration")]
83impl metrique::writer::Value for PollTimeHistogram {
84    fn write(&self, writer: impl metrique::writer::ValueWriter) {
85        use metrique::writer::unit::NegativeScale;
86        use metrique::writer::{MetricFlags, Observation, Unit};
87
88        // Use the bucket midpoint as the representative value. 
89        // Tokio's last bucket has range_end of Duration::from_nanos(u64::MAX),
90        // so use range_start for it since the midpoint wouldn't be representative.
91        const LAST_BUCKET_END: Duration = Duration::from_nanos(u64::MAX);
92        writer.metric(
93            self.buckets.iter().filter(|b| b.count > 0).map(|b| {
94                let value_us = if b.range_end == LAST_BUCKET_END {
95                    b.range_start.as_micros() as f64
96                } else {
97                    #[allow(clippy::incompatible_msrv)] // metrique-integration requires 1.89+
98                    f64::midpoint(
99                        b.range_start.as_micros() as f64,
100                        b.range_end.as_micros() as f64,
101                    )
102                };
103                Observation::Repeated {
104                    total: value_us * b.count as f64,
105                    occurrences: b.count,
106                }
107            }),
108            Unit::Second(NegativeScale::Micro),
109            [],
110            MetricFlags::empty(),
111        );
112    }
113}
114
115#[cfg(feature = "metrique-integration")]
116impl metrique::CloseValue for PollTimeHistogram {
117    type Closed = Self;
118
119    fn close(self) -> Self {
120        self
121    }
122}
123
124// `poll_time_histogram_last_bucket_uses_range_start` constructs a
125// `RuntimeMetrics` and reads its `poll_time_histogram` field, both of which
126// require `tokio_unstable`.
127#[cfg(all(test, tokio_unstable, feature = "metrique-integration"))]
128mod tests {
129    use super::*;
130    use crate::runtime::RuntimeMetrics;
131    use metrique::CloseValue;
132    use metrique::test_util::test_metric;
133
134    #[test]
135    fn poll_time_histogram_close_value() {
136        let hist = PollTimeHistogram::new(vec![
137            HistogramBucket::new(Duration::from_micros(0), Duration::from_micros(100), 5),
138            HistogramBucket::new(Duration::from_micros(100), Duration::from_micros(200), 0),
139            HistogramBucket::new(Duration::from_micros(200), Duration::from_micros(500), 3),
140        ]);
141
142        let closed = hist.close();
143        let buckets = closed.buckets();
144        assert_eq!(buckets.len(), 3);
145        assert_eq!(buckets[0].count(), 5);
146        assert_eq!(buckets[0].range_start(), Duration::from_micros(0));
147        assert_eq!(buckets[0].range_end(), Duration::from_micros(100));
148        assert_eq!(buckets[1].count(), 0);
149        assert_eq!(buckets[2].count(), 3);
150        assert_eq!(buckets[2].range_start(), Duration::from_micros(200));
151        assert_eq!(buckets[2].range_end(), Duration::from_micros(500));
152    }
153
154    #[test]
155    fn poll_time_histogram_last_bucket_uses_range_start() {
156        let last_bucket_start = Duration::from_millis(500);
157        let metrics = RuntimeMetrics {
158            poll_time_histogram: PollTimeHistogram::new(vec![
159                HistogramBucket::new(Duration::from_micros(0), Duration::from_micros(100), 0),
160                HistogramBucket::new(last_bucket_start, Duration::from_nanos(u64::MAX), 2),
161            ]),
162            ..Default::default()
163        };
164
165        let entry = test_metric(metrics);
166        let hist = &entry.metrics["poll_time_histogram"];
167        assert_eq!(hist.distribution.len(), 1);
168
169        match hist.distribution[0] {
170            metrique::writer::Observation::Repeated { total, occurrences } => {
171                assert_eq!(occurrences, 2);
172                let expected = last_bucket_start.as_micros() as f64 * 2.0;
173                assert!((total - expected).abs() < 0.01);
174            }
175            other => panic!("expected Repeated, got {other:?}"),
176        }
177    }
178}