blob: 6581ef10afc2408bfdbeca06e3ab9b2cb0901eac [file] [log] [blame]
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]>,
/// The type of the histogram
///
/// This handles `fn(bucket) -> Range` and `fn(value) -> bucket`
histogram_type: HistogramType,
}
#[derive(Debug, Clone)]
pub(crate) struct HistogramBuilder {
pub(crate) histogram_type: HistogramType,
pub(crate) legacy: Option<LegacyBuilder>,
}
#[derive(Debug, Clone)]
pub(crate) struct LegacyBuilder {
pub(crate) resolution: u64,
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]>,
configuration: HistogramType,
}
cfg_unstable! {
/// Whether the histogram used to aggregate a metric uses a linear or
/// logarithmic scale.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum HistogramScale {
/// Linear bucket scale
Linear,
/// 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 {
pub(crate) fn num_buckets(&self) -> usize {
self.buckets.len()
}
cfg_64bit_metrics! {
pub(crate) fn get(&self, bucket: usize) -> u64 {
self.buckets[bucket].load(Relaxed)
}
}
pub(crate) fn bucket_range(&self, bucket: usize) -> Range<u64> {
self.histogram_type.bucket_range(bucket)
}
}
impl HistogramBatch {
pub(crate) fn from_histogram(histogram: &Histogram) -> HistogramBatch {
let buckets = vec![0; histogram.buckets.len()].into_boxed_slice();
HistogramBatch {
buckets,
configuration: histogram.histogram_type,
}
}
pub(crate) fn measure(&mut self, value: u64, count: u64) {
self.buckets[self.value_to_bucket(value)] += count;
}
pub(crate) fn submit(&self, histogram: &Histogram) {
debug_assert_eq!(self.configuration, histogram.histogram_type);
debug_assert_eq!(self.buckets.len(), histogram.buckets.len());
for i in 0..self.buckets.len() {
histogram.buckets[i].store(self.buckets[i], Relaxed);
}
}
fn value_to_bucket(&self, value: u64) -> usize {
self.configuration.value_to_bucket(value)
}
}
impl HistogramBuilder {
pub(crate) fn new() -> HistogramBuilder {
HistogramBuilder {
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 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 = histogram_type.num_buckets();
Histogram {
buckets: (0..num_buckets)
.map(|_| MetricAtomicU64::new(0))
.collect::<Vec<_>>()
.into_boxed_slice(),
histogram_type,
}
}
}
impl Default for HistogramBuilder {
fn default() -> HistogramBuilder {
HistogramBuilder::new()
}
}
#[cfg(all(test, target_has_atomic = "64"))]
mod test {
use super::*;
macro_rules! assert_bucket_eq {
($h:expr, $bucket:expr, $val:expr) => {{
assert_eq!($h.buckets[$bucket], $val);
}};
}
fn linear(resolution: u64, num_buckets: usize) -> Histogram {
HistogramBuilder {
histogram_type: HistogramType::Linear(LinearHistogram {
bucket_width: resolution,
num_buckets,
}),
legacy: None,
}
.build()
}
#[test]
fn test_legacy_builder() {
let mut builder = HistogramBuilder::new();
builder.legacy_mut(|b| b.num_buckets = 20);
assert_eq!(builder.build().num_buckets(), 20);
}
#[test]
fn log_scale_resolution_1() {
let h = HistogramBuilder {
histogram_type: HistogramType::LogLegacy(LegacyLogHistogram {
first_bucket_width: 1,
num_buckets: 10,
}),
legacy: None,
}
.build();
assert_eq!(h.bucket_range(0), 0..1);
assert_eq!(h.bucket_range(1), 1..2);
assert_eq!(h.bucket_range(2), 2..4);
assert_eq!(h.bucket_range(3), 4..8);
assert_eq!(h.bucket_range(9), 256..u64::MAX);
let mut b = HistogramBatch::from_histogram(&h);
b.measure(0, 1);
assert_bucket_eq!(b, 0, 1);
assert_bucket_eq!(b, 1, 0);
b.measure(1, 1);
assert_bucket_eq!(b, 0, 1);
assert_bucket_eq!(b, 1, 1);
assert_bucket_eq!(b, 2, 0);
b.measure(2, 1);
assert_bucket_eq!(b, 0, 1);
assert_bucket_eq!(b, 1, 1);
assert_bucket_eq!(b, 2, 1);
b.measure(3, 1);
assert_bucket_eq!(b, 0, 1);
assert_bucket_eq!(b, 1, 1);
assert_bucket_eq!(b, 2, 2);
b.measure(4, 1);
assert_bucket_eq!(b, 0, 1);
assert_bucket_eq!(b, 1, 1);
assert_bucket_eq!(b, 2, 2);
assert_bucket_eq!(b, 3, 1);
b.measure(100, 1);
assert_bucket_eq!(b, 7, 1);
b.measure(128, 1);
assert_bucket_eq!(b, 8, 1);
b.measure(4096, 1);
assert_bucket_eq!(b, 9, 1);
b.measure(u64::MAX, 1);
assert_bucket_eq!(b, 9, 2);
}
#[test]
fn log_scale_resolution_2() {
let h = HistogramBuilder {
histogram_type: HistogramType::LogLegacy(LegacyLogHistogram {
num_buckets: 10,
first_bucket_width: 2,
}),
legacy: None,
}
.build();
assert_eq!(h.bucket_range(0), 0..2);
assert_eq!(h.bucket_range(1), 2..4);
assert_eq!(h.bucket_range(2), 4..8);
assert_eq!(h.bucket_range(3), 8..16);
assert_eq!(h.bucket_range(9), 512..u64::MAX);
let mut b = HistogramBatch::from_histogram(&h);
b.measure(0, 1);
assert_bucket_eq!(b, 0, 1);
assert_bucket_eq!(b, 1, 0);
b.measure(1, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 0);
b.measure(2, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 1);
assert_bucket_eq!(b, 2, 0);
b.measure(3, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 2);
assert_bucket_eq!(b, 2, 0);
b.measure(4, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 2);
assert_bucket_eq!(b, 2, 1);
b.measure(5, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 2);
assert_bucket_eq!(b, 2, 2);
b.measure(6, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 2);
assert_bucket_eq!(b, 2, 3);
b.measure(7, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 2);
assert_bucket_eq!(b, 2, 4);
b.measure(8, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 2);
assert_bucket_eq!(b, 2, 4);
assert_bucket_eq!(b, 3, 1);
b.measure(100, 1);
assert_bucket_eq!(b, 6, 1);
b.measure(128, 1);
assert_bucket_eq!(b, 7, 1);
b.measure(4096, 1);
assert_bucket_eq!(b, 9, 1);
for bucket in h.buckets.iter() {
assert_eq!(bucket.load(Relaxed), 0);
}
b.submit(&h);
for i in 0..h.buckets.len() {
assert_eq!(h.buckets[i].load(Relaxed), b.buckets[i]);
}
b.submit(&h);
for i in 0..h.buckets.len() {
assert_eq!(h.buckets[i].load(Relaxed), b.buckets[i]);
}
}
#[test]
fn linear_scale_resolution_1() {
let h = linear(1, 10);
assert_eq!(h.bucket_range(0), 0..1);
assert_eq!(h.bucket_range(1), 1..2);
assert_eq!(h.bucket_range(2), 2..3);
assert_eq!(h.bucket_range(3), 3..4);
assert_eq!(h.bucket_range(9), 9..u64::MAX);
let mut b = HistogramBatch::from_histogram(&h);
b.measure(0, 1);
assert_bucket_eq!(b, 0, 1);
assert_bucket_eq!(b, 1, 0);
b.measure(1, 1);
assert_bucket_eq!(b, 0, 1);
assert_bucket_eq!(b, 1, 1);
assert_bucket_eq!(b, 2, 0);
b.measure(2, 1);
assert_bucket_eq!(b, 0, 1);
assert_bucket_eq!(b, 1, 1);
assert_bucket_eq!(b, 2, 1);
assert_bucket_eq!(b, 3, 0);
b.measure(3, 1);
assert_bucket_eq!(b, 0, 1);
assert_bucket_eq!(b, 1, 1);
assert_bucket_eq!(b, 2, 1);
assert_bucket_eq!(b, 3, 1);
b.measure(5, 1);
assert_bucket_eq!(b, 5, 1);
b.measure(4096, 1);
assert_bucket_eq!(b, 9, 1);
for bucket in h.buckets.iter() {
assert_eq!(bucket.load(Relaxed), 0);
}
b.submit(&h);
for i in 0..h.buckets.len() {
assert_eq!(h.buckets[i].load(Relaxed), b.buckets[i]);
}
b.submit(&h);
for i in 0..h.buckets.len() {
assert_eq!(h.buckets[i].load(Relaxed), b.buckets[i]);
}
}
#[test]
fn linear_scale_resolution_100() {
let h = linear(100, 10);
assert_eq!(h.bucket_range(0), 0..100);
assert_eq!(h.bucket_range(1), 100..200);
assert_eq!(h.bucket_range(2), 200..300);
assert_eq!(h.bucket_range(3), 300..400);
assert_eq!(h.bucket_range(9), 900..u64::MAX);
let mut b = HistogramBatch::from_histogram(&h);
b.measure(0, 1);
assert_bucket_eq!(b, 0, 1);
assert_bucket_eq!(b, 1, 0);
b.measure(50, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 0);
b.measure(100, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 1);
assert_bucket_eq!(b, 2, 0);
b.measure(101, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 2);
assert_bucket_eq!(b, 2, 0);
b.measure(200, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 2);
assert_bucket_eq!(b, 2, 1);
b.measure(299, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 2);
assert_bucket_eq!(b, 2, 2);
b.measure(222, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 2);
assert_bucket_eq!(b, 2, 3);
b.measure(300, 1);
assert_bucket_eq!(b, 0, 2);
assert_bucket_eq!(b, 1, 2);
assert_bucket_eq!(b, 2, 3);
assert_bucket_eq!(b, 3, 1);
b.measure(888, 1);
assert_bucket_eq!(b, 8, 1);
b.measure(4096, 1);
assert_bucket_eq!(b, 9, 1);
for bucket in h.buckets.iter() {
assert_eq!(bucket.load(Relaxed), 0);
}
b.submit(&h);
for i in 0..h.buckets.len() {
assert_eq!(h.buckets[i].load(Relaxed), b.buckets[i]);
}
b.submit(&h);
for i in 0..h.buckets.len() {
assert_eq!(h.buckets[i].load(Relaxed), b.buckets[i]);
}
}
#[test]
fn inc_by_more_than_one() {
let h = linear(100, 10);
let mut b = HistogramBatch::from_histogram(&h);
b.measure(0, 3);
assert_bucket_eq!(b, 0, 3);
assert_bucket_eq!(b, 1, 0);
b.measure(50, 5);
assert_bucket_eq!(b, 0, 8);
assert_bucket_eq!(b, 1, 0);
b.measure(100, 2);
assert_bucket_eq!(b, 0, 8);
assert_bucket_eq!(b, 1, 2);
assert_bucket_eq!(b, 2, 0);
b.measure(101, 19);
assert_bucket_eq!(b, 0, 8);
assert_bucket_eq!(b, 1, 21);
assert_bucket_eq!(b, 2, 0);
for bucket in h.buckets.iter() {
assert_eq!(bucket.load(Relaxed), 0);
}
b.submit(&h);
for i in 0..h.buckets.len() {
assert_eq!(h.buckets[i].load(Relaxed), b.buckets[i]);
}
b.submit(&h);
for i in 0..h.buckets.len() {
assert_eq!(h.buckets[i].load(Relaxed), b.buckets[i]);
}
}
}