metrics_exporter_prometheus/
native_histogram.rs

1//! Native histogram support for Prometheus.
2//!
3//! This module implements Prometheus native histograms, which use exponential buckets
4//! to efficiently represent histogram data without requiring predefined bucket boundaries.
5
6use std::collections::btree_map::Entry;
7use std::sync::atomic::{AtomicI32, AtomicU64, Ordering};
8
9/// IEEE 754 frexp implementation matching Go's math.Frexp behavior.
10/// Returns (mantissa, exponent) such that f = mantissa × 2^exponent,
11/// where mantissa is in the range [0.5, 1) for finite non-zero f.
12fn frexp(f: f64) -> (f64, i32) {
13    if f == 0.0 || !f.is_finite() {
14        return (f, 0);
15    }
16
17    let bits = f.to_bits();
18    let sign_bit = bits & (1u64 << 63);
19    let mut exp_bits = (bits >> 52) & 0x7FF;
20    let mut mantissa_bits = bits & 0x000F_FFFF_FFFF_FFFF;
21
22    if exp_bits == 0 {
23        // Subnormal number - normalize it
24        let shift = mantissa_bits.leading_zeros() - 12; // 12 = 64 - 52
25        mantissa_bits <<= shift + 1;
26        mantissa_bits &= 0x000F_FFFF_FFFF_FFFF; // Clear the implicit 1 bit
27        #[allow(clippy::cast_sign_loss, clippy::cast_possible_wrap)]
28        {
29            exp_bits = (1022_i32 - shift as i32) as u64; // Adjust exponent
30        }
31        let mantissa_f64_bits = sign_bit | (1022u64 << 52) | mantissa_bits;
32        let mantissa = f64::from_bits(mantissa_f64_bits);
33        #[allow(clippy::cast_possible_truncation)]
34        let exponent = exp_bits as i32 - 1022;
35        (mantissa, exponent)
36    } else {
37        // Normal number
38        #[allow(clippy::cast_possible_truncation)]
39        let exponent = exp_bits as i32 - 1023; // Remove IEEE bias, then subtract 1 more for [0.5,1) range
40        let mantissa_f64_bits = sign_bit | (1022u64 << 52) | mantissa_bits; // Set exponent to 1022 (bias-1) for [0.5,1) range
41        let mantissa = f64::from_bits(mantissa_f64_bits);
42        (mantissa, exponent + 1)
43    }
44}
45
46/// Schema constants
47const MIN_SCHEMA: i32 = -4;
48const MAX_SCHEMA: i32 = 8;
49
50/// Native histogram bounds for different schemas (from Go implementation)
51#[allow(clippy::unreadable_literal)]
52const NATIVE_HISTOGRAM_BOUNDS: &[&[f64]] = &[
53    // Schema "0":
54    &[0.5],
55    // Schema 1:
56    &[0.5, 0.7071067811865475],
57    // Schema 2:
58    &[0.5, 0.5946035575013605, 0.7071067811865475, 0.8408964152537144],
59    // Schema 3:
60    &[
61        0.5,
62        0.5452538663326288,
63        0.5946035575013605,
64        0.6484197773255048,
65        0.7071067811865475,
66        0.7711054127039704,
67        0.8408964152537144,
68        0.9170040432046711,
69    ],
70    // Schema 4:
71    &[
72        0.5,
73        0.5221368912137069,
74        0.5452538663326288,
75        0.5693943173783458,
76        0.5946035575013605,
77        0.620928906036742,
78        0.6484197773255048,
79        0.6771277734684463,
80        0.7071067811865475,
81        0.7384130729697496,
82        0.7711054127039704,
83        0.805245165974627,
84        0.8408964152537144,
85        0.8781260801866495,
86        0.9170040432046711,
87        0.9576032806985735,
88    ],
89    // Schema 5:
90    &[
91        0.5,
92        0.5109485743270583,
93        0.5221368912137069,
94        0.5335702003384117,
95        0.5452538663326288,
96        0.5571933712979462,
97        0.5693943173783458,
98        0.5818624293887887,
99        0.5946035575013605,
100        0.6076236799902344,
101        0.620928906036742,
102        0.6345254785958666,
103        0.6484197773255048,
104        0.6626183215798706,
105        0.6771277734684463,
106        0.6919549409819159,
107        0.7071067811865475,
108        0.7225904034885232,
109        0.7384130729697496,
110        0.7545822137967112,
111        0.7711054127039704,
112        0.7879904225539431,
113        0.805245165974627,
114        0.8228777390769823,
115        0.8408964152537144,
116        0.8593096490612387,
117        0.8781260801866495,
118        0.8973545375015533,
119        0.9170040432046711,
120        0.9370838170551498,
121        0.9576032806985735,
122        0.9785720620876999,
123    ],
124    // Schema 6:
125    &[
126        0.5,
127        0.5054446430258502,
128        0.5109485743270583,
129        0.5165124395106142,
130        0.5221368912137069,
131        0.5278225891802786,
132        0.5335702003384117,
133        0.5393803988785598,
134        0.5452538663326288,
135        0.5511912916539204,
136        0.5571933712979462,
137        0.5632608093041209,
138        0.5693943173783458,
139        0.5755946149764913,
140        0.5818624293887887,
141        0.5881984958251406,
142        0.5946035575013605,
143        0.6010783657263515,
144        0.6076236799902344,
145        0.6142402680534349,
146        0.620928906036742,
147        0.6276903785123455,
148        0.6345254785958666,
149        0.6414350080393891,
150        0.6484197773255048,
151        0.6554806057623822,
152        0.6626183215798706,
153        0.6698337620266515,
154        0.6771277734684463,
155        0.6845012114872953,
156        0.6919549409819159,
157        0.6994898362691555,
158        0.7071067811865475,
159        0.7148066691959849,
160        0.7225904034885232,
161        0.7304588970903234,
162        0.7384130729697496,
163        0.7464538641456323,
164        0.7545822137967112,
165        0.762799075372269,
166        0.7711054127039704,
167        0.7795022001189185,
168        0.7879904225539431,
169        0.7965710756711334,
170        0.805245165974627,
171        0.8140137109286738,
172        0.8228777390769823,
173        0.8318382901633681,
174        0.8408964152537144,
175        0.8500531768592616,
176        0.8593096490612387,
177        0.8686669176368529,
178        0.8781260801866495,
179        0.8876882462632604,
180        0.8973545375015533,
181        0.9071260877501991,
182        0.9170040432046711,
183        0.9269895625416926,
184        0.9370838170551498,
185        0.9472879907934827,
186        0.9576032806985735,
187        0.9680308967461471,
188        0.9785720620876999,
189        0.9892280131939752,
190    ],
191    // Schema 7:
192    &[
193        0.5,
194        0.5027149505564014,
195        0.5054446430258502,
196        0.5081891574554764,
197        0.5109485743270583,
198        0.5137229745593818,
199        0.5165124395106142,
200        0.5193170509806894,
201        0.5221368912137069,
202        0.5249720429003435,
203        0.5278225891802786,
204        0.5306886136446309,
205        0.5335702003384117,
206        0.5364674337629877,
207        0.5393803988785598,
208        0.5423091811066545,
209        0.5452538663326288,
210        0.5482145409081883,
211        0.5511912916539204,
212        0.5541842058618393,
213        0.5571933712979462,
214        0.5602188762048033,
215        0.5632608093041209,
216        0.5663192597993595,
217        0.5693943173783458,
218        0.572486072215902,
219        0.5755946149764913,
220        0.5787200368168754,
221        0.5818624293887887,
222        0.585021884841625,
223        0.5881984958251406,
224        0.5913923554921704,
225        0.5946035575013605,
226        0.5978321960199137,
227        0.6010783657263515,
228        0.6043421618132907,
229        0.6076236799902344,
230        0.6109230164863786,
231        0.6142402680534349,
232        0.6175755319684665,
233        0.620928906036742,
234        0.6243004885946023,
235        0.6276903785123455,
236        0.6310986751971253,
237        0.6345254785958666,
238        0.637970889198196,
239        0.6414350080393891,
240        0.6449179367033329,
241        0.6484197773255048,
242        0.6519406325959679,
243        0.6554806057623822,
244        0.659039800633032,
245        0.6626183215798706,
246        0.6662162735415805,
247        0.6698337620266515,
248        0.6734708931164728,
249        0.6771277734684463,
250        0.6808045103191123,
251        0.6845012114872953,
252        0.688217985377265,
253        0.6919549409819159,
254        0.6957121878859629,
255        0.6994898362691555,
256        0.7032879969095076,
257        0.7071067811865475,
258        0.7109463010845827,
259        0.7148066691959849,
260        0.718687998724491,
261        0.7225904034885232,
262        0.7265139979245261,
263        0.7304588970903234,
264        0.7344252166684908,
265        0.7384130729697496,
266        0.7424225829363761,
267        0.7464538641456323,
268        0.7505070348132126,
269        0.7545822137967112,
270        0.7586795205991071,
271        0.762799075372269,
272        0.7669409989204777,
273        0.7711054127039704,
274        0.7752924388424999,
275        0.7795022001189185,
276        0.7837348199827764,
277        0.7879904225539431,
278        0.7922691326262467,
279        0.7965710756711334,
280        0.8008963778413465,
281        0.805245165974627,
282        0.8096175675974316,
283        0.8140137109286738,
284        0.8184337248834821,
285        0.8228777390769823,
286        0.8273458838280969,
287        0.8318382901633681,
288        0.8363550898207981,
289        0.8408964152537144,
290        0.8454623996346523,
291        0.8500531768592616,
292        0.8546688815502312,
293        0.8593096490612387,
294        0.8639756154809185,
295        0.8686669176368529,
296        0.8733836930995842,
297        0.8781260801866495,
298        0.8828942179666361,
299        0.8876882462632604,
300        0.8925083056594671,
301        0.8973545375015533,
302        0.9022270839033115,
303        0.9071260877501991,
304        0.9120516927035263,
305        0.9170040432046711,
306        0.9219832844793128,
307        0.9269895625416926,
308        0.9320230241988943,
309        0.9370838170551498,
310        0.9421720895161669,
311        0.9472879907934827,
312        0.9524316709088368,
313        0.9576032806985735,
314        0.9628029718180622,
315        0.9680308967461471,
316        0.9732872087896164,
317        0.9785720620876999,
318        0.9838856116165875,
319        0.9892280131939752,
320        0.9945994234836328,
321    ],
322    // Schema 8:
323    &[
324        0.5,
325        0.5013556375251013,
326        0.5027149505564014,
327        0.5040779490592088,
328        0.5054446430258502,
329        0.5068150424757447,
330        0.5081891574554764,
331        0.509566998038869,
332        0.5109485743270583,
333        0.5123338964485679,
334        0.5137229745593818,
335        0.5151158188430205,
336        0.5165124395106142,
337        0.5179128468009786,
338        0.5193170509806894,
339        0.520725062344158,
340        0.5221368912137069,
341        0.5235525479396449,
342        0.5249720429003435,
343        0.526395386502313,
344        0.5278225891802786,
345        0.5292536613972564,
346        0.5306886136446309,
347        0.5321274564422321,
348        0.5335702003384117,
349        0.5350168559101208,
350        0.5364674337629877,
351        0.5379219445313954,
352        0.5393803988785598,
353        0.5408428074966075,
354        0.5423091811066545,
355        0.5437795304588847,
356        0.5452538663326288,
357        0.5467321995364429,
358        0.5482145409081883,
359        0.549700901315111,
360        0.5511912916539204,
361        0.5526857228508706,
362        0.5541842058618393,
363        0.5556867516724088,
364        0.5571933712979462,
365        0.5587040757836845,
366        0.5602188762048033,
367        0.5617377836665098,
368        0.5632608093041209,
369        0.564787964283144,
370        0.5663192597993595,
371        0.5678547070789026,
372        0.5693943173783458,
373        0.5709381019847808,
374        0.572486072215902,
375        0.5740382394200894,
376        0.5755946149764913,
377        0.5771552102951081,
378        0.5787200368168754,
379        0.5802891060137493,
380        0.5818624293887887,
381        0.5834400184762408,
382        0.585021884841625,
383        0.5866080400818185,
384        0.5881984958251406,
385        0.5897932637314379,
386        0.5913923554921704,
387        0.5929957828304968,
388        0.5946035575013605,
389        0.5962156912915756,
390        0.5978321960199137,
391        0.5994530835371903,
392        0.6010783657263515,
393        0.6027080545025619,
394        0.6043421618132907,
395        0.6059806996384005,
396        0.6076236799902344,
397        0.6092711149137041,
398        0.6109230164863786,
399        0.6125793968185725,
400        0.6142402680534349,
401        0.6159056423670379,
402        0.6175755319684665,
403        0.6192499490999082,
404        0.620928906036742,
405        0.622612415087629,
406        0.6243004885946023,
407        0.6259931389331581,
408        0.6276903785123455,
409        0.6293922197748583,
410        0.6310986751971253,
411        0.6328097572894031,
412        0.6345254785958666,
413        0.6362458516947014,
414        0.637970889198196,
415        0.6397006037528346,
416        0.6414350080393891,
417        0.6431741147730128,
418        0.6449179367033329,
419        0.6466664866145447,
420        0.6484197773255048,
421        0.6501778216898253,
422        0.6519406325959679,
423        0.6537082229673385,
424        0.6554806057623822,
425        0.6572577939746774,
426        0.659039800633032,
427        0.6608266388015788,
428        0.6626183215798706,
429        0.6644148621029772,
430        0.6662162735415805,
431        0.6680225691020727,
432        0.6698337620266515,
433        0.6716498655934177,
434        0.6734708931164728,
435        0.6752968579460171,
436        0.6771277734684463,
437        0.6789636531064505,
438        0.6808045103191123,
439        0.6826503586020058,
440        0.6845012114872953,
441        0.6863570825438342,
442        0.688217985377265,
443        0.690083933630119,
444        0.6919549409819159,
445        0.6938310211492645,
446        0.6957121878859629,
447        0.6975984549830999,
448        0.6994898362691555,
449        0.7013863456101023,
450        0.7032879969095076,
451        0.7051948041086352,
452        0.7071067811865475,
453        0.7090239421602076,
454        0.7109463010845827,
455        0.7128738720527471,
456        0.7148066691959849,
457        0.7167447066838943,
458        0.718687998724491,
459        0.7206365595643126,
460        0.7225904034885232,
461        0.7245495448210174,
462        0.7265139979245261,
463        0.7284837772007218,
464        0.7304588970903234,
465        0.7324393720732029,
466        0.7344252166684908,
467        0.7364164454346837,
468        0.7384130729697496,
469        0.7404151139112358,
470        0.7424225829363761,
471        0.7444354947621984,
472        0.7464538641456323,
473        0.7484777058836176,
474        0.7505070348132126,
475        0.7525418658117031,
476        0.7545822137967112,
477        0.7566280937263048,
478        0.7586795205991071,
479        0.7607365094544071,
480        0.762799075372269,
481        0.7648672334736434,
482        0.7669409989204777,
483        0.7690203869158282,
484        0.7711054127039704,
485        0.7731960915705107,
486        0.7752924388424999,
487        0.7773944698885442,
488        0.7795022001189185,
489        0.7816156449856788,
490        0.7837348199827764,
491        0.7858597406461707,
492        0.7879904225539431,
493        0.7901268813264122,
494        0.7922691326262467,
495        0.7944171921585818,
496        0.7965710756711334,
497        0.7987307989543135,
498        0.8008963778413465,
499        0.8030678282083853,
500        0.805245165974627,
501        0.8074284071024302,
502        0.8096175675974316,
503        0.8118126635086642,
504        0.8140137109286738,
505        0.8162207259936375,
506        0.8184337248834821,
507        0.820652723822003,
508        0.8228777390769823,
509        0.8251087869603088,
510        0.8273458838280969,
511        0.8295890460808079,
512        0.8318382901633681,
513        0.8340936325652911,
514        0.8363550898207981,
515        0.8386226785089391,
516        0.8408964152537144,
517        0.8431763167241966,
518        0.8454623996346523,
519        0.8477546807446661,
520        0.8500531768592616,
521        0.8523579048290255,
522        0.8546688815502312,
523        0.8569861239649629,
524        0.8593096490612387,
525        0.8616394738731368,
526        0.8639756154809185,
527        0.8663180910111553,
528        0.8686669176368529,
529        0.871022112577578,
530        0.8733836930995842,
531        0.8757516765159389,
532        0.8781260801866495,
533        0.8805069215187917,
534        0.8828942179666361,
535        0.8852879870317771,
536        0.8876882462632604,
537        0.890095013257712,
538        0.8925083056594671,
539        0.8949281411607002,
540        0.8973545375015533,
541        0.8997875124702672,
542        0.9022270839033115,
543        0.9046732696855155,
544        0.9071260877501991,
545        0.909585556079304,
546        0.9120516927035263,
547        0.9145245157024483,
548        0.9170040432046711,
549        0.9194902933879467,
550        0.9219832844793128,
551        0.9244830347552253,
552        0.9269895625416926,
553        0.92950288621441,
554        0.9320230241988943,
555        0.9345499949706191,
556        0.9370838170551498,
557        0.93962450902828,
558        0.9421720895161669,
559        0.9447265771954693,
560        0.9472879907934827,
561        0.9498563490882775,
562        0.9524316709088368,
563        0.9550139751351947,
564        0.9576032806985735,
565        0.9601996065815236,
566        0.9628029718180622,
567        0.9654133954938133,
568        0.9680308967461471,
569        0.9706554947643201,
570        0.9732872087896164,
571        0.9759260581154889,
572        0.9785720620876999,
573        0.9812252401044634,
574        0.9838856116165875,
575        0.9865531961276168,
576        0.9892280131939752,
577        0.9919100824251095,
578        0.9945994234836328,
579        0.9972960560854698,
580    ],
581];
582
583/// Calculate the schema value for a given bucket factor (like Go's pickSchema function).
584///
585/// The schema defines the bucket schema for native histograms.
586/// For base-2 bucket schemas where `bucket_factor` = 2^(2^-n), the schema is n.
587///
588/// Examples:
589/// - `bucket_factor` = 2.0 -> schema = 0 (1 bucket per power of 2)
590/// - `bucket_factor` = sqrt(2) ≈ 1.414 -> schema = 1 (2 buckets per power of 2)
591/// - `bucket_factor` = 2^(1/4) ≈ 1.189 -> schema = 2 (4 buckets per power of 2)
592pub(crate) fn calculate_schema_from_bucket_factor(bucket_factor: f64) -> i32 {
593    // For bucket_factor = 2^(2^-n), we want to solve for n
594    // bucket_factor = 2^(2^-n)
595    // log2(bucket_factor) = 2^-n
596    // log2(log2(bucket_factor)) = -n
597    // n = -log2(log2(bucket_factor))
598
599    assert!(bucket_factor > 1.0, "bucket_factor must be greater than 1.0");
600
601    let log_bucket_factor = bucket_factor.log2();
602    assert!(log_bucket_factor > 0.0, "log of bucket_factor must be positive");
603
604    #[allow(clippy::cast_possible_truncation)]
605    let schema = -(log_bucket_factor.log2()).round() as i32;
606
607    // Clamp to valid schema range
608    schema.clamp(MIN_SCHEMA, MAX_SCHEMA)
609}
610
611/// Configuration for native histograms.
612#[derive(Debug, Clone)]
613pub struct NativeHistogramConfig {
614    /// The base for the exponential buckets. Must be greater than 1.
615    /// Common values are 2.0 for power-of-2 buckets, or smaller values
616    /// like 1.1 for finer granularity.
617    bucket_factor: f64,
618    /// Maximum number of buckets. This limits memory usage.
619    max_buckets: u32,
620    /// The zero threshold. Values within [`-zero_threshold`, `zero_threshold`] are
621    /// considered zero and tracked in a special zero bucket.
622    zero_threshold: f64,
623}
624
625impl NativeHistogramConfig {
626    /// Creates a new native histogram configuration.
627    ///
628    /// # Arguments
629    /// * `bucket_factor` - The base for exponential buckets (must be > 1.0)
630    /// * `max_buckets` - Maximum number of buckets to limit memory usage
631    /// * `zero_threshold` - Threshold for considering values as zero (must be >= 0.0)
632    ///
633    /// # Returns
634    /// A new configuration, or an error if parameters are invalid.
635    ///
636    /// # Errors
637    /// Returns an error if `bucket_factor` is not greater than 1.0, `max_buckets` is 0,
638    /// or `zero_threshold` is negative.
639    pub fn new(
640        bucket_factor: f64,
641        max_buckets: u32,
642        zero_threshold: f64,
643    ) -> Result<Self, &'static str> {
644        if bucket_factor <= 1.0 {
645            return Err("bucket_factor must be greater than 1.0");
646        }
647        if max_buckets == 0 {
648            return Err("max_buckets must be greater than 0");
649        }
650        if zero_threshold < 0.0 {
651            return Err("zero_threshold must be non-negative");
652        }
653
654        Ok(Self { bucket_factor, max_buckets, zero_threshold })
655    }
656
657    /// Returns the bucket factor.
658    pub fn bucket_factor(&self) -> f64 {
659        self.bucket_factor
660    }
661
662    /// Returns the maximum number of buckets.
663    pub fn max_buckets(&self) -> u32 {
664        self.max_buckets
665    }
666
667    /// Returns the zero threshold.
668    pub fn zero_threshold(&self) -> f64 {
669        self.zero_threshold
670    }
671}
672
673/// A native histogram implementation using exponential buckets.
674///
675/// This implementation follows the Prometheus native histogram specification,
676/// using sparse representation with exponential buckets and schema-based indexing.
677#[derive(Debug)]
678pub struct NativeHistogram {
679    config: NativeHistogramConfig,
680    /// Count of observations
681    count: AtomicU64,
682    /// Sum of all observations (stored as atomic u64 bits)
683    sum: AtomicU64,
684    /// Count of zero observations (values within `zero_threshold`)
685    zero_count: AtomicU64,
686    /// Positive buckets: maps bucket index to count
687    positive_buckets: std::sync::RwLock<std::collections::BTreeMap<i32, u64>>,
688    /// Negative buckets: maps bucket index to count
689    negative_buckets: std::sync::RwLock<std::collections::BTreeMap<i32, u64>>,
690    /// Schema for bucket calculations (atomic for thread safety) - calculated from `bucket_factor`
691    schema: AtomicI32,
692    /// Number of buckets currently used (for limiting)
693    bucket_count: AtomicU64,
694}
695
696impl NativeHistogram {
697    /// Creates a new native histogram with the given configuration.
698    pub(crate) fn new(config: NativeHistogramConfig) -> Self {
699        let schema = calculate_schema_from_bucket_factor(config.bucket_factor());
700
701        Self {
702            schema: AtomicI32::new(schema),
703            config,
704            count: AtomicU64::new(0),
705            sum: AtomicU64::new((0.0f64).to_bits()),
706            zero_count: AtomicU64::new(0),
707            positive_buckets: std::sync::RwLock::new(std::collections::BTreeMap::new()),
708            negative_buckets: std::sync::RwLock::new(std::collections::BTreeMap::new()),
709            bucket_count: AtomicU64::new(0),
710        }
711    }
712
713    /// Records a single observation.
714    pub(crate) fn observe(&self, value: f64) {
715        self.count.fetch_add(1, Ordering::Relaxed);
716
717        // Skip sparse bucket logic and sum updates for NaN values
718        if value.is_nan() {
719            return;
720        }
721
722        // Atomically update the sum using compare-and-swap loop
723        loop {
724            let current_sum_bits = self.sum.load(Ordering::Relaxed);
725            let current_sum = f64::from_bits(current_sum_bits);
726            let new_sum = current_sum + value;
727
728            if self
729                .sum
730                .compare_exchange_weak(
731                    current_sum_bits,
732                    new_sum.to_bits(),
733                    Ordering::Relaxed,
734                    Ordering::Relaxed,
735                )
736                .is_ok()
737            {
738                break;
739            }
740        }
741
742        let mut v = value;
743        let mut key: i32;
744        let schema = self.schema.load(Ordering::Relaxed);
745        let zero_threshold = self.config.zero_threshold();
746        let mut is_inf = false;
747
748        if v.is_infinite() {
749            if v.is_sign_positive() {
750                v = f64::MAX;
751            } else {
752                v = f64::MIN;
753            }
754            is_inf = true;
755        }
756
757        // Calculate bucket key using Go's algorithm with frexp
758        let (frac, exp) = frexp(v.abs());
759
760        if schema > 0 {
761            // Use predefined bounds for positive schemas
762            #[allow(clippy::cast_sign_loss)]
763            let bounds = &NATIVE_HISTOGRAM_BOUNDS[schema as usize];
764            // Binary search for the bucket
765            let idx = bounds
766                .binary_search_by(|&bound| {
767                    bound.partial_cmp(&frac).unwrap_or(std::cmp::Ordering::Equal)
768                })
769                .unwrap_or_else(|x| x);
770            #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
771            let key_base = idx as i32;
772            #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
773            let bounds_len = bounds.len() as i32;
774            key = key_base + (exp - 1) * bounds_len;
775        } else {
776            // For schema <= 0, use simpler calculation
777            key = exp;
778            if (frac - 0.5).abs() < f64::EPSILON {
779                key -= 1;
780            }
781            if schema < 0 {
782                let offset = (1 << (-schema)) - 1;
783                key = (key + offset) >> (-schema);
784            }
785        }
786
787        // Increment key for infinity values
788        if is_inf {
789            key += 1;
790        }
791
792        // Track if we added a new bucket
793        let mut added_new_bucket = false;
794
795        // Determine which bucket to update based on value and zero threshold
796        if value > zero_threshold {
797            let mut buckets = self.positive_buckets.write().unwrap();
798            // Use single entry API call to avoid race condition
799            match buckets.entry(key) {
800                Entry::Vacant(entry) => {
801                    entry.insert(1);
802                    self.bucket_count.fetch_add(1, Ordering::Relaxed);
803                    added_new_bucket = true;
804                }
805                Entry::Occupied(mut entry) => {
806                    *entry.get_mut() += 1;
807                }
808            }
809        } else if value < -zero_threshold {
810            let mut buckets = self.negative_buckets.write().unwrap();
811            // Use single entry API call to avoid race condition
812            match buckets.entry(key) {
813                Entry::Vacant(entry) => {
814                    entry.insert(1);
815                    self.bucket_count.fetch_add(1, Ordering::Relaxed);
816                    added_new_bucket = true;
817                }
818                Entry::Occupied(mut entry) => {
819                    *entry.get_mut() += 1;
820                }
821            }
822        } else {
823            // Value is within zero threshold
824            self.zero_count.fetch_add(1, Ordering::Relaxed);
825        }
826
827        // Check bucket limit after releasing locks
828        if added_new_bucket {
829            self.limit_buckets();
830        }
831    }
832
833    /// Limits the number of buckets.
834    fn limit_buckets(&self) {
835        if self.config.max_buckets() == 0 {
836            return; // No limit configured
837        }
838
839        let current_bucket_count = self.bucket_count();
840        if current_bucket_count <= u64::from(self.config.max_buckets()) {
841            return; // Under the limit
842        }
843
844        self.reduce_bucket_resolution();
845    }
846
847    /// Reduces bucket resolution.
848    /// This reduces the schema by 1 (doubles bucket width) and re-buckets
849    /// existing data.
850    fn reduce_bucket_resolution(&self) {
851        let current_schema = self.schema.load(Ordering::Relaxed);
852
853        // If we're already at minimum schema, we can't reduce further
854        if current_schema <= MIN_SCHEMA {
855            return;
856        }
857
858        // Reduce schema by 1 (double bucket width)
859        let new_schema = current_schema - 1;
860        self.schema.store(new_schema, Ordering::Relaxed);
861
862        // Re-bucket positive buckets
863        {
864            let mut pos_buckets = self.positive_buckets.write().unwrap();
865            let old_buckets = std::mem::take(&mut *pos_buckets);
866            let old_count = old_buckets.len() as u64;
867
868            for (mut k, v) in old_buckets {
869                if k > 0 {
870                    k += 1;
871                }
872                k /= 2;
873
874                *pos_buckets.entry(k).or_insert(0) += v;
875            }
876
877            // Update bucket count
878            let new_count = pos_buckets.len() as u64;
879            if new_count < old_count {
880                self.bucket_count.fetch_sub(old_count - new_count, Ordering::Relaxed);
881            }
882        }
883
884        // Re-bucket negative buckets
885        {
886            let mut neg_buckets = self.negative_buckets.write().unwrap();
887            let old_buckets = std::mem::take(&mut *neg_buckets);
888            let old_count = old_buckets.len() as u64;
889
890            for (mut k, v) in old_buckets {
891                if k > 0 {
892                    k += 1;
893                }
894                k /= 2;
895
896                *neg_buckets.entry(k).or_insert(0) += v;
897            }
898
899            // Update bucket count
900            let new_count = neg_buckets.len() as u64;
901            if new_count < old_count {
902                self.bucket_count.fetch_sub(old_count - new_count, Ordering::Relaxed);
903            }
904        }
905
906        // Note: This operation preserves all bucket counts by merging them into wider buckets,
907        // maintaining count, sum, and zero_count while reducing resolution.
908    }
909
910    /// Returns the total count of observations.
911    #[cfg(any(feature = "protobuf", test))]
912    pub(crate) fn count(&self) -> u64 {
913        self.count.load(Ordering::Relaxed)
914    }
915
916    /// Returns the sum of all observations.
917    #[cfg(any(feature = "protobuf", test))]
918    pub(crate) fn sum(&self) -> f64 {
919        f64::from_bits(self.sum.load(Ordering::Relaxed))
920    }
921
922    /// Returns the count of zero observations.
923    #[cfg(any(feature = "protobuf", test))]
924    pub(crate) fn zero_count(&self) -> u64 {
925        self.zero_count.load(Ordering::Relaxed)
926    }
927
928    /// Returns a snapshot of the positive buckets.
929    #[cfg(any(feature = "protobuf", test))]
930    pub(crate) fn positive_buckets(&self) -> std::collections::BTreeMap<i32, u64> {
931        self.positive_buckets.read().unwrap().clone()
932    }
933
934    /// Returns a snapshot of the negative buckets.
935    #[cfg(any(feature = "protobuf", test))]
936    pub(crate) fn negative_buckets(&self) -> std::collections::BTreeMap<i32, u64> {
937        self.negative_buckets.read().unwrap().clone()
938    }
939
940    /// Returns the configuration used by this histogram.
941    #[cfg(feature = "protobuf")]
942    pub(crate) fn config(&self) -> &NativeHistogramConfig {
943        &self.config
944    }
945
946    /// Returns the current schema being used.
947    #[cfg(any(feature = "protobuf", test))]
948    pub(crate) fn schema(&self) -> i32 {
949        self.schema.load(Ordering::Relaxed)
950    }
951
952    /// Returns the total number of buckets currently in use.
953    fn bucket_count(&self) -> u64 {
954        self.bucket_count.load(Ordering::Relaxed)
955    }
956}
957
958impl Clone for NativeHistogram {
959    fn clone(&self) -> Self {
960        Self {
961            config: self.config.clone(),
962            count: AtomicU64::new(self.count.load(Ordering::Relaxed)),
963            sum: AtomicU64::new(self.sum.load(Ordering::Relaxed)),
964            zero_count: AtomicU64::new(self.zero_count.load(Ordering::Relaxed)),
965            positive_buckets: std::sync::RwLock::new(self.positive_buckets.read().unwrap().clone()),
966            negative_buckets: std::sync::RwLock::new(self.negative_buckets.read().unwrap().clone()),
967            schema: AtomicI32::new(self.schema.load(Ordering::Relaxed)),
968            bucket_count: AtomicU64::new(self.bucket_count()),
969        }
970    }
971}
972
973#[cfg(test)]
974mod tests {
975    use super::*;
976
977    #[test]
978    fn test_frexp_function() {
979        let (m, e) = frexp(1.0);
980        assert!((m - 0.5).abs() < f64::EPSILON);
981        assert_eq!(e, 1);
982
983        let (m, e) = frexp(2.0);
984        assert!((m - 0.5).abs() < f64::EPSILON);
985        assert_eq!(e, 2);
986
987        let (m, e) = frexp(0.5);
988        assert!((m - 0.5).abs() < f64::EPSILON);
989        assert_eq!(e, 0);
990
991        let (m, e) = frexp(4.0);
992        assert!((m - 0.5).abs() < f64::EPSILON);
993        assert_eq!(e, 3);
994
995        // Test zero
996        let (m, e) = frexp(0.0);
997        assert_eq!(m, 0.0);
998        assert_eq!(e, 0);
999
1000        // Test negative numbers
1001        let (m, e) = frexp(-2.0);
1002        assert!((m - (-0.5)).abs() < f64::EPSILON);
1003        assert_eq!(e, 2);
1004    }
1005
1006    #[test]
1007    fn test_observe_nan_values() {
1008        let config = NativeHistogramConfig::new(2.0, 160, 0.1).unwrap();
1009        let histogram = NativeHistogram::new(config);
1010
1011        // Observe some normal values first
1012        histogram.observe(1.0);
1013        histogram.observe(2.0);
1014
1015        // Observe NaN - should increment count but not affect sum or buckets
1016        histogram.observe(f64::NAN);
1017
1018        assert_eq!(histogram.count(), 3);
1019        assert!((histogram.sum() - 3.0).abs() < f64::EPSILON); // Sum should still be 1.0 + 2.0
1020        assert_eq!(histogram.zero_count(), 0);
1021        assert!(!histogram.positive_buckets().is_empty()); // Should have buckets from normal values
1022    }
1023
1024    #[test]
1025    fn test_bucket_reduction() {
1026        // Create histogram with very low bucket limit to trigger reduction
1027        let config = NativeHistogramConfig::new(2.0, 2, 0.1).unwrap(); // Only 2 buckets max
1028        let histogram = NativeHistogram::new(config);
1029
1030        let initial_schema = histogram.schema();
1031
1032        // Add observations that will create multiple buckets
1033        histogram.observe(1.0);
1034        histogram.observe(2.0);
1035        histogram.observe(4.0);
1036        histogram.observe(8.0);
1037
1038        // Should have triggered bucket reduction and schema change
1039        let final_schema = histogram.schema();
1040        assert!(final_schema < initial_schema, "Schema should have been reduced");
1041
1042        // Count and sum should be preserved
1043        assert_eq!(histogram.count(), 4);
1044        assert!((histogram.sum() - 15.0).abs() < f64::EPSILON);
1045
1046        // Buckets should be preserved (merged, not cleared)
1047        let pos_buckets = histogram.positive_buckets();
1048        assert!(!pos_buckets.is_empty(), "Buckets should be preserved after reduction");
1049
1050        // The total count across all buckets should still be 4
1051        let total_bucket_count: u64 = pos_buckets.values().sum();
1052        assert_eq!(total_bucket_count, 4, "All observations should be preserved in buckets");
1053    }
1054
1055    #[test]
1056    fn test_new_native_histogram() {
1057        let config = NativeHistogramConfig::new(2.0, 160, 0.1).unwrap();
1058        let histogram = NativeHistogram::new(config);
1059        assert_eq!(histogram.count(), 0);
1060        assert!((histogram.sum() - 0.0).abs() < f64::EPSILON);
1061        assert_eq!(histogram.zero_count(), 0);
1062        assert_eq!(histogram.schema(), 0); // 2.0 -> schema 0
1063    }
1064
1065    #[test]
1066    fn test_observe_positive_values() {
1067        let config = NativeHistogramConfig::new(2.0, 160, 0.1).unwrap();
1068        let histogram = NativeHistogram::new(config);
1069        histogram.observe(1.0);
1070        histogram.observe(2.0);
1071        histogram.observe(4.0);
1072
1073        assert_eq!(histogram.count(), 3);
1074        assert!((histogram.sum() - 7.0).abs() < f64::EPSILON);
1075        assert_eq!(histogram.zero_count(), 0);
1076
1077        let pos_buckets = histogram.positive_buckets();
1078        assert!(!pos_buckets.is_empty());
1079    }
1080
1081    #[test]
1082    fn test_observe_negative_values() {
1083        let config = NativeHistogramConfig::new(2.0, 160, 0.1).unwrap();
1084        let histogram = NativeHistogram::new(config);
1085        histogram.observe(-1.0);
1086        histogram.observe(-2.0);
1087
1088        assert_eq!(histogram.count(), 2);
1089        assert!((histogram.sum() - (-3.0)).abs() < f64::EPSILON);
1090        assert_eq!(histogram.zero_count(), 0);
1091
1092        let neg_buckets = histogram.negative_buckets();
1093        assert!(!neg_buckets.is_empty());
1094        assert!(histogram.positive_buckets().is_empty());
1095    }
1096
1097    #[test]
1098    fn test_observe_zero_values() {
1099        let config = NativeHistogramConfig::new(2.0, 160, 0.1).unwrap();
1100        let histogram = NativeHistogram::new(config);
1101        histogram.observe(0.0);
1102        histogram.observe(0.05);
1103        histogram.observe(-0.05);
1104
1105        assert_eq!(histogram.count(), 3);
1106        assert_eq!(histogram.zero_count(), 3);
1107        assert!(histogram.positive_buckets().is_empty());
1108        assert!(histogram.negative_buckets().is_empty());
1109    }
1110
1111    #[test]
1112    fn test_schema_based_bucketing() {
1113        let config = NativeHistogramConfig::new(2.0, 160, 0.1).unwrap();
1114        let histogram = NativeHistogram::new(config);
1115
1116        // Test that different values go to different buckets
1117        histogram.observe(1.0);
1118        histogram.observe(2.0);
1119        histogram.observe(4.0);
1120
1121        assert_eq!(histogram.count(), 3);
1122        assert_eq!(histogram.zero_count(), 0);
1123
1124        let pos_buckets = histogram.positive_buckets();
1125        // Should have buckets for different values
1126        assert!(!pos_buckets.is_empty());
1127    }
1128
1129    #[test]
1130    fn test_invalid_config() {
1131        // Invalid bucket_factor
1132        assert!(NativeHistogramConfig::new(0.5, 160, 1e-128).is_err());
1133
1134        // Invalid max_buckets
1135        assert!(NativeHistogramConfig::new(2.0, 0, 1e-128).is_err());
1136
1137        // Invalid zero_threshold
1138        assert!(NativeHistogramConfig::new(2.0, 160, -1.0).is_err());
1139    }
1140}