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