1use super::cache::{CacheableSeries, LabelCache, SERIES_CACHE_SIZE};
4#[cfg(feature = "eviction")]
5use super::current_cycle;
6use super::{DynamicIndexMap, DynamicLabelSet, HISTOGRAM_IDS, dynamic_index_map, thread_id};
7use crossbeam_utils::CachePadded;
8use parking_lot::RwLock;
9use std::cell::RefCell;
10#[cfg(feature = "eviction")]
11use std::sync::atomic::AtomicU32;
12use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicU64, AtomicUsize, Ordering};
13use std::sync::{Arc, Weak};
14
15const DEFAULT_MAX_SERIES: usize = 2000;
16const OVERFLOW_LABEL_KEY: &str = "__ft_overflow";
17const OVERFLOW_LABEL_VALUE: &str = "true";
18
19type HistogramIndexShard = CachePadded<RwLock<DynamicIndexMap<Arc<HistogramSeries>>>>;
20type HistogramSnapshotEntry = (DynamicLabelSet, Vec<(u64, u64)>, u64, u64);
21
22struct ShardedCounter {
23 cells: Vec<CachePadded<AtomicIsize>>,
24}
25
26impl ShardedCounter {
27 fn new(shard_count: usize) -> Self {
28 Self {
29 cells: (0..shard_count)
30 .map(|_| CachePadded::new(AtomicIsize::new(0)))
31 .collect(),
32 }
33 }
34
35 #[inline]
36 fn add_at(&self, shard_idx: usize, value: isize) {
37 self.cells[shard_idx].fetch_add(value, Ordering::Relaxed);
38 }
39
40 #[inline]
41 fn inc_at(&self, shard_idx: usize) {
42 self.add_at(shard_idx, 1);
43 }
44
45 #[inline]
46 fn sum(&self) -> isize {
47 self.cells
48 .iter()
49 .map(|cell| cell.load(Ordering::Relaxed))
50 .sum()
51 }
52}
53
54struct HistogramSeries {
55 bounds: Arc<Vec<u64>>,
56 buckets: Vec<ShardedCounter>,
57 sum: ShardedCounter,
58 count: ShardedCounter,
59 evicted: AtomicBool,
61 #[cfg(feature = "eviction")]
63 last_accessed_cycle: AtomicU32,
64}
65
66impl HistogramSeries {
67 #[cfg(feature = "eviction")]
68 fn new(bounds: Arc<Vec<u64>>, shard_count: usize, cycle: u32) -> Self {
69 let buckets = (0..=bounds.len())
70 .map(|_| ShardedCounter::new(shard_count))
71 .collect();
72 Self {
73 bounds,
74 buckets,
75 sum: ShardedCounter::new(shard_count),
76 count: ShardedCounter::new(shard_count),
77 evicted: AtomicBool::new(false),
78 last_accessed_cycle: AtomicU32::new(cycle),
79 }
80 }
81
82 #[cfg(not(feature = "eviction"))]
83 fn new(bounds: Arc<Vec<u64>>, shard_count: usize) -> Self {
84 let buckets = (0..=bounds.len())
85 .map(|_| ShardedCounter::new(shard_count))
86 .collect();
87 Self {
88 bounds,
89 buckets,
90 sum: ShardedCounter::new(shard_count),
91 count: ShardedCounter::new(shard_count),
92 evicted: AtomicBool::new(false),
93 }
94 }
95
96 #[inline]
97 fn is_evicted(&self) -> bool {
98 self.evicted.load(Ordering::Relaxed)
99 }
100
101 #[cfg(feature = "eviction")]
102 fn mark_evicted(&self) {
103 self.evicted.store(true, Ordering::Relaxed);
104 }
105
106 #[inline]
107 fn record_at(&self, shard_idx: usize, value: u64) {
108 let bucket_idx = self
109 .bounds
110 .iter()
111 .position(|&bound| value <= bound)
112 .unwrap_or(self.bounds.len());
113 self.buckets[bucket_idx].inc_at(shard_idx);
114 self.sum.add_at(shard_idx, value as isize);
115 self.count.inc_at(shard_idx);
116 }
119
120 #[cfg(feature = "eviction")]
122 #[inline]
123 fn touch(&self, cycle: u32) {
124 self.last_accessed_cycle.store(cycle, Ordering::Relaxed);
125 }
126
127 fn buckets_cumulative(&self) -> Vec<(u64, u64)> {
128 let mut result = Vec::with_capacity(self.buckets.len());
129 for (bound, cumulative) in self.buckets_cumulative_iter() {
130 result.push((bound, cumulative));
131 }
132 result
133 }
134
135 fn buckets_cumulative_iter(&self) -> impl Iterator<Item = (u64, u64)> + '_ {
136 let mut cumulative = 0i64;
137 self.buckets.iter().enumerate().map(move |(i, counter)| {
138 cumulative += counter.sum() as i64;
139 let bound = if i < self.bounds.len() {
140 self.bounds[i]
141 } else {
142 u64::MAX
143 };
144 (bound, cumulative as u64)
145 })
146 }
147
148 fn sum(&self) -> u64 {
149 self.sum.sum() as u64
150 }
151
152 fn count(&self) -> u64 {
153 self.count.sum() as u64
154 }
155}
156
157impl CacheableSeries for HistogramSeries {
158 fn is_evicted(&self) -> bool {
159 self.is_evicted()
160 }
161}
162
163#[derive(Clone)]
169pub struct DynamicHistogramSeries {
170 series: Arc<HistogramSeries>,
171 shard_mask: usize,
172}
173
174#[doc(hidden)]
176pub struct DynamicHistogramSeriesView<'a> {
177 series: &'a HistogramSeries,
178}
179
180impl<'a> DynamicHistogramSeriesView<'a> {
181 #[doc(hidden)]
183 pub fn buckets_cumulative_iter(&self) -> impl Iterator<Item = (u64, u64)> + '_ {
184 self.series.buckets_cumulative_iter()
185 }
186
187 #[doc(hidden)]
188 pub fn sum(&self) -> u64 {
189 self.series.sum()
190 }
191
192 #[doc(hidden)]
193 pub fn count(&self) -> u64 {
194 self.series.count()
195 }
196}
197
198impl DynamicHistogramSeries {
199 #[inline]
201 pub fn record(&self, value: u64) {
202 let shard_idx = thread_id() & self.shard_mask;
203 self.series.record_at(shard_idx, value);
204 }
205
206 pub fn buckets_cumulative(&self) -> Vec<(u64, u64)> {
208 self.series.buckets_cumulative()
209 }
210
211 pub fn sum(&self) -> u64 {
213 self.series.sum()
214 }
215
216 pub fn count(&self) -> u64 {
218 self.series.count()
219 }
220
221 #[inline]
223 pub fn is_evicted(&self) -> bool {
224 self.series.is_evicted()
225 }
226}
227
228thread_local! {
229 static SERIES_CACHE: RefCell<LabelCache<Weak<HistogramSeries>, SERIES_CACHE_SIZE>> =
230 RefCell::new(LabelCache::new());
231}
232
233pub struct DynamicHistogram {
238 id: usize,
239 bounds: Arc<Vec<u64>>,
240 shard_count: usize,
241 max_series: usize,
242 shard_mask: usize,
243 index_shards: Vec<HistogramIndexShard>,
244 series_count: AtomicUsize,
246 overflow_count: AtomicU64,
248}
249
250impl DynamicHistogram {
251 pub fn new(bounds: &[u64], shard_count: usize) -> Self {
253 Self::with_limits(bounds, shard_count, DEFAULT_MAX_SERIES)
254 }
255
256 pub fn with_limits(bounds: &[u64], shard_count: usize, max_series: usize) -> Self {
264 let shard_count = shard_count.next_power_of_two();
265 let id = HISTOGRAM_IDS.fetch_add(1, Ordering::Relaxed);
266 Self {
267 id,
268 bounds: Arc::new(bounds.to_vec()),
269 shard_count,
270 max_series,
271 shard_mask: shard_count - 1,
272 index_shards: (0..shard_count)
273 .map(|_| CachePadded::new(RwLock::new(dynamic_index_map())))
274 .collect(),
275 series_count: AtomicUsize::new(0),
276 overflow_count: AtomicU64::new(0),
277 }
278 }
279
280 pub fn with_latency_buckets(shard_count: usize) -> Self {
282 Self::with_limits(
283 &[
284 10, 50, 100, 500, 1_000, 5_000, 10_000, 50_000, 100_000, 500_000, 1_000_000, 5_000_000, 10_000_000, ],
298 shard_count,
299 DEFAULT_MAX_SERIES,
300 )
301 }
302
303 pub fn series(&self, labels: &[(&str, &str)]) -> DynamicHistogramSeries {
307 if let Some(series) = self.cached_series(labels) {
308 return DynamicHistogramSeries {
309 series,
310 shard_mask: self.shard_mask,
311 };
312 }
313 let series = self.lookup_or_create(labels);
314 self.update_cache(labels, &series);
315 DynamicHistogramSeries {
316 series,
317 shard_mask: self.shard_mask,
318 }
319 }
320
321 #[inline]
323 pub fn record(&self, labels: &[(&str, &str)], value: u64) {
324 if let Some(series) = self.cached_series(labels) {
325 let shard_idx = thread_id() & self.shard_mask;
326 series.record_at(shard_idx, value);
327 return;
328 }
329
330 let series = self.lookup_or_create(labels);
331 self.update_cache(labels, &series);
332 let shard_idx = thread_id() & self.shard_mask;
333 series.record_at(shard_idx, value);
334 }
335
336 pub fn buckets_cumulative(&self, labels: &[(&str, &str)]) -> Vec<(u64, u64)> {
338 let key = DynamicLabelSet::from_pairs(labels);
339 let index_shard = self.index_shard_for(&key);
340 self.index_shards[index_shard]
341 .read()
342 .get(&key)
343 .map(|series| series.buckets_cumulative())
344 .unwrap_or_default()
345 }
346
347 pub fn sum(&self, labels: &[(&str, &str)]) -> u64 {
349 let key = DynamicLabelSet::from_pairs(labels);
350 let index_shard = self.index_shard_for(&key);
351 self.index_shards[index_shard]
352 .read()
353 .get(&key)
354 .map(|series| series.sum())
355 .unwrap_or(0)
356 }
357
358 pub fn count(&self, labels: &[(&str, &str)]) -> u64 {
360 let key = DynamicLabelSet::from_pairs(labels);
361 let index_shard = self.index_shard_for(&key);
362 self.index_shards[index_shard]
363 .read()
364 .get(&key)
365 .map(|series| series.count())
366 .unwrap_or(0)
367 }
368
369 pub fn snapshot(&self) -> Vec<HistogramSnapshotEntry> {
371 let mut out = Vec::new();
372 for shard in &self.index_shards {
373 let guard = shard.read();
374 for (labels, series) in guard.iter() {
375 out.push((
376 labels.clone(),
377 series.buckets_cumulative(),
378 series.sum(),
379 series.count(),
380 ));
381 }
382 }
383 out
384 }
385
386 pub fn cardinality(&self) -> usize {
388 self.index_shards
389 .iter()
390 .map(|shard| shard.read().len())
391 .sum()
392 }
393
394 pub fn overflow_count(&self) -> u64 {
399 self.overflow_count.load(Ordering::Relaxed)
400 }
401
402 #[doc(hidden)]
409 pub fn visit_series<F>(&self, mut f: F)
410 where
411 F: for<'a> FnMut(&'a [(String, String)], DynamicHistogramSeriesView<'a>),
412 {
413 for shard in &self.index_shards {
414 let guard = shard.read();
415 for (labels, series) in guard.iter() {
416 f(labels.pairs(), DynamicHistogramSeriesView { series });
417 }
418 }
419 }
420
421 #[cfg(feature = "eviction")]
432 pub fn evict_stale(&self, max_staleness: u32) -> usize {
433 let cycle = current_cycle();
434 let mut removed = 0;
435
436 for shard in &self.index_shards {
437 let mut guard = shard.write();
438 guard.retain(|_labels, series| {
439 if Arc::strong_count(series) > 1 {
442 return true;
443 }
444 let last = series.last_accessed_cycle.load(Ordering::Relaxed);
446 let stale = cycle.saturating_sub(last) > max_staleness;
447 if stale {
448 series.mark_evicted();
449 removed += 1;
450 self.series_count.fetch_sub(1, Ordering::Relaxed);
451 }
452 !stale
453 });
454 }
455
456 removed
457 }
458
459 fn lookup_or_create(&self, labels: &[(&str, &str)]) -> Arc<HistogramSeries> {
460 let requested_key = DynamicLabelSet::from_pairs(labels);
461 let requested_shard = self.index_shard_for(&requested_key);
462 #[cfg(feature = "eviction")]
463 let cycle = current_cycle();
464
465 if let Some(series) = self.index_shards[requested_shard]
467 .read()
468 .get(&requested_key)
469 {
470 #[cfg(feature = "eviction")]
471 series.touch(cycle);
472 return Arc::clone(series);
473 }
474
475 let key = if self.max_series > 0
477 && self.series_count.load(Ordering::Relaxed) >= self.max_series
478 {
479 self.overflow_count.fetch_add(1, Ordering::Relaxed);
480 DynamicLabelSet::from_pairs(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)])
481 } else {
482 requested_key
483 };
484 let shard = self.index_shard_for(&key);
485
486 if let Some(series) = self.index_shards[shard].read().get(&key) {
487 #[cfg(feature = "eviction")]
488 series.touch(cycle);
489 return Arc::clone(series);
490 }
491
492 let mut guard = self.index_shards[shard].write();
493 if let Some(series) = guard.get(&key) {
494 #[cfg(feature = "eviction")]
495 series.touch(cycle);
496 return Arc::clone(series);
497 }
498 #[cfg(feature = "eviction")]
499 let series = Arc::new(HistogramSeries::new(
500 Arc::clone(&self.bounds),
501 self.shard_count,
502 cycle,
503 ));
504 #[cfg(not(feature = "eviction"))]
505 let series = Arc::new(HistogramSeries::new(
506 Arc::clone(&self.bounds),
507 self.shard_count,
508 ));
509 guard.insert(key, Arc::clone(&series));
510 self.series_count.fetch_add(1, Ordering::Relaxed);
511 series
512 }
513
514 fn index_shard_for(&self, key: &DynamicLabelSet) -> usize {
515 key.shard_index(self.shard_mask)
516 }
517
518 fn cached_series(&self, labels: &[(&str, &str)]) -> Option<Arc<HistogramSeries>> {
519 SERIES_CACHE.with(|cache| {
520 let series = cache.borrow_mut().get(self.id, labels)?;
521 #[cfg(feature = "eviction")]
522 series.touch(current_cycle());
523 Some(series)
524 })
525 }
526
527 fn update_cache(&self, labels: &[(&str, &str)], series: &Arc<HistogramSeries>) {
528 SERIES_CACHE.with(|cache| {
529 cache
530 .borrow_mut()
531 .insert(self.id, labels, Arc::downgrade(series));
532 });
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
541 fn test_basic_recording() {
542 let h = DynamicHistogram::new(&[10, 100, 1000], 4);
543 let labels = &[("org_id", "42")];
544
545 h.record(labels, 5); h.record(labels, 50); h.record(labels, 500); h.record(labels, 5000); let buckets = h.buckets_cumulative(labels);
551 assert_eq!(buckets.len(), 4);
552 assert_eq!(buckets[0], (10, 1));
553 assert_eq!(buckets[1], (100, 2));
554 assert_eq!(buckets[2], (1000, 3));
555 assert_eq!(buckets[3], (u64::MAX, 4));
556
557 assert_eq!(h.count(labels), 4);
558 assert_eq!(h.sum(labels), 5 + 50 + 500 + 5000);
559 }
560
561 #[test]
562 fn test_label_order_is_canonicalized() {
563 let h = DynamicHistogram::new(&[10, 100], 4);
564
565 h.record(&[("org_id", "42"), ("endpoint", "abc")], 5);
566
567 assert_eq!(h.count(&[("endpoint", "abc"), ("org_id", "42")]), 1);
568 }
569
570 #[test]
571 fn test_series_handle() {
572 let h = DynamicHistogram::new(&[10, 100, 1000], 4);
573 let series = h.series(&[("org_id", "42")]);
574
575 series.record(5);
576 series.record(50);
577 series.record(500);
578
579 assert_eq!(series.count(), 3);
580 assert_eq!(series.sum(), 555);
581 assert_eq!(h.count(&[("org_id", "42")]), 3);
582 }
583
584 #[test]
585 fn test_multiple_label_sets() {
586 let h = DynamicHistogram::new(&[100], 4);
587
588 h.record(&[("org_id", "1")], 50);
589 h.record(&[("org_id", "2")], 150);
590
591 assert_eq!(h.count(&[("org_id", "1")]), 1);
592 assert_eq!(h.count(&[("org_id", "2")]), 1);
593
594 let snap = h.snapshot();
595 assert_eq!(snap.len(), 2);
596 }
597
598 #[test]
599 fn test_overflow_bucket_routes_new_series_at_capacity() {
600 let h = DynamicHistogram::with_limits(&[100], 4, 1);
601
602 h.record(&[("org_id", "1")], 50);
603 h.record(&[("org_id", "2")], 150);
604
605 assert_eq!(h.cardinality(), 2);
606 assert_eq!(h.count(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)]), 1);
607 assert_eq!(h.sum(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)]), 150);
608 }
609
610 #[test]
611 fn test_concurrent_cap_bounded_overshoot() {
612 use std::sync::{Arc, Barrier};
613 use std::thread;
614
615 let cap = 10;
616 let threads = 16;
617 let h = Arc::new(DynamicHistogram::with_limits(&[100, 1000], 4, cap));
618 let barrier = Arc::new(Barrier::new(threads));
619
620 let handles: Vec<_> = (0..threads)
621 .map(|t| {
622 let h = Arc::clone(&h);
623 let barrier = Arc::clone(&barrier);
624 thread::spawn(move || {
625 barrier.wait();
626 for i in 0..5 {
627 let label = format!("t{t}_s{i}");
628 h.record(&[("key", &label)], 42);
629 }
630 })
631 })
632 .collect();
633
634 for handle in handles {
635 handle.join().unwrap();
636 }
637
638 let card = h.cardinality();
639 assert!(
640 card <= cap + threads + 1,
641 "cardinality {card} exceeded bounded overshoot (cap={cap}, threads={threads})"
642 );
643 assert!(h.overflow_count() > 0, "overflow should have triggered");
644 }
645}