Skip to main content

prometheus_client/metrics/
histogram.rs

1//! Module implementing an Open Metrics histogram.
2//!
3//! See [`Histogram`] for details.
4
5use 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
18/// Special zero-threshold sentinel that configures a zero-width zero bucket.
19pub const NATIVE_HISTOGRAM_ZERO_THRESHOLD_ZERO: f64 = -1.0;
20
21/// Default bucket factor for native histograms.
22pub const DEFAULT_NATIVE_HISTOGRAM_BUCKET_FACTOR: f64 = 1.1;
23
24/// Default zero threshold for native histograms.
25pub const DEFAULT_NATIVE_HISTOGRAM_ZERO_THRESHOLD: f64 = 2.938735877055719e-39;
26
27const SCHEMA_MIN: i8 = -4;
28const SCHEMA_MAX: i8 = 8;
29
30/// Open Metrics [`Histogram`] to measure distributions of discrete events.
31///
32/// ```
33/// # use prometheus_client::metrics::histogram::{Histogram, exponential_buckets};
34/// let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10));
35/// histogram.observe(4.2);
36/// ```
37///
38/// [`Histogram`] does not implement [`Default`], given that the choice of
39/// bucket values depends on the situation [`Histogram`] is used in. As an
40/// example, to measure HTTP request latency, the values suggested in the
41/// Golang implementation might work for you:
42///
43/// ```
44/// # use prometheus_client::metrics::histogram::Histogram;
45/// // Default values from go client(https://github.com/prometheus/client_golang/blob/5d584e2717ef525673736d72cd1d12e304f243d7/prometheus/histogram.go#L68)
46/// let custom_buckets = [
47///    0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
48/// ];
49/// let histogram = Histogram::new(custom_buckets);
50/// histogram.observe(4.2);
51/// ```
52// TODO: Consider using atomics. See
53// https://github.com/tikv/rust-prometheus/pull/314.
54#[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    // TODO: Consider allowing integer observe values.
70    sum: f64,
71    count: u64,
72    // TODO: Consider being generic over the bucket length.
73    buckets: Vec<(f64, u64)>,
74    native: Option<NativeHistogramState>,
75}
76
77/// Configuration for native histogram buckets.
78///
79/// Native histogram exemplars are not currently emitted.
80#[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    /// Create a native histogram configuration with the provided bucket factor.
91    ///
92    /// The schema is chosen so that the bucket growth factor is the largest
93    /// supported factor that is still <= `bucket_factor`. The default zero
94    /// threshold is 2^-128, as recommended by the Prometheus native histogram
95    /// specification.
96    pub fn new(bucket_factor: f64) -> Self {
97        Self::with_bucket_factor(bucket_factor)
98    }
99
100    /// Create a native histogram configuration with the provided schema.
101    ///
102    /// Valid standard schema values are in [-4, 8]. Prefer [`Self::new`] unless
103    /// you explicitly need a schema-level configuration.
104    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    /// Create a native histogram configuration by choosing the schema whose
117    /// growth factor is the largest factor <= `bucket_factor`.
118    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    /// Set a custom zero threshold.
124    ///
125    /// A value of `0.0` keeps the default threshold. A negative value configures
126    /// a zero-width zero bucket.
127    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    /// Set a best-effort maximum number of sparse buckets across both sides.
140    ///
141    /// `0` means unbounded. If the lowest supported resolution still exceeds
142    /// the limit, observations remain in sparse buckets until a later zero
143    /// bucket widening can merge them.
144    pub fn max_buckets(mut self, max_buckets: usize) -> Self {
145        self.max_buckets = max_buckets;
146        self
147    }
148
149    /// Reset the histogram instead of reducing resolution if the bucket limit
150    /// is exceeded after at least this duration has elapsed since creation or
151    /// the last reset. A zero duration disables reset-based limiting.
152    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    /// Set the maximum zero threshold allowed while enforcing `max_buckets`.
159    ///
160    /// The default is `0.0`, which disables zero-bucket widening unless the
161    /// current threshold is already below a configured positive maximum.
162    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        // Distinguish a native histogram from a classic histogram in protobuf
288        // when it has no sparse bucket data and zero threshold is zero.
289        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    /// Create a new [`Histogram`].
314    ///
315    /// ```rust
316    /// # use prometheus_client::metrics::histogram::Histogram;
317    /// let histogram = Histogram::new([10.0, 100.0, 1_000.0]);
318    /// ```
319    ///
320    /// The overflow bucket is implicit and always present, so non-finite bounds in `buckets` are
321    /// dropped: passing `f64::INFINITY` explicitly would otherwise produce two `le="+Inf"` series
322    /// for the same metric. `client_golang` strips an explicitly-configured `+Inf` bound for the
323    /// same reason. A `NaN` bound is dropped too, since no observation can ever match it.
324    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    /// Create a new native [`Histogram`] without classic buckets.
341    ///
342    /// Native-only histograms can only be encoded by the Prometheus protobuf
343    /// encoder. Text and OpenMetrics protobuf encoders do not have native
344    /// histogram fields and reject native-only histograms.
345    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    /// Create a new [`Histogram`] with both classic and native buckets.
357    ///
358    /// Prometheus protobuf encodes both representations in one histogram
359    /// message. Encoders without native histogram support encode the classic
360    /// buckets.
361    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    /// Observe the given value.
371    pub fn observe(&self, v: f64) {
372        self.observe_and_bucket(v);
373    }
374
375    /// Returns the current sum of all observations.
376    #[cfg(any(test, feature = "test-util"))]
377    pub fn sum(&self) -> f64 {
378        self.inner.lock().sum
379    }
380
381    /// Returns the current number of observations.
382    #[cfg(any(test, feature = "test-util"))]
383    pub fn count(&self) -> u64 {
384        self.inner.lock().count
385    }
386
387    /// Observes the given value, returning the index of the first bucket the
388    /// value is added to.
389    ///
390    /// Needed in
391    /// [`HistogramWithExemplars`](crate::metrics::exemplar::HistogramWithExemplars).
392    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
472/// Exponential bucket distribution.
473pub 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
480/// Exponential bucket distribution within a range
481///
482/// Creates `length` buckets, where the lowest bucket is `min` and the highest bucket is `max`.
483///
484/// If `length` is less than 1, or `min` is less than or equal to 0, an empty iterator is returned.
485pub 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    // length needs a positive length and min needs to be greater than 0
489    // set len_observed to 0 and min_bucket to 1.0
490    // this will return an empty iterator in the result
491    if length < 1 || min <= 0.0 {
492        len_observed = 0;
493        min_bucket = 1.0;
494    }
495    // We know max/min and highest bucket. Solve for growth_factor.
496    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
504/// Linear bucket distribution.
505pub 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
604// copied from https://github.com/prometheus/client_golang/blob/f23aad527b9740eda20fe5db147e6cd621c2c1bc/prometheus/histogram.go#L46
605const 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    /// Checks that [`Histogram::count()`] works properly.
1390    #[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    /// Checks that [`Histogram::sum()`] works properly.
1410    #[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); // 3 + 4 + 15 + 101 = 123
1421        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    // Copying tests from Go https://github.com/prometheus/client_golang/blob/ef80579ce2dff4ae60710045fd1d38e366adaed0/prometheus/histogram_test.go#L472
1668    // 1:1 to check compatibility
1669    #[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    /// Every observation, including `+Inf` and `NaN`, must be counted in some bucket.
2242    #[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    /// An explicit `+Inf` bound must not duplicate the implicit overflow bucket.
2268    #[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, &registry).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}