@@ -9,6 +9,7 @@ const DEFAULT_MAX_VALUE: Duration = Duration::from_secs(60);
9
9
10
10
/// Default precision is 2^-2 = 25% max error
11
11
const DEFAULT_PRECISION : u32 = 2 ;
12
+ const MAX_PRECISION : u32 = 10 ;
12
13
13
14
/// Log Histogram
14
15
///
@@ -57,15 +58,23 @@ impl LogHistogram {
57
58
}
58
59
}
59
60
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
+
60
70
/// Creates a builder for [`LogHistogram`]
61
71
pub fn builder ( ) -> LogHistogramBuilder {
62
72
LogHistogramBuilder :: default ( )
63
73
}
64
74
65
75
/// The maximum value that can be stored before truncation in this histogram
66
76
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
69
78
}
70
79
71
80
pub ( crate ) fn value_to_bucket ( & self , value : u64 ) -> usize {
@@ -155,23 +164,23 @@ impl LogHistogramBuilder {
155
164
/// such that `2^-p` is less than `precision`. To set `p` directly, use
156
165
/// [`LogHistogramBuilder::precision_exact`].
157
166
///
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
+ ///
158
171
/// The default value is 25% (2^-2)
159
172
///
160
173
/// The highest supported precision is `0.0977%` `(2^-10)`. Provided values
161
174
/// less than this will be truncated.
162
175
///
163
176
/// # Panics
164
- /// - `precision ` < 0
165
- /// - `precision ` > 1
177
+ /// - `max_error ` < 0
178
+ /// - `max_error ` > 1
166
179
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" ) ;
173
182
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 {
175
184
p += 1 ;
176
185
}
177
186
self . precision = Some ( p) ;
@@ -180,16 +189,20 @@ impl LogHistogramBuilder {
180
189
181
190
/// Sets the precision of this histogram directly.
182
191
///
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
+ ///
183
200
/// # Panics
184
- /// - `p` < 2
185
201
/// - `p` > 10
202
+ ///
203
+ /// [`HistogramScale::Log`]: [crate::runtime::HistogramScale]
186
204
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}" ) ;
193
206
self . precision = Some ( p) ;
194
207
self
195
208
}
@@ -234,16 +247,17 @@ impl LogHistogramBuilder {
234
247
235
248
/// Builds the log histogram
236
249
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 ( ) ;
239
252
let min_value = duration_as_u64 ( self . min_value . unwrap_or ( DEFAULT_MIN_VALUE ) ) ;
240
253
let p = self . precision . unwrap_or ( DEFAULT_PRECISION ) ;
241
254
// determine the bucket offset by finding the bucket for the minimum value. We need to lower
242
255
// this by one to ensure we are at least as granular as requested.
243
256
let bucket_offset = cmp:: max ( bucket_index ( min_value, p) , 1 ) - 1 ;
244
257
// 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 ;
246
259
LogHistogram :: from_n_p ( n, p, bucket_offset as usize )
260
+ . truncate_to_max_value ( requested_max_value)
247
261
}
248
262
}
249
263
@@ -295,17 +309,55 @@ mod test {
295
309
#[ cfg( not( target_family = "wasm" ) ) ]
296
310
mod proptests {
297
311
use super :: * ;
312
+ use crate :: runtime:: metrics:: batch:: duration_as_u64;
313
+ use crate :: runtime:: metrics:: histogram:: h2_histogram:: MAX_PRECISION ;
298
314
use proptest:: prelude:: * ;
315
+ use std:: time:: Duration ;
316
+
299
317
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) | {
301
319
let p = p. min ( n) ;
302
320
let base = LogHistogram :: from_n_p ( n, p, 0 ) ;
303
321
LogHistogram :: from_n_p ( n, p, bucket_offset. min ( base. num_buckets - 1 ) )
304
322
} )
305
323
}
306
324
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
+
307
333
// test against a wide assortment of different histogram configurations to ensure invariants hold
308
334
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
+
309
361
#[ test]
310
362
fn proptest_log_histogram_invariants( histogram in valid_log_histogram_strategy( ) ) {
311
363
// 1. Assert that the first bucket always starts at 0
@@ -469,7 +521,7 @@ mod test {
469
521
required_bucket_count,
470
522
} => required_bucket_count,
471
523
} ;
472
- assert_eq ! ( num_buckets, 27549 ) ;
524
+ assert_eq ! ( num_buckets, 27291 ) ;
473
525
}
474
526
475
527
#[ test]
0 commit comments