diff --git a/README.md b/README.md index f777908..3fdea9b 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Low-cost generation of performance percentiles (p50, p90, p99, p99.9, etc.). ### Performance Claims -* **Zero Allocation**: `Histogram` does not allocate memory on the heap during creation, event insertion, or percentile queries. It is a compact (~512-byte) structure that can reside entirely on the stack or be embedded in other structures. +* **Zero Allocation**: `Histogram` does not allocate memory on the heap during creation, event insertion, or percentile queries. It is a compact (~576-byte) structure that can reside entirely on the stack or be embedded in other structures. * **Ultra-Low Latency Insertion**: Recording a latency measurement (`push_event_time_ns`) takes approximately **11 nanoseconds** (about 35 CPU cycles on modern hardware). * **Blazing-Fast Queries**: Querying percentiles (such as `value_at_p99()`) takes only **11 to 17 nanoseconds**, depending on the distribution of events across the buckets. * **Instruction-Cache Friendly**: The query methods are designed with a "thin caller / heavy worker" pattern to prevent instruction-cache bloat and maintain high CPU cache locality under real-world workloads. @@ -133,7 +133,7 @@ use p99::Histogram; use std::time::Duration; fn main() { - // 1. Initialize a default histogram (zero-allocated, ~512 bytes on the stack) + // 1. Initialize a default histogram (zero-allocated, ~576 bytes on the stack) let mut histogram = Histogram::default(); // 2. Record event times using various units diff --git a/TODO.md b/TODO.md index 5b7f408..2b4e18a 100644 --- a/TODO.md +++ b/TODO.md @@ -8,6 +8,6 @@ ## TODOs -- [ ] `Debug` form; +- [x] ~~~`Debug` form~~~; - [ ] binary scaling; diff --git a/src/lib.rs b/src/lib.rs index 64844e7..8b38639 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,10 @@ // src/lib.rs : `Histogram` -use std::time as std_time; +#[rustfmt::skip] +use std::{ + fmt as std_fmt, + time as std_time, +}; /// Low-cost performance percentile histogram using 64-buckets. /// @@ -9,7 +13,6 @@ use std::time as std_time; /// 64 logarithmic power-of-two spacing buckets. This is extremely efficient /// and suited for high-frequency low-overhead timing measurements. #[derive(Clone)] -#[derive(Debug)] pub struct Histogram { event_count: usize, @@ -399,6 +402,65 @@ impl Histogram { // Trait implementations +impl std_fmt::Debug for Histogram { + fn fmt( + &self, + f: &mut std_fmt::Formatter<'_>, + ) -> std_fmt::Result { + struct BucketsDebug<'a>(&'a [u64; 64], bool); + + impl std_fmt::Debug for BucketsDebug<'_> { + fn fmt( + &self, + f: &mut std_fmt::Formatter<'_>, + ) -> std_fmt::Result { + struct PowerOfTwoKey(usize); + + impl std_fmt::Debug for PowerOfTwoKey { + fn fmt( + &self, + f: &mut std_fmt::Formatter<'_>, + ) -> std_fmt::Result { + write!(f, "\"2^{}\"", self.0) + } + } + + let mut m = f.debug_map(); + for (i, &count) in self.0.iter().enumerate() { + if count > 0 { + if self.1 { + m.entry(&PowerOfTwoKey(i), &count); + } else { + m.entry(&i, &count); + } + } + } + m.finish() + } + } + + if f.alternate() { + f.debug_struct("Histogram") + .field("event_count", &self.event_count) + .field("event_time_total", &self.event_time_total()) + .field("has_overflowed", &self.has_overflowed) + .field("min_event_time", &self.min_event_time) + .field("max_event_time", &self.max_event_time) + .field("buckets", &BucketsDebug(&self.buckets, true)) + .finish() + } else { + f.debug_struct("Histogram") + .field("n", &self.event_count) + .field("∑", &self.event_time_total()) + .field("∞", &self.has_overflowed) + .field("↓", &self.min_event_time) + .field("↑", &self.max_event_time) + .field("b", &BucketsDebug(&self.buckets, false)) + .finish() + } + } +} + impl Default for Histogram { fn default() -> Self { Self { @@ -589,6 +651,7 @@ mod tests { use super::Histogram; + #[rustfmt::skip] use test_helpers::{ assert_scalar_eq_approx, multiplier, @@ -596,6 +659,80 @@ mod tests { use std::time as std_time; + #[test] + fn TEST_Histogram_Debug() { + + // empty + { + let h = Histogram::default(); + + let expected = "Histogram { n: 0, ∑: Some(0), ∞: false, ↓: None, ↑: None, b: {} }"; + assert_eq!(expected, format!("{:?}", h)); + } + + // populated + { + let mut h = Histogram::default(); + + let expected = "Histogram { n: 3, ∑: Some(500), ∞: false, ↓: Some(100), ↑: Some(200), b: {6: 1, 7: 2} }"; + + assert!(h.push_event_time_ns(100)); // bucket 6 + assert!(h.push_event_time_ns(200)); // bucket 7 + assert!(h.push_event_time_ns(200)); // bucket 7 + + assert_eq!(expected, format!("{:?}", h)); + } + } + + #[test] + fn TEST_Histogram_Debug_alternate() { + + // empty + { + let h = Histogram::default(); + + let expected = r#"Histogram { + event_count: 0, + event_time_total: Some( + 0, + ), + has_overflowed: false, + min_event_time: None, + max_event_time: None, + buckets: {}, +}"#; + assert_eq!(expected, format!("{:#?}", h)); + } + + // populated + { + let mut h = Histogram::default(); + + assert!(h.push_event_time_ns(100)); // bucket 6 + assert!(h.push_event_time_ns(10_000)); // bucket 13 + assert!(h.push_event_time_ns(10_001)); // bucket 13 + + let expected = r#"Histogram { + event_count: 3, + event_time_total: Some( + 20101, + ), + has_overflowed: false, + min_event_time: Some( + 100, + ), + max_event_time: Some( + 10001, + ), + buckets: { + "2^6": 1, + "2^13": 2, + }, +}"#; + assert_eq!(expected, format!("{:#?}", h)); + } + } + #[test] fn TEST_Histogram_Default() { let h = Histogram::default();