metrics: add H2 Histogram option to improve histogram granularity (#6897)

This commit is contained in:
Russell Cohen 2024-10-21 08:05:45 -04:00 committed by GitHub
parent ce1c74f1cc
commit da745ff335
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 872 additions and 130 deletions

View File

@ -1,4 +1,4 @@
287
292
&
+
<
@ -12,12 +12,17 @@
0xA
0xD
100ms
100ns
10ms
10μs
~12
120s
12.5%
±1m
±1ms
1ms
1s
25%
250ms
2x
~4
@ -116,9 +121,11 @@ GID
goroutines
Growable
gzip
H2
hashmaps
HashMaps
hashsets
HdrHistogram
ie
Illumos
impl

View File

@ -96,7 +96,7 @@ mio = { version = "1.0.1", optional = true, default-features = false }
parking_lot = { version = "0.12.0", optional = true }
[target.'cfg(not(target_family = "wasm"))'.dependencies]
socket2 = { version = "0.5.5", optional = true, features = [ "all" ] }
socket2 = { version = "0.5.5", optional = true, features = ["all"] }
# Currently unstable. The API exposed by these features may be broken at any time.
# Requires `--cfg tokio_unstable` to enable.
@ -123,8 +123,8 @@ optional = true
[target.'cfg(windows)'.dev-dependencies.windows-sys]
version = "0.52"
features = [
"Win32_Foundation",
"Win32_Security_Authorization",
"Win32_Foundation",
"Win32_Security_Authorization",
]
[dev-dependencies]
@ -137,6 +137,7 @@ async-stream = "0.3"
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
socket2 = "0.5.5"
tempfile = "3.1.0"
proptest = "1"
[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies]
rand = "0.8.0"
@ -165,8 +166,7 @@ features = ["full", "test-util"]
# The following are types that are allowed to be exposed in Tokio's public API.
# The standard library is allowed by default.
allowed_external_types = [
"bytes::buf::buf_impl::Buf",
"bytes::buf::buf_mut::BufMut",
"tokio_macros::*",
"bytes::buf::buf_impl::Buf",
"bytes::buf::buf_mut::BufMut",
"tokio_macros::*",
]

View File

@ -3,7 +3,7 @@
use crate::runtime::handle::Handle;
use crate::runtime::{blocking, driver, Callback, HistogramBuilder, Runtime, TaskCallback};
#[cfg(tokio_unstable)]
use crate::runtime::{LocalOptions, LocalRuntime, TaskMeta};
use crate::runtime::{metrics::HistogramConfiguration, LocalOptions, LocalRuntime, TaskMeta};
use crate::util::rand::{RngSeed, RngSeedGenerator};
use crate::runtime::blocking::BlockingPool;
@ -1102,6 +1102,11 @@ impl Builder {
/// `metrics_poll_count_histogram_` builder methods to configure the
/// histogram details.
///
/// By default, a linear histogram with 10 buckets each 100 microseconds wide will be used.
/// This has an extremely low memory footprint, but may not provide enough granularity. For
/// better granularity with low memory usage, use [`metrics_poll_count_histogram_configuration()`]
/// to select [`LogHistogram`] instead.
///
/// # Examples
///
/// ```
@ -1121,6 +1126,8 @@ impl Builder {
///
/// [`Handle::metrics()`]: crate::runtime::Handle::metrics
/// [`Instant::now()`]: std::time::Instant::now
/// [`LogHistogram`]: crate::runtime::LogHistogram
/// [`metrics_poll_count_histogram_configuration()`]: Builder::metrics_poll_count_histogram_configuration
pub fn enable_metrics_poll_count_histogram(&mut self) -> &mut Self {
self.metrics_poll_count_histogram_enable = true;
self
@ -1142,14 +1149,83 @@ impl Builder {
/// ```
/// use tokio::runtime::{self, HistogramScale};
///
/// # #[allow(deprecated)]
/// let rt = runtime::Builder::new_multi_thread()
/// .enable_metrics_poll_count_histogram()
/// .metrics_poll_count_histogram_scale(HistogramScale::Log)
/// .build()
/// .unwrap();
/// ```
#[deprecated(note = "use metrics_poll_count_histogram_configuration")]
pub fn metrics_poll_count_histogram_scale(&mut self, histogram_scale: crate::runtime::HistogramScale) -> &mut Self {
self.metrics_poll_count_histogram.scale = histogram_scale;
self.metrics_poll_count_histogram.legacy_mut(|b|b.scale = histogram_scale);
self
}
/// Configure the histogram for tracking poll times
///
/// By default, a linear histogram with 10 buckets each 100 microseconds wide will be used.
/// This has an extremely low memory footprint, but may not provide enough granularity. For
/// better granularity with low memory usage, use [`LogHistogram`] instead.
///
/// # Examples
/// Configure a [`LogHistogram`] with [default configuration]:
/// ```
/// use tokio::runtime;
/// use tokio::runtime::{HistogramConfiguration, LogHistogram};
///
/// let rt = runtime::Builder::new_multi_thread()
/// .enable_metrics_poll_count_histogram()
/// .metrics_poll_count_histogram_configuration(
/// HistogramConfiguration::log(LogHistogram::default())
/// )
/// .build()
/// .unwrap();
/// ```
///
/// Configure a linear histogram with 100 buckets, each 10μs wide
/// ```
/// use tokio::runtime;
/// use std::time::Duration;
/// use tokio::runtime::HistogramConfiguration;
///
/// let rt = runtime::Builder::new_multi_thread()
/// .enable_metrics_poll_count_histogram()
/// .metrics_poll_count_histogram_configuration(
/// HistogramConfiguration::linear(Duration::from_micros(10), 100)
/// )
/// .build()
/// .unwrap();
/// ```
///
/// Configure a [`LogHistogram`] with the following settings:
/// - Measure times from 100ns to 120s
/// - Max error of 0.1
/// - No more than 1024 buckets
/// ```
/// use std::time::Duration;
/// use tokio::runtime;
/// use tokio::runtime::{HistogramConfiguration, LogHistogram};
///
/// let rt = runtime::Builder::new_multi_thread()
/// .enable_metrics_poll_count_histogram()
/// .metrics_poll_count_histogram_configuration(
/// HistogramConfiguration::log(LogHistogram::builder()
/// .max_value(Duration::from_secs(120))
/// .min_value(Duration::from_nanos(100))
/// .max_error(0.1)
/// .max_buckets(1024)
/// .expect("configuration uses 488 buckets")
/// )
/// )
/// .build()
/// .unwrap();
/// ```
///
/// [`LogHistogram`]: crate::runtime::LogHistogram
/// [default configuration]: crate::runtime::LogHistogramBuilder
pub fn metrics_poll_count_histogram_configuration(&mut self, configuration: HistogramConfiguration) -> &mut Self {
self.metrics_poll_count_histogram.histogram_type = configuration.inner;
self
}
@ -1173,19 +1249,22 @@ impl Builder {
/// use tokio::runtime;
/// use std::time::Duration;
///
/// # #[allow(deprecated)]
/// let rt = runtime::Builder::new_multi_thread()
/// .enable_metrics_poll_count_histogram()
/// .metrics_poll_count_histogram_resolution(Duration::from_micros(100))
/// .build()
/// .unwrap();
/// ```
#[deprecated(note = "use metrics_poll_count_histogram_configuration")]
pub fn metrics_poll_count_histogram_resolution(&mut self, resolution: Duration) -> &mut Self {
assert!(resolution > Duration::from_secs(0));
// Sanity check the argument and also make the cast below safe.
assert!(resolution <= Duration::from_secs(1));
let resolution = resolution.as_nanos() as u64;
self.metrics_poll_count_histogram.resolution = resolution;
self.metrics_poll_count_histogram.legacy_mut(|b|b.resolution = resolution);
self
}
@ -1204,14 +1283,16 @@ impl Builder {
/// ```
/// use tokio::runtime;
///
/// # #[allow(deprecated)]
/// let rt = runtime::Builder::new_multi_thread()
/// .enable_metrics_poll_count_histogram()
/// .metrics_poll_count_histogram_buckets(15)
/// .build()
/// .unwrap();
/// ```
#[deprecated(note = "use `metrics_poll_count_histogram_configuration")]
pub fn metrics_poll_count_histogram_buckets(&mut self, buckets: usize) -> &mut Self {
self.metrics_poll_count_histogram.num_buckets = buckets;
self.metrics_poll_count_histogram.legacy_mut(|b|b.num_buckets = buckets);
self
}
}

View File

@ -171,6 +171,6 @@ cfg_rt_multi_thread! {
}
}
fn duration_as_u64(dur: Duration) -> u64 {
pub(crate) fn duration_as_u64(dur: Duration) -> u64 {
u64::try_from(dur.as_nanos()).unwrap_or(u64::MAX)
}

View File

@ -1,38 +1,53 @@
mod h2_histogram;
pub use h2_histogram::{InvalidHistogramConfiguration, LogHistogram, LogHistogramBuilder};
use crate::util::metric_atomics::MetricAtomicU64;
use std::sync::atomic::Ordering::Relaxed;
use crate::runtime::metrics::batch::duration_as_u64;
use std::cmp;
use std::ops::Range;
use std::time::Duration;
#[derive(Debug)]
pub(crate) struct Histogram {
/// The histogram buckets
buckets: Box<[MetricAtomicU64]>,
/// Bucket scale, linear or log
scale: HistogramScale,
/// Minimum resolution
resolution: u64,
/// The type of the histogram
///
/// This handles `fn(bucket) -> Range` and `fn(value) -> bucket`
histogram_type: HistogramType,
}
#[derive(Debug, Clone)]
pub(crate) struct HistogramBuilder {
/// Histogram scale
pub(crate) scale: HistogramScale,
pub(crate) histogram_type: HistogramType,
pub(crate) legacy: Option<LegacyBuilder>,
}
/// Must be a power of 2
#[derive(Debug, Clone)]
pub(crate) struct LegacyBuilder {
pub(crate) resolution: u64,
/// Number of buckets
pub(crate) scale: HistogramScale,
pub(crate) num_buckets: usize,
}
impl Default for LegacyBuilder {
fn default() -> Self {
Self {
resolution: 100_000,
num_buckets: 10,
scale: HistogramScale::Linear,
}
}
}
#[derive(Debug)]
pub(crate) struct HistogramBatch {
buckets: Box<[u64]>,
scale: HistogramScale,
resolution: u64,
configuration: HistogramType,
}
cfg_unstable! {
@ -47,6 +62,129 @@ cfg_unstable! {
/// Logarithmic bucket scale
Log,
}
/// Configuration for the poll count histogram
#[derive(Debug, Clone)]
pub struct HistogramConfiguration {
pub(crate) inner: HistogramType
}
impl HistogramConfiguration {
/// Create a linear bucketed histogram
///
/// # Arguments
///
/// * `bucket_width`: The width of each bucket
/// * `num_buckets`: The number of buckets
pub fn linear(bucket_width: Duration, num_buckets: usize) -> Self {
Self {
inner: HistogramType::Linear(LinearHistogram {
num_buckets,
bucket_width: duration_as_u64(bucket_width),
}),
}
}
/// Creates a log-scaled bucketed histogram
///
/// See [`LogHistogramBuilder`] for information about configuration & defaults
pub fn log(configuration: impl Into<LogHistogram>) -> Self {
Self {
inner: HistogramType::H2(configuration.into()),
}
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) enum HistogramType {
/// Linear histogram with fixed width buckets
Linear(LinearHistogram),
/// Old log histogram where each bucket doubles in size
LogLegacy(LegacyLogHistogram),
/// Log histogram implementation based on H2 Histograms
H2(LogHistogram),
}
impl HistogramType {
pub(crate) fn num_buckets(&self) -> usize {
match self {
HistogramType::Linear(linear) => linear.num_buckets,
HistogramType::LogLegacy(log) => log.num_buckets,
HistogramType::H2(h2) => h2.num_buckets,
}
}
fn value_to_bucket(&self, value: u64) -> usize {
match self {
HistogramType::Linear(LinearHistogram {
num_buckets,
bucket_width,
}) => {
let max = num_buckets - 1;
cmp::min(value / *bucket_width, max as u64) as usize
}
HistogramType::LogLegacy(LegacyLogHistogram {
num_buckets,
first_bucket_width,
}) => {
let max = num_buckets - 1;
if value < *first_bucket_width {
0
} else {
let significant_digits = 64 - value.leading_zeros();
let bucket_digits = 64 - (first_bucket_width - 1).leading_zeros();
cmp::min(significant_digits as usize - bucket_digits as usize, max)
}
}
HistogramType::H2(log_histogram) => log_histogram.value_to_bucket(value),
}
}
fn bucket_range(&self, bucket: usize) -> Range<u64> {
match self {
HistogramType::Linear(LinearHistogram {
num_buckets,
bucket_width,
}) => Range {
start: bucket_width * bucket as u64,
end: if bucket == num_buckets - 1 {
u64::MAX
} else {
bucket_width * (bucket as u64 + 1)
},
},
HistogramType::LogLegacy(LegacyLogHistogram {
num_buckets,
first_bucket_width,
}) => Range {
start: if bucket == 0 {
0
} else {
first_bucket_width << (bucket - 1)
},
end: if bucket == num_buckets - 1 {
u64::MAX
} else {
first_bucket_width << bucket
},
},
HistogramType::H2(log) => log.bucket_range(bucket),
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) struct LinearHistogram {
num_buckets: usize,
bucket_width: u64,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) struct LegacyLogHistogram {
num_buckets: usize,
first_bucket_width: u64,
}
impl Histogram {
@ -61,28 +199,7 @@ impl Histogram {
}
pub(crate) fn bucket_range(&self, bucket: usize) -> Range<u64> {
match self.scale {
HistogramScale::Log => Range {
start: if bucket == 0 {
0
} else {
self.resolution << (bucket - 1)
},
end: if bucket == self.buckets.len() - 1 {
u64::MAX
} else {
self.resolution << bucket
},
},
HistogramScale::Linear => Range {
start: self.resolution * bucket as u64,
end: if bucket == self.buckets.len() - 1 {
u64::MAX
} else {
self.resolution * (bucket as u64 + 1)
},
},
}
self.histogram_type.bucket_range(bucket)
}
}
@ -92,8 +209,7 @@ impl HistogramBatch {
HistogramBatch {
buckets,
scale: histogram.scale,
resolution: histogram.resolution,
configuration: histogram.histogram_type,
}
}
@ -102,8 +218,7 @@ impl HistogramBatch {
}
pub(crate) fn submit(&self, histogram: &Histogram) {
debug_assert_eq!(self.scale, histogram.scale);
debug_assert_eq!(self.resolution, histogram.resolution);
debug_assert_eq!(self.configuration, histogram.histogram_type);
debug_assert_eq!(self.buckets.len(), histogram.buckets.len());
for i in 0..self.buckets.len() {
@ -112,52 +227,51 @@ impl HistogramBatch {
}
fn value_to_bucket(&self, value: u64) -> usize {
match self.scale {
HistogramScale::Linear => {
let max = self.buckets.len() - 1;
cmp::min(value / self.resolution, max as u64) as usize
}
HistogramScale::Log => {
let max = self.buckets.len() - 1;
if value < self.resolution {
0
} else {
let significant_digits = 64 - value.leading_zeros();
let bucket_digits = 64 - (self.resolution - 1).leading_zeros();
cmp::min(significant_digits as usize - bucket_digits as usize, max)
}
}
}
self.configuration.value_to_bucket(value)
}
}
impl HistogramBuilder {
pub(crate) fn new() -> HistogramBuilder {
HistogramBuilder {
scale: HistogramScale::Linear,
// Resolution is in nanoseconds.
resolution: 100_000,
num_buckets: 10,
histogram_type: HistogramType::Linear(LinearHistogram {
num_buckets: 10,
bucket_width: 100_000,
}),
legacy: None,
}
}
pub(crate) fn legacy_mut(&mut self, f: impl Fn(&mut LegacyBuilder)) {
let legacy = self.legacy.get_or_insert_with(|| LegacyBuilder::default());
f(legacy);
}
pub(crate) fn build(&self) -> Histogram {
let mut resolution = self.resolution;
assert!(resolution > 0);
if matches!(self.scale, HistogramScale::Log) {
resolution = resolution.next_power_of_two();
}
let histogram_type = match &self.legacy {
Some(legacy) => {
assert!(legacy.resolution > 0);
match legacy.scale {
HistogramScale::Linear => HistogramType::Linear(LinearHistogram {
num_buckets: legacy.num_buckets,
bucket_width: legacy.resolution,
}),
HistogramScale::Log => HistogramType::LogLegacy(LegacyLogHistogram {
num_buckets: legacy.num_buckets,
first_bucket_width: legacy.resolution.next_power_of_two(),
}),
}
}
None => self.histogram_type,
};
let num_buckets = self.histogram_type.num_buckets();
Histogram {
buckets: (0..self.num_buckets)
buckets: (0..num_buckets)
.map(|_| MetricAtomicU64::new(0))
.collect::<Vec<_>>()
.into_boxed_slice(),
resolution,
scale: self.scale,
histogram_type: histogram_type,
}
}
}
@ -178,12 +292,25 @@ mod test {
}};
}
fn linear(resolution: u64, num_buckets: usize) -> Histogram {
HistogramBuilder {
histogram_type: HistogramType::Linear(LinearHistogram {
bucket_width: resolution,
num_buckets,
}),
legacy: None,
}
.build()
}
#[test]
fn log_scale_resolution_1() {
let h = HistogramBuilder {
scale: HistogramScale::Log,
resolution: 1,
num_buckets: 10,
histogram_type: HistogramType::LogLegacy(LegacyLogHistogram {
first_bucket_width: 1,
num_buckets: 10,
}),
legacy: None,
}
.build();
@ -233,9 +360,11 @@ mod test {
#[test]
fn log_scale_resolution_2() {
let h = HistogramBuilder {
scale: HistogramScale::Log,
resolution: 2,
num_buckets: 10,
histogram_type: HistogramType::LogLegacy(LegacyLogHistogram {
num_buckets: 10,
first_bucket_width: 2,
}),
legacy: None,
}
.build();
@ -319,12 +448,7 @@ mod test {
#[test]
fn linear_scale_resolution_1() {
let h = HistogramBuilder {
scale: HistogramScale::Linear,
resolution: 1,
num_buckets: 10,
}
.build();
let h = linear(1, 10);
assert_eq!(h.bucket_range(0), 0..1);
assert_eq!(h.bucket_range(1), 1..2);
@ -380,12 +504,7 @@ mod test {
#[test]
fn linear_scale_resolution_100() {
let h = HistogramBuilder {
scale: HistogramScale::Linear,
resolution: 100,
num_buckets: 10,
}
.build();
let h = linear(100, 10);
assert_eq!(h.bucket_range(0), 0..100);
assert_eq!(h.bucket_range(1), 100..200);
@ -459,12 +578,7 @@ mod test {
#[test]
fn inc_by_more_than_one() {
let h = HistogramBuilder {
scale: HistogramScale::Linear,
resolution: 100,
num_buckets: 10,
}
.build();
let h = linear(100, 10);
let mut b = HistogramBatch::from_histogram(&h);

View File

@ -0,0 +1,480 @@
use crate::runtime::metrics::batch::duration_as_u64;
use std::cmp;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::time::Duration;
const DEFAULT_MIN_VALUE: Duration = Duration::from_nanos(100);
const DEFAULT_MAX_VALUE: Duration = Duration::from_secs(60);
/// Default precision is 2^-2 = 25% max error
const DEFAULT_PRECISION: u32 = 2;
/// Log Histogram
///
/// This implements an [H2 Histogram](https://iop.systems/blog/h2-histogram/), a histogram similar
/// to HdrHistogram, but with better performance. It guarantees an error bound of `2^-p`.
///
/// Unlike a traditional H2 histogram this has two small changes:
/// 1. The 0th bucket runs for `0..min_value`. This allows truncating a large number of buckets that
/// would cover extremely short timescales that customers usually don't care about.
/// 2. The final bucket runs all the way to `u64::MAX` — traditional H2 histograms would truncate
/// or reject these values.
///
/// For information about the default configuration, see [`LogHistogramBuilder`].
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct LogHistogram {
/// Number of buckets in the histogram
pub(crate) num_buckets: usize,
/// Precision of histogram. Error is bounded to 2^-p.
pub(crate) p: u32,
/// All buckets `idx < bucket_offset` are grouped into bucket 0.
///
/// This increases the smallest measurable value of the histogram.
pub(crate) bucket_offset: usize,
}
impl Default for LogHistogram {
fn default() -> Self {
LogHistogramBuilder::default().build()
}
}
impl LogHistogram {
/// Create a Histogram configuration directly from values for `n` and `p`.
///
/// # Panics
/// - If `bucket_offset` is greater than the specified number of buckets, `(n - p + 1) * 2^p`
fn from_n_p(n: u32, p: u32, bucket_offset: usize) -> Self {
assert!(n >= p, "{n} (n) must be at least as large as {p} (p)");
let num_buckets = ((n - p + 1) * 1 << p) as usize - bucket_offset;
Self {
num_buckets,
p,
bucket_offset,
}
}
/// Creates a builder for [`LogHistogram`]
pub fn builder() -> LogHistogramBuilder {
LogHistogramBuilder::default()
}
/// The maximum value that can be stored before truncation in this histogram
pub fn max_value(&self) -> u64 {
let n = (self.num_buckets / (1 << self.p)) - 1 + self.p as usize;
(1_u64 << n) - 1
}
pub(crate) fn value_to_bucket(&self, value: u64) -> usize {
let index = bucket_index(value, self.p);
let offset_bucket = if index < self.bucket_offset as u64 {
0
} else {
index - self.bucket_offset as u64
};
let max = self.num_buckets - 1;
offset_bucket.min(max as u64) as usize
}
pub(crate) fn bucket_range(&self, bucket: usize) -> std::ops::Range<u64> {
let LogHistogram {
p,
bucket_offset,
num_buckets,
} = self;
let input_bucket = bucket;
let bucket = bucket + bucket_offset;
let range_start_0th_bucket = match input_bucket {
0 => Some(0_u64),
_ => None,
};
let range_end_last_bucket = match input_bucket {
n if n == num_buckets - 1 => Some(u64::MAX),
_ => None,
};
if bucket < 1 << p {
// The first set of buckets are all size 1
let bucket = bucket as u64;
range_start_0th_bucket.unwrap_or(bucket)..range_end_last_bucket.unwrap_or(bucket + 1)
} else {
// Determine which range of buckets we're in, then determine which bucket in the range it is
let bucket = bucket as u64;
let p = *p as u64;
let w = (bucket >> p) - 1;
let base_bucket = (w + 1) * (1_u64 << p);
let offset = bucket - base_bucket;
let s = 1_u64 << (w + p);
let start = s + (offset << w);
let end = s + ((offset + 1) << w);
range_start_0th_bucket.unwrap_or(start)..range_end_last_bucket.unwrap_or(end)
}
}
}
/// Configuration for a [`LogHistogram`]
///
/// The log-scaled histogram implements an H2 histogram where the first bucket covers
/// the range from 0 to [`LogHistogramBuilder::min_value`] and the final bucket covers
/// [`LogHistogramBuilder::max_value`] to infinity. The precision is bounded to the specified
/// [`LogHistogramBuilder::max_error`]. Specifically, the precision is the next smallest value
/// of `2^-p` such that it is smaller than the requested max error. You can also select `p` directly
/// with [`LogHistogramBuilder::precision_exact`].
///
/// Depending on the selected parameters, the number of buckets required is variable. To ensure
/// that the histogram size is acceptable, callers may call [`LogHistogramBuilder::max_buckets`].
/// If the resulting histogram would require more buckets, then the method will return an error.
///
/// ## Default values
/// The default configuration provides the following settings:
/// 1. `min_value`: 100ns
/// 2. `max_value`: 68 seconds. The final bucket covers all values >68 seconds
/// 3. `precision`: max error of 25%
///
/// This uses 237 64-bit buckets.
#[derive(Default, Debug, Copy, Clone)]
pub struct LogHistogramBuilder {
max_value: Option<Duration>,
min_value: Option<Duration>,
precision: Option<u32>,
}
impl From<LogHistogramBuilder> for LogHistogram {
fn from(value: LogHistogramBuilder) -> Self {
value.build()
}
}
impl LogHistogramBuilder {
/// Set the precision for this histogram
///
/// This function determines the smallest value of `p` that would satisfy the requested precision
/// such that `2^-p` is less than `precision`. To set `p` directly, use
/// [`LogHistogramBuilder::precision_exact`].
///
/// The default value is 25% (2^-2)
///
/// The highest supported precision is `0.0977%` `(2^-10)`. Provided values
/// less than this will be truncated.
///
/// # Panics
/// - `precision` < 0
/// - `precision` > 1
pub fn max_error(mut self, max_error: f64) -> Self {
if max_error < 0.0 {
panic!("precision must be >= 0");
};
if max_error > 1.0 {
panic!("precision must be > 1");
};
let mut p = 2;
while 2_f64.powf(-1.0 * p as f64) > max_error && p <= 10 {
p += 1;
}
self.precision = Some(p);
self
}
/// Sets the precision of this histogram directly.
///
/// # Panics
/// - `p` < 2
/// - `p` > 10
pub fn precision_exact(mut self, p: u32) -> Self {
if p < 2 {
panic!("precision must be >= 2");
};
if p > 10 {
panic!("precision must be <= 10");
};
self.precision = Some(p);
self
}
/// Sets the minimum duration that can be accurately stored by this histogram.
///
/// This sets the resolution. The first bucket will be no larger than
/// the provided duration. Setting this value will reduce the number of required buckets,
/// sometimes quite significantly.
pub fn min_value(mut self, duration: Duration) -> Self {
self.min_value = Some(duration);
self
}
/// Sets the maximum value that can by this histogram without truncation
///
/// Values greater than this fall in the final bucket that stretches to `u64::MAX`.
///
/// # Panics
/// The provided value is 0
pub fn max_value(mut self, duration: Duration) -> Self {
if duration.is_zero() {
panic!("max value must be greater than 0");
}
self.max_value = Some(duration);
self
}
/// Builds the log histogram, enforcing the max buckets requirement
pub fn max_buckets(
&mut self,
max_buckets: usize,
) -> Result<LogHistogram, InvalidHistogramConfiguration> {
let histogram = self.build();
if histogram.num_buckets > max_buckets {
return Err(InvalidHistogramConfiguration::TooManyBuckets {
required_bucket_count: histogram.num_buckets,
});
}
Ok(histogram)
}
/// Builds the log histogram
pub fn build(&self) -> LogHistogram {
let max_value = duration_as_u64(self.max_value.unwrap_or(DEFAULT_MAX_VALUE));
let max_value = max_value.next_power_of_two();
let min_value = duration_as_u64(self.min_value.unwrap_or(DEFAULT_MIN_VALUE));
let p = self.precision.unwrap_or(DEFAULT_PRECISION);
// determine the bucket offset by finding the bucket for the minimum value. We need to lower
// this by one to ensure we are at least as granular as requested.
let bucket_offset = cmp::max(bucket_index(min_value, p), 1) - 1;
// n must be at least as large as p
let n = max_value.ilog2().max(p);
LogHistogram::from_n_p(n, p, bucket_offset as usize)
}
}
/// Error constructing a histogram
#[derive(Debug)]
pub enum InvalidHistogramConfiguration {
/// This histogram required more than the specified number of buckets
TooManyBuckets {
/// The number of buckets that would have been required
required_bucket_count: usize,
},
}
impl Display for InvalidHistogramConfiguration {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
InvalidHistogramConfiguration::TooManyBuckets { required_bucket_count } =>
write!(f, "The configuration for this histogram would have required {required_bucket_count} buckets")
}
}
}
impl Error for InvalidHistogramConfiguration {}
/// Compute the index for a given value + p combination
///
/// This function does NOT enforce that the value is within the number of expected buckets.
fn bucket_index(value: u64, p: u32) -> u64 {
// Algorithm described here: https://iop.systems/blog/h2-histogram/
// find the highest non-zero digit
if value == 0 {
return 0;
}
let h = 63 - value.leading_zeros();
if h <= p {
value
} else {
let w = h - p;
((w + 1) * (1_u32 << p)) as u64 + ((value - (1_u64 << h)) >> w)
}
}
#[cfg(test)]
mod test {
use super::InvalidHistogramConfiguration;
use crate::runtime::metrics::histogram::h2_histogram::LogHistogram;
use crate::runtime::metrics::histogram::HistogramType;
#[cfg(not(target_family = "wasm"))]
mod proptests {
use super::*;
use proptest::prelude::*;
fn valid_log_histogram_strategy() -> impl Strategy<Value = LogHistogram> {
(2..=50u32, 2..=16u32, 0..100usize).prop_map(|(n, p, bucket_offset)| {
let p = p.min(n);
let base = LogHistogram::from_n_p(n, p, 0);
LogHistogram::from_n_p(n, p, bucket_offset.min(base.num_buckets - 1))
})
}
// test against a wide assortment of different histogram configurations to ensure invariants hold
proptest! {
#[test]
fn proptest_log_histogram_invariants(histogram in valid_log_histogram_strategy()) {
// 1. Assert that the first bucket always starts at 0
let first_range = histogram.bucket_range(0);
prop_assert_eq!(first_range.start, 0, "First bucket doesn't start at 0");
// Check that bucket ranges are disjoint and contiguous
let mut prev_end = 0;
let mut prev_size = 0;
for bucket in 0..histogram.num_buckets {
let range = histogram.bucket_range(bucket);
prop_assert_eq!(range.start, prev_end, "Bucket ranges are not contiguous");
prop_assert!(range.start < range.end, "Bucket range is empty or reversed");
let size = range.end - range.start;
// 2. Assert that the sizes of the buckets are always powers of 2
if bucket > 0 && bucket < histogram.num_buckets - 1 {
prop_assert!(size.is_power_of_two(), "Bucket size is not a power of 2");
}
if bucket > 1 {
// Assert that the sizes of the buckets are monotonically increasing
// (after the first bucket, which may be smaller than the 0 bucket)
prop_assert!(size >= prev_size, "Bucket sizes are not monotonically increasing: This size {size} (previous: {prev_size}). Bucket: {bucket}");
}
// 4. Assert that the size of the buckets is always within the error bound of 2^-p
if bucket > 0 && bucket < histogram.num_buckets - 1 {
let p = histogram.p as f64;
let error_bound = 2.0_f64.powf(-p);
// the most it could be wrong is by the length of the range / 2
let relative_error = ((size as f64 - 1.0) / 2.0) / range.start as f64;
prop_assert!(
relative_error <= error_bound,
"Bucket size error exceeds bound: {:?} > {:?} ({range:?})",
relative_error,
error_bound
);
}
prev_end = range.end;
prev_size = size;
}
prop_assert_eq!(prev_end, u64::MAX, "Last bucket should end at u64::MAX");
// Check bijection between value_to_bucket and bucket_range
for bucket in 0..histogram.num_buckets {
let range = histogram.bucket_range(bucket);
for value in [range.start, range.end - 1] {
prop_assert_eq!(
histogram.value_to_bucket(value),
bucket,
"value_to_bucket is not consistent with bucket_range"
);
}
}
}
}
}
#[test]
fn bucket_ranges_are_correct() {
let p = 2;
let config = HistogramType::H2(LogHistogram {
num_buckets: 1024,
p,
bucket_offset: 0,
});
// check precise buckets. There are 2^(p+1) precise buckets
for i in 0..2_usize.pow(p + 1) {
assert_eq!(
config.value_to_bucket(i as u64),
i,
"{} should be in bucket {}",
i,
i
);
}
let mut value = 2_usize.pow(p + 1);
let current_bucket = value;
while value < current_bucket * 2 {
assert_eq!(
config.value_to_bucket(value as u64),
current_bucket + ((value - current_bucket) / 2),
"bucket for {value}"
);
value += 1;
}
}
// test buckets against known values
#[test]
fn bucket_computation_spot_check() {
let p = 9;
let config = HistogramType::H2(LogHistogram {
num_buckets: 4096,
p,
bucket_offset: 0,
});
struct T {
v: u64,
bucket: usize,
}
let tests = [
T { v: 1, bucket: 1 },
T {
v: 1023,
bucket: 1023,
},
T {
v: 1024,
bucket: 1024,
},
T {
v: 2048,
bucket: 1536,
},
T {
v: 2052,
bucket: 1537,
},
];
for test in tests {
assert_eq!(config.value_to_bucket(test.v), test.bucket);
}
}
#[test]
fn last_bucket_goes_to_infinity() {
let conf = HistogramType::H2(LogHistogram::from_n_p(16, 3, 10));
assert_eq!(conf.bucket_range(conf.num_buckets() - 1).end, u64::MAX);
}
#[test]
fn bucket_offset() {
// skip the first 10 buckets
let conf = HistogramType::H2(LogHistogram::from_n_p(16, 3, 10));
for i in 0..10 {
assert_eq!(conf.value_to_bucket(i), 0);
}
// There are 16 1-element buckets. We skipped 10 of them. The first 2 element bucket starts
// at 16
assert_eq!(conf.value_to_bucket(10), 0);
assert_eq!(conf.value_to_bucket(16), 6);
assert_eq!(conf.value_to_bucket(17), 6);
assert_eq!(conf.bucket_range(6), 16..18);
}
#[test]
fn max_buckets_enforcement() {
let error = LogHistogram::builder()
.max_error(0.001)
.max_buckets(5)
.expect_err("this produces way more than 5 buckets");
let num_buckets = match error {
InvalidHistogramConfiguration::TooManyBuckets {
required_bucket_count,
} => required_bucket_count,
};
assert_eq!(num_buckets, 27549);
}
#[test]
fn default_configuration_size() {
let conf = LogHistogram::builder().build();
assert_eq!(conf.num_buckets, 119);
}
}

View File

@ -18,7 +18,7 @@ cfg_unstable_metrics! {
mod histogram;
pub(crate) use histogram::{Histogram, HistogramBatch, HistogramBuilder};
#[allow(unreachable_pub)] // rust-lang/rust#57411
pub use histogram::HistogramScale;
pub use histogram::{HistogramScale, HistogramConfiguration, LogHistogram, LogHistogramBuilder, InvalidHistogramConfiguration};
mod scheduler;

View File

@ -767,7 +767,7 @@ impl RuntimeMetrics {
/// task poll times.
///
/// This value is configured by calling
/// [`metrics_poll_count_histogram_buckets()`] when building the runtime.
/// [`metrics_poll_count_histogram_configuration()`] when building the runtime.
///
/// # Examples
///
@ -788,8 +788,8 @@ impl RuntimeMetrics {
/// }
/// ```
///
/// [`metrics_poll_count_histogram_buckets()`]:
/// crate::runtime::Builder::metrics_poll_count_histogram_buckets
/// [`metrics_poll_count_histogram_configuration()`]:
/// crate::runtime::Builder::metrics_poll_count_histogram_configuration
pub fn poll_count_histogram_num_buckets(&self) -> usize {
self.handle
.inner
@ -803,7 +803,7 @@ impl RuntimeMetrics {
/// Returns the range of task poll times tracked by the given bucket.
///
/// This value is configured by calling
/// [`metrics_poll_count_histogram_resolution()`] when building the runtime.
/// [`metrics_poll_count_histogram_configuration()`] when building the runtime.
///
/// # Panics
///
@ -832,8 +832,8 @@ impl RuntimeMetrics {
/// }
/// ```
///
/// [`metrics_poll_count_histogram_resolution()`]:
/// crate::runtime::Builder::metrics_poll_count_histogram_resolution
/// [`metrics_poll_count_histogram_configuration()`]:
/// crate::runtime::Builder::metrics_poll_count_histogram_configuration
#[track_caller]
pub fn poll_count_histogram_bucket_range(&self, bucket: usize) -> Range<Duration> {
self.handle

View File

@ -410,7 +410,7 @@ cfg_rt! {
pub use metrics::RuntimeMetrics;
cfg_unstable_metrics! {
pub use metrics::HistogramScale;
pub use metrics::{HistogramScale, HistogramConfiguration, LogHistogram, LogHistogramBuilder, InvalidHistogramConfiguration} ;
cfg_net! {
pub(crate) use metrics::IoDriverMetrics;

View File

@ -13,7 +13,7 @@ use std::task::Poll;
use std::thread;
use tokio::macros::support::poll_fn;
use tokio::runtime::Runtime;
use tokio::runtime::{HistogramConfiguration, LogHistogram, Runtime};
use tokio::task::consume_budget;
use tokio::time::{self, Duration};
@ -390,6 +390,62 @@ fn worker_poll_count_and_time() {
}
}
#[test]
fn log_histogram() {
const N: u64 = 50;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.enable_metrics_poll_count_histogram()
.metrics_poll_count_histogram_configuration(HistogramConfiguration::log(
LogHistogram::builder()
.max_value(Duration::from_secs(60))
.min_value(Duration::from_nanos(100))
.max_error(0.25),
))
.build()
.unwrap();
let metrics = rt.metrics();
let num_buckets = rt.metrics().poll_count_histogram_num_buckets();
assert_eq!(num_buckets, 119);
rt.block_on(async {
for _ in 0..N {
tokio::spawn(async {}).await.unwrap();
}
});
drop(rt);
assert_eq!(
metrics.poll_count_histogram_bucket_range(0),
Duration::from_nanos(0)..Duration::from_nanos(96)
);
assert_eq!(
metrics.poll_count_histogram_bucket_range(1),
Duration::from_nanos(96)..Duration::from_nanos(96 + 2_u64.pow(4))
);
assert_eq!(
metrics.poll_count_histogram_bucket_range(118).end,
Duration::from_nanos(u64::MAX)
);
let n = (0..metrics.num_workers())
.flat_map(|i| (0..num_buckets).map(move |j| (i, j)))
.map(|(worker, bucket)| metrics.poll_count_histogram_bucket_count(worker, bucket))
.sum();
assert_eq!(N, n);
}
#[test]
fn log_histogram_default_configuration() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.enable_metrics_poll_count_histogram()
.metrics_poll_count_histogram_configuration(HistogramConfiguration::log(
LogHistogram::default(),
))
.build()
.unwrap();
let num_buckets = rt.metrics().poll_count_histogram_num_buckets();
assert_eq!(num_buckets, 119);
}
#[test]
fn worker_poll_count_histogram() {
const N: u64 = 5;
@ -398,18 +454,20 @@ fn worker_poll_count_histogram() {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.enable_metrics_poll_count_histogram()
.metrics_poll_count_histogram_scale(tokio::runtime::HistogramScale::Linear)
.metrics_poll_count_histogram_buckets(3)
.metrics_poll_count_histogram_resolution(Duration::from_millis(50))
.metrics_poll_count_histogram_configuration(HistogramConfiguration::linear(
Duration::from_millis(50),
3,
))
.build()
.unwrap(),
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.enable_metrics_poll_count_histogram()
.metrics_poll_count_histogram_scale(tokio::runtime::HistogramScale::Linear)
.metrics_poll_count_histogram_buckets(3)
.metrics_poll_count_histogram_resolution(Duration::from_millis(50))
.metrics_poll_count_histogram_configuration(HistogramConfiguration::linear(
Duration::from_millis(50),
3,
))
.build()
.unwrap(),
];
@ -444,9 +502,7 @@ fn worker_poll_count_histogram_range() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.enable_metrics_poll_count_histogram()
.metrics_poll_count_histogram_scale(tokio::runtime::HistogramScale::Linear)
.metrics_poll_count_histogram_buckets(3)
.metrics_poll_count_histogram_resolution(us(50))
.metrics_poll_count_histogram_configuration(HistogramConfiguration::linear(us(50), 3))
.build()
.unwrap();
let metrics = rt.metrics();
@ -458,6 +514,8 @@ fn worker_poll_count_histogram_range() {
);
assert_eq!(metrics.poll_count_histogram_bucket_range(2), us(100)..max);
// ensure the old methods work too
#[allow(deprecated)]
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.enable_metrics_poll_count_histogram()
@ -481,17 +539,19 @@ fn worker_poll_count_histogram_disabled_without_explicit_enable() {
let rts = [
tokio::runtime::Builder::new_current_thread()
.enable_all()
.metrics_poll_count_histogram_scale(tokio::runtime::HistogramScale::Linear)
.metrics_poll_count_histogram_buckets(3)
.metrics_poll_count_histogram_resolution(Duration::from_millis(50))
.metrics_poll_count_histogram_configuration(HistogramConfiguration::linear(
Duration::from_millis(50),
3,
))
.build()
.unwrap(),
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.metrics_poll_count_histogram_scale(tokio::runtime::HistogramScale::Linear)
.metrics_poll_count_histogram_buckets(3)
.metrics_poll_count_histogram_resolution(Duration::from_millis(50))
.metrics_poll_count_histogram_configuration(HistogramConfiguration::linear(
Duration::from_millis(50),
3,
))
.build()
.unwrap(),
];