tokio/runtime/metrics/histogram/
h2_histogram.rs1use crate::runtime::metrics::batch::duration_as_u64;
2use std::cmp;
3use std::error::Error;
4use std::fmt::{Display, Formatter};
5use std::time::Duration;
6
7const DEFAULT_MIN_VALUE: Duration = Duration::from_nanos(100);
8const DEFAULT_MAX_VALUE: Duration = Duration::from_secs(60);
9
10const DEFAULT_PRECISION: u32 = 2;
12const MAX_PRECISION: u32 = 10;
13
14#[derive(Debug, Copy, Clone, PartialEq, Eq)]
27pub struct LogHistogram {
28 pub(crate) num_buckets: usize,
30
31 pub(crate) p: u32,
33
34 pub(crate) bucket_offset: usize,
38}
39
40impl Default for LogHistogram {
41 fn default() -> Self {
42 LogHistogramBuilder::default().build()
43 }
44}
45
46impl LogHistogram {
47 fn from_n_p(n: u32, p: u32, bucket_offset: usize) -> Self {
52 assert!(n >= p, "{n} (n) must be at least as large as {p} (p)");
53 let num_buckets = ((n - p + 1) << p) as usize - bucket_offset;
54 Self {
55 num_buckets,
56 p,
57 bucket_offset,
58 }
59 }
60
61 fn truncate_to_max_value(&self, max_value: u64) -> LogHistogram {
62 let mut hist = *self;
63 while hist.max_value() >= max_value {
64 hist.num_buckets -= 1;
65 }
66 hist.num_buckets += 1;
67 hist
68 }
69
70 pub fn builder() -> LogHistogramBuilder {
72 LogHistogramBuilder::default()
73 }
74
75 pub fn max_value(&self) -> u64 {
77 self.bucket_range(self.num_buckets - 2).end
78 }
79
80 pub(crate) fn value_to_bucket(&self, value: u64) -> usize {
81 let index = bucket_index(value, self.p);
82 let offset_bucket = index.saturating_sub(self.bucket_offset as u64);
83 let max = self.num_buckets - 1;
84 offset_bucket.min(max as u64) as usize
85 }
86
87 pub(crate) fn bucket_range(&self, bucket: usize) -> std::ops::Range<u64> {
88 let LogHistogram {
89 p,
90 bucket_offset,
91 num_buckets,
92 } = self;
93 let input_bucket = bucket;
94 let bucket = bucket + bucket_offset;
95 let range_start_0th_bucket = match input_bucket {
96 0 => Some(0_u64),
97 _ => None,
98 };
99 let range_end_last_bucket = match input_bucket {
100 n if n == num_buckets - 1 => Some(u64::MAX),
101 _ => None,
102 };
103 if bucket < 1 << p {
104 let bucket = bucket as u64;
106 range_start_0th_bucket.unwrap_or(bucket)..range_end_last_bucket.unwrap_or(bucket + 1)
107 } else {
108 let bucket = bucket as u64;
110 let p = *p as u64;
111 let w = (bucket >> p) - 1;
112 let base_bucket = (w + 1) * (1_u64 << p);
113 let offset = bucket - base_bucket;
114 let s = 1_u64 << (w + p);
115 let start = s + (offset << w);
116 let end = s + ((offset + 1) << w);
117
118 range_start_0th_bucket.unwrap_or(start)..range_end_last_bucket.unwrap_or(end)
119 }
120 }
121}
122
123#[derive(Default, Debug, Copy, Clone)]
144pub struct LogHistogramBuilder {
145 max_value: Option<Duration>,
146 min_value: Option<Duration>,
147 precision: Option<u32>,
148}
149
150impl From<LogHistogramBuilder> for LogHistogram {
151 fn from(value: LogHistogramBuilder) -> Self {
152 value.build()
153 }
154}
155
156impl LogHistogramBuilder {
157 pub fn max_error(mut self, max_error: f64) -> Self {
176 assert!(max_error > 0.0, "max_error must be greater than 0");
177 assert!(max_error < 1.0, "max_error must be less than 1");
178 let mut p = 2;
179 while 2_f64.powf(-(p as f64)) > max_error && p <= MAX_PRECISION {
180 p += 1;
181 }
182 self.precision = Some(p);
183 self
184 }
185
186 pub fn precision_exact(mut self, p: u32) -> Self {
201 assert!(p <= MAX_PRECISION, "precision must be <= {MAX_PRECISION}");
202 self.precision = Some(p);
203 self
204 }
205
206 pub fn min_value(mut self, duration: Duration) -> Self {
212 self.min_value = Some(duration);
213 self
214 }
215
216 pub fn max_value(mut self, duration: Duration) -> Self {
223 if duration.is_zero() {
224 panic!("max value must be greater than 0");
225 }
226 self.max_value = Some(duration);
227 self
228 }
229
230 pub fn max_buckets(
232 &mut self,
233 max_buckets: usize,
234 ) -> Result<LogHistogram, InvalidHistogramConfiguration> {
235 let histogram = self.build();
236 if histogram.num_buckets > max_buckets {
237 return Err(InvalidHistogramConfiguration::TooManyBuckets {
238 required_bucket_count: histogram.num_buckets,
239 });
240 }
241 Ok(histogram)
242 }
243
244 pub fn build(&self) -> LogHistogram {
246 let requested_max_value = duration_as_u64(self.max_value.unwrap_or(DEFAULT_MAX_VALUE));
247 let max_value = requested_max_value.next_power_of_two();
248 let min_value = duration_as_u64(self.min_value.unwrap_or(DEFAULT_MIN_VALUE));
249 let p = self.precision.unwrap_or(DEFAULT_PRECISION);
250 let bucket_offset = cmp::max(bucket_index(min_value, p), 1) - 1;
253 let n = max_value.ilog2().max(p) + 1;
255 LogHistogram::from_n_p(n, p, bucket_offset as usize)
256 .truncate_to_max_value(requested_max_value)
257 }
258}
259
260#[derive(Debug)]
262pub enum InvalidHistogramConfiguration {
263 TooManyBuckets {
265 required_bucket_count: usize,
267 },
268}
269
270impl Display for InvalidHistogramConfiguration {
271 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
272 match self {
273 InvalidHistogramConfiguration::TooManyBuckets { required_bucket_count } =>
274 write!(f, "The configuration for this histogram would have required {required_bucket_count} buckets")
275 }
276 }
277}
278
279impl Error for InvalidHistogramConfiguration {}
280
281fn bucket_index(value: u64, p: u32) -> u64 {
285 if value == 0 {
288 return 0;
289 }
290 let h = 63 - value.leading_zeros();
291 if h <= p {
292 value
293 } else {
294 let w = h - p;
295 ((w + 1) * (1_u32 << p)) as u64 + ((value - (1_u64 << h)) >> w)
296 }
297}
298
299#[cfg(test)]
300mod test {
301 use super::InvalidHistogramConfiguration;
302 use crate::runtime::metrics::histogram::h2_histogram::LogHistogram;
303 use crate::runtime::metrics::histogram::HistogramType;
304
305 #[cfg(not(target_family = "wasm"))]
306 mod proptests {
307 use super::*;
308 use crate::runtime::metrics::batch::duration_as_u64;
309 use crate::runtime::metrics::histogram::h2_histogram::MAX_PRECISION;
310 use proptest::prelude::*;
311 use std::time::Duration;
312
313 fn valid_log_histogram_strategy() -> impl Strategy<Value = LogHistogram> {
314 (1..=50u32, 0..=MAX_PRECISION, 0..100usize).prop_map(|(n, p, bucket_offset)| {
315 let p = p.min(n);
316 let base = LogHistogram::from_n_p(n, p, 0);
317 LogHistogram::from_n_p(n, p, bucket_offset.min(base.num_buckets - 1))
318 })
319 }
320
321 fn log_histogram_settings() -> impl Strategy<Value = (u64, u64, u32)> {
322 (
323 duration_as_u64(Duration::from_nanos(1))..duration_as_u64(Duration::from_secs(20)),
324 duration_as_u64(Duration::from_secs(1))..duration_as_u64(Duration::from_secs(1000)),
325 0..MAX_PRECISION,
326 )
327 }
328
329 proptest! {
331 #[test]
332 fn log_histogram_settings_maintain_invariants((min_value, max_value, p) in log_histogram_settings()) {
333 if max_value < min_value {
334 return Ok(())
335 }
336 let (min_value, max_value) = (Duration::from_nanos(min_value), Duration::from_nanos(max_value));
337 let histogram = LogHistogram::builder().min_value(min_value).max_value(max_value).precision_exact(p).build();
338 let first_bucket_end = Duration::from_nanos(histogram.bucket_range(0).end);
339 let last_bucket_start = Duration::from_nanos(histogram.bucket_range(histogram.num_buckets - 1).start);
340 let second_last_bucket_start = Duration::from_nanos(histogram.bucket_range(histogram.num_buckets - 2).start);
341 prop_assert!(
342 first_bucket_end <= min_value,
343 "first bucket {first_bucket_end:?} must be less than {min_value:?}"
344 );
345 prop_assert!(
346 last_bucket_start > max_value,
347 "last bucket start ({last_bucket_start:?} must be at least as big as `max_value` ({max_value:?})"
348 );
349
350 prop_assert!(
352 second_last_bucket_start < max_value,
353 "second last bucket end ({second_last_bucket_start:?} must be at least as big as `max_value` ({max_value:?})"
354 );
355 }
356
357 #[test]
358 fn proptest_log_histogram_invariants(histogram in valid_log_histogram_strategy()) {
359 let first_range = histogram.bucket_range(0);
361 prop_assert_eq!(first_range.start, 0, "First bucket doesn't start at 0");
362
363 let mut prev_end = 0;
365 let mut prev_size = 0;
366 for bucket in 0..histogram.num_buckets {
367 let range = histogram.bucket_range(bucket);
368 prop_assert_eq!(range.start, prev_end, "Bucket ranges are not contiguous");
369 prop_assert!(range.start < range.end, "Bucket range is empty or reversed");
370
371 let size = range.end - range.start;
372
373 if bucket > 0 && bucket < histogram.num_buckets - 1 {
375 prop_assert!(size.is_power_of_two(), "Bucket size is not a power of 2");
376 }
377
378 if bucket > 1 {
379 prop_assert!(size >= prev_size, "Bucket sizes are not monotonically increasing: This size {size} (previous: {prev_size}). Bucket: {bucket}");
382 }
383
384
385 if bucket > 0 && bucket < histogram.num_buckets - 1 {
387 let p = histogram.p as f64;
388 let error_bound = 2.0_f64.powf(-p);
389 let relative_error = ((size as f64 - 1.0) / 2.0) / range.start as f64;
391 prop_assert!(
392 relative_error <= error_bound,
393 "Bucket size error exceeds bound: {:?} > {:?} ({range:?})",
394 relative_error,
395 error_bound
396 );
397 }
398
399 prev_end = range.end;
400 prev_size = size;
401 }
402 prop_assert_eq!(prev_end, u64::MAX, "Last bucket should end at u64::MAX");
403
404 for bucket in 0..histogram.num_buckets {
406 let range = histogram.bucket_range(bucket);
407 for value in [range.start, range.end - 1] {
408 prop_assert_eq!(
409 histogram.value_to_bucket(value),
410 bucket,
411 "value_to_bucket is not consistent with bucket_range"
412 );
413 }
414 }
415 }
416 }
417 }
418
419 #[test]
420 fn bucket_ranges_are_correct() {
421 let p = 2;
422 let config = HistogramType::H2(LogHistogram {
423 num_buckets: 1024,
424 p,
425 bucket_offset: 0,
426 });
427
428 for i in 0..2_usize.pow(p + 1) {
430 assert_eq!(
431 config.value_to_bucket(i as u64),
432 i,
433 "{i} should be in bucket {i}"
434 );
435 }
436
437 let mut value = 2_usize.pow(p + 1);
438 let current_bucket = value;
439 while value < current_bucket * 2 {
440 assert_eq!(
441 config.value_to_bucket(value as u64),
442 current_bucket + ((value - current_bucket) / 2),
443 "bucket for {value}"
444 );
445 value += 1;
446 }
447 }
448
449 #[test]
451 fn bucket_computation_spot_check() {
452 let p = 9;
453 let config = HistogramType::H2(LogHistogram {
454 num_buckets: 4096,
455 p,
456 bucket_offset: 0,
457 });
458 struct T {
459 v: u64,
460 bucket: usize,
461 }
462 let tests = [
463 T { v: 1, bucket: 1 },
464 T {
465 v: 1023,
466 bucket: 1023,
467 },
468 T {
469 v: 1024,
470 bucket: 1024,
471 },
472 T {
473 v: 2048,
474 bucket: 1536,
475 },
476 T {
477 v: 2052,
478 bucket: 1537,
479 },
480 ];
481 for test in tests {
482 assert_eq!(config.value_to_bucket(test.v), test.bucket);
483 }
484 }
485
486 #[test]
487 fn last_bucket_goes_to_infinity() {
488 let conf = HistogramType::H2(LogHistogram::from_n_p(16, 3, 10));
489 assert_eq!(conf.bucket_range(conf.num_buckets() - 1).end, u64::MAX);
490 }
491
492 #[test]
493 fn bucket_offset() {
494 let conf = HistogramType::H2(LogHistogram::from_n_p(16, 3, 10));
496 for i in 0..10 {
497 assert_eq!(conf.value_to_bucket(i), 0);
498 }
499 assert_eq!(conf.value_to_bucket(10), 0);
502 assert_eq!(conf.value_to_bucket(16), 6);
503 assert_eq!(conf.value_to_bucket(17), 6);
504 assert_eq!(conf.bucket_range(6), 16..18);
505 }
506
507 #[test]
508 fn max_buckets_enforcement() {
509 let error = LogHistogram::builder()
510 .max_error(0.001)
511 .max_buckets(5)
512 .expect_err("this produces way more than 5 buckets");
513 let num_buckets = match error {
514 InvalidHistogramConfiguration::TooManyBuckets {
515 required_bucket_count,
516 } => required_bucket_count,
517 };
518 assert_eq!(num_buckets, 27291);
519 }
520
521 #[test]
522 fn default_configuration_size() {
523 let conf = LogHistogram::builder().build();
524 assert_eq!(conf.num_buckets, 119);
525 }
526}