@@ -9,6 +9,7 @@ const DEFAULT_MAX_VALUE: Duration = Duration::from_secs(60);
99
1010/// Default precision is 2^-2 = 25% max error
1111const 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]
0 commit comments