1use std::cmp::Ordering;
4
5use datadog_protos::metrics::Dogsketch;
6use ordered_float::OrderedFloat;
7use smallvec::SmallVec;
8
9use super::bin::Bin;
10use super::bucket::Bucket;
11use super::config::{
12 Config, DDSKETCH_CONF_BIN_LIMIT, DDSKETCH_CONF_GAMMA_LN, DDSKETCH_CONF_GAMMA_V, DDSKETCH_CONF_NORM_BIAS,
13 DDSKETCH_CONF_NORM_MIN,
14};
15use crate::canonical::mapping::LogarithmicMapping;
16use crate::common::float_eq;
17
18static SKETCH_CONFIG: Config = Config::new(
19 DDSKETCH_CONF_BIN_LIMIT,
20 DDSKETCH_CONF_GAMMA_V,
21 DDSKETCH_CONF_GAMMA_LN,
22 DDSKETCH_CONF_NORM_MIN,
23 DDSKETCH_CONF_NORM_BIAS,
24);
25const MAX_BIN_WIDTH: u32 = u32::MAX;
26
27#[cfg(feature = "serde")]
28mod serde_helpers {
29 use serde::{Deserialize, Deserializer};
30
31 pub(super) fn null_as_nan<'de, D>(deserializer: D) -> Result<f64, D::Error>
36 where
37 D: Deserializer<'de>,
38 {
39 Ok(Option::<f64>::deserialize(deserializer)?.unwrap_or(f64::NAN))
40 }
41}
42
43#[derive(Clone, Debug)]
72#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
73pub struct DDSketch {
74 bins: SmallVec<[Bin; 4]>,
76
77 count: u64,
79
80 #[cfg_attr(feature = "serde", serde(deserialize_with = "serde_helpers::null_as_nan"))]
82 min: f64,
83
84 #[cfg_attr(feature = "serde", serde(deserialize_with = "serde_helpers::null_as_nan"))]
86 max: f64,
87
88 #[cfg_attr(feature = "serde", serde(deserialize_with = "serde_helpers::null_as_nan"))]
90 sum: f64,
91
92 #[cfg_attr(feature = "serde", serde(deserialize_with = "serde_helpers::null_as_nan"))]
94 avg: f64,
95}
96
97impl DDSketch {
98 pub fn remap_mapping() -> LogarithmicMapping {
100 LogarithmicMapping::new_with_gamma_and_offset(DDSKETCH_CONF_GAMMA_V, f64::from(DDSKETCH_CONF_NORM_BIAS) + 0.5)
101 .expect("agent sketch gamma and offset are always valid")
102 }
103
104 pub fn value_for_key(key: i16) -> f64 {
106 SKETCH_CONFIG.bin_lower_bound(key)
107 }
108
109 pub fn bin_count(&self) -> usize {
111 self.bins.len()
112 }
113
114 pub fn is_empty(&self) -> bool {
116 self.count == 0
117 }
118
119 pub fn count(&self) -> u64 {
121 self.count
122 }
123
124 pub fn set_count(&mut self, count: u64) {
126 self.count = count;
127 }
128
129 pub fn set_sum(&mut self, sum: f64) {
131 self.sum = sum;
132 }
133
134 pub fn set_avg(&mut self, avg: f64) {
136 self.avg = avg;
137 }
138
139 pub fn set_min(&mut self, min: f64) {
141 self.min = min;
142 }
143
144 pub fn set_max(&mut self, max: f64) {
146 self.max = max;
147 }
148
149 pub fn min(&self) -> Option<f64> {
153 if self.is_empty() {
154 None
155 } else {
156 Some(self.min)
157 }
158 }
159
160 pub fn max(&self) -> Option<f64> {
164 if self.is_empty() {
165 None
166 } else {
167 Some(self.max)
168 }
169 }
170
171 pub fn sum(&self) -> Option<f64> {
175 if self.is_empty() {
176 None
177 } else {
178 Some(self.sum)
179 }
180 }
181
182 pub fn stored_sum(&self) -> f64 {
184 self.sum
185 }
186
187 pub fn avg(&self) -> Option<f64> {
191 if self.is_empty() {
192 None
193 } else {
194 Some(self.avg)
195 }
196 }
197
198 pub fn stored_avg(&self) -> f64 {
200 self.avg
201 }
202
203 pub fn stored_min(&self) -> f64 {
205 self.min
206 }
207
208 pub fn stored_max(&self) -> f64 {
210 self.max
211 }
212
213 pub fn bins(&self) -> &[Bin] {
215 &self.bins
216 }
217
218 pub fn clear(&mut self) {
220 self.count = 0;
221 self.min = f64::MAX;
222 self.max = f64::MIN;
223 self.avg = 0.0;
224 self.sum = 0.0;
225 self.bins.clear();
226 }
227
228 fn adjust_basic_stats(&mut self, v: f64, n: u64) {
229 saluki_antithesis::always!(v.is_finite(), "DDSketch sample is finite at insert");
232
233 if v < self.min {
234 self.min = v;
235 }
236
237 if v > self.max {
238 self.max = v;
239 }
240
241 saluki_antithesis::always_le!(self.min, self.max, "DDSketch min does not exceed max after insert");
242
243 self.count += n;
244 self.sum += v * n as f64;
248
249 if n == 1 {
250 self.avg += (v - self.avg) / self.count as f64;
251 } else {
252 self.avg = self.avg + (v - self.avg) * n as f64 / self.count as f64;
255 }
256 }
257
258 fn insert_key_counts(&mut self, counts: &[(i16, u64)]) {
259 let mut temp = SmallVec::<[Bin; 4]>::new();
260
261 let mut bins_idx = 0;
262 let mut key_idx = 0;
263 let bins_len = self.bins.len();
264 let counts_len = counts.len();
265
266 while bins_idx < bins_len && key_idx < counts_len {
274 let bin = self.bins[bins_idx];
275 let vk = counts[key_idx].0;
276 let kn = counts[key_idx].1;
277
278 match bin.k.cmp(&vk) {
279 Ordering::Greater => {
280 generate_bins(&mut temp, vk, kn);
281 key_idx += 1;
282 }
283 Ordering::Less => {
284 temp.push(bin);
285 bins_idx += 1;
286 }
287 Ordering::Equal => {
288 generate_bins(&mut temp, bin.k, u64::from(bin.n) + kn);
289 bins_idx += 1;
290 key_idx += 1;
291 }
292 }
293 }
294
295 temp.extend_from_slice(&self.bins[bins_idx..]);
296
297 while key_idx < counts_len {
298 let vk = counts[key_idx].0;
299 let kn = counts[key_idx].1;
300 generate_bins(&mut temp, vk, kn);
301 key_idx += 1;
302 }
303
304 trim_left(&mut temp, SKETCH_CONFIG.bin_limit);
305
306 self.bins = temp;
309 }
310
311 fn insert_keys(&mut self, mut keys: Vec<i16>) {
312 assert!(keys.len() <= u32::MAX.try_into().expect("we don't support 16-bit systems"));
319
320 keys.sort_unstable();
321
322 let mut temp = SmallVec::<[Bin; 4]>::new();
323
324 let mut bins_idx = 0;
325 let mut key_idx = 0;
326 let bins_len = self.bins.len();
327 let keys_len = keys.len();
328
329 while bins_idx < bins_len && key_idx < keys_len {
337 let bin = self.bins[bins_idx];
338 let vk = keys[key_idx];
339
340 match bin.k.cmp(&vk) {
341 Ordering::Greater => {
342 let kn = buf_count_leading_equal(&keys, key_idx);
343 generate_bins(&mut temp, vk, kn);
344 key_idx += kn as usize;
345 }
346 Ordering::Less => {
347 temp.push(bin);
348 bins_idx += 1;
349 }
350 Ordering::Equal => {
351 let kn = buf_count_leading_equal(&keys, key_idx);
352 generate_bins(&mut temp, bin.k, u64::from(bin.n) + kn);
353 bins_idx += 1;
354 key_idx += kn as usize;
355 }
356 }
357 }
358
359 temp.extend_from_slice(&self.bins[bins_idx..]);
360
361 while key_idx < keys_len {
362 let vk = keys[key_idx];
363 let kn = buf_count_leading_equal(&keys, key_idx);
364 generate_bins(&mut temp, vk, kn);
365 key_idx += kn as usize;
366 }
367
368 trim_left(&mut temp, SKETCH_CONFIG.bin_limit);
369
370 self.bins = temp;
373 }
374
375 pub fn insert(&mut self, v: f64) {
377 self.adjust_basic_stats(v, 1);
380
381 let key = SKETCH_CONFIG.key(v);
382
383 let mut insert_at = None;
384
385 for (bin_idx, b) in self.bins.iter_mut().enumerate() {
386 if b.k == key {
387 if b.n < MAX_BIN_WIDTH {
388 b.n += 1;
390 return;
391 } else {
392 insert_at = Some(bin_idx);
393 break;
394 }
395 }
396 if b.k > key {
397 insert_at = Some(bin_idx);
398 break;
399 }
400 }
401
402 if let Some(bin_idx) = insert_at {
403 self.bins.insert(bin_idx, Bin { k: key, n: 1 });
404 } else {
405 self.bins.push(Bin { k: key, n: 1 });
406 }
407 trim_left(&mut self.bins, SKETCH_CONFIG.bin_limit);
408 }
409
410 pub fn insert_many(&mut self, vs: &[f64]) {
412 let mut keys = Vec::with_capacity(vs.len());
415 for v in vs {
416 self.adjust_basic_stats(*v, 1);
417 keys.push(SKETCH_CONFIG.key(*v));
418 }
419 self.insert_keys(keys);
420 }
421
422 pub fn insert_n(&mut self, v: f64, n: u64) {
424 if n == 1 {
427 self.insert(v);
428 } else {
429 self.adjust_basic_stats(v, n);
430
431 let key = SKETCH_CONFIG.key(v);
432 self.insert_key_counts(&[(key, n)]);
433 }
434 }
435
436 fn insert_interpolate_bucket(&mut self, lower: f64, upper: f64, count: u64) {
437 let lower_key = SKETCH_CONFIG.key(lower);
440 let upper_key = SKETCH_CONFIG.key(upper);
441 let keys = (lower_key..=upper_key).collect::<Vec<_>>();
442
443 let mut key_counts = Vec::new();
444 let mut remaining_count = count;
445 let distance = upper - lower;
446 let mut start_idx = 0;
447 let mut end_idx = 1;
448 let mut lower_bound = SKETCH_CONFIG.bin_lower_bound(keys[start_idx]);
449 let mut remainder = 0.0;
450
451 while end_idx < keys.len() && remaining_count > 0 {
452 let upper_bound = SKETCH_CONFIG.bin_lower_bound(keys[end_idx]);
456 let fkn = ((upper_bound - lower_bound) / distance) * count as f64;
457 if fkn > 1.0 {
458 remainder += fkn - fkn.trunc();
459 }
460
461 #[allow(clippy::cast_possible_truncation)]
464 let mut kn = fkn as u64;
465 if remainder > 1.0 {
466 kn += 1;
467 remainder -= 1.0;
468 }
469
470 if kn > 0 {
471 if kn > remaining_count {
472 kn = remaining_count;
473 }
474
475 self.adjust_basic_stats(lower_bound, kn);
476 key_counts.push((keys[start_idx], kn));
477
478 remaining_count -= kn;
479 start_idx = end_idx;
480 lower_bound = upper_bound;
481 }
482
483 end_idx += 1;
484 }
485
486 if remaining_count > 0 {
487 let last_key = keys[start_idx];
488 lower_bound = SKETCH_CONFIG.bin_lower_bound(last_key);
489 self.adjust_basic_stats(lower_bound, remaining_count);
490 key_counts.push((last_key, remaining_count));
491 }
492
493 key_counts.sort_unstable_by_key(|(k, _)| *k);
495
496 self.insert_key_counts(&key_counts);
497 }
498
499 pub fn insert_interpolate_buckets(&mut self, mut buckets: Vec<Bucket>) -> Result<(), &'static str> {
505 buckets.sort_by(|a, b| {
508 let oa = OrderedFloat(a.upper_limit);
509 let ob = OrderedFloat(b.upper_limit);
510
511 oa.cmp(&ob)
512 });
513
514 let mut lower = f64::NEG_INFINITY;
515
516 if buckets.iter().any(|bucket| bucket.count > u64::from(u32::MAX)) {
517 return Err("bucket size greater than u32::MAX");
518 }
519
520 for bucket in buckets {
521 let original_upper = bucket.upper_limit;
522 let mut upper = bucket.upper_limit;
523 if upper.is_sign_positive() && upper.is_infinite() {
524 upper = lower;
525 } else if lower.is_sign_negative() && lower.is_infinite() {
526 lower = upper;
527 } else if lower == 0.0 && upper > 0.0 {
528 lower = upper;
532 }
533
534 self.insert_interpolate_bucket(lower, upper, bucket.count);
535 lower = original_upper;
536 }
537
538 Ok(())
539 }
540
541 #[allow(dead_code)]
546 pub(crate) fn insert_raw_bin(&mut self, k: i16, n: u32) {
547 let v = SKETCH_CONFIG.bin_lower_bound(k);
548 self.adjust_basic_stats(v, u64::from(n));
549 self.bins.push(Bin { k, n });
550 }
551
552 pub fn quantile(&self, q: f64) -> Option<f64> {
554 if self.count == 0 {
555 return None;
556 }
557
558 if q <= 0.0 {
559 return Some(self.min);
560 }
561
562 if q >= 1.0 {
563 return Some(self.max);
564 }
565
566 let mut n = 0.0;
567 let mut estimated = None;
568 let wanted_rank = rank(self.count, q);
569
570 for (i, bin) in self.bins.iter().enumerate() {
571 n += f64::from(bin.n);
572 if n <= wanted_rank {
573 continue;
574 }
575
576 let weight = (n - wanted_rank) / f64::from(bin.n);
577 let mut v_low = SKETCH_CONFIG.bin_lower_bound(bin.k);
578 let mut v_high = v_low * SKETCH_CONFIG.gamma_v;
579
580 if i == self.bins.len() {
581 v_high = self.max;
582 } else if i == 0 {
583 v_low = self.min;
584 }
585
586 estimated = Some(v_low * weight + v_high * (1.0 - weight));
587 break;
588 }
589
590 estimated.map(|v| v.clamp(self.min, self.max)).or(Some(f64::NAN))
591 }
592
593 pub fn merge(&mut self, other: &DDSketch) {
602 if self.count == 0 {
604 self.count = other.count;
605 self.min = other.min;
606 self.max = other.max;
607 self.sum = other.sum;
608 self.avg = other.avg;
609 } else if other.count > 0 {
610 self.count += other.count;
611 if other.max > self.max {
612 self.max = other.max;
613 }
614 if other.min < self.min {
615 self.min = other.min;
616 }
617 self.sum += other.sum;
618 self.avg = self.avg + (other.avg - self.avg) * other.count as f64 / self.count as f64;
619 }
620
621 let mut temp = SmallVec::<[Bin; 4]>::new();
623
624 let mut bins_idx = 0;
625 for other_bin in &other.bins {
626 let start = bins_idx;
627 while bins_idx < self.bins.len() && self.bins[bins_idx].k < other_bin.k {
628 bins_idx += 1;
629 }
630
631 temp.extend_from_slice(&self.bins[start..bins_idx]);
632
633 if bins_idx >= self.bins.len() || self.bins[bins_idx].k > other_bin.k {
634 temp.push(*other_bin);
635 } else if self.bins[bins_idx].k == other_bin.k {
636 generate_bins(
637 &mut temp,
638 other_bin.k,
639 u64::from(other_bin.n) + u64::from(self.bins[bins_idx].n),
640 );
641 bins_idx += 1;
642 }
643 }
644
645 temp.extend_from_slice(&self.bins[bins_idx..]);
646 trim_left(&mut temp, SKETCH_CONFIG.bin_limit);
647
648 self.bins = temp;
649 }
650
651 pub fn merge_to_dogsketch(&self, dogsketch: &mut Dogsketch) {
653 dogsketch.set_cnt(i64::try_from(self.count).unwrap_or(i64::MAX));
654 dogsketch.set_min(self.min);
655 dogsketch.set_max(self.max);
656 dogsketch.set_avg(self.avg);
657 dogsketch.set_sum(self.sum);
658
659 let mut k = Vec::new();
660 let mut n = Vec::new();
661
662 for bin in &self.bins {
663 k.push(i32::from(bin.k));
664 n.push(bin.n);
665 }
666
667 dogsketch.set_k(k);
668 dogsketch.set_n(n);
669 }
670}
671
672impl PartialEq for DDSketch {
673 fn eq(&self, other: &Self) -> bool {
674 self.count == other.count
682 && float_eq(self.min, other.min)
683 && float_eq(self.max, other.max)
684 && float_eq(self.sum, other.sum)
685 && float_eq(self.avg, other.avg)
686 && self.bins == other.bins
687 }
688}
689
690impl Default for DDSketch {
691 fn default() -> Self {
692 Self {
693 bins: SmallVec::new(),
694 count: 0,
695 min: f64::MAX,
696 max: f64::MIN,
697 sum: 0.0,
698 avg: 0.0,
699 }
700 }
701}
702
703impl Eq for DDSketch {}
704
705impl TryFrom<Dogsketch> for DDSketch {
706 type Error = &'static str;
707
708 fn try_from(value: Dogsketch) -> Result<Self, Self::Error> {
709 let mut sketch = DDSketch {
710 count: u64::try_from(value.cnt).map_err(|_| "sketch count overflows u64 or is negative")?,
711 min: value.min,
712 max: value.max,
713 avg: value.avg,
714 sum: value.sum,
715 ..Default::default()
716 };
717
718 let k = value.k;
719 let n = value.n;
720
721 if k.len() != n.len() {
722 return Err("k and n bin vectors have differing lengths");
723 }
724
725 for (k, n) in k.into_iter().zip(n) {
726 let k = i16::try_from(k).map_err(|_| "bin key overflows i16")?;
727
728 sketch.bins.push(Bin { k, n });
729 }
730
731 Ok(sketch)
732 }
733}
734
735fn rank(count: u64, q: f64) -> f64 {
736 let rank = q * (count - 1) as f64;
737 rank.round_ties_even()
738}
739
740#[allow(clippy::cast_possible_truncation)]
741fn buf_count_leading_equal(keys: &[i16], start_idx: usize) -> u64 {
742 if start_idx == keys.len() - 1 {
743 return 1;
744 }
745
746 let mut idx = start_idx;
747 while idx < keys.len() && keys[idx] == keys[start_idx] {
748 idx += 1;
749 }
750
751 (idx - start_idx) as u64
754}
755
756fn trim_left(bins: &mut SmallVec<[Bin; 4]>, bin_limit: u16) {
757 let bin_limit = bin_limit as usize;
760 if bin_limit == 0 || bins.len() <= bin_limit {
761 return;
762 }
763
764 let num_to_remove = bins.len() - bin_limit;
765 let mut missing: u64 = 0;
766
767 for bin in bins.iter().take(num_to_remove) {
770 missing += u64::from(bin.n);
771 }
772
773 bins[num_to_remove].increment(missing);
778
779 bins.drain(0..num_to_remove);
781
782 saluki_antithesis::reachable!("DDSketch bin collapse reached");
785 saluki_antithesis::always_le!(bins.len(), bin_limit, "DDSketch bin count within bin_limit");
786}
787
788#[allow(clippy::cast_possible_truncation)]
789fn generate_bins(bins: &mut SmallVec<[Bin; 4]>, k: i16, n: u64) {
790 if n < u64::from(MAX_BIN_WIDTH) {
791 bins.push(Bin { k, n: n as u32 });
793 } else {
794 let overflow = n % u64::from(MAX_BIN_WIDTH);
795 if overflow != 0 {
796 bins.push(Bin {
797 k,
798 n: overflow as u32,
800 });
801 }
802
803 for _ in 0..(n / u64::from(MAX_BIN_WIDTH)) {
804 bins.push(Bin { k, n: MAX_BIN_WIDTH });
805 }
806 }
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812
813 fn make_bins(pairs: &[(i16, u32)]) -> SmallVec<[Bin; 4]> {
815 pairs.iter().map(|&(k, n)| Bin { k, n }).collect()
816 }
817
818 fn to_pairs(bins: &SmallVec<[Bin; 4]>) -> Vec<(i16, u32)> {
820 bins.iter().map(|b| (b.k, b.n)).collect()
821 }
822
823 #[test]
833 fn trim_left_collapses_removed_mass_into_first_kept_bin() {
834 let mut bins = make_bins(&[(0, 2), (1, 3), (2, 4), (3, 5)]);
835 trim_left(&mut bins, 2);
836 assert_eq!(to_pairs(&bins), vec![(2, 9), (3, 5)]);
837 }
838
839 #[test]
845 fn trim_left_preserves_total_count_when_no_overflow() {
846 let mut bins = make_bins(&[(10, 5), (20, 3), (30, 7)]);
847 let total_before: u64 = bins.iter().map(|b| u64::from(b.n)).sum();
848 trim_left(&mut bins, 2);
849 let total_after: u64 = bins.iter().map(|b| u64::from(b.n)).sum();
850 assert_eq!(to_pairs(&bins), vec![(20, 8), (30, 7)]);
851 assert_eq!(total_before, total_after);
852 }
853
854 #[test]
864 fn trim_left_preserves_exact_count_with_u32_bins() {
865 let mut bins = make_bins(&[(0, 50000), (1, 50000), (2, 1)]);
866 let total_before: u64 = bins.iter().map(|b| u64::from(b.n)).sum();
867 trim_left(&mut bins, 1);
868 let total_after: u64 = bins.iter().map(|b| u64::from(b.n)).sum();
869 assert_eq!(bins.len(), 1);
870 assert_eq!(bins[0].k, 2);
871 assert_eq!(bins[0].n, 100001);
872 assert_eq!(total_before, total_after, "all mass must be preserved with u32 bins");
873 }
874
875 #[test]
877 fn trim_left_no_op_when_within_limit() {
878 let original = make_bins(&[(5, 10), (6, 20)]);
879 let mut bins = original.clone();
880 trim_left(&mut bins, 2);
881 assert_eq!(to_pairs(&bins), to_pairs(&original));
882 trim_left(&mut bins, 3);
883 assert_eq!(to_pairs(&bins), to_pairs(&original));
884 }
885
886 #[test]
897 fn trim_left_respects_bin_limit_with_large_weights() {
898 let weight: u64 = 260_078_024;
901 let bin_limit = usize::from(DDSKETCH_CONF_BIN_LIMIT);
902
903 let mut sketch = DDSketch::default();
904
905 for i in 1..=10_i32 {
908 sketch.insert_n(f64::from(i), weight);
909 assert!(
910 sketch.bins().len() <= bin_limit,
911 "bin count {} exceeded limit {} after inserting {} value(s) at weight {}",
912 sketch.bins().len(),
913 bin_limit,
914 i,
915 weight,
916 );
917 }
918 }
919
920 #[test]
921 fn interpolate_buckets_does_not_seed_zero_bin_for_positive_bucket_after_zero_bound() {
922 let mut sketch = DDSketch::default();
923
924 sketch
925 .insert_interpolate_buckets(vec![
926 Bucket {
927 upper_limit: 0.0,
928 count: 0,
929 },
930 Bucket {
931 upper_limit: 10.0,
932 count: 10,
933 },
934 ])
935 .expect("buckets should interpolate");
936
937 assert_eq!(sketch.count(), 10);
938 assert!(
939 sketch.min().expect("sketch should not be empty") > 0.0,
940 "positive bucket after a zero lower bound must not seed a zero minimum"
941 );
942 }
943
944 #[test]
952 fn trim_left_saturates_first_kept_bin_and_discards_remainder() {
953 let mut bins = make_bins(&[(0, u32::MAX), (1, 1)]);
954 trim_left(&mut bins, 1);
955 assert_eq!(bins.len(), 1);
956 assert_eq!(bins[0].k, 1);
957 assert_eq!(bins[0].n, u32::MAX);
958 }
959
960 #[test]
968 fn trim_left_collapses_negative_keys_correctly() {
969 let mut bins = make_bins(&[(-3, 5), (-2, 3), (-1, 2), (0, 1)]);
970 trim_left(&mut bins, 2);
971 assert_eq!(to_pairs(&bins), vec![(-1, 10), (0, 1)]);
972 }
973
974 #[test]
982 fn trim_left_monotonic_ascending_keeps_top_keys() {
983 let pairs: Vec<(i16, u32)> = (0..10).map(|i| (i, 1)).collect();
984 let mut bins = make_bins(&pairs);
985 trim_left(&mut bins, 4);
986 assert_eq!(to_pairs(&bins), vec![(6, 7), (7, 1), (8, 1), (9, 1)]);
987 }
988
989 #[test]
990 fn merge_handles_zero_count_summaries_without_dropping_bins() {
991 for (name, destination_is_zero_count, source_is_zero_count, expected_summary) in [
992 ("zero-count source", false, true, 10.0),
993 ("zero-count destination", true, false, 20.0),
994 ] {
995 let mut destination = DDSketch::default();
996 destination.insert(10.0);
997
998 let mut source = DDSketch::default();
999 source.insert(20.0);
1000
1001 if destination_is_zero_count {
1002 destination.set_count(0);
1003 destination.set_min(-1.0);
1004 destination.set_max(100.0);
1005 destination.set_sum(999.0);
1006 destination.set_avg(f64::NAN);
1007 }
1008
1009 if source_is_zero_count {
1010 source.set_count(0);
1011 source.set_min(-1.0);
1012 source.set_max(100.0);
1013 source.set_sum(999.0);
1014 source.set_avg(f64::NAN);
1015 }
1016
1017 destination.merge(&source);
1018
1019 assert_eq!(destination.count(), 1, "{name}");
1020 assert_eq!(destination.stored_min(), expected_summary, "{name}");
1021 assert_eq!(destination.stored_max(), expected_summary, "{name}");
1022 assert_eq!(destination.stored_sum(), expected_summary, "{name}");
1023 assert_eq!(destination.stored_avg(), expected_summary, "{name}");
1024 assert_eq!(destination.bin_count(), 2, "{name}");
1025 }
1026 }
1027
1028 #[cfg(feature = "serde")]
1029 #[test]
1030 fn json_null_summary_round_trips_as_nan() {
1031 let mut sketch = DDSketch::default();
1032 sketch.insert(42.0);
1033 sketch.set_avg(f64::NAN);
1034
1035 let encoded = serde_json::to_string(&sketch).expect("sketch should serialize");
1036 assert!(encoded.contains("\"avg\":null"));
1037
1038 let decoded: DDSketch = serde_json::from_str(&encoded).expect("sketch should deserialize");
1039 assert!(decoded.avg().expect("sketch should not be empty").is_nan());
1040 }
1041}
1042
1043#[cfg(test)]
1051mod property_tests {
1052 use proptest::prelude::*;
1053
1054 use super::*;
1055
1056 fn arb_bins(max_len: usize) -> impl Strategy<Value = SmallVec<[Bin; 4]>> {
1059 proptest::collection::btree_map(any::<i16>(), 1u32..=u32::MAX, 1..=max_len)
1060 .prop_map(|map| map.into_iter().map(|(k, n)| Bin { k, n }).collect())
1061 }
1062
1063 fn arb_limit() -> impl Strategy<Value = u16> {
1065 1u16..=32
1066 }
1067
1068 proptest! {
1069 #[test]
1074 fn prop_bin_count_never_exceeds_limit(
1075 mut bins in arb_bins(64),
1076 limit in arb_limit(),
1077 ) {
1078 trim_left(&mut bins, limit);
1079 prop_assert!(
1080 bins.len() <= limit as usize,
1081 "bin count {} exceeded limit {}",
1082 bins.len(),
1083 limit,
1084 );
1085 }
1086
1087 #[test]
1092 fn prop_output_bins_are_sorted_and_distinct(
1093 mut bins in arb_bins(64),
1094 limit in arb_limit(),
1095 ) {
1096 trim_left(&mut bins, limit);
1097 for window in bins.windows(2) {
1098 prop_assert!(
1099 window[0].k < window[1].k,
1100 "bins not strictly sorted after trim_left: {:?} >= {:?}",
1101 window[0].k,
1102 window[1].k,
1103 );
1104 }
1105 }
1106
1107 #[test]
1109 fn prop_no_op_when_within_limit(
1110 bins in arb_bins(16),
1111 extra in 0u16..=16,
1112 ) {
1113 let limit = bins.len() as u16 + extra;
1114 let original = bins.clone();
1115 let mut bins = bins;
1116 trim_left(&mut bins, limit);
1117 prop_assert_eq!(bins, original);
1118 }
1119
1120 #[test]
1131 fn prop_total_count_preserved_when_no_overflow(
1132 mut bins in proptest::collection::btree_map(any::<i16>(), 1u32..=1000, 1..=64usize)
1133 .prop_map(|map| -> SmallVec<[Bin; 4]> {
1134 map.into_iter().map(|(k, n)| Bin { k, n }).collect()
1135 }),
1136 limit in arb_limit(),
1137 ) {
1138 let total_before: u64 = bins.iter().map(|b| u64::from(b.n)).sum();
1139 trim_left(&mut bins, limit);
1140 let total_after: u64 = bins.iter().map(|b| u64::from(b.n)).sum();
1141 prop_assert_eq!(
1142 total_before, total_after,
1143 "total count changed: before={}, after={}",
1144 total_before, total_after,
1145 );
1146 }
1147
1148 #[test]
1155 fn prop_output_keys_are_highest_from_input(
1156 mut bins in arb_bins(64),
1157 limit in arb_limit(),
1158 ) {
1159 let all_keys: Vec<i16> = bins.iter().map(|b| b.k).collect();
1160 let expected_len = all_keys.len().min(limit as usize);
1161 let expected_keys: Vec<i16> = all_keys[all_keys.len() - expected_len..].to_vec();
1162
1163 trim_left(&mut bins, limit);
1164
1165 let actual_keys: Vec<i16> = bins.iter().map(|b| b.k).collect();
1166 prop_assert_eq!(
1167 actual_keys, expected_keys,
1168 "output keys don't match top {} keys of input",
1169 limit,
1170 );
1171 }
1172 }
1173}
1174
1175#[cfg(test)]
1181mod public_api_tests {
1182 use datadog_protos::metrics::Dogsketch;
1183
1184 use super::*;
1185 use crate::canonical::mapping::IndexMapping as _;
1187
1188 const QUANTILE_ABS_TOLERANCE: f64 = 2.0;
1191
1192 #[test]
1193 fn default_sketch_is_empty() {
1194 let sketch = DDSketch::default();
1195
1196 assert!(sketch.is_empty());
1197 assert_eq!(sketch.count(), 0);
1198 assert_eq!(sketch.bin_count(), 0);
1199 assert_eq!(sketch.min(), None);
1200 assert_eq!(sketch.max(), None);
1201 assert_eq!(sketch.sum(), None);
1202 assert_eq!(sketch.avg(), None);
1203 assert_eq!(sketch.quantile(0.5), None);
1204 }
1205
1206 #[test]
1207 fn insert_tracks_exact_basic_statistics() {
1208 let mut sketch = DDSketch::default();
1209 sketch.insert(1.0);
1210 sketch.insert(2.0);
1211 sketch.insert(3.0);
1212
1213 assert!(!sketch.is_empty());
1214 assert_eq!(sketch.count(), 3);
1215 assert_eq!(sketch.min(), Some(1.0));
1217 assert_eq!(sketch.max(), Some(3.0));
1218 assert_eq!(sketch.sum(), Some(6.0));
1219 assert_eq!(sketch.avg(), Some(2.0));
1220 }
1221
1222 #[test]
1223 fn insert_n_applies_the_weight_to_every_statistic() {
1224 let mut sketch = DDSketch::default();
1225 sketch.insert_n(10.0, 5);
1226
1227 assert_eq!(sketch.count(), 5);
1228 assert_eq!(sketch.min(), Some(10.0));
1229 assert_eq!(sketch.max(), Some(10.0));
1230 assert_eq!(sketch.sum(), Some(50.0));
1231 assert_eq!(sketch.avg(), Some(10.0));
1232 }
1233
1234 #[test]
1235 fn insert_many_matches_repeated_single_inserts() {
1236 let mut many = DDSketch::default();
1237 many.insert_many(&[1.0, 2.0, 3.0, 4.0]);
1238
1239 let mut single = DDSketch::default();
1240 for v in [1.0, 2.0, 3.0, 4.0] {
1241 single.insert(v);
1242 }
1243
1244 assert_eq!(many.count(), 4);
1245 assert_eq!(many, single);
1247 }
1248
1249 #[test]
1250 fn quantile_at_extremes_returns_exact_min_and_max() {
1251 let mut sketch = DDSketch::default();
1252 for i in 1..=100 {
1253 sketch.insert(f64::from(i));
1254 }
1255
1256 assert_eq!(sketch.quantile(0.0), Some(1.0));
1258 assert_eq!(sketch.quantile(-1.0), Some(1.0));
1259 assert_eq!(sketch.quantile(1.0), Some(100.0));
1260 assert_eq!(sketch.quantile(2.0), Some(100.0));
1261 }
1262
1263 #[test]
1264 fn quantile_estimates_interior_percentiles_within_relative_accuracy() {
1265 let mut sketch = DDSketch::default();
1266 for i in 1..=100 {
1267 sketch.insert(f64::from(i));
1268 }
1269
1270 let median = sketch.quantile(0.5).expect("non-empty sketch has a median");
1271 assert!(
1272 (median - 50.0).abs() <= QUANTILE_ABS_TOLERANCE,
1273 "median {} should be within {} of 50",
1274 median,
1275 QUANTILE_ABS_TOLERANCE
1276 );
1277
1278 let p90 = sketch.quantile(0.9).expect("non-empty sketch has a p90");
1279 assert!(
1280 (p90 - 90.0).abs() <= QUANTILE_ABS_TOLERANCE,
1281 "p90 {} should be near 90",
1282 p90
1283 );
1284 }
1285
1286 #[test]
1287 fn merge_combines_counts_bounds_and_sums() {
1288 let mut sketch1 = DDSketch::default();
1289 sketch1.insert(1.0);
1290 sketch1.insert(2.0);
1291
1292 let mut sketch2 = DDSketch::default();
1293 sketch2.insert(3.0);
1294 sketch2.insert(4.0);
1295
1296 sketch1.merge(&sketch2);
1297
1298 assert_eq!(sketch1.count(), 4);
1299 assert_eq!(sketch1.min(), Some(1.0));
1300 assert_eq!(sketch1.max(), Some(4.0));
1301 assert_eq!(sketch1.sum(), Some(10.0));
1302 assert_eq!(sketch1.avg(), Some(2.5));
1303 }
1304
1305 #[test]
1306 fn merge_produces_same_sketch_as_inserting_all_samples() {
1307 let mut merged = DDSketch::default();
1308 let mut left = DDSketch::default();
1309 let mut right = DDSketch::default();
1310 for i in 1..=50 {
1311 left.insert(f64::from(i));
1312 }
1313 for i in 51..=100 {
1314 right.insert(f64::from(i));
1315 }
1316 merged.merge(&left);
1317 merged.merge(&right);
1318
1319 let mut all_at_once = DDSketch::default();
1320 for i in 1..=100 {
1321 all_at_once.insert(f64::from(i));
1322 }
1323
1324 assert_eq!(merged.count(), all_at_once.count());
1325 assert_eq!(merged.bins(), all_at_once.bins());
1326 }
1327
1328 #[test]
1329 fn clear_resets_the_sketch_to_empty() {
1330 let mut sketch = DDSketch::default();
1331 sketch.insert(1.0);
1332 sketch.insert(2.0);
1333
1334 sketch.clear();
1335
1336 assert!(sketch.is_empty());
1337 assert_eq!(sketch.count(), 0);
1338 assert_eq!(sketch.bin_count(), 0);
1339 assert_eq!(sketch.min(), None);
1340 }
1341
1342 #[test]
1343 fn setters_override_the_tracked_statistics() {
1344 let mut sketch = DDSketch::default();
1345 sketch.insert(1.0);
1346
1347 sketch.set_count(100);
1348 sketch.set_sum(42.0);
1349 sketch.set_avg(0.42);
1350 sketch.set_min(-5.0);
1351 sketch.set_max(500.0);
1352
1353 assert_eq!(sketch.count(), 100);
1354 assert_eq!(sketch.sum(), Some(42.0));
1355 assert_eq!(sketch.avg(), Some(0.42));
1356 assert_eq!(sketch.min(), Some(-5.0));
1357 assert_eq!(sketch.max(), Some(500.0));
1358 }
1359
1360 #[test]
1361 fn insert_interpolate_buckets_preserves_total_count() {
1362 let mut sketch = DDSketch::default();
1363 sketch
1364 .insert_interpolate_buckets(vec![
1365 Bucket {
1366 upper_limit: 10.0,
1367 count: 4,
1368 },
1369 Bucket {
1370 upper_limit: 20.0,
1371 count: 6,
1372 },
1373 ])
1374 .expect("well-formed buckets should interpolate successfully");
1375
1376 assert_eq!(sketch.count(), 10);
1378 let median = sketch.quantile(0.5).expect("non-empty sketch has a median");
1379 assert!(
1380 (5.0..=25.0).contains(&median),
1381 "median {} should land within the interpolated 10..20 range",
1382 median
1383 );
1384 }
1385
1386 #[test]
1387 fn insert_interpolate_buckets_rejects_oversized_buckets() {
1388 let mut sketch = DDSketch::default();
1389 let result = sketch.insert_interpolate_buckets(vec![Bucket {
1390 upper_limit: 10.0,
1391 count: u64::from(u32::MAX) + 1,
1392 }]);
1393
1394 assert_eq!(result, Err("bucket size greater than u32::MAX"));
1395 }
1396
1397 #[test]
1398 fn dogsketch_round_trip_preserves_the_sketch() {
1399 let mut sketch = DDSketch::default();
1400 for i in 1..=25 {
1401 sketch.insert(f64::from(i));
1402 }
1403
1404 let mut dogsketch = Dogsketch::new();
1405 sketch.merge_to_dogsketch(&mut dogsketch);
1406 let recovered = DDSketch::try_from(dogsketch).expect("dogsketch produced by merge_to_dogsketch is valid");
1407
1408 assert_eq!(sketch, recovered);
1409 }
1410
1411 #[test]
1412 fn try_from_dogsketch_rejects_mismatched_bin_vectors() {
1413 let mut dogsketch = Dogsketch::new();
1414 dogsketch.set_cnt(3);
1415 dogsketch.set_k(vec![1, 2]);
1416 dogsketch.set_n(vec![1]); let result = DDSketch::try_from(dogsketch);
1419 assert_eq!(result, Err("k and n bin vectors have differing lengths"));
1420 }
1421
1422 #[test]
1423 fn value_for_key_is_zero_at_the_origin_and_monotonic() {
1424 assert_eq!(DDSketch::value_for_key(0), 0.0);
1425
1426 let low = DDSketch::value_for_key(100);
1428 let high = DDSketch::value_for_key(200);
1429 assert!(low > 0.0, "positive key should map to a positive value, got {}", low);
1430 assert!(high > low, "value_for_key should be monotonic: {} !> {}", high, low);
1431 }
1432
1433 #[test]
1434 fn remap_mapping_uses_the_agent_gamma() {
1435 let mapping = DDSketch::remap_mapping();
1436 assert!(crate::common::float_eq(mapping.gamma(), DDSKETCH_CONF_GAMMA_V));
1437 }
1438}