diff --git a/histogram/Cargo.toml b/histogram/Cargo.toml index 257d189..1ae3f86 100644 --- a/histogram/Cargo.toml +++ b/histogram/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "histogram" -version = "0.9.1" +version = "0.10.0" edition = "2021" authors = ["Brian Martin "] license = "MIT OR Apache-2.0" diff --git a/histogram/src/atomic.rs b/histogram/src/atomic.rs index c67784b..e182251 100644 --- a/histogram/src/atomic.rs +++ b/histogram/src/atomic.rs @@ -1,6 +1,5 @@ -use crate::{Config, Error, Histogram, Snapshot}; +use crate::{Config, Error, Histogram}; use core::sync::atomic::{AtomicU64, Ordering}; -use std::time::SystemTime; /// A histogram that uses atomic 64bit counters for each bucket. /// @@ -9,7 +8,6 @@ use std::time::SystemTime; /// the histogram at a point in time. pub struct AtomicHistogram { config: Config, - start: SystemTime, buckets: Box<[AtomicU64]>, } @@ -29,7 +27,6 @@ impl AtomicHistogram { Self { config: *config, - start: SystemTime::now(), buckets: buckets.into(), } } @@ -54,23 +51,18 @@ impl AtomicHistogram { Ok(()) } - /// Produce a snapshot from this histogram. - pub fn snapshot(&self) -> Snapshot { - let end = SystemTime::now(); - + /// Read the bucket values into a new `Histogram` + pub fn load(&self) -> Histogram { let buckets: Vec = self .buckets .iter() .map(|bucket| bucket.load(Ordering::Relaxed)) .collect(); - let histogram = Histogram { + Histogram { config: self.config, - start: self.start, buckets: buckets.into(), - }; - - Snapshot { end, histogram } + } } } @@ -80,11 +72,7 @@ mod test { #[test] fn size() { - #[cfg(not(target_os = "windows"))] - assert_eq!(std::mem::size_of::(), 64); - - #[cfg(target_os = "windows")] - assert_eq!(std::mem::size_of::(), 56); + assert_eq!(std::mem::size_of::(), 48); } #[test] @@ -94,56 +82,38 @@ mod test { for i in 0..=100 { let _ = histogram.increment(i); assert_eq!( - histogram.snapshot().percentile(0.0), + histogram.load().percentile(0.0), Ok(Bucket { count: 1, range: 0..=0, }) ); assert_eq!( - histogram.snapshot().percentile(100.0), + histogram.load().percentile(100.0), Ok(Bucket { count: 1, range: i..=i, }) ); } - assert_eq!( - histogram.snapshot().percentile(25.0).map(|b| b.end()), - Ok(25) - ); - assert_eq!( - histogram.snapshot().percentile(50.0).map(|b| b.end()), - Ok(50) - ); - assert_eq!( - histogram.snapshot().percentile(75.0).map(|b| b.end()), - Ok(75) - ); - assert_eq!( - histogram.snapshot().percentile(90.0).map(|b| b.end()), - Ok(90) - ); - assert_eq!( - histogram.snapshot().percentile(99.0).map(|b| b.end()), - Ok(99) - ); - assert_eq!( - histogram.snapshot().percentile(99.9).map(|b| b.end()), - Ok(100) - ); + assert_eq!(histogram.load().percentile(25.0).map(|b| b.end()), Ok(25)); + assert_eq!(histogram.load().percentile(50.0).map(|b| b.end()), Ok(50)); + assert_eq!(histogram.load().percentile(75.0).map(|b| b.end()), Ok(75)); + assert_eq!(histogram.load().percentile(90.0).map(|b| b.end()), Ok(90)); + assert_eq!(histogram.load().percentile(99.0).map(|b| b.end()), Ok(99)); + assert_eq!(histogram.load().percentile(99.9).map(|b| b.end()), Ok(100)); assert_eq!( - histogram.snapshot().percentile(-1.0), + histogram.load().percentile(-1.0), Err(Error::InvalidPercentile) ); assert_eq!( - histogram.snapshot().percentile(101.0), + histogram.load().percentile(101.0), Err(Error::InvalidPercentile) ); let percentiles: Vec<(f64, u64)> = histogram - .snapshot() + .load() .percentiles(&[50.0, 90.0, 99.0, 99.9]) .unwrap() .iter() @@ -157,7 +127,7 @@ mod test { let _ = histogram.increment(1024); assert_eq!( - histogram.snapshot().percentile(99.9), + histogram.load().percentile(99.9), Ok(Bucket { count: 1, range: 1024..=1031, diff --git a/histogram/src/config.rs b/histogram/src/config.rs index a52b45e..9270990 100644 --- a/histogram/src/config.rs +++ b/histogram/src/config.rs @@ -55,7 +55,7 @@ use serde::{Deserialize, Serialize}; /// # Constraints: /// * `max_value_power` must be in the range `0..=64` /// * `max_value_power` must be greater than `grouping_power -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Config { diff --git a/histogram/src/lib.rs b/histogram/src/lib.rs index e6fca63..c53aa3e 100644 --- a/histogram/src/lib.rs +++ b/histogram/src/lib.rs @@ -15,7 +15,6 @@ mod atomic; mod bucket; mod config; mod errors; -mod snapshot; mod sparse; mod standard; @@ -23,6 +22,5 @@ pub use atomic::AtomicHistogram; pub use bucket::Bucket; pub use config::Config; pub use errors::Error; -pub use snapshot::Snapshot; pub use sparse::SparseHistogram; pub use standard::Histogram; diff --git a/histogram/src/snapshot.rs b/histogram/src/snapshot.rs deleted file mode 100644 index 3c88ba4..0000000 --- a/histogram/src/snapshot.rs +++ /dev/null @@ -1,189 +0,0 @@ -use crate::{Bucket, Config, Error, Histogram}; -use std::time::SystemTime; - -/// A snapshot of a histogram across a time range. -#[derive(Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Snapshot { - // note: `Histogram` contains the start time - pub(crate) end: SystemTime, - pub(crate) histogram: Histogram, -} - -impl Snapshot { - /// Return the time range of the snapshot. - pub fn range(&self) -> core::ops::Range { - self.histogram.start..self.end - } - - /// Return a collection of percentiles from this snapshot. - /// - /// Each percentile should be in the inclusive range `0.0..=100.0`. For - /// example, the 50th percentile (median) can be found using `50.0`. - /// - /// The results will be sorted by the percentile. - pub fn percentiles(&self, percentiles: &[f64]) -> Result, Error> { - self.histogram.percentiles(percentiles) - } - - /// Return a single percentile from this snapshot. - /// - /// The percentile should be in the inclusive range `0.0..=100.0`. For - /// example, the 50th percentile (median) can be found using `50.0`. - pub fn percentile(&self, percentile: f64) -> Result { - self.histogram.percentile(percentile) - } - - /// Returns a new downsampled histogram with a reduced grouping power. - /// - /// The new histogram is smaller but with greater relative error. The - /// reduction factor should be smaller than the histogram's existing - /// grouping power. - pub fn downsample(&self, factor: u8) -> Result { - let histogram = self.histogram.downsample(factor)?; - - Ok(Self { - end: self.end, - histogram, - }) - } - - /// Merges two snapshots which cover the same time range. - /// - /// An error is raised on overflow. - pub fn checked_merge(&self, rhs: &Self) -> Result { - if self.range() != rhs.range() { - return Err(Error::IncompatibleTimeRange); - } - - let histogram = self.histogram.checked_add(&rhs.histogram)?; - - Ok(Self { - end: rhs.end, - histogram, - }) - } - - /// Appends the provided snapshot onto this snapshot, extending the covered - /// time range and combining the bucket counts. - /// - /// An error is raised on overflow. - pub fn checked_add(&self, rhs: &Self) -> Result { - if self.end != rhs.histogram.start { - return Err(Error::IncompatibleTimeRange); - } - - let histogram = self.histogram.checked_add(&rhs.histogram)?; - - Ok(Self { - end: rhs.end, - histogram, - }) - } - - /// Appends the provided snapshot onto this snapshot, extending the covered - /// time range and combining the bucket counts. - /// - /// Bucket counters will wrap on overflow. - pub fn wrapping_add(&self, rhs: &Self) -> Result { - if self.end != rhs.histogram.start { - return Err(Error::IncompatibleTimeRange); - } - - let histogram = self.histogram.wrapping_add(&rhs.histogram)?; - - Ok(Self { - end: rhs.end, - histogram, - }) - } - - /// Appends the provided snapshot onto this snapshot, shrinking the covered - /// time range and producing a delta of the bucket counts. - /// - /// An error is raised on overflow. - pub fn checked_sub(&self, rhs: &Self) -> Result { - if self.histogram.start < rhs.histogram.start { - return Err(Error::IncompatibleTimeRange); - } - - if self.end < rhs.end { - return Err(Error::IncompatibleTimeRange); - } - - let mut histogram = self.histogram.checked_sub(&rhs.histogram)?; - - histogram.start = rhs.end; - - Ok(Self { - end: self.end, - histogram, - }) - } - - /// Appends the provided snapshot onto this snapshot, extending the covered - /// time range and combining the bucket counts. - /// - /// Bucket counters will wrap on overflow. - pub fn wrapping_sub(&self, rhs: &Self) -> Result { - if self.histogram.start != rhs.histogram.start { - return Err(Error::IncompatibleTimeRange); - } - - if self.end < rhs.end { - return Err(Error::IncompatibleTimeRange); - } - - let mut histogram = self.histogram.wrapping_sub(&rhs.histogram)?; - - histogram.start = rhs.end; - - Ok(Self { - end: self.end, - histogram, - }) - } - - /// Returns the bucket configuration of the snapshot. - pub fn config(&self) -> Config { - self.histogram.config() - } -} - -impl<'a> IntoIterator for &'a Snapshot { - type Item = Bucket; - type IntoIter = Iter<'a>; - - fn into_iter(self) -> Self::IntoIter { - Iter { - iter: self.histogram.into_iter(), - } - } -} - -/// An iterator across the histogram buckets. -pub struct Iter<'a> { - iter: crate::standard::Iter<'a>, -} - -impl<'a> Iterator for Iter<'a> { - type Item = Bucket; - - fn next(&mut self) -> Option<::Item> { - self.iter.next() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn size() { - #[cfg(not(target_os = "windows"))] - assert_eq!(std::mem::size_of::(), 80); - - #[cfg(target_os = "windows")] - assert_eq!(std::mem::size_of::(), 64); - } -} diff --git a/histogram/src/sparse.rs b/histogram/src/sparse.rs index fe64b6a..0646bda 100644 --- a/histogram/src/sparse.rs +++ b/histogram/src/sparse.rs @@ -1,4 +1,4 @@ -use crate::{Bucket, Config, Error, Histogram, Snapshot}; +use crate::{Bucket, Config, Error, Histogram}; /// This histogram is a sparse, columnar representation of the regular /// Histogram. It is significantly smaller than a regular Histogram @@ -171,6 +171,46 @@ impl SparseHistogram { } } +impl<'a> IntoIterator for &'a SparseHistogram { + type Item = Bucket; + type IntoIter = Iter<'a>; + + fn into_iter(self) -> Self::IntoIter { + Iter { + index: 0, + histogram: self, + } + } +} + +/// An iterator across the histogram buckets. +pub struct Iter<'a> { + index: usize, + histogram: &'a SparseHistogram, +} + +impl<'a> Iterator for Iter<'a> { + type Item = Bucket; + + fn next(&mut self) -> Option<::Item> { + if self.index >= self.histogram.index.len() { + return None; + } + + let bucket = Bucket { + count: self.histogram.count[self.index], + range: self + .histogram + .config + .index_to_range(self.histogram.index[self.index]), + }; + + self.index += 1; + + Some(bucket) + } +} + impl From<&Histogram> for SparseHistogram { fn from(histogram: &Histogram) -> Self { let mut index = Vec::new(); @@ -191,12 +231,6 @@ impl From<&Histogram> for SparseHistogram { } } -impl From<&Snapshot> for SparseHistogram { - fn from(snapshot: &Snapshot) -> Self { - SparseHistogram::from(&snapshot.histogram) - } -} - #[cfg(test)] mod tests { use rand::Rng; @@ -282,7 +316,7 @@ mod tests { } // Convert to sparse and store buckets in a hash for random lookup - let hsparse = SparseHistogram::from(&hstandard.snapshot()); + let hsparse = SparseHistogram::from(&hstandard); compare_histograms(&hstandard, &hsparse); } diff --git a/histogram/src/standard.rs b/histogram/src/standard.rs index cdf5f36..19c7a8e 100644 --- a/histogram/src/standard.rs +++ b/histogram/src/standard.rs @@ -1,12 +1,10 @@ -use crate::{Bucket, Config, Error, Snapshot}; -use std::time::SystemTime; +use crate::{Bucket, Config, Error, SparseHistogram}; /// A histogram that uses plain 64bit counters for each bucket. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Histogram { pub(crate) config: Config, - pub(crate) start: SystemTime, pub(crate) buckets: Box<[u64]>, } @@ -25,11 +23,29 @@ impl Histogram { Self { config: *config, - start: SystemTime::now(), buckets, } } + /// Creates a new histogram using a provided [`crate::Config`] and the + /// provided collection of buckets. + pub fn from_buckets( + grouping_power: u8, + max_value_power: u8, + buckets: Vec, + ) -> Result { + let config = Config::new(grouping_power, max_value_power)?; + + if config.total_buckets() != buckets.len() { + return Err(Error::IncompatibleParameters); + } + + Ok(Self { + config, + buckets: buckets.into(), + }) + } + /// Increment the counter for the bucket corresponding to the provided value /// by one. pub fn increment(&mut self, value: u64) -> Result<(), Error> { @@ -121,16 +137,6 @@ impl Histogram { .map(|v| v.first().unwrap().1.clone()) } - /// Produce a snapshot from this histogram. - pub fn snapshot(&self) -> Snapshot { - let end = SystemTime::now(); - - Snapshot { - end, - histogram: self.clone(), - } - } - /// Returns a new histogram with a reduced grouping power. The reduced /// grouping power should lie in the range (0..existing grouping power). /// @@ -203,7 +209,7 @@ impl Histogram { /// /// An error is returned if the two histograms have incompatible parameters /// or if there is an overflow. - pub(crate) fn checked_sub(&self, other: &Histogram) -> Result { + pub fn checked_sub(&self, other: &Histogram) -> Result { if self.config != other.config { return Err(Error::IncompatibleParameters); } @@ -221,7 +227,7 @@ impl Histogram { /// as a new histogram. /// /// An error is returned if the two histograms have incompatible parameters. - pub(crate) fn wrapping_sub(&self, other: &Histogram) -> Result { + pub fn wrapping_sub(&self, other: &Histogram) -> Result { if self.config != other.config { return Err(Error::IncompatibleParameters); } @@ -278,6 +284,18 @@ impl<'a> Iterator for Iter<'a> { } } +impl From<&SparseHistogram> for Histogram { + fn from(other: &SparseHistogram) -> Self { + let mut histogram = Histogram::with_config(&other.config); + + for (index, count) in other.index.iter().zip(other.count.iter()) { + histogram.buckets[*index] = *count; + } + + histogram + } +} + #[cfg(test)] mod tests { use super::*; @@ -285,11 +303,7 @@ mod tests { #[test] fn size() { - #[cfg(not(target_os = "windows"))] - assert_eq!(std::mem::size_of::(), 64); - - #[cfg(target_os = "windows")] - assert_eq!(std::mem::size_of::(), 56); + assert_eq!(std::mem::size_of::(), 48); } #[test] @@ -473,4 +487,18 @@ mod tests { let r = h.wrapping_sub(&h_overflow).unwrap(); assert_eq!(r.as_slice(), &[2, 2, 2, 2, 2, 2]); } + + #[test] + // Test creating the histogram from buckets + fn from_buckets() { + let mut histogram = Histogram::new(8, 32).unwrap(); + for i in 0..=100 { + let _ = histogram.increment(i); + } + + let buckets = histogram.as_slice(); + let constructed = Histogram::from_buckets(8, 32, buckets.to_vec()).unwrap(); + + assert!(constructed == histogram); + } }