1use crate::encoding::{
6 EncodeLabelSet, EncodeMetric, MetricEncoder, NativeHistogram, NativeHistogramBuckets,
7 NoLabelSet,
8};
9use crate::metrics::exemplar::Exemplar;
10
11use super::{MetricType, TypedMetric};
12use parking_lot::Mutex;
13use std::collections::HashMap;
14use std::iter::{self, once};
15use std::sync::Arc;
16use std::time::{Duration, SystemTime};
17
18pub const NATIVE_HISTOGRAM_ZERO_THRESHOLD_ZERO: f64 = -1.0;
20
21pub const DEFAULT_NATIVE_HISTOGRAM_BUCKET_FACTOR: f64 = 1.1;
23
24pub const DEFAULT_NATIVE_HISTOGRAM_ZERO_THRESHOLD: f64 = 2.938735877055719e-39;
26
27const SCHEMA_MIN: i8 = -4;
28const SCHEMA_MAX: i8 = 8;
29
30#[derive(Debug)]
55pub struct Histogram {
56 inner: Arc<Mutex<Inner>>,
57}
58
59impl Clone for Histogram {
60 fn clone(&self) -> Self {
61 Histogram {
62 inner: self.inner.clone(),
63 }
64 }
65}
66
67#[derive(Debug)]
68pub(crate) struct Inner {
69 sum: f64,
71 count: u64,
72 buckets: Vec<(f64, u64)>,
74 native: Option<NativeHistogramState>,
75}
76
77#[derive(Clone, Copy, Debug)]
81pub struct NativeHistogramConfig {
82 schema: i8,
83 zero_threshold: f64,
84 max_buckets: usize,
85 min_reset_duration: Option<Duration>,
86 max_zero_threshold: f64,
87}
88
89impl NativeHistogramConfig {
90 pub fn new(bucket_factor: f64) -> Self {
97 Self::with_bucket_factor(bucket_factor)
98 }
99
100 pub fn with_schema(schema: i8) -> Self {
105 assert!((SCHEMA_MIN..=SCHEMA_MAX).contains(&schema));
106
107 Self {
108 schema,
109 zero_threshold: DEFAULT_NATIVE_HISTOGRAM_ZERO_THRESHOLD,
110 max_buckets: 0,
111 min_reset_duration: None,
112 max_zero_threshold: 0.0,
113 }
114 }
115
116 pub fn with_bucket_factor(bucket_factor: f64) -> Self {
119 assert!(bucket_factor > 1.0);
120 Self::with_schema(pick_schema(bucket_factor))
121 }
122
123 pub fn zero_threshold(mut self, zero_threshold: f64) -> Self {
128 assert!(zero_threshold.is_finite());
129 self.zero_threshold = if zero_threshold > 0.0 {
130 zero_threshold
131 } else if zero_threshold == 0.0 {
132 DEFAULT_NATIVE_HISTOGRAM_ZERO_THRESHOLD
133 } else {
134 NATIVE_HISTOGRAM_ZERO_THRESHOLD_ZERO
135 };
136 self
137 }
138
139 pub fn max_buckets(mut self, max_buckets: usize) -> Self {
145 self.max_buckets = max_buckets;
146 self
147 }
148
149 pub fn min_reset_duration(mut self, min_reset_duration: Duration) -> Self {
153 self.min_reset_duration =
154 (min_reset_duration != Duration::ZERO).then_some(min_reset_duration);
155 self
156 }
157
158 pub fn max_zero_threshold(mut self, max_zero_threshold: f64) -> Self {
163 assert!(max_zero_threshold.is_finite());
164 assert!(max_zero_threshold >= 0.0);
165 self.max_zero_threshold = max_zero_threshold;
166 self
167 }
168}
169
170impl Default for NativeHistogramConfig {
171 fn default() -> Self {
172 Self::new(DEFAULT_NATIVE_HISTOGRAM_BUCKET_FACTOR)
173 }
174}
175
176#[derive(Debug)]
177struct NativeHistogramState {
178 initial_zero_threshold: f64,
179 initial_schema: i8,
180 zero_threshold: f64,
181 zero_count: u64,
182 schema: i8,
183 max_buckets: usize,
184 min_reset_duration: Option<Duration>,
185 max_zero_threshold: f64,
186 created: SystemTime,
187 scheduled_reset: Option<SystemTime>,
188 positive: NativeBuckets,
189 negative: NativeBuckets,
190}
191
192type NativeBuckets = Vec<(i32, u64)>;
193
194#[derive(Debug)]
195struct NativeHistogramSnapshot {
196 schema: i32,
197 zero_threshold: f64,
198 zero_count: u64,
199 negative: NativeBucketEncoding,
200 positive: NativeBucketEncoding,
201 created: SystemTime,
202}
203
204#[derive(Debug)]
205struct NativeBucketEncoding {
206 spans: Vec<(i32, u32)>,
207 deltas: Vec<i64>,
208}
209
210#[derive(Debug)]
211struct HistogramSnapshot {
212 sum: f64,
213 count: u64,
214 buckets: Vec<(f64, u64)>,
215 native: Option<NativeHistogramSnapshot>,
216}
217
218impl NativeHistogramState {
219 fn new(config: NativeHistogramConfig) -> Self {
220 let created = SystemTime::now();
221 Self {
222 initial_zero_threshold: config.zero_threshold,
223 initial_schema: config.schema,
224 zero_threshold: config.zero_threshold,
225 zero_count: 0,
226 schema: config.schema,
227 max_buckets: config.max_buckets,
228 min_reset_duration: config.min_reset_duration,
229 max_zero_threshold: config.max_zero_threshold,
230 created,
231 scheduled_reset: None,
232 positive: Vec::new(),
233 negative: Vec::new(),
234 }
235 }
236
237 fn observe(&mut self, v: f64) -> bool {
238 if in_zero_bucket(self.zero_threshold, v) {
239 self.zero_count += 1;
240 return enforce_bucket_limit(self);
241 }
242
243 let index = bucket_index(self.schema, v.abs(), v.is_infinite());
244 if v.is_sign_negative() {
245 increment_bucket(&mut self.negative, index);
246 } else {
247 increment_bucket(&mut self.positive, index);
248 }
249
250 enforce_bucket_limit(self)
251 }
252
253 fn reset(&mut self, created: SystemTime) {
254 self.zero_threshold = self.initial_zero_threshold;
255 self.zero_count = 0;
256 self.schema = self.initial_schema;
257 self.created = created;
258 self.scheduled_reset = None;
259 self.positive.clear();
260 self.negative.clear();
261 }
262
263 fn reset_is_due(&self, now: SystemTime) -> bool {
264 self.scheduled_reset
265 .and_then(|scheduled_reset| now.duration_since(scheduled_reset).ok())
266 .is_some()
267 }
268
269 fn schedule_reset_after_degradation(&mut self) {
270 if self.scheduled_reset.is_none() {
271 self.scheduled_reset = self
272 .min_reset_duration
273 .and_then(|duration| self.created.checked_add(duration));
274 }
275 }
276
277 fn snapshot(&self) -> Result<NativeHistogramSnapshot, std::fmt::Error> {
278 let mut negative = encode_spans_and_deltas(&self.negative)?;
279 let mut positive = encode_spans_and_deltas(&self.positive)?;
280
281 let exported_zero_threshold = if self.zero_threshold < 0.0 {
282 0.0
283 } else {
284 self.zero_threshold
285 };
286
287 if exported_zero_threshold == 0.0
290 && self.zero_count == 0
291 && positive.spans.is_empty()
292 && negative.spans.is_empty()
293 {
294 positive.spans.push((0, 0));
295 }
296
297 if self.negative.is_empty() {
298 negative.spans.clear();
299 }
300
301 Ok(NativeHistogramSnapshot {
302 schema: i32::from(self.schema),
303 zero_threshold: exported_zero_threshold,
304 zero_count: self.zero_count,
305 negative,
306 positive,
307 created: self.created,
308 })
309 }
310}
311
312impl Histogram {
313 pub fn new(buckets: impl IntoIterator<Item = f64>) -> Self {
325 Self {
326 inner: Arc::new(Mutex::new(Inner {
327 sum: Default::default(),
328 count: Default::default(),
329 buckets: buckets
330 .into_iter()
331 .filter(|upper_bound| upper_bound.is_finite())
332 .chain(once(f64::INFINITY))
333 .map(|upper_bound| (upper_bound, 0))
334 .collect(),
335 native: None,
336 })),
337 }
338 }
339
340 pub fn new_native(native: NativeHistogramConfig) -> Self {
346 Self {
347 inner: Arc::new(Mutex::new(Inner {
348 sum: Default::default(),
349 count: Default::default(),
350 buckets: Vec::new(),
351 native: Some(NativeHistogramState::new(native)),
352 })),
353 }
354 }
355
356 pub fn new_classic_and_native(
362 buckets: impl IntoIterator<Item = f64>,
363 native: NativeHistogramConfig,
364 ) -> Self {
365 let histogram = Self::new(buckets);
366 histogram.inner.lock().native = Some(NativeHistogramState::new(native));
367 histogram
368 }
369
370 pub fn observe(&self, v: f64) {
372 self.observe_and_bucket(v);
373 }
374
375 #[cfg(any(test, feature = "test-util"))]
377 pub fn sum(&self) -> f64 {
378 self.inner.lock().sum
379 }
380
381 #[cfg(any(test, feature = "test-util"))]
383 pub fn count(&self) -> u64 {
384 self.inner.lock().count
385 }
386
387 pub(crate) fn observe_and_bucket(&self, v: f64) -> Option<usize> {
393 let mut inner = self.inner.lock();
394 reset_if_scheduled(&mut inner);
395 let mut bucket = observe_classic(&mut inner, v);
396
397 let reset = if let Some(native) = &mut inner.native {
398 !v.is_nan() && native.observe(v)
399 } else {
400 false
401 };
402
403 if reset {
404 let created = SystemTime::now();
405 reset_observations(&mut inner, created);
406 bucket = observe_classic(&mut inner, v);
407 if let Some(native) = &mut inner.native {
408 if !v.is_nan() {
409 native.observe(v);
410 }
411 }
412 }
413
414 bucket
415 }
416
417 fn snapshot(&self) -> Result<HistogramSnapshot, std::fmt::Error> {
418 let mut inner = self.inner.lock();
419 reset_if_scheduled(&mut inner);
420 let native = inner
421 .native
422 .as_ref()
423 .map(NativeHistogramState::snapshot)
424 .transpose()?;
425
426 Ok(HistogramSnapshot {
427 sum: inner.sum,
428 count: inner.count,
429 buckets: inner.buckets.clone(),
430 native,
431 })
432 }
433
434 pub(crate) fn encode_with_exemplars<S: EncodeLabelSet>(
435 &self,
436 encoder: &mut MetricEncoder,
437 exemplars: Option<&HashMap<usize, Exemplar<S, f64>>>,
438 ) -> Result<(), std::fmt::Error> {
439 let snapshot = self.snapshot()?;
440 match snapshot.native {
441 Some(native) => encoder.encode_histogram_with_native(
442 snapshot.sum,
443 snapshot.count,
444 &snapshot.buckets,
445 exemplars,
446 NativeHistogram {
447 schema: native.schema,
448 zero_threshold: native.zero_threshold,
449 zero_count: native.zero_count,
450 negative: NativeHistogramBuckets {
451 spans: &native.negative.spans,
452 deltas: &native.negative.deltas,
453 },
454 positive: NativeHistogramBuckets {
455 spans: &native.positive.spans,
456 deltas: &native.positive.deltas,
457 },
458 created: Some(native.created),
459 },
460 ),
461 None => {
462 encoder.encode_histogram(snapshot.sum, snapshot.count, &snapshot.buckets, exemplars)
463 }
464 }
465 }
466}
467
468impl TypedMetric for Histogram {
469 const TYPE: MetricType = MetricType::Histogram;
470}
471
472pub fn exponential_buckets(start: f64, factor: f64, length: u16) -> impl Iterator<Item = f64> {
474 iter::repeat(())
475 .enumerate()
476 .map(move |(i, _)| start * factor.powf(i as f64))
477 .take(length.into())
478}
479
480pub fn exponential_buckets_range(min: f64, max: f64, length: u16) -> impl Iterator<Item = f64> {
486 let mut len_observed = length;
487 let mut min_bucket = min;
488 if length < 1 || min <= 0.0 {
492 len_observed = 0;
493 min_bucket = 1.0;
494 }
495 let growth_factor = (max / min_bucket).powf(1.0 / (len_observed as f64 - 1.0));
497
498 iter::repeat(())
499 .enumerate()
500 .map(move |(i, _)| min_bucket * growth_factor.powf(i as f64))
501 .take(len_observed.into())
502}
503
504pub fn linear_buckets(start: f64, width: f64, length: u16) -> impl Iterator<Item = f64> {
506 iter::repeat(())
507 .enumerate()
508 .map(move |(i, _)| start + (width * (i as f64)))
509 .take(length.into())
510}
511
512impl EncodeMetric for Histogram {
513 fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> {
514 self.encode_with_exemplars::<NoLabelSet>(&mut encoder, None)
515 }
516
517 fn metric_type(&self) -> MetricType {
518 Self::TYPE
519 }
520}
521
522fn observe_classic(inner: &mut Inner, v: f64) -> Option<usize> {
523 inner.sum += v;
524 inner.count += 1;
525
526 if inner.buckets.is_empty() {
527 return None;
528 }
529
530 let first_bucket = if v.is_nan() {
531 inner.buckets.iter_mut().enumerate().next_back()
532 } else {
533 inner
534 .buckets
535 .iter_mut()
536 .enumerate()
537 .find(|(_i, (upper_bound, _value))| upper_bound >= &v)
538 };
539
540 match first_bucket {
541 Some((i, (_upper_bound, value))) => {
542 *value += 1;
543 Some(i)
544 }
545 None => None,
546 }
547}
548
549fn reset_observations(inner: &mut Inner, created: SystemTime) {
550 inner.sum = 0.0;
551 inner.count = 0;
552 for (_, count) in &mut inner.buckets {
553 *count = 0;
554 }
555 if let Some(native) = &mut inner.native {
556 native.reset(created);
557 }
558}
559
560fn reset_if_scheduled(inner: &mut Inner) {
561 let Some(native) = inner.native.as_ref() else {
562 return;
563 };
564 if native.scheduled_reset.is_none() {
565 return;
566 }
567
568 let now = SystemTime::now();
569 if native.reset_is_due(now) {
570 reset_observations(inner, now);
571 }
572}
573
574fn in_zero_bucket(zero_threshold: f64, v: f64) -> bool {
575 if zero_threshold < 0.0 {
576 v == 0.0
577 } else {
578 v.abs() <= zero_threshold
579 }
580}
581
582fn increment_bucket(buckets: &mut NativeBuckets, index: i32) {
583 match buckets.binary_search_by_key(&index, |(bucket_index, _)| *bucket_index) {
584 Ok(position) => buckets[position].1 += 1,
585 Err(position) => buckets.insert(position, (index, 1)),
586 }
587}
588
589fn pick_schema(bucket_factor: f64) -> i8 {
590 let floor = bucket_factor.log2().log2().floor();
591 if floor <= -f64::from(SCHEMA_MAX) {
592 return SCHEMA_MAX;
593 }
594
595 if floor >= -f64::from(SCHEMA_MIN) {
596 return SCHEMA_MIN;
597 }
598
599 (SCHEMA_MIN..=SCHEMA_MAX)
600 .find(|schema| f64::from(*schema) == -floor)
601 .expect("schema is in range")
602}
603
604const NATIVE_HISTOGRAM_BOUNDS: [&[f64]; 9] = [
606 &[0.5],
607 &[0.5, 0.7071067811865475],
608 &[
609 0.5,
610 0.5946035575013605,
611 0.7071067811865475,
612 0.8408964152537144,
613 ],
614 &[
615 0.5,
616 0.5452538663326288,
617 0.5946035575013605,
618 0.6484197773255048,
619 0.7071067811865475,
620 0.7711054127039704,
621 0.8408964152537144,
622 0.9170040432046711,
623 ],
624 &[
625 0.5,
626 0.5221368912137069,
627 0.5452538663326288,
628 0.5693943173783458,
629 0.5946035575013605,
630 0.620928906036742,
631 0.6484197773255048,
632 0.6771277734684463,
633 0.7071067811865475,
634 0.7384130729697496,
635 0.7711054127039704,
636 0.805245165974627,
637 0.8408964152537144,
638 0.8781260801866495,
639 0.9170040432046711,
640 0.9576032806985735,
641 ],
642 &[
643 0.5,
644 0.5109485743270583,
645 0.5221368912137069,
646 0.5335702003384117,
647 0.5452538663326288,
648 0.5571933712979462,
649 0.5693943173783458,
650 0.5818624293887887,
651 0.5946035575013605,
652 0.6076236799902344,
653 0.620928906036742,
654 0.6345254785958666,
655 0.6484197773255048,
656 0.6626183215798706,
657 0.6771277734684463,
658 0.6919549409819159,
659 0.7071067811865475,
660 0.7225904034885232,
661 0.7384130729697496,
662 0.7545822137967112,
663 0.7711054127039704,
664 0.7879904225539431,
665 0.805245165974627,
666 0.8228777390769823,
667 0.8408964152537144,
668 0.8593096490612387,
669 0.8781260801866495,
670 0.8973545375015533,
671 0.9170040432046711,
672 0.9370838170551498,
673 0.9576032806985735,
674 0.9785720620876999,
675 ],
676 &[
677 0.5,
678 0.5054446430258502,
679 0.5109485743270583,
680 0.5165124395106142,
681 0.5221368912137069,
682 0.5278225891802786,
683 0.5335702003384117,
684 0.5393803988785598,
685 0.5452538663326288,
686 0.5511912916539204,
687 0.5571933712979462,
688 0.5632608093041209,
689 0.5693943173783458,
690 0.5755946149764913,
691 0.5818624293887887,
692 0.5881984958251406,
693 0.5946035575013605,
694 0.6010783657263515,
695 0.6076236799902344,
696 0.6142402680534349,
697 0.620928906036742,
698 0.6276903785123455,
699 0.6345254785958666,
700 0.6414350080393891,
701 0.6484197773255048,
702 0.6554806057623822,
703 0.6626183215798706,
704 0.6698337620266515,
705 0.6771277734684463,
706 0.6845012114872953,
707 0.6919549409819159,
708 0.6994898362691555,
709 0.7071067811865475,
710 0.7148066691959849,
711 0.7225904034885232,
712 0.7304588970903234,
713 0.7384130729697496,
714 0.7464538641456323,
715 0.7545822137967112,
716 0.762799075372269,
717 0.7711054127039704,
718 0.7795022001189185,
719 0.7879904225539431,
720 0.7965710756711334,
721 0.805245165974627,
722 0.8140137109286738,
723 0.8228777390769823,
724 0.8318382901633681,
725 0.8408964152537144,
726 0.8500531768592616,
727 0.8593096490612387,
728 0.8686669176368529,
729 0.8781260801866495,
730 0.8876882462632604,
731 0.8973545375015533,
732 0.9071260877501991,
733 0.9170040432046711,
734 0.9269895625416926,
735 0.9370838170551498,
736 0.9472879907934827,
737 0.9576032806985735,
738 0.9680308967461471,
739 0.9785720620876999,
740 0.9892280131939752,
741 ],
742 &[
743 0.5,
744 0.5027149505564014,
745 0.5054446430258502,
746 0.5081891574554764,
747 0.5109485743270583,
748 0.5137229745593818,
749 0.5165124395106142,
750 0.5193170509806894,
751 0.5221368912137069,
752 0.5249720429003435,
753 0.5278225891802786,
754 0.5306886136446309,
755 0.5335702003384117,
756 0.5364674337629877,
757 0.5393803988785598,
758 0.5423091811066545,
759 0.5452538663326288,
760 0.5482145409081883,
761 0.5511912916539204,
762 0.5541842058618393,
763 0.5571933712979462,
764 0.5602188762048033,
765 0.5632608093041209,
766 0.5663192597993595,
767 0.5693943173783458,
768 0.572486072215902,
769 0.5755946149764913,
770 0.5787200368168754,
771 0.5818624293887887,
772 0.585021884841625,
773 0.5881984958251406,
774 0.5913923554921704,
775 0.5946035575013605,
776 0.5978321960199137,
777 0.6010783657263515,
778 0.6043421618132907,
779 0.6076236799902344,
780 0.6109230164863786,
781 0.6142402680534349,
782 0.6175755319684665,
783 0.620928906036742,
784 0.6243004885946023,
785 0.6276903785123455,
786 0.6310986751971253,
787 0.6345254785958666,
788 0.637970889198196,
789 0.6414350080393891,
790 0.6449179367033329,
791 0.6484197773255048,
792 0.6519406325959679,
793 0.6554806057623822,
794 0.659039800633032,
795 0.6626183215798706,
796 0.6662162735415805,
797 0.6698337620266515,
798 0.6734708931164728,
799 0.6771277734684463,
800 0.6808045103191123,
801 0.6845012114872953,
802 0.688217985377265,
803 0.6919549409819159,
804 0.6957121878859629,
805 0.6994898362691555,
806 0.7032879969095076,
807 0.7071067811865475,
808 0.7109463010845827,
809 0.7148066691959849,
810 0.718687998724491,
811 0.7225904034885232,
812 0.7265139979245261,
813 0.7304588970903234,
814 0.7344252166684908,
815 0.7384130729697496,
816 0.7424225829363761,
817 0.7464538641456323,
818 0.7505070348132126,
819 0.7545822137967112,
820 0.7586795205991071,
821 0.762799075372269,
822 0.7669409989204777,
823 0.7711054127039704,
824 0.7752924388424999,
825 0.7795022001189185,
826 0.7837348199827764,
827 0.7879904225539431,
828 0.7922691326262467,
829 0.7965710756711334,
830 0.8008963778413465,
831 0.805245165974627,
832 0.8096175675974316,
833 0.8140137109286738,
834 0.8184337248834821,
835 0.8228777390769823,
836 0.8273458838280969,
837 0.8318382901633681,
838 0.8363550898207981,
839 0.8408964152537144,
840 0.8454623996346523,
841 0.8500531768592616,
842 0.8546688815502312,
843 0.8593096490612387,
844 0.8639756154809185,
845 0.8686669176368529,
846 0.8733836930995842,
847 0.8781260801866495,
848 0.8828942179666361,
849 0.8876882462632604,
850 0.8925083056594671,
851 0.8973545375015533,
852 0.9022270839033115,
853 0.9071260877501991,
854 0.9120516927035263,
855 0.9170040432046711,
856 0.9219832844793128,
857 0.9269895625416926,
858 0.9320230241988943,
859 0.9370838170551498,
860 0.9421720895161669,
861 0.9472879907934827,
862 0.9524316709088368,
863 0.9576032806985735,
864 0.9628029718180622,
865 0.9680308967461471,
866 0.9732872087896164,
867 0.9785720620876999,
868 0.9838856116165875,
869 0.9892280131939752,
870 0.9945994234836328,
871 ],
872 &[
873 0.5,
874 0.5013556375251013,
875 0.5027149505564014,
876 0.5040779490592088,
877 0.5054446430258502,
878 0.5068150424757447,
879 0.5081891574554764,
880 0.509566998038869,
881 0.5109485743270583,
882 0.5123338964485679,
883 0.5137229745593818,
884 0.5151158188430205,
885 0.5165124395106142,
886 0.5179128468009786,
887 0.5193170509806894,
888 0.520725062344158,
889 0.5221368912137069,
890 0.5235525479396449,
891 0.5249720429003435,
892 0.526395386502313,
893 0.5278225891802786,
894 0.5292536613972564,
895 0.5306886136446309,
896 0.5321274564422321,
897 0.5335702003384117,
898 0.5350168559101208,
899 0.5364674337629877,
900 0.5379219445313954,
901 0.5393803988785598,
902 0.5408428074966075,
903 0.5423091811066545,
904 0.5437795304588847,
905 0.5452538663326288,
906 0.5467321995364429,
907 0.5482145409081883,
908 0.549700901315111,
909 0.5511912916539204,
910 0.5526857228508706,
911 0.5541842058618393,
912 0.5556867516724088,
913 0.5571933712979462,
914 0.5587040757836845,
915 0.5602188762048033,
916 0.5617377836665098,
917 0.5632608093041209,
918 0.564787964283144,
919 0.5663192597993595,
920 0.5678547070789026,
921 0.5693943173783458,
922 0.5709381019847808,
923 0.572486072215902,
924 0.5740382394200894,
925 0.5755946149764913,
926 0.5771552102951081,
927 0.5787200368168754,
928 0.5802891060137493,
929 0.5818624293887887,
930 0.5834400184762408,
931 0.585021884841625,
932 0.5866080400818185,
933 0.5881984958251406,
934 0.5897932637314379,
935 0.5913923554921704,
936 0.5929957828304968,
937 0.5946035575013605,
938 0.5962156912915756,
939 0.5978321960199137,
940 0.5994530835371903,
941 0.6010783657263515,
942 0.6027080545025619,
943 0.6043421618132907,
944 0.6059806996384005,
945 0.6076236799902344,
946 0.6092711149137041,
947 0.6109230164863786,
948 0.6125793968185725,
949 0.6142402680534349,
950 0.6159056423670379,
951 0.6175755319684665,
952 0.6192499490999082,
953 0.620928906036742,
954 0.622612415087629,
955 0.6243004885946023,
956 0.6259931389331581,
957 0.6276903785123455,
958 0.6293922197748583,
959 0.6310986751971253,
960 0.6328097572894031,
961 0.6345254785958666,
962 0.6362458516947014,
963 0.637970889198196,
964 0.6397006037528346,
965 0.6414350080393891,
966 0.6431741147730128,
967 0.6449179367033329,
968 0.6466664866145447,
969 0.6484197773255048,
970 0.6501778216898253,
971 0.6519406325959679,
972 0.6537082229673385,
973 0.6554806057623822,
974 0.6572577939746774,
975 0.659039800633032,
976 0.6608266388015788,
977 0.6626183215798706,
978 0.6644148621029772,
979 0.6662162735415805,
980 0.6680225691020727,
981 0.6698337620266515,
982 0.6716498655934177,
983 0.6734708931164728,
984 0.6752968579460171,
985 0.6771277734684463,
986 0.6789636531064505,
987 0.6808045103191123,
988 0.6826503586020058,
989 0.6845012114872953,
990 0.6863570825438342,
991 0.688217985377265,
992 0.690083933630119,
993 0.6919549409819159,
994 0.6938310211492645,
995 0.6957121878859629,
996 0.6975984549830999,
997 0.6994898362691555,
998 0.7013863456101023,
999 0.7032879969095076,
1000 0.7051948041086352,
1001 0.7071067811865475,
1002 0.7090239421602076,
1003 0.7109463010845827,
1004 0.7128738720527471,
1005 0.7148066691959849,
1006 0.7167447066838943,
1007 0.718687998724491,
1008 0.7206365595643126,
1009 0.7225904034885232,
1010 0.7245495448210174,
1011 0.7265139979245261,
1012 0.7284837772007218,
1013 0.7304588970903234,
1014 0.7324393720732029,
1015 0.7344252166684908,
1016 0.7364164454346837,
1017 0.7384130729697496,
1018 0.7404151139112358,
1019 0.7424225829363761,
1020 0.7444354947621984,
1021 0.7464538641456323,
1022 0.7484777058836176,
1023 0.7505070348132126,
1024 0.7525418658117031,
1025 0.7545822137967112,
1026 0.7566280937263048,
1027 0.7586795205991071,
1028 0.7607365094544071,
1029 0.762799075372269,
1030 0.7648672334736434,
1031 0.7669409989204777,
1032 0.7690203869158282,
1033 0.7711054127039704,
1034 0.7731960915705107,
1035 0.7752924388424999,
1036 0.7773944698885442,
1037 0.7795022001189185,
1038 0.7816156449856788,
1039 0.7837348199827764,
1040 0.7858597406461707,
1041 0.7879904225539431,
1042 0.7901268813264122,
1043 0.7922691326262467,
1044 0.7944171921585818,
1045 0.7965710756711334,
1046 0.7987307989543135,
1047 0.8008963778413465,
1048 0.8030678282083853,
1049 0.805245165974627,
1050 0.8074284071024302,
1051 0.8096175675974316,
1052 0.8118126635086642,
1053 0.8140137109286738,
1054 0.8162207259936375,
1055 0.8184337248834821,
1056 0.820652723822003,
1057 0.8228777390769823,
1058 0.8251087869603088,
1059 0.8273458838280969,
1060 0.8295890460808079,
1061 0.8318382901633681,
1062 0.8340936325652911,
1063 0.8363550898207981,
1064 0.8386226785089391,
1065 0.8408964152537144,
1066 0.8431763167241966,
1067 0.8454623996346523,
1068 0.8477546807446661,
1069 0.8500531768592616,
1070 0.8523579048290255,
1071 0.8546688815502312,
1072 0.8569861239649629,
1073 0.8593096490612387,
1074 0.8616394738731368,
1075 0.8639756154809185,
1076 0.8663180910111553,
1077 0.8686669176368529,
1078 0.871022112577578,
1079 0.8733836930995842,
1080 0.8757516765159389,
1081 0.8781260801866495,
1082 0.8805069215187917,
1083 0.8828942179666361,
1084 0.8852879870317771,
1085 0.8876882462632604,
1086 0.890095013257712,
1087 0.8925083056594671,
1088 0.8949281411607002,
1089 0.8973545375015533,
1090 0.8997875124702672,
1091 0.9022270839033115,
1092 0.9046732696855155,
1093 0.9071260877501991,
1094 0.909585556079304,
1095 0.9120516927035263,
1096 0.9145245157024483,
1097 0.9170040432046711,
1098 0.9194902933879467,
1099 0.9219832844793128,
1100 0.9244830347552253,
1101 0.9269895625416926,
1102 0.92950288621441,
1103 0.9320230241988943,
1104 0.9345499949706191,
1105 0.9370838170551498,
1106 0.93962450902828,
1107 0.9421720895161669,
1108 0.9447265771954693,
1109 0.9472879907934827,
1110 0.9498563490882775,
1111 0.9524316709088368,
1112 0.9550139751351947,
1113 0.9576032806985735,
1114 0.9601996065815236,
1115 0.9628029718180622,
1116 0.9654133954938133,
1117 0.9680308967461471,
1118 0.9706554947643201,
1119 0.9732872087896164,
1120 0.9759260581154889,
1121 0.9785720620876999,
1122 0.9812252401044634,
1123 0.9838856116165875,
1124 0.9865531961276168,
1125 0.9892280131939752,
1126 0.9919100824251095,
1127 0.9945994234836328,
1128 0.9972960560854698,
1129 ],
1130];
1131
1132fn bucket_index(schema: i8, v: f64, is_infinite: bool) -> i32 {
1133 let v = if is_infinite { f64::MAX } else { v };
1134 let (frac, exp) = frexp(v);
1135 let mut index = if schema > 0 {
1136 let bounds = NATIVE_HISTOGRAM_BOUNDS
1137 [usize::try_from(schema).expect("positive schema can be indexed")];
1138 let bucket =
1139 i32::try_from(bounds.partition_point(|bound| *bound < frac)).expect("bounds fit i32");
1140 bucket + (exp - 1) * i32::try_from(bounds.len()).expect("bounds length fits i32")
1141 } else {
1142 let mut key = exp;
1143 if frac == 0.5 {
1144 key -= 1;
1145 }
1146 let shift = u32::try_from(-i32::from(schema)).expect("non-positive schema shift");
1147 let offset = (1_i32 << shift) - 1;
1148 (key + offset) >> shift
1149 };
1150
1151 if is_infinite {
1152 index = index.saturating_add(1);
1153 }
1154 index
1155}
1156
1157fn frexp(v: f64) -> (f64, i32) {
1158 debug_assert!(v >= 0.0);
1159 debug_assert!(!v.is_nan());
1160
1161 if v == 0.0 {
1162 return (0.0, 0);
1163 }
1164
1165 let bits = v.to_bits();
1166 let exponent = i32::try_from((bits >> 52) & 0x7ff).expect("f64 exponent fits i32");
1167 let mantissa = bits & ((1_u64 << 52) - 1);
1168
1169 if exponent == 0 {
1170 let p = 63 - i32::try_from(mantissa.leading_zeros()).expect("leading zeros fit i32");
1171 let frac = mantissa as f64 / 2f64.powi(p + 1);
1172 (frac, p - 1073)
1173 } else {
1174 let frac = ((1_u64 << 52) | mantissa) as f64 / 2f64.powi(53);
1175 (frac, exponent - 1022)
1176 }
1177}
1178
1179fn enforce_bucket_limit(inner: &mut NativeHistogramState) -> bool {
1180 if inner.max_buckets == 0 {
1181 return false;
1182 }
1183
1184 if inner.positive.len() + inner.negative.len() <= inner.max_buckets {
1185 return false;
1186 }
1187
1188 if inner.scheduled_reset.is_none()
1189 && inner
1190 .min_reset_duration
1191 .and_then(|min_reset_duration| {
1192 inner
1193 .created
1194 .elapsed()
1195 .ok()
1196 .map(|e| e >= min_reset_duration)
1197 })
1198 .unwrap_or(false)
1199 {
1200 return true;
1201 }
1202
1203 if widen_zero_bucket(inner) {
1204 inner.schedule_reset_after_degradation();
1205 return false;
1206 }
1207
1208 if inner.schema > SCHEMA_MIN {
1209 inner.schema -= 1;
1210 inner.positive = downsample_buckets(&inner.positive);
1211 inner.negative = downsample_buckets(&inner.negative);
1212 inner.schedule_reset_after_degradation();
1213 }
1214
1215 false
1216}
1217
1218fn widen_zero_bucket(inner: &mut NativeHistogramState) -> bool {
1219 let smallest_key = match (
1220 inner.positive.first().map(|(index, _)| *index),
1221 inner.negative.first().map(|(index, _)| *index),
1222 ) {
1223 (Some(positive), Some(negative)) => positive.min(negative),
1224 (Some(positive), None) => positive,
1225 (None, Some(negative)) => negative,
1226 (None, None) => return false,
1227 };
1228
1229 let new_threshold = positive_upper_bound(inner.schema, smallest_key);
1230 let current_threshold = if inner.zero_threshold < 0.0 {
1231 0.0
1232 } else {
1233 inner.zero_threshold
1234 };
1235 if new_threshold <= current_threshold || new_threshold > inner.max_zero_threshold {
1236 return false;
1237 }
1238
1239 let moved_positive = move_to_zero_bucket(inner.schema, new_threshold, &mut inner.positive);
1240 let moved_negative = move_to_zero_bucket(inner.schema, new_threshold, &mut inner.negative);
1241
1242 let moved = moved_positive + moved_negative;
1243 if moved == 0 {
1244 return false;
1245 }
1246
1247 inner.zero_count += moved;
1248 inner.zero_threshold = new_threshold;
1249 true
1250}
1251
1252fn move_to_zero_bucket(schema: i8, threshold: f64, buckets: &mut NativeBuckets) -> u64 {
1253 if buckets.is_empty() {
1254 return 0;
1255 }
1256
1257 let mut split = 0;
1258 for (index, _count) in buckets.iter() {
1259 let upper = positive_upper_bound(schema, *index);
1260 if upper <= threshold {
1261 split += 1;
1262 } else {
1263 break;
1264 }
1265 }
1266
1267 let moved = buckets[..split].iter().map(|(_, count)| *count).sum();
1268 buckets.drain(..split);
1269
1270 moved
1271}
1272
1273fn positive_upper_bound(schema: i8, index: i32) -> f64 {
1274 if schema < 0 {
1275 let exp = index << u32::try_from(-i32::from(schema)).expect("negative schema shift");
1276 if exp == 1024 {
1277 return f64::MAX;
1278 }
1279 return 2f64.powi(exp);
1280 }
1281
1282 let schema = u32::try_from(schema).expect("non-negative schema shift");
1283 let bounds = NATIVE_HISTOGRAM_BOUNDS[usize::try_from(schema).expect("schema can be indexed")];
1284 let frac = bounds[usize::try_from(index & ((1 << schema) - 1)).expect("index can be indexed")];
1285 let exp = (index >> schema) + 1;
1286 if frac == 0.5 && exp == 1025 {
1287 return f64::MAX;
1288 }
1289 frac * 2f64.powi(exp)
1290}
1291
1292fn downsample_buckets(buckets: &NativeBuckets) -> NativeBuckets {
1293 let mut downsampled: NativeBuckets = Vec::with_capacity(buckets.len());
1294 for (index, count) in buckets {
1295 let mut key = *index;
1296 if key > 0 {
1297 key += 1;
1298 }
1299 key /= 2;
1300 if let Some((last_key, last_count)) = downsampled.last_mut() {
1301 if *last_key == key {
1302 *last_count += *count;
1303 continue;
1304 }
1305 }
1306 downsampled.push((key, *count));
1307 }
1308 downsampled
1309}
1310
1311fn encode_spans_and_deltas(
1312 buckets: &NativeBuckets,
1313) -> Result<NativeBucketEncoding, std::fmt::Error> {
1314 let mut deltas = Vec::with_capacity(buckets.len());
1315 let mut previous_count = 0i64;
1316 let mut next_index = 0i32;
1317 let mut spans: Vec<(i32, u32)> = Vec::new();
1318
1319 let mut append_delta = |count: i64, spans: &mut Vec<(i32, u32)>| {
1320 if let Some((_, len)) = spans.last_mut() {
1321 *len += 1;
1322 }
1323 deltas.push(count - previous_count);
1324 previous_count = count;
1325 };
1326
1327 for (n, &(index, count)) in buckets.iter().enumerate() {
1328 let count = i64::try_from(count).map_err(|_| std::fmt::Error)?;
1329 let index_delta = index - next_index;
1330
1331 if n == 0 || index_delta > 2 {
1332 spans.push((index_delta, 0));
1333 } else {
1334 for _ in 0..index_delta {
1335 append_delta(0, &mut spans);
1336 }
1337 }
1338
1339 append_delta(count, &mut spans);
1340 next_index = index + 1;
1341 }
1342
1343 Ok(NativeBucketEncoding { spans, deltas })
1344}
1345
1346#[cfg(test)]
1347mod tests {
1348 use super::*;
1349
1350 #[test]
1351 fn histogram() {
1352 let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10));
1353 histogram.observe(1.0);
1354 }
1355
1356 #[test]
1357 fn exponential() {
1358 assert_eq!(
1359 vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0],
1360 exponential_buckets(1.0, 2.0, 10).collect::<Vec<_>>()
1361 );
1362 }
1363
1364 #[test]
1365 fn linear() {
1366 assert_eq!(
1367 vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
1368 linear_buckets(0.0, 1.0, 10).collect::<Vec<_>>()
1369 );
1370 }
1371
1372 #[test]
1373 fn exponential_range() {
1374 assert_eq!(
1375 vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0],
1376 exponential_buckets_range(1.0, 32.0, 6).collect::<Vec<_>>()
1377 );
1378 }
1379
1380 #[test]
1381 fn exponential_range_incorrect() {
1382 let res = exponential_buckets_range(1.0, 32.0, 0).collect::<Vec<_>>();
1383 assert!(res.is_empty());
1384
1385 let res = exponential_buckets_range(0.0, 32.0, 6).collect::<Vec<_>>();
1386 assert!(res.is_empty());
1387 }
1388
1389 #[test]
1391 fn count() {
1392 let histogram = Histogram::new([1.0_f64, 2.0, 3.0, 4.0, 5.0]);
1393 assert_eq!(
1394 histogram.count(),
1395 0,
1396 "histogram has zero observations when instantiated"
1397 );
1398
1399 histogram.observe(1.0);
1400 assert_eq!(histogram.count(), 1, "histogram has one observation");
1401
1402 histogram.observe(2.5);
1403 assert_eq!(histogram.count(), 2, "histogram has two observations");
1404
1405 histogram.observe(6.0);
1406 assert_eq!(histogram.count(), 3, "histogram has three observations");
1407 }
1408
1409 #[test]
1411 fn sum() {
1412 const BUCKETS: [f64; 3] = [10.0, 100.0, 1000.0];
1413 let histogram = Histogram::new(BUCKETS);
1414 assert_eq!(
1415 histogram.sum(),
1416 0.0,
1417 "histogram sum is zero when instantiated"
1418 );
1419
1420 histogram.observe(3.0); histogram.observe(4.0);
1422 histogram.observe(15.0);
1423 histogram.observe(101.0);
1424 assert_eq!(
1425 histogram.sum(),
1426 123.0,
1427 "histogram sum records accurate sum of observations"
1428 );
1429
1430 histogram.observe(1111.0);
1431 assert_eq!(
1432 histogram.sum(),
1433 1234.0,
1434 "histogram sum records accurate sum of observations"
1435 );
1436 }
1437
1438 #[test]
1439 fn native_histogram_stores_sparse_buckets() {
1440 let h = Histogram::new_native(NativeHistogramConfig::with_schema(0));
1441 h.observe(1.0);
1442 h.observe(4.0);
1443 h.observe(-2.0);
1444 h.observe(0.0);
1445
1446 let inner = h.inner.lock();
1447 let native = inner.native.as_ref().unwrap();
1448 assert_eq!(4, inner.count);
1449 assert_eq!(1, native.zero_count);
1450 assert_eq!(2, native.positive.len());
1451 assert_eq!(1, native.negative.len());
1452 }
1453
1454 #[test]
1455 fn native_histogram_counts_nan_without_sparse_bucket() {
1456 let h = Histogram::new_native(NativeHistogramConfig::with_schema(2));
1457 h.observe(f64::NAN);
1458 assert_eq!(1, h.count());
1459 assert!(h.sum().is_nan());
1460
1461 let inner = h.inner.lock();
1462 let native = inner.native.as_ref().unwrap();
1463 assert_eq!(0, native.zero_count);
1464 assert!(native.positive.is_empty());
1465 assert!(native.negative.is_empty());
1466 }
1467
1468 #[test]
1469 fn classic_histogram_counts_nan_in_infinity_bucket() {
1470 let h = Histogram::new([1.0, 2.0]);
1471 h.observe(f64::NAN);
1472
1473 let inner = h.inner.lock();
1474 assert_eq!(1, inner.count);
1475 assert!(inner.sum.is_nan());
1476 assert_eq!(1, inner.buckets[2].1);
1477 }
1478
1479 #[test]
1480 fn native_histogram_supports_zero_width_zero_bucket() {
1481 let h = Histogram::new_native(
1482 NativeHistogramConfig::with_schema(0)
1483 .zero_threshold(NATIVE_HISTOGRAM_ZERO_THRESHOLD_ZERO),
1484 );
1485 h.observe(0.0);
1486 h.observe(0.01);
1487
1488 let inner = h.inner.lock();
1489 let native = inner.native.as_ref().unwrap();
1490 assert_eq!(1, native.zero_count);
1491 assert_eq!(
1492 1,
1493 native.positive.iter().map(|(_, count)| *count).sum::<u64>()
1494 );
1495 }
1496
1497 #[test]
1498 fn native_histogram_zero_threshold_zero_uses_default() {
1499 let h = Histogram::new_native(NativeHistogramConfig::with_schema(0).zero_threshold(0.0));
1500 let inner = h.inner.lock();
1501 let native = inner.native.as_ref().unwrap();
1502 assert_eq!(
1503 DEFAULT_NATIVE_HISTOGRAM_ZERO_THRESHOLD,
1504 native.zero_threshold
1505 );
1506 }
1507
1508 #[test]
1509 fn native_histogram_negative_zero_threshold_uses_zero_width_zero_bucket() {
1510 let h = Histogram::new_native(NativeHistogramConfig::with_schema(0).zero_threshold(-2.0));
1511 h.observe(0.0);
1512 h.observe(0.01);
1513
1514 let inner = h.inner.lock();
1515 let native = inner.native.as_ref().unwrap();
1516 assert_eq!(NATIVE_HISTOGRAM_ZERO_THRESHOLD_ZERO, native.zero_threshold);
1517 assert_eq!(1, native.zero_count);
1518 assert_eq!(
1519 1,
1520 native.positive.iter().map(|(_, count)| *count).sum::<u64>()
1521 );
1522 }
1523
1524 #[test]
1525 fn native_histogram_reduces_resolution_when_max_bucket_limit_is_hit() {
1526 let h = Histogram::new_native(NativeHistogramConfig::with_schema(8).max_buckets(1));
1527 h.observe(1.0);
1528 h.observe(1.1);
1529
1530 let inner = h.inner.lock();
1531 let native = inner.native.as_ref().unwrap();
1532 assert!(native.schema < 8);
1533 }
1534
1535 #[test]
1536 fn native_histogram_widens_zero_bucket_before_reducing_resolution() {
1537 let h = Histogram::new_native(
1538 NativeHistogramConfig::with_schema(8)
1539 .max_buckets(1)
1540 .max_zero_threshold(1.0),
1541 );
1542 h.observe(2f64.powi(-100));
1543 h.observe(1.0);
1544
1545 let inner = h.inner.lock();
1546 let native = inner.native.as_ref().unwrap();
1547 assert_eq!(8, native.schema);
1548 assert_eq!(1, native.zero_count);
1549 assert_eq!(1, native.positive.len());
1550 }
1551
1552 #[test]
1553 fn native_histogram_schedules_reset_after_degradation() {
1554 let h = Histogram::new_native(
1555 NativeHistogramConfig::with_schema(8)
1556 .max_buckets(1)
1557 .min_reset_duration(Duration::from_secs(60)),
1558 );
1559 h.observe(1.0);
1560 h.observe(1.1);
1561
1562 {
1563 let mut inner = h.inner.lock();
1564 let native = inner.native.as_mut().unwrap();
1565 assert!(native.schema < 8);
1566 assert!(native.scheduled_reset.is_some());
1567 native.scheduled_reset = Some(SystemTime::now() - Duration::from_secs(1));
1568 }
1569
1570 let snapshot = h.snapshot().unwrap();
1571 let native = snapshot.native.unwrap();
1572
1573 assert_eq!(0, snapshot.count);
1574 assert_eq!(8, native.schema);
1575 assert_eq!(0, native.zero_count);
1576 assert!(native.positive.spans.is_empty());
1577 assert!(native.negative.spans.is_empty());
1578 }
1579
1580 #[test]
1581 fn native_histogram_scheduled_reset_is_triggered_by_observe() {
1582 let h = Histogram::new_native(
1583 NativeHistogramConfig::with_schema(8)
1584 .max_buckets(1)
1585 .min_reset_duration(Duration::from_secs(60)),
1586 );
1587 h.observe(1.0);
1588 h.observe(1.1);
1589
1590 {
1591 let mut inner = h.inner.lock();
1592 let native = inner.native.as_mut().unwrap();
1593 assert!(native.schema < 8);
1594 assert!(native.scheduled_reset.is_some());
1595 native.scheduled_reset = Some(SystemTime::now() - Duration::from_secs(1));
1596 }
1597
1598 h.observe(2.0);
1599
1600 let inner = h.inner.lock();
1601 let native = inner.native.as_ref().unwrap();
1602 assert_eq!(1, inner.count);
1603 assert_eq!(2.0, inner.sum);
1604 assert_eq!(8, native.schema);
1605 assert_eq!(0, native.zero_count);
1606 assert_eq!(1, native.positive.len());
1607 assert_eq!(1, native.positive[0].1);
1608 assert!(native.negative.is_empty());
1609 }
1610
1611 #[test]
1612 fn native_histogram_resets_when_limit_is_hit_after_min_reset_duration() {
1613 let h = Histogram::new_native(
1614 NativeHistogramConfig::with_schema(8)
1615 .max_buckets(1)
1616 .min_reset_duration(Duration::from_secs(1)),
1617 );
1618 h.observe(1.0);
1619 {
1620 let mut inner = h.inner.lock();
1621 inner.native.as_mut().unwrap().created = SystemTime::now() - Duration::from_secs(2);
1622 }
1623 h.observe(2.0);
1624
1625 let inner = h.inner.lock();
1626 let native = inner.native.as_ref().unwrap();
1627 assert_eq!(1, inner.count);
1628 assert_eq!(2.0, inner.sum);
1629 assert_eq!(8, native.schema);
1630 assert_eq!(
1631 1,
1632 native.positive.iter().map(|(_, count)| *count).sum::<u64>()
1633 );
1634 }
1635
1636 #[test]
1637 fn native_histogram_zero_min_reset_duration_does_not_reset() {
1638 let h = Histogram::new_native(
1639 NativeHistogramConfig::with_schema(8)
1640 .max_buckets(1)
1641 .min_reset_duration(Duration::ZERO),
1642 );
1643 h.observe(1.0);
1644 h.observe(1.1);
1645
1646 let inner = h.inner.lock();
1647 let native = inner.native.as_ref().unwrap();
1648 assert_eq!(2, inner.count);
1649 assert!(native.schema < 8);
1650 assert!(native.scheduled_reset.is_none());
1651 }
1652
1653 #[test]
1654 fn native_histogram_supports_bucket_factor_constructor() {
1655 let h = Histogram::new_native(NativeHistogramConfig::new(1.1));
1656 let inner = h.inner.lock();
1657 assert_eq!(3, inner.native.as_ref().unwrap().schema);
1658 }
1659
1660 #[test]
1661 fn native_histogram_default_uses_recommended_bucket_factor() {
1662 let h = Histogram::new_native(NativeHistogramConfig::default());
1663 let inner = h.inner.lock();
1664 assert_eq!(3, inner.native.as_ref().unwrap().schema);
1665 }
1666
1667 #[derive(Debug)]
1670 struct NativeHistogramParityCase {
1671 name: &'static str,
1672 observations: &'static [f64],
1673 bucket_factor: f64,
1674 zero_threshold: Option<f64>,
1675 max_buckets: usize,
1676 max_zero_threshold: f64,
1677 sample_count: u64,
1678 sample_sum: f64,
1679 schema: i32,
1680 expected_zero_threshold: f64,
1681 zero_count: u64,
1682 negative_spans: &'static [(i32, u32)],
1683 negative_deltas: &'static [i64],
1684 positive_spans: &'static [(i32, u32)],
1685 positive_deltas: &'static [i64],
1686 }
1687
1688 #[test]
1689 fn native_histogram_matches_client_golang_scenarios() {
1690 const DEFAULT_ZERO_THRESHOLD: f64 = 2.938735877055719e-39;
1691 const ZERO_WIDTH_ZERO_BUCKET: f64 = NATIVE_HISTOGRAM_ZERO_THRESHOLD_ZERO;
1692 const NAN: f64 = f64::NAN;
1693 const INF: f64 = f64::INFINITY;
1694 const NEG_INF: f64 = f64::NEG_INFINITY;
1695
1696 let cases = [
1697 NativeHistogramParityCase {
1698 name: "no observations",
1699 observations: &[],
1700 bucket_factor: 1.1,
1701 zero_threshold: None,
1702 max_buckets: 0,
1703 max_zero_threshold: 0.0,
1704 sample_count: 0,
1705 sample_sum: 0.0,
1706 schema: 3,
1707 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
1708 zero_count: 0,
1709 negative_spans: &[],
1710 negative_deltas: &[],
1711 positive_spans: &[],
1712 positive_deltas: &[],
1713 },
1714 NativeHistogramParityCase {
1715 name: "no observations and zero threshold of zero resulting in no-op span",
1716 observations: &[],
1717 bucket_factor: 1.1,
1718 zero_threshold: Some(ZERO_WIDTH_ZERO_BUCKET),
1719 max_buckets: 0,
1720 max_zero_threshold: 0.0,
1721 sample_count: 0,
1722 sample_sum: 0.0,
1723 schema: 3,
1724 expected_zero_threshold: 0.0,
1725 zero_count: 0,
1726 negative_spans: &[],
1727 negative_deltas: &[],
1728 positive_spans: &[(0, 0)],
1729 positive_deltas: &[],
1730 },
1731 NativeHistogramParityCase {
1732 name: "factor 1.1 results in schema 3",
1733 observations: &[0.0, 1.0, 2.0, 3.0],
1734 bucket_factor: 1.1,
1735 zero_threshold: None,
1736 max_buckets: 0,
1737 max_zero_threshold: 0.0,
1738 sample_count: 4,
1739 sample_sum: 6.0,
1740 schema: 3,
1741 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
1742 zero_count: 1,
1743 negative_spans: &[],
1744 negative_deltas: &[],
1745 positive_spans: &[(0, 1), (7, 1), (4, 1)],
1746 positive_deltas: &[1, 0, 0],
1747 },
1748 NativeHistogramParityCase {
1749 name: "factor 1.2 results in schema 2",
1750 observations: &[0.0, 1.0, 1.2, 1.4, 1.8, 2.0],
1751 bucket_factor: 1.2,
1752 zero_threshold: None,
1753 max_buckets: 0,
1754 max_zero_threshold: 0.0,
1755 sample_count: 6,
1756 sample_sum: 7.4,
1757 schema: 2,
1758 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
1759 zero_count: 1,
1760 negative_spans: &[],
1761 negative_deltas: &[],
1762 positive_spans: &[(0, 5)],
1763 positive_deltas: &[1, -1, 2, -2, 2],
1764 },
1765 NativeHistogramParityCase {
1766 name: "factor 4 results in schema -1",
1767 observations: &[
1768 0.0156251, 0.0625, 0.1, 0.25, 0.5, 1.0, 1.5, 2.0, 3.0, 3.5, 5.0, 6.0, 7.0,
1769 33.33,
1770 ],
1771 bucket_factor: 4.0,
1772 zero_threshold: None,
1773 max_buckets: 0,
1774 max_zero_threshold: 0.0,
1775 sample_count: 14,
1776 sample_sum: 63.2581251,
1777 schema: -1,
1778 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
1779 zero_count: 0,
1780 negative_spans: &[],
1781 negative_deltas: &[],
1782 positive_spans: &[(-2, 6)],
1783 positive_deltas: &[2, 0, 0, 2, -1, -2],
1784 },
1785 NativeHistogramParityCase {
1786 name: "factor 17 results in schema -2",
1787 observations: &[
1788 0.0156251, 0.0625, 0.1, 0.25, 0.5, 1.0, 1.5, 2.0, 3.0, 3.5, 5.0, 6.0, 7.0,
1789 33.33,
1790 ],
1791 bucket_factor: 17.0,
1792 zero_threshold: None,
1793 max_buckets: 0,
1794 max_zero_threshold: 0.0,
1795 sample_count: 14,
1796 sample_sum: 63.2581251,
1797 schema: -2,
1798 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
1799 zero_count: 0,
1800 negative_spans: &[],
1801 negative_deltas: &[],
1802 positive_spans: &[(-1, 4)],
1803 positive_deltas: &[2, 2, 3, -6],
1804 },
1805 NativeHistogramParityCase {
1806 name: "negative buckets",
1807 observations: &[0.0, -1.0, -1.2, -1.4, -1.8, -2.0],
1808 bucket_factor: 1.2,
1809 zero_threshold: None,
1810 max_buckets: 0,
1811 max_zero_threshold: 0.0,
1812 sample_count: 6,
1813 sample_sum: -7.4,
1814 schema: 2,
1815 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
1816 zero_count: 1,
1817 negative_spans: &[(0, 5)],
1818 negative_deltas: &[1, -1, 2, -2, 2],
1819 positive_spans: &[],
1820 positive_deltas: &[],
1821 },
1822 NativeHistogramParityCase {
1823 name: "negative and positive buckets",
1824 observations: &[0.0, -1.0, -1.2, -1.4, -1.8, -2.0, 1.0, 1.2, 1.4, 1.8, 2.0],
1825 bucket_factor: 1.2,
1826 zero_threshold: None,
1827 max_buckets: 0,
1828 max_zero_threshold: 0.0,
1829 sample_count: 11,
1830 sample_sum: 0.0,
1831 schema: 2,
1832 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
1833 zero_count: 1,
1834 negative_spans: &[(0, 5)],
1835 negative_deltas: &[1, -1, 2, -2, 2],
1836 positive_spans: &[(0, 5)],
1837 positive_deltas: &[1, -1, 2, -2, 2],
1838 },
1839 NativeHistogramParityCase {
1840 name: "wide zero bucket",
1841 observations: &[0.0, -1.0, -1.2, -1.4, -1.8, -2.0, 1.0, 1.2, 1.4, 1.8, 2.0],
1842 bucket_factor: 1.2,
1843 zero_threshold: Some(1.4),
1844 max_buckets: 0,
1845 max_zero_threshold: 0.0,
1846 sample_count: 11,
1847 sample_sum: 0.0,
1848 schema: 2,
1849 expected_zero_threshold: 1.4,
1850 zero_count: 7,
1851 negative_spans: &[(4, 1)],
1852 negative_deltas: &[2],
1853 positive_spans: &[(4, 1)],
1854 positive_deltas: &[2],
1855 },
1856 NativeHistogramParityCase {
1857 name: "NaN observation",
1858 observations: &[0.0, 1.0, 1.2, 1.4, 1.8, 2.0, NAN],
1859 bucket_factor: 1.2,
1860 zero_threshold: None,
1861 max_buckets: 0,
1862 max_zero_threshold: 0.0,
1863 sample_count: 7,
1864 sample_sum: NAN,
1865 schema: 2,
1866 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
1867 zero_count: 1,
1868 negative_spans: &[],
1869 negative_deltas: &[],
1870 positive_spans: &[(0, 5)],
1871 positive_deltas: &[1, -1, 2, -2, 2],
1872 },
1873 NativeHistogramParityCase {
1874 name: "+Inf observation",
1875 observations: &[0.0, 1.0, 1.2, 1.4, 1.8, 2.0, INF],
1876 bucket_factor: 1.2,
1877 zero_threshold: None,
1878 max_buckets: 0,
1879 max_zero_threshold: 0.0,
1880 sample_count: 7,
1881 sample_sum: INF,
1882 schema: 2,
1883 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
1884 zero_count: 1,
1885 negative_spans: &[],
1886 negative_deltas: &[],
1887 positive_spans: &[(0, 5), (4092, 1)],
1888 positive_deltas: &[1, -1, 2, -2, 2, -1],
1889 },
1890 NativeHistogramParityCase {
1891 name: "-Inf observation",
1892 observations: &[0.0, 1.0, 1.2, 1.4, 1.8, 2.0, NEG_INF],
1893 bucket_factor: 1.2,
1894 zero_threshold: None,
1895 max_buckets: 0,
1896 max_zero_threshold: 0.0,
1897 sample_count: 7,
1898 sample_sum: NEG_INF,
1899 schema: 2,
1900 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
1901 zero_count: 1,
1902 negative_spans: &[(4097, 1)],
1903 negative_deltas: &[1],
1904 positive_spans: &[(0, 5)],
1905 positive_deltas: &[1, -1, 2, -2, 2],
1906 },
1907 NativeHistogramParityCase {
1908 name: "limited buckets but nothing triggered",
1909 observations: &[0.0, 1.0, 1.2, 1.4, 1.8, 2.0],
1910 bucket_factor: 1.2,
1911 zero_threshold: None,
1912 max_buckets: 4,
1913 max_zero_threshold: 0.0,
1914 sample_count: 6,
1915 sample_sum: 7.4,
1916 schema: 2,
1917 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
1918 zero_count: 1,
1919 negative_spans: &[],
1920 negative_deltas: &[],
1921 positive_spans: &[(0, 5)],
1922 positive_deltas: &[1, -1, 2, -2, 2],
1923 },
1924 NativeHistogramParityCase {
1925 name: "buckets limited by halving resolution",
1926 observations: &[0.0, 1.0, 1.1, 1.2, 1.4, 1.8, 2.0, 3.0],
1927 bucket_factor: 1.2,
1928 zero_threshold: None,
1929 max_buckets: 4,
1930 max_zero_threshold: 0.0,
1931 sample_count: 8,
1932 sample_sum: 11.5,
1933 schema: 1,
1934 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
1935 zero_count: 1,
1936 negative_spans: &[],
1937 negative_deltas: &[],
1938 positive_spans: &[(0, 5)],
1939 positive_deltas: &[1, 2, -1, -2, 1],
1940 },
1941 NativeHistogramParityCase {
1942 name: "buckets limited by widening the zero bucket",
1943 observations: &[0.0, 1.0, 1.1, 1.2, 1.4, 1.8, 2.0, 3.0],
1944 bucket_factor: 1.2,
1945 zero_threshold: None,
1946 max_buckets: 4,
1947 max_zero_threshold: 1.2,
1948 sample_count: 8,
1949 sample_sum: 11.5,
1950 schema: 2,
1951 expected_zero_threshold: 1.0,
1952 zero_count: 2,
1953 negative_spans: &[],
1954 negative_deltas: &[],
1955 positive_spans: &[(1, 7)],
1956 positive_deltas: &[1, 1, -2, 2, -2, 0, 1],
1957 },
1958 NativeHistogramParityCase {
1959 name: "buckets limited by widening the zero bucket twice",
1960 observations: &[0.0, 1.0, 1.1, 1.2, 1.4, 1.8, 2.0, 3.0, 4.0],
1961 bucket_factor: 1.2,
1962 zero_threshold: None,
1963 max_buckets: 4,
1964 max_zero_threshold: 1.2,
1965 sample_count: 9,
1966 sample_sum: 15.5,
1967 schema: 2,
1968 expected_zero_threshold: 1.189207115002721,
1969 zero_count: 3,
1970 negative_spans: &[],
1971 negative_deltas: &[],
1972 positive_spans: &[(2, 7)],
1973 positive_deltas: &[2, -2, 2, -2, 0, 1, 0],
1974 },
1975 NativeHistogramParityCase {
1976 name: "limited buckets but nothing triggered, negative observations",
1977 observations: &[0.0, -1.0, -1.2, -1.4, -1.8, -2.0],
1978 bucket_factor: 1.2,
1979 zero_threshold: None,
1980 max_buckets: 4,
1981 max_zero_threshold: 0.0,
1982 sample_count: 6,
1983 sample_sum: -7.4,
1984 schema: 2,
1985 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
1986 zero_count: 1,
1987 negative_spans: &[(0, 5)],
1988 negative_deltas: &[1, -1, 2, -2, 2],
1989 positive_spans: &[],
1990 positive_deltas: &[],
1991 },
1992 NativeHistogramParityCase {
1993 name: "buckets limited by halving resolution, negative observations",
1994 observations: &[0.0, -1.0, -1.1, -1.2, -1.4, -1.8, -2.0, -3.0],
1995 bucket_factor: 1.2,
1996 zero_threshold: None,
1997 max_buckets: 4,
1998 max_zero_threshold: 0.0,
1999 sample_count: 8,
2000 sample_sum: -11.5,
2001 schema: 1,
2002 expected_zero_threshold: DEFAULT_ZERO_THRESHOLD,
2003 zero_count: 1,
2004 negative_spans: &[(0, 5)],
2005 negative_deltas: &[1, 2, -1, -2, 1],
2006 positive_spans: &[],
2007 positive_deltas: &[],
2008 },
2009 NativeHistogramParityCase {
2010 name: "buckets limited by widening the zero bucket, negative observations",
2011 observations: &[0.0, -1.0, -1.1, -1.2, -1.4, -1.8, -2.0, -3.0],
2012 bucket_factor: 1.2,
2013 zero_threshold: None,
2014 max_buckets: 4,
2015 max_zero_threshold: 1.2,
2016 sample_count: 8,
2017 sample_sum: -11.5,
2018 schema: 2,
2019 expected_zero_threshold: 1.0,
2020 zero_count: 2,
2021 negative_spans: &[(1, 7)],
2022 negative_deltas: &[1, 1, -2, 2, -2, 0, 1],
2023 positive_spans: &[],
2024 positive_deltas: &[],
2025 },
2026 NativeHistogramParityCase {
2027 name: "buckets limited by widening the zero bucket twice, negative observations",
2028 observations: &[0.0, -1.0, -1.1, -1.2, -1.4, -1.8, -2.0, -3.0, -4.0],
2029 bucket_factor: 1.2,
2030 zero_threshold: None,
2031 max_buckets: 4,
2032 max_zero_threshold: 1.2,
2033 sample_count: 9,
2034 sample_sum: -15.5,
2035 schema: 2,
2036 expected_zero_threshold: 1.189207115002721,
2037 zero_count: 3,
2038 negative_spans: &[(2, 7)],
2039 negative_deltas: &[2, -2, 2, -2, 0, 1, 0],
2040 positive_spans: &[],
2041 positive_deltas: &[],
2042 },
2043 ];
2044
2045 for case in cases {
2046 let mut config = NativeHistogramConfig::new(case.bucket_factor)
2047 .max_buckets(case.max_buckets)
2048 .max_zero_threshold(case.max_zero_threshold);
2049 if let Some(zero_threshold) = case.zero_threshold {
2050 config = config.zero_threshold(zero_threshold);
2051 }
2052 let h = Histogram::new_native(config);
2053 for observation in case.observations {
2054 h.observe(*observation);
2055 }
2056
2057 let snapshot = h.snapshot().unwrap();
2058 let native = snapshot.native.unwrap();
2059
2060 assert_eq!(case.sample_count, snapshot.count, "{}", case.name);
2061 if case.sample_sum.is_nan() {
2062 assert!(snapshot.sum.is_nan(), "{}", case.name);
2063 } else {
2064 assert_eq!(case.sample_sum, snapshot.sum, "{}", case.name);
2065 }
2066 assert_eq!(case.schema, native.schema, "{}", case.name);
2067 assert_eq!(
2068 case.expected_zero_threshold, native.zero_threshold,
2069 "{}",
2070 case.name
2071 );
2072 assert_eq!(case.zero_count, native.zero_count, "{}", case.name);
2073 assert_eq!(case.negative_spans, native.negative.spans, "{}", case.name);
2074 assert_eq!(
2075 case.negative_deltas, native.negative.deltas,
2076 "{}",
2077 case.name
2078 );
2079 assert_eq!(case.positive_spans, native.positive.spans, "{}", case.name);
2080 assert_eq!(
2081 case.positive_deltas, native.positive.deltas,
2082 "{}",
2083 case.name
2084 );
2085 }
2086 }
2087
2088 #[test]
2089 fn native_histogram_maps_positive_infinity_into_sparse_bucket() {
2090 let h = Histogram::new_native(NativeHistogramConfig::with_schema(4));
2091 h.observe(f64::INFINITY);
2092 let inner = h.inner.lock();
2093 let native = inner.native.as_ref().unwrap();
2094 assert_eq!(
2095 Some(1),
2096 native
2097 .positive
2098 .iter()
2099 .find(|(index, _)| *index == 16385)
2100 .map(|(_, count)| *count)
2101 );
2102 assert!(!native.positive.iter().any(|(index, _)| *index == i32::MAX));
2103 }
2104
2105 #[test]
2106 fn native_histogram_widens_zero_bucket_at_min_schema_when_limited() {
2107 let mut inner = NativeHistogramState {
2108 initial_zero_threshold: 0.0,
2109 initial_schema: SCHEMA_MIN,
2110 zero_threshold: 0.0,
2111 zero_count: 0,
2112 schema: SCHEMA_MIN,
2113 max_buckets: 1,
2114 min_reset_duration: None,
2115 max_zero_threshold: 1.0,
2116 created: SystemTime::now(),
2117 scheduled_reset: None,
2118 positive: vec![(-10, 2), (0, 1)],
2119 negative: Vec::new(),
2120 };
2121
2122 assert!(widen_zero_bucket(&mut inner));
2123 assert!(inner.zero_threshold > 0.0);
2124 assert!(inner.zero_count >= 2);
2125 }
2126
2127 #[test]
2128 fn native_histogram_encodes_spans_and_deltas() {
2129 let buckets = vec![(-10, 1), (-7, 7), (-5, 7), (2, 8)];
2130
2131 let encoding = encode_spans_and_deltas(&buckets).unwrap();
2132
2133 assert_eq!(encoding.spans, vec![(-10, 6), (6, 1)]);
2134 assert_eq!(encoding.deltas, vec![1, -1, 0, 7, -7, 7, 1]);
2135 }
2136
2137 #[test]
2138 fn native_histogram_downsamples_like_go_client() {
2139 let buckets = vec![(-2, 1), (-1, 2), (0, 4), (1, 8)];
2140
2141 let downsampled = downsample_buckets(&buckets);
2142
2143 assert_eq!(downsampled, vec![(-1, 1), (0, 6), (1, 8)]);
2144 }
2145
2146 #[test]
2147 fn native_histogram_positive_upper_bound_uses_bucket_index() {
2148 assert_eq!(1.0, positive_upper_bound(0, 0));
2149 assert_eq!(2.0, positive_upper_bound(0, 1));
2150 assert!((positive_upper_bound(3, 8) - 2.0).abs() <= 4.0 * f64::EPSILON);
2151 assert_eq!(f64::MAX, positive_upper_bound(0, 1024));
2152 assert_eq!(f64::INFINITY, positive_upper_bound(0, 1025));
2153 assert_eq!(f64::MAX, positive_upper_bound(8, 262144));
2154 assert_eq!(f64::INFINITY, positive_upper_bound(8, 262145));
2155 }
2156
2157 #[test]
2158 fn native_histogram_bucket_index_matches_standard_boundaries() {
2159 assert_eq!(0, bucket_index(0, 1.0, false));
2160 assert_eq!(
2161 1,
2162 bucket_index(0, f64::from_bits(1.0f64.to_bits() + 1), false)
2163 );
2164 assert_eq!(8, bucket_index(3, 2.0, false));
2165 assert_eq!(
2166 -1074,
2167 bucket_index(0, f64::MIN_POSITIVE / 2f64.powi(52), false)
2168 );
2169 assert_eq!(1024, bucket_index(0, f64::MAX, false));
2170 assert_eq!(1025, bucket_index(0, f64::INFINITY, true));
2171 assert_eq!(262144, bucket_index(8, f64::MAX, false));
2172 assert_eq!(262145, bucket_index(8, f64::INFINITY, true));
2173 }
2174
2175 #[test]
2176 fn native_histogram_bounds_match_standard_formula() {
2177 for (schema, bounds) in NATIVE_HISTOGRAM_BOUNDS.iter().enumerate() {
2178 assert_eq!(1 << schema, bounds.len());
2179
2180 for (i, bound) in bounds.iter().enumerate() {
2181 let expected = 2f64.powf(i as f64 / (1 << schema) as f64) / 2.0;
2182 assert!(
2183 (*bound - expected).abs() <= 4.0 * f64::EPSILON,
2184 "schema {schema} index {i}: expected {expected}, got {bound}"
2185 );
2186 }
2187 }
2188 }
2189
2190 #[test]
2191 fn native_histogram_no_op_span_marks_zero_width_nan_only_histogram() {
2192 let h = Histogram::new_native(
2193 NativeHistogramConfig::with_schema(0)
2194 .zero_threshold(NATIVE_HISTOGRAM_ZERO_THRESHOLD_ZERO),
2195 );
2196 h.observe(f64::NAN);
2197
2198 let snapshot = h.snapshot().unwrap();
2199 let native = snapshot.native.unwrap();
2200
2201 assert_eq!(1, snapshot.count);
2202 assert_eq!(0.0, native.zero_threshold);
2203 assert_eq!(0, native.zero_count);
2204 assert_eq!(vec![(0, 0)], native.positive.spans);
2205 assert!(native.positive.deltas.is_empty());
2206 assert!(native.negative.spans.is_empty());
2207 assert!(native.negative.deltas.is_empty());
2208 }
2209
2210 #[test]
2211 fn native_histogram_fails_for_counts_not_fitting_delta_wire_type() {
2212 let buckets = vec![(0, i64::MAX as u64 + 1)];
2213
2214 assert!(encode_spans_and_deltas(&buckets).is_err());
2215 }
2216
2217 #[test]
2218 fn classic_and_native_histogram_updates_both_representations() {
2219 let h =
2220 Histogram::new_classic_and_native([1.0, 2.0], NativeHistogramConfig::with_schema(0));
2221 h.observe(1.0);
2222 h.observe(4.0);
2223
2224 let inner = h.inner.lock();
2225 let native = inner.native.as_ref().unwrap();
2226 assert_eq!(2, inner.count);
2227 assert_eq!(5.0, inner.sum);
2228 assert_eq!(1, inner.buckets[0].1);
2229 assert_eq!(1, inner.buckets[2].1);
2230 assert_eq!(
2231 2,
2232 native.positive.iter().map(|(_, count)| *count).sum::<u64>()
2233 );
2234 }
2235}
2236
2237#[cfg(test)]
2238mod overflow_bucket_tests {
2239 use super::Histogram;
2240
2241 #[test]
2243 fn infinite_observation_lands_in_the_overflow_bucket() {
2244 let histogram = Histogram::new([1.0, 2.0]);
2245 histogram.observe(0.5);
2246 histogram.observe(f64::INFINITY);
2247 histogram.observe(f64::NAN);
2248
2249 let inner = histogram.inner.lock();
2250 let cumulative: u64 = inner.buckets.iter().map(|(_, count)| count).sum();
2251 assert_eq!(
2252 cumulative, inner.count,
2253 "every observation must be in some bucket; buckets={:?} count={}",
2254 inner.buckets, inner.count
2255 );
2256 let (last_bound, last_count) = *inner.buckets.last().expect("no buckets");
2257 assert!(
2258 last_bound.is_infinite(),
2259 "overflow bound is {last_bound:e}, not +Inf"
2260 );
2261 assert_eq!(
2262 last_count, 2,
2263 "the +Inf and NaN observations belong to the overflow bucket"
2264 );
2265 }
2266
2267 #[test]
2269 fn explicit_infinite_bound_does_not_duplicate_the_overflow_bucket() {
2270 use crate::encoding::text::encode;
2271 use crate::registry::Registry;
2272
2273 let histogram = Histogram::new([1.0, f64::INFINITY, f64::NAN, f64::NEG_INFINITY]);
2274 histogram.observe(0.5);
2275
2276 {
2277 let inner = histogram.inner.lock();
2278 let infinite = inner
2279 .buckets
2280 .iter()
2281 .filter(|(upper_bound, _)| upper_bound.is_infinite())
2282 .count();
2283 assert_eq!(
2284 infinite, 1,
2285 "exactly one infinite bucket expected, got buckets={:?}",
2286 inner.buckets
2287 );
2288 assert_eq!(
2289 inner.buckets.len(),
2290 2,
2291 "only the finite 1.0 bound plus the implicit overflow bucket should remain, got {:?}",
2292 inner.buckets
2293 );
2294 }
2295
2296 let mut registry = Registry::default();
2297 registry.register("my_histogram", "My histogram", histogram);
2298 let mut encoded = String::new();
2299 encode(&mut encoded, ®istry).expect("encode");
2300 assert_eq!(
2301 encoded.matches(r#"le="+Inf""#).count(),
2302 1,
2303 "the exposition must contain exactly one +Inf bucket:\n{encoded}"
2304 );
2305 }
2306}