Skip to content

Commit 15495fd

Browse files
authored
metrics: improve flexibility of H2Histogram Configuration (#6963)
1 parent ad41834 commit 15495fd

File tree

3 files changed

+126
-23
lines changed

3 files changed

+126
-23
lines changed

tokio/src/runtime/builder.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,8 +1245,31 @@ impl Builder {
12451245
/// .unwrap();
12461246
/// ```
12471247
///
1248+
/// When migrating from the legacy histogram ([`HistogramScale::Log`]) and wanting
1249+
/// to match the previous behavior, use `precision_exact(0)`. This creates a histogram
1250+
/// where each bucket is twice the size of the previous bucket.
1251+
/// ```rust
1252+
/// use std::time::Duration;
1253+
/// use tokio::runtime::{HistogramConfiguration, LogHistogram};
1254+
/// let rt = tokio::runtime::Builder::new_current_thread()
1255+
/// .enable_all()
1256+
/// .enable_metrics_poll_time_histogram()
1257+
/// .metrics_poll_time_histogram_configuration(HistogramConfiguration::log(
1258+
/// LogHistogram::builder()
1259+
/// .min_value(Duration::from_micros(20))
1260+
/// .max_value(Duration::from_millis(4))
1261+
/// // Set `precision_exact` to `0` to match `HistogramScale::Log`
1262+
/// .precision_exact(0)
1263+
/// .max_buckets(10)
1264+
/// .unwrap(),
1265+
/// ))
1266+
/// .build()
1267+
/// .unwrap();
1268+
/// ```
1269+
///
12481270
/// [`LogHistogram`]: crate::runtime::LogHistogram
12491271
/// [default configuration]: crate::runtime::LogHistogramBuilder
1272+
/// [`HistogramScale::Log`]: crate::runtime::HistogramScale::Log
12501273
pub fn metrics_poll_time_histogram_configuration(&mut self, configuration: HistogramConfiguration) -> &mut Self {
12511274
self.metrics_poll_count_histogram.histogram_type = configuration.inner;
12521275
self

tokio/src/runtime/metrics/histogram/h2_histogram.rs

Lines changed: 75 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const DEFAULT_MAX_VALUE: Duration = Duration::from_secs(60);
99

1010
/// Default precision is 2^-2 = 25% max error
1111
const DEFAULT_PRECISION: u32 = 2;
12+
const MAX_PRECISION: u32 = 10;
1213

1314
/// Log Histogram
1415
///
@@ -57,15 +58,23 @@ impl LogHistogram {
5758
}
5859
}
5960

61+
fn truncate_to_max_value(&self, max_value: u64) -> LogHistogram {
62+
let mut hist = self.clone();
63+
while hist.max_value() >= max_value {
64+
hist.num_buckets -= 1;
65+
}
66+
hist.num_buckets += 1;
67+
hist
68+
}
69+
6070
/// Creates a builder for [`LogHistogram`]
6171
pub fn builder() -> LogHistogramBuilder {
6272
LogHistogramBuilder::default()
6373
}
6474

6575
/// The maximum value that can be stored before truncation in this histogram
6676
pub fn max_value(&self) -> u64 {
67-
let n = (self.num_buckets / (1 << self.p)) - 1 + self.p as usize;
68-
(1_u64 << n) - 1
77+
self.bucket_range(self.num_buckets - 2).end
6978
}
7079

7180
pub(crate) fn value_to_bucket(&self, value: u64) -> usize {
@@ -155,23 +164,23 @@ impl LogHistogramBuilder {
155164
/// such that `2^-p` is less than `precision`. To set `p` directly, use
156165
/// [`LogHistogramBuilder::precision_exact`].
157166
///
167+
/// Precision controls the size of the "bucket groups" (consecutive buckets with identical
168+
/// ranges). When `p` is 0, each bucket will be twice the size of the previous bucket. To match
169+
/// the behavior of the legacy log histogram implementation, use `builder.precision_exact(0)`.
170+
///
158171
/// The default value is 25% (2^-2)
159172
///
160173
/// The highest supported precision is `0.0977%` `(2^-10)`. Provided values
161174
/// less than this will be truncated.
162175
///
163176
/// # Panics
164-
/// - `precision` < 0
165-
/// - `precision` > 1
177+
/// - `max_error` < 0
178+
/// - `max_error` > 1
166179
pub fn max_error(mut self, max_error: f64) -> Self {
167-
if max_error < 0.0 {
168-
panic!("precision must be >= 0");
169-
};
170-
if max_error > 1.0 {
171-
panic!("precision must be > 1");
172-
};
180+
assert!(max_error > 0.0, "max_error must be greater than 0");
181+
assert!(max_error < 1.0, "max_error must be less than 1");
173182
let mut p = 2;
174-
while 2_f64.powf(-1.0 * p as f64) > max_error && p <= 10 {
183+
while 2_f64.powf(-1.0 * p as f64) > max_error && p <= MAX_PRECISION {
175184
p += 1;
176185
}
177186
self.precision = Some(p);
@@ -180,16 +189,20 @@ impl LogHistogramBuilder {
180189

181190
/// Sets the precision of this histogram directly.
182191
///
192+
/// The precision (meaning: the ratio `n/bucket_range(n)` for some given `n`) will be `2^-p`.
193+
///
194+
/// Precision controls the number consecutive buckets with identically sized ranges.
195+
/// When `p` is 0, each bucket will be twice the size of the previous bucket (bucket groups are
196+
/// only a single bucket wide).
197+
///
198+
/// To match the behavior of the legacy implementation ([`HistogramScale::Log`]), use `builder.precision_exact(0)`.
199+
///
183200
/// # Panics
184-
/// - `p` < 2
185201
/// - `p` > 10
202+
///
203+
/// [`HistogramScale::Log`]: [crate::runtime::HistogramScale]
186204
pub fn precision_exact(mut self, p: u32) -> Self {
187-
if p < 2 {
188-
panic!("precision must be >= 2");
189-
};
190-
if p > 10 {
191-
panic!("precision must be <= 10");
192-
};
205+
assert!(p <= MAX_PRECISION, "precision must be <= {MAX_PRECISION}");
193206
self.precision = Some(p);
194207
self
195208
}
@@ -234,16 +247,17 @@ impl LogHistogramBuilder {
234247

235248
/// Builds the log histogram
236249
pub fn build(&self) -> LogHistogram {
237-
let max_value = duration_as_u64(self.max_value.unwrap_or(DEFAULT_MAX_VALUE));
238-
let max_value = max_value.next_power_of_two();
250+
let requested_max_value = duration_as_u64(self.max_value.unwrap_or(DEFAULT_MAX_VALUE));
251+
let max_value = requested_max_value.next_power_of_two();
239252
let min_value = duration_as_u64(self.min_value.unwrap_or(DEFAULT_MIN_VALUE));
240253
let p = self.precision.unwrap_or(DEFAULT_PRECISION);
241254
// determine the bucket offset by finding the bucket for the minimum value. We need to lower
242255
// this by one to ensure we are at least as granular as requested.
243256
let bucket_offset = cmp::max(bucket_index(min_value, p), 1) - 1;
244257
// n must be at least as large as p
245-
let n = max_value.ilog2().max(p);
258+
let n = max_value.ilog2().max(p) + 1;
246259
LogHistogram::from_n_p(n, p, bucket_offset as usize)
260+
.truncate_to_max_value(requested_max_value)
247261
}
248262
}
249263

@@ -295,17 +309,55 @@ mod test {
295309
#[cfg(not(target_family = "wasm"))]
296310
mod proptests {
297311
use super::*;
312+
use crate::runtime::metrics::batch::duration_as_u64;
313+
use crate::runtime::metrics::histogram::h2_histogram::MAX_PRECISION;
298314
use proptest::prelude::*;
315+
use std::time::Duration;
316+
299317
fn valid_log_histogram_strategy() -> impl Strategy<Value = LogHistogram> {
300-
(2..=50u32, 2..=16u32, 0..100usize).prop_map(|(n, p, bucket_offset)| {
318+
(1..=50u32, 0..=MAX_PRECISION, 0..100usize).prop_map(|(n, p, bucket_offset)| {
301319
let p = p.min(n);
302320
let base = LogHistogram::from_n_p(n, p, 0);
303321
LogHistogram::from_n_p(n, p, bucket_offset.min(base.num_buckets - 1))
304322
})
305323
}
306324

325+
fn log_histogram_settings() -> impl Strategy<Value = (u64, u64, u32)> {
326+
(
327+
duration_as_u64(Duration::from_nanos(1))..duration_as_u64(Duration::from_secs(20)),
328+
duration_as_u64(Duration::from_secs(1))..duration_as_u64(Duration::from_secs(1000)),
329+
0..MAX_PRECISION,
330+
)
331+
}
332+
307333
// test against a wide assortment of different histogram configurations to ensure invariants hold
308334
proptest! {
335+
#[test]
336+
fn log_histogram_settings_maintain_invariants((min_value, max_value, p) in log_histogram_settings()) {
337+
if max_value < min_value {
338+
return Ok(())
339+
}
340+
let (min_value, max_value) = (Duration::from_nanos(min_value), Duration::from_nanos(max_value));
341+
let histogram = LogHistogram::builder().min_value(min_value).max_value(max_value).precision_exact(p).build();
342+
let first_bucket_end = Duration::from_nanos(histogram.bucket_range(0).end);
343+
let last_bucket_start = Duration::from_nanos(histogram.bucket_range(histogram.num_buckets - 1).start);
344+
let second_last_bucket_start = Duration::from_nanos(histogram.bucket_range(histogram.num_buckets - 2).start);
345+
prop_assert!(
346+
first_bucket_end <= min_value,
347+
"first bucket {first_bucket_end:?} must be less than {min_value:?}"
348+
);
349+
prop_assert!(
350+
last_bucket_start > max_value,
351+
"last bucket start ({last_bucket_start:?} must be at least as big as `max_value` ({max_value:?})"
352+
);
353+
354+
// We should have the exact right number of buckets. The second to last bucket should be strictly less than max value.
355+
prop_assert!(
356+
second_last_bucket_start < max_value,
357+
"second last bucket end ({second_last_bucket_start:?} must be at least as big as `max_value` ({max_value:?})"
358+
);
359+
}
360+
309361
#[test]
310362
fn proptest_log_histogram_invariants(histogram in valid_log_histogram_strategy()) {
311363
// 1. Assert that the first bucket always starts at 0
@@ -469,7 +521,7 @@ mod test {
469521
required_bucket_count,
470522
} => required_bucket_count,
471523
};
472-
assert_eq!(num_buckets, 27549);
524+
assert_eq!(num_buckets, 27291);
473525
}
474526

475527
#[test]

tokio/tests/rt_unstable_metrics.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,34 @@ fn log_histogram() {
424424
assert_eq!(N, n);
425425
}
426426

427+
#[test]
428+
fn minimal_log_histogram() {
429+
let rt = tokio::runtime::Builder::new_current_thread()
430+
.enable_all()
431+
.enable_metrics_poll_time_histogram()
432+
.metrics_poll_time_histogram_configuration(HistogramConfiguration::log(
433+
LogHistogram::builder()
434+
.max_value(Duration::from_millis(4))
435+
.min_value(Duration::from_micros(20))
436+
.precision_exact(0),
437+
))
438+
.build()
439+
.unwrap();
440+
let metrics = rt.metrics();
441+
let num_buckets = rt.metrics().poll_time_histogram_num_buckets();
442+
for b in 1..num_buckets - 1 {
443+
let range = metrics.poll_time_histogram_bucket_range(b);
444+
let size = range.end - range.start;
445+
// Assert the buckets continue doubling in size
446+
assert_eq!(
447+
size,
448+
Duration::from_nanos((1 << (b - 1)) * 16384),
449+
"incorrect range for {b}"
450+
);
451+
}
452+
assert_eq!(num_buckets, 10);
453+
}
454+
427455
#[test]
428456
#[allow(deprecated)]
429457
fn legacy_log_histogram() {

0 commit comments

Comments
 (0)