1use super::cache::{CacheableSeries, LabelCache, SERIES_CACHE_SIZE};
4#[cfg(feature = "eviction")]
5use super::current_cycle;
6use super::{COUNTER_IDS, DynamicIndexMap, DynamicLabelSet, dynamic_index_map, thread_id};
7use crossbeam_utils::CachePadded;
8use parking_lot::RwLock;
9use std::cell::RefCell;
10use std::collections::BTreeMap;
11#[cfg(feature = "eviction")]
12use std::sync::atomic::AtomicU32;
13use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicU64, AtomicUsize, Ordering};
14use std::sync::{Arc, Weak};
15
16const DEFAULT_MAX_SERIES: usize = 2000;
17const OVERFLOW_LABEL_KEY: &str = "__ft_overflow";
18const OVERFLOW_LABEL_VALUE: &str = "true";
19
20struct CounterSetSeries {
21 cells: Vec<AtomicIsize>,
22 counters: usize,
23 stride: usize,
24 evicted: AtomicBool,
25 #[cfg(feature = "eviction")]
26 last_accessed_cycle: AtomicU32,
27}
28
29type CounterSetIndexShard = CachePadded<RwLock<DynamicIndexMap<Arc<CounterSetSeries>>>>;
30
31impl CounterSetSeries {
32 #[cfg(feature = "eviction")]
33 fn new(shards: usize, counters: usize, current_cycle: u32) -> Self {
34 let stride = counter_row_stride(counters);
35 Self {
36 cells: (0..(shards * stride))
37 .map(|_| AtomicIsize::new(0))
38 .collect(),
39 counters,
40 stride,
41 evicted: AtomicBool::new(false),
42 last_accessed_cycle: AtomicU32::new(current_cycle),
43 }
44 }
45
46 #[cfg(not(feature = "eviction"))]
47 fn new(shards: usize, counters: usize) -> Self {
48 let stride = counter_row_stride(counters);
49 Self {
50 cells: (0..(shards * stride))
51 .map(|_| AtomicIsize::new(0))
52 .collect(),
53 counters,
54 stride,
55 evicted: AtomicBool::new(false),
56 }
57 }
58
59 #[inline]
60 fn cell_at(&self, index: usize) -> &AtomicIsize {
61 if cfg!(debug_assertions) {
62 self.cells.get(index).expect("index out of bounds")
63 } else {
64 unsafe { self.cells.get_unchecked(index) }
67 }
68 }
69
70 #[inline]
71 fn row_offset(&self, shard_idx: usize) -> usize {
72 shard_idx * self.stride
73 }
74
75 #[inline]
76 fn add_at(&self, shard_idx: usize, counter_idx: usize, value: isize) {
77 assert!(counter_idx < self.counters, "counter index out of bounds");
78 let offset = self.row_offset(shard_idx);
79 self.cell_at(offset + counter_idx)
80 .fetch_add(value, Ordering::Relaxed);
81 }
82
83 #[inline]
84 fn add_index_values_at(&self, shard_idx: usize, updates: &[(usize, isize)]) {
85 let offset = self.row_offset(shard_idx);
86 for (counter_idx, value) in updates {
87 assert!(*counter_idx < self.counters, "counter index out of bounds");
88 self.cell_at(offset + *counter_idx)
89 .fetch_add(*value, Ordering::Relaxed);
90 }
91 }
92
93 #[inline(always)]
94 fn add_values_at(&self, shard_idx: usize, values: &[isize]) {
95 assert_eq!(values.len(), self.counters, "values must match counters");
96 let offset = self.row_offset(shard_idx);
97 match values {
98 [a] => {
99 self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
100 }
101 [a, b] => {
102 self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
103 self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
104 }
105 [a, b, c] => {
106 self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
107 self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
108 self.cell_at(offset + 2).fetch_add(*c, Ordering::Relaxed);
109 }
110 [a, b, c, d] => {
111 self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
112 self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
113 self.cell_at(offset + 2).fetch_add(*c, Ordering::Relaxed);
114 self.cell_at(offset + 3).fetch_add(*d, Ordering::Relaxed);
115 }
116 [a, b, c, d, e] => {
117 self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
118 self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
119 self.cell_at(offset + 2).fetch_add(*c, Ordering::Relaxed);
120 self.cell_at(offset + 3).fetch_add(*d, Ordering::Relaxed);
121 self.cell_at(offset + 4).fetch_add(*e, Ordering::Relaxed);
122 }
123 [a, b, c, d, e, f] => {
124 self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
125 self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
126 self.cell_at(offset + 2).fetch_add(*c, Ordering::Relaxed);
127 self.cell_at(offset + 3).fetch_add(*d, Ordering::Relaxed);
128 self.cell_at(offset + 4).fetch_add(*e, Ordering::Relaxed);
129 self.cell_at(offset + 5).fetch_add(*f, Ordering::Relaxed);
130 }
131 [a, b, c, d, e, f, g] => {
132 self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
133 self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
134 self.cell_at(offset + 2).fetch_add(*c, Ordering::Relaxed);
135 self.cell_at(offset + 3).fetch_add(*d, Ordering::Relaxed);
136 self.cell_at(offset + 4).fetch_add(*e, Ordering::Relaxed);
137 self.cell_at(offset + 5).fetch_add(*f, Ordering::Relaxed);
138 self.cell_at(offset + 6).fetch_add(*g, Ordering::Relaxed);
139 }
140 [a, b, c, d, e, f, g, h] => {
141 self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
142 self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
143 self.cell_at(offset + 2).fetch_add(*c, Ordering::Relaxed);
144 self.cell_at(offset + 3).fetch_add(*d, Ordering::Relaxed);
145 self.cell_at(offset + 4).fetch_add(*e, Ordering::Relaxed);
146 self.cell_at(offset + 5).fetch_add(*f, Ordering::Relaxed);
147 self.cell_at(offset + 6).fetch_add(*g, Ordering::Relaxed);
148 self.cell_at(offset + 7).fetch_add(*h, Ordering::Relaxed);
149 }
150 values => {
151 let row = if cfg!(debug_assertions) {
152 self.cells
153 .get(offset..offset + self.counters)
154 .expect("row index out of bounds")
155 } else {
156 unsafe {
159 std::slice::from_raw_parts(self.cells.as_ptr().add(offset), self.counters)
160 }
161 };
162 for (cell, value) in row.iter().zip(values.iter().copied()) {
163 cell.fetch_add(value, Ordering::Relaxed);
164 }
165 }
166 }
167 }
168
169 #[inline]
170 fn sum(&self, counter_idx: usize) -> isize {
171 assert!(counter_idx < self.counters, "counter index out of bounds");
172 let shards = self.cells.len() / self.stride;
173 (0..shards)
174 .map(|shard| {
175 self.cell_at((shard * self.stride) + counter_idx)
176 .load(Ordering::Relaxed)
177 })
178 .sum()
179 }
180
181 #[inline]
182 fn values(&self) -> Vec<isize> {
183 (0..self.counters).map(|idx| self.sum(idx)).collect()
184 }
185
186 #[inline]
187 fn sum_all(&self) -> isize {
188 let shards = self.cells.len() / self.stride;
189 let mut total = 0isize;
190 for shard in 0..shards {
191 let offset = shard * self.stride;
192 for counter_idx in 0..self.counters {
193 total += self.cell_at(offset + counter_idx).load(Ordering::Relaxed);
194 }
195 }
196 total
197 }
198
199 #[inline]
200 fn sum_and_reset(&self, counter_idx: usize) -> isize {
201 assert!(counter_idx < self.counters, "counter index out of bounds");
202 let shards = self.cells.len() / self.stride;
203 (0..shards)
204 .map(|shard| {
205 self.cell_at((shard * self.stride) + counter_idx)
206 .swap(0, Ordering::Relaxed)
207 })
208 .sum()
209 }
210
211 #[inline]
212 fn snapshot_and_reset(&self) -> Vec<isize> {
213 let mut values = vec![0; self.counters];
214 let shards = self.cells.len() / self.stride;
215 for shard in 0..shards {
216 let offset = shard * self.stride;
217 for (counter_idx, value) in values.iter_mut().enumerate() {
218 *value += self
219 .cell_at(offset + counter_idx)
220 .swap(0, Ordering::Relaxed);
221 }
222 }
223 values
224 }
225
226 #[cfg(feature = "eviction")]
227 #[inline]
228 fn touch(&self, cycle: u32) {
229 self.last_accessed_cycle.store(cycle, Ordering::Relaxed);
230 }
231
232 #[inline]
233 fn is_evicted(&self) -> bool {
234 self.evicted.load(Ordering::Relaxed)
235 }
236
237 #[cfg(feature = "eviction")]
238 fn mark_evicted(&self) {
239 self.evicted.store(true, Ordering::Relaxed);
240 }
241}
242
243impl CacheableSeries for CounterSetSeries {
244 fn is_evicted(&self) -> bool {
245 self.is_evicted()
246 }
247}
248
249#[derive(Clone)]
254pub struct DynamicCounterSetSeries {
255 series: Arc<CounterSetSeries>,
256 names: Arc<Vec<String>>,
257 indexes: Arc<BTreeMap<String, usize>>,
258 shard_mask: usize,
259}
260
261impl DynamicCounterSetSeries {
262 #[inline]
264 pub fn inc(&self, counter: &str) {
265 self.add(counter, 1);
266 }
267
268 #[inline]
270 pub fn add(&self, counter: &str, value: isize) {
271 let counter_idx = self
272 .counter_index(counter)
273 .unwrap_or_else(|| panic!("unknown counter name: {counter}"));
274 self.add_index(counter_idx, value);
275 }
276
277 #[inline]
279 pub fn add_index(&self, counter_idx: usize, value: isize) {
280 let shard_idx = thread_id() & self.shard_mask;
281 self.series.add_at(shard_idx, counter_idx, value);
282 }
283
284 #[inline]
286 pub fn add_values(&self, updates: &[(&str, isize)]) {
287 let shard_idx = thread_id() & self.shard_mask;
288 for (counter, value) in updates {
289 let counter_idx = self
290 .counter_index(counter)
291 .unwrap_or_else(|| panic!("unknown counter name: {counter}"));
292 self.series.add_at(shard_idx, counter_idx, *value);
293 }
294 }
295
296 #[inline]
298 pub fn add_index_values(&self, updates: &[(usize, isize)]) {
299 let shard_idx = thread_id() & self.shard_mask;
300 self.series.add_index_values_at(shard_idx, updates);
301 }
302
303 #[inline]
305 pub fn add_all_values(&self, values: &[isize]) {
306 let shard_idx = thread_id() & self.shard_mask;
307 self.series.add_values_at(shard_idx, values);
308 }
309
310 #[inline]
312 pub fn get(&self, counter: &str) -> isize {
313 let counter_idx = self
314 .counter_index(counter)
315 .unwrap_or_else(|| panic!("unknown counter name: {counter}"));
316 self.get_index(counter_idx)
317 }
318
319 #[inline]
321 pub fn get_index(&self, counter_idx: usize) -> isize {
322 self.series.sum(counter_idx)
323 }
324
325 #[inline]
327 pub fn values(&self) -> Vec<isize> {
328 self.series.values()
329 }
330
331 #[inline]
333 pub fn counter_index(&self, counter: &str) -> Option<usize> {
334 self.indexes.get(counter).copied()
335 }
336
337 #[inline]
339 pub fn counter_names(&self) -> &[String] {
340 &self.names
341 }
342
343 #[inline]
349 pub fn is_evicted(&self) -> bool {
350 self.series.is_evicted()
351 }
352}
353
354thread_local! {
355 static SERIES_CACHE: RefCell<LabelCache<Weak<CounterSetSeries>, SERIES_CACHE_SIZE>> =
356 RefCell::new(LabelCache::new());
357}
358
359pub struct DynamicCounterSet {
386 id: usize,
387 shard_count: usize,
388 max_series: usize,
389 shard_mask: usize,
390 names: Arc<Vec<String>>,
391 indexes: Arc<BTreeMap<String, usize>>,
392 index_shards: Vec<CounterSetIndexShard>,
393 series_count: AtomicUsize,
394 overflow_count: AtomicU64,
395}
396
397impl DynamicCounterSet {
398 pub fn new<I, S>(counter_names: I) -> Self
403 where
404 I: IntoIterator<Item = S>,
405 S: AsRef<str>,
406 {
407 Self::with_shards(default_shard_count(), counter_names)
408 }
409
410 pub fn with_shards<I, S>(shard_count: usize, counter_names: I) -> Self
412 where
413 I: IntoIterator<Item = S>,
414 S: AsRef<str>,
415 {
416 Self::with_max_series(shard_count, DEFAULT_MAX_SERIES, counter_names)
417 }
418
419 pub fn with_max_series<I, S>(shard_count: usize, max_series: usize, counter_names: I) -> Self
425 where
426 I: IntoIterator<Item = S>,
427 S: AsRef<str>,
428 {
429 let shard_count = shard_count.next_power_of_two();
430 let (names, indexes) = build_counter_index(counter_names);
431 let id = COUNTER_IDS.fetch_add(1, Ordering::Relaxed);
432 Self {
433 id,
434 shard_count,
435 max_series,
436 shard_mask: shard_count - 1,
437 names: Arc::new(names),
438 indexes: Arc::new(indexes),
439 index_shards: (0..shard_count)
440 .map(|_| CachePadded::new(RwLock::new(dynamic_index_map())))
441 .collect(),
442 series_count: AtomicUsize::new(0),
443 overflow_count: AtomicU64::new(0),
444 }
445 }
446
447 pub fn series(&self, labels: &[(&str, &str)]) -> DynamicCounterSetSeries {
449 self.get_or_create(labels)
450 }
451
452 pub fn get_or_create(&self, labels: &[(&str, &str)]) -> DynamicCounterSetSeries {
454 self.series_handle(self.cached_or_create_series(labels))
455 }
456
457 #[inline]
459 pub fn inc(&self, labels: &[(&str, &str)], counter: &str) {
460 self.add(labels, counter, 1);
461 }
462
463 #[inline]
465 pub fn add(&self, labels: &[(&str, &str)], counter: &str, value: isize) {
466 let counter_idx = self
467 .counter_index(counter)
468 .unwrap_or_else(|| panic!("unknown counter name: {counter}"));
469 self.add_index(labels, counter_idx, value);
470 }
471
472 #[inline]
474 pub fn add_index(&self, labels: &[(&str, &str)], counter_idx: usize, value: isize) {
475 let series = self.cached_or_create_series(labels);
476 let shard_idx = thread_id() & self.shard_mask;
477 series.add_at(shard_idx, counter_idx, value);
478 }
479
480 #[inline]
482 pub fn add_values(&self, labels: &[(&str, &str)], updates: &[(&str, isize)]) {
483 let series = self.cached_or_create_series(labels);
484 let shard_idx = thread_id() & self.shard_mask;
485 for (counter, value) in updates {
486 let counter_idx = self
487 .counter_index(counter)
488 .unwrap_or_else(|| panic!("unknown counter name: {counter}"));
489 series.add_at(shard_idx, counter_idx, *value);
490 }
491 }
492
493 #[inline]
495 pub fn add_index_values(&self, labels: &[(&str, &str)], updates: &[(usize, isize)]) {
496 let series = self.cached_or_create_series(labels);
497 let shard_idx = thread_id() & self.shard_mask;
498 series.add_index_values_at(shard_idx, updates);
499 }
500
501 #[inline]
503 pub fn add_all_values(&self, labels: &[(&str, &str)], values: &[isize]) {
504 let series = self.cached_or_create_series(labels);
505 let shard_idx = thread_id() & self.shard_mask;
506 series.add_values_at(shard_idx, values);
507 }
508
509 pub fn get(&self, labels: &[(&str, &str)], counter: &str) -> isize {
511 let counter_idx = self
512 .counter_index(counter)
513 .unwrap_or_else(|| panic!("unknown counter name: {counter}"));
514 self.get_index(labels, counter_idx)
515 }
516
517 pub fn get_index(&self, labels: &[(&str, &str)], counter_idx: usize) -> isize {
519 assert!(
520 counter_idx < self.names.len(),
521 "counter index out of bounds"
522 );
523 let key = DynamicLabelSet::from_pairs(labels);
524 let index_shard = self.index_shard_for(&key);
525 self.index_shards[index_shard]
526 .read()
527 .get(&key)
528 .map(|series| series.sum(counter_idx))
529 .unwrap_or(0)
530 }
531
532 pub fn values(&self, labels: &[(&str, &str)]) -> Vec<isize> {
534 let key = DynamicLabelSet::from_pairs(labels);
535 let index_shard = self.index_shard_for(&key);
536 self.index_shards[index_shard]
537 .read()
538 .get(&key)
539 .map(|series| series.values())
540 .unwrap_or_else(|| vec![0; self.names.len()])
541 }
542
543 pub fn sum_all(&self) -> isize {
545 self.index_shards
546 .iter()
547 .map(|shard| {
548 let guard = shard.read();
549 guard.values().map(|series| series.sum_all()).sum::<isize>()
550 })
551 .sum()
552 }
553
554 pub fn snapshot(&self) -> Vec<(DynamicLabelSet, Vec<isize>)> {
556 let mut out = Vec::new();
557 for shard in &self.index_shards {
558 let guard = shard.read();
559 for (labels, series) in guard.iter() {
560 out.push((labels.clone(), series.values()));
561 }
562 }
563 out
564 }
565
566 pub fn snapshot_and_reset(&self) -> Vec<(DynamicLabelSet, Vec<isize>)> {
571 let mut out = Vec::new();
572 for shard in &self.index_shards {
573 let guard = shard.read();
574 for (labels, series) in guard.iter() {
575 out.push((labels.clone(), series.snapshot_and_reset()));
576 }
577 }
578 out
579 }
580
581 pub fn sum_and_reset(&self, counter_idx: usize) -> isize {
583 assert!(
584 counter_idx < self.names.len(),
585 "counter index out of bounds"
586 );
587 self.index_shards
588 .iter()
589 .map(|shard| {
590 let guard = shard.read();
591 guard
592 .values()
593 .map(|series| series.sum_and_reset(counter_idx))
594 .sum::<isize>()
595 })
596 .sum()
597 }
598
599 pub fn visit_series(&self, mut f: impl FnMut(&str, &[(String, String)], isize)) {
605 for shard in &self.index_shards {
606 let guard = shard.read();
607 for (labels, series) in guard.iter() {
608 for (counter_idx, counter_name) in self.names.iter().enumerate() {
609 f(
610 counter_name.as_str(),
611 labels.pairs(),
612 series.sum(counter_idx),
613 );
614 }
615 }
616 }
617 }
618
619 #[inline]
621 pub fn counter_names(&self) -> &[String] {
622 &self.names
623 }
624
625 #[inline]
627 pub fn counter_index(&self, counter: &str) -> Option<usize> {
628 self.indexes.get(counter).copied()
629 }
630
631 pub fn cardinality(&self) -> usize {
633 self.index_shards
634 .iter()
635 .map(|shard| shard.read().len())
636 .sum()
637 }
638
639 pub fn overflow_count(&self) -> u64 {
641 self.overflow_count.load(Ordering::Relaxed)
642 }
643
644 #[cfg(feature = "eviction")]
649 pub fn evict_stale(&self, max_staleness: u32) -> usize {
650 let cycle = current_cycle();
651 let mut removed = 0;
652
653 for shard in &self.index_shards {
654 let mut guard = shard.write();
655 guard.retain(|_labels, series| {
656 if Arc::strong_count(series) > 1 {
657 return true;
658 }
659 let last = series.last_accessed_cycle.load(Ordering::Relaxed);
660 let stale = cycle.saturating_sub(last) > max_staleness;
661 if stale {
662 series.mark_evicted();
663 removed += 1;
664 self.series_count.fetch_sub(1, Ordering::Relaxed);
665 }
666 !stale
667 });
668 }
669
670 removed
671 }
672
673 fn series_handle(&self, series: Arc<CounterSetSeries>) -> DynamicCounterSetSeries {
674 DynamicCounterSetSeries {
675 series,
676 names: Arc::clone(&self.names),
677 indexes: Arc::clone(&self.indexes),
678 shard_mask: self.shard_mask,
679 }
680 }
681
682 fn cached_or_create_series(&self, labels: &[(&str, &str)]) -> Arc<CounterSetSeries> {
683 if let Some(series) = self.cached_series(labels) {
684 return series;
685 }
686 let series = self.lookup_or_create(labels);
687 self.update_cache(labels, &series);
688 series
689 }
690
691 fn lookup_or_create(&self, labels: &[(&str, &str)]) -> Arc<CounterSetSeries> {
692 let requested_key = DynamicLabelSet::from_pairs(labels);
693 let requested_shard = self.index_shard_for(&requested_key);
694 #[cfg(feature = "eviction")]
695 let cycle = current_cycle();
696
697 if let Some(series) = self.index_shards[requested_shard]
698 .read()
699 .get(&requested_key)
700 {
701 #[cfg(feature = "eviction")]
702 series.touch(cycle);
703 return Arc::clone(series);
704 }
705
706 let key = if self.max_series > 0
707 && self.series_count.load(Ordering::Relaxed) >= self.max_series
708 {
709 self.overflow_count.fetch_add(1, Ordering::Relaxed);
710 DynamicLabelSet::from_pairs(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)])
711 } else {
712 requested_key
713 };
714 let shard = self.index_shard_for(&key);
715
716 if let Some(series) = self.index_shards[shard].read().get(&key) {
717 #[cfg(feature = "eviction")]
718 series.touch(cycle);
719 return Arc::clone(series);
720 }
721
722 let mut guard = self.index_shards[shard].write();
723 if let Some(series) = guard.get(&key) {
724 #[cfg(feature = "eviction")]
725 series.touch(cycle);
726 return Arc::clone(series);
727 }
728 #[cfg(feature = "eviction")]
729 let series = Arc::new(CounterSetSeries::new(
730 self.shard_count,
731 self.names.len(),
732 cycle,
733 ));
734 #[cfg(not(feature = "eviction"))]
735 let series = Arc::new(CounterSetSeries::new(self.shard_count, self.names.len()));
736 guard.insert(key, Arc::clone(&series));
737 self.series_count.fetch_add(1, Ordering::Relaxed);
738 series
739 }
740
741 fn index_shard_for(&self, key: &DynamicLabelSet) -> usize {
742 key.shard_index(self.shard_mask)
743 }
744
745 fn cached_series(&self, labels: &[(&str, &str)]) -> Option<Arc<CounterSetSeries>> {
746 SERIES_CACHE.with(|cache| {
747 let series = cache.borrow_mut().get(self.id, labels)?;
748 #[cfg(feature = "eviction")]
749 series.touch(current_cycle());
750 Some(series)
751 })
752 }
753
754 fn update_cache(&self, labels: &[(&str, &str)], series: &Arc<CounterSetSeries>) {
755 SERIES_CACHE.with(|cache| {
756 cache
757 .borrow_mut()
758 .insert(self.id, labels, Arc::downgrade(series));
759 });
760 }
761}
762
763fn build_counter_index<I, S>(counter_names: I) -> (Vec<String>, BTreeMap<String, usize>)
764where
765 I: IntoIterator<Item = S>,
766 S: AsRef<str>,
767{
768 let mut names = Vec::new();
769 let mut indexes = BTreeMap::new();
770 for name in counter_names {
771 let name = name.as_ref();
772 assert!(!name.is_empty(), "counter names must not be empty");
773 let index = names.len();
774 let inserted = indexes.insert(name.to_string(), index);
775 assert!(inserted.is_none(), "duplicate counter name: {name}");
776 names.push(name.to_string());
777 }
778 assert!(!names.is_empty(), "counter_names must not be empty");
779 (names, indexes)
780}
781
782#[inline]
783fn counter_row_stride(counters: usize) -> usize {
784 let cells_per_padded_counter =
785 std::mem::size_of::<CachePadded<AtomicIsize>>() / std::mem::size_of::<AtomicIsize>();
786 let row_padding = cells_per_padded_counter.max(1);
787 counters.div_ceil(row_padding) * row_padding
788}
789
790#[inline]
791fn default_shard_count() -> usize {
792 std::thread::available_parallelism().map_or(4, usize::from)
793}
794
795#[cfg(test)]
796mod tests {
797 #[cfg(feature = "eviction")]
798 use super::super::{advance_cycle, lock_eviction_cycle_for_test};
799 use super::*;
800
801 #[test]
802 fn dynamic_counter_set_updates_named_counters() {
803 let counters = DynamicCounterSet::with_shards(4, ["requests", "bytes", "errors"]);
804 let labels = &[("org", "eden"), ("endpoint", "ingest")];
805
806 counters.add_values(labels, &[("requests", 1), ("bytes", 4096)]);
807 counters.inc(labels, "errors");
808
809 assert_eq!(counters.get(labels, "requests"), 1);
810 assert_eq!(counters.get(labels, "bytes"), 4096);
811 assert_eq!(counters.get(labels, "errors"), 1);
812 assert_eq!(counters.values(labels), vec![1, 4096, 1]);
813 assert_eq!(counters.sum_all(), 4098);
814 }
815
816 #[test]
817 fn dynamic_counter_set_label_order_is_canonicalized() {
818 let counters = DynamicCounterSet::with_shards(4, ["requests"]);
819
820 counters.inc(&[("org", "eden"), ("endpoint", "ingest")], "requests");
821
822 assert_eq!(
823 counters.get(&[("endpoint", "ingest"), ("org", "eden")], "requests"),
824 1
825 );
826 }
827
828 #[test]
829 fn dynamic_counter_set_series_handle_supports_index_updates() {
830 let counters = DynamicCounterSet::with_shards(4, ["requests", "bytes", "errors"]);
831 let requests = counters.counter_index("requests").unwrap();
832 let bytes = counters.counter_index("bytes").unwrap();
833 let errors = counters.counter_index("errors").unwrap();
834 let series = counters.series(&[("org", "eden")]);
835
836 series.add_index_values(&[(requests, 2), (bytes, 512)]);
837 series.add_index(errors, 1);
838
839 assert_eq!(series.values(), vec![2, 512, 1]);
840 assert_eq!(counters.values(&[("org", "eden")]), vec![2, 512, 1]);
841 }
842
843 #[test]
844 fn dynamic_counter_set_add_all_values_uses_index_order() {
845 let counters = DynamicCounterSet::with_shards(4, ["requests", "bytes", "errors"]);
846
847 counters.add_all_values(&[("org", "eden")], &[1, 1024, 0]);
848
849 assert_eq!(counters.values(&[("org", "eden")]), vec![1, 1024, 0]);
850 }
851
852 #[test]
853 fn dynamic_counter_set_snapshot_and_reset_returns_all_series() {
854 let counters = DynamicCounterSet::with_shards(4, ["requests", "bytes"]);
855
856 counters.add_all_values(&[("org", "eden")], &[2, 10]);
857 counters.add_all_values(&[("org", "acme")], &[3, 15]);
858
859 let mut snapshot = counters.snapshot_and_reset();
860 snapshot.sort_by(|(left, _), (right, _)| left.cmp(right));
861
862 assert_eq!(snapshot.len(), 2);
863 assert_eq!(
864 snapshot[0].0.pairs()[0],
865 ("org".to_string(), "acme".to_string())
866 );
867 assert_eq!(snapshot[0].1, vec![3, 15]);
868 assert_eq!(
869 snapshot[1].0.pairs()[0],
870 ("org".to_string(), "eden".to_string())
871 );
872 assert_eq!(snapshot[1].1, vec![2, 10]);
873 assert_eq!(counters.sum_all(), 0);
874 }
875
876 #[test]
877 fn dynamic_counter_set_sum_and_reset_resets_one_counter_across_series() {
878 let counters = DynamicCounterSet::with_shards(4, ["requests", "bytes"]);
879 let requests = counters.counter_index("requests").unwrap();
880
881 counters.add_all_values(&[("org", "eden")], &[2, 10]);
882 counters.add_all_values(&[("org", "acme")], &[3, 15]);
883
884 assert_eq!(counters.sum_and_reset(requests), 5);
885 assert_eq!(counters.values(&[("org", "eden")]), vec![0, 10]);
886 assert_eq!(counters.values(&[("org", "acme")]), vec![0, 15]);
887 }
888
889 #[test]
890 fn dynamic_counter_set_visit_series_exposes_counter_names() {
891 let counters = DynamicCounterSet::with_shards(4, ["requests", "bytes"]);
892 counters.add_all_values(&[("org", "eden")], &[2, 10]);
893
894 let mut visited = Vec::new();
895 counters.visit_series(|counter, labels, value| {
896 visited.push((counter.to_string(), labels.to_vec(), value));
897 });
898 visited.sort_by(|left, right| left.0.cmp(&right.0));
899
900 assert_eq!(visited.len(), 2);
901 assert_eq!(visited[0].0, "bytes");
902 assert_eq!(visited[0].2, 10);
903 assert_eq!(visited[1].0, "requests");
904 assert_eq!(visited[1].2, 2);
905 }
906
907 #[test]
908 fn dynamic_counter_set_overflow_bucket_routes_new_series_at_capacity() {
909 let counters = DynamicCounterSet::with_max_series(4, 2, ["requests"]);
910
911 counters.inc(&[("org", "1")], "requests");
912 counters.inc(&[("org", "2")], "requests");
913 counters.inc(&[("org", "3")], "requests");
914
915 assert_eq!(counters.cardinality(), 3);
916 assert_eq!(
917 counters.get(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)], "requests"),
918 1
919 );
920 assert!(counters.overflow_count() > 0);
921 }
922
923 #[test]
924 fn dynamic_counter_set_concurrent_index_updates() {
925 let counters = Arc::new(DynamicCounterSet::with_shards(
926 8,
927 ["requests", "bytes", "errors"],
928 ));
929 let requests = counters.counter_index("requests").unwrap();
930 let bytes = counters.counter_index("bytes").unwrap();
931
932 std::thread::scope(|scope| {
933 for _ in 0..8 {
934 let counters = Arc::clone(&counters);
935 scope.spawn(move || {
936 let series = counters.series(&[("org", "eden")]);
937 for _ in 0..10_000 {
938 series.add_index_values(&[(requests, 1), (bytes, 2)]);
939 }
940 });
941 }
942 });
943
944 assert_eq!(counters.get(&[("org", "eden")], "requests"), 80_000);
945 assert_eq!(counters.get(&[("org", "eden")], "bytes"), 160_000);
946 }
947
948 #[cfg(feature = "eviction")]
949 #[test]
950 fn dynamic_counter_set_eviction_tombstone_invalidates_cache() {
951 let _cycle_guard = lock_eviction_cycle_for_test();
952 let counters = DynamicCounterSet::with_shards(4, ["requests"]);
953 let labels = &[("org", "eden")];
954
955 counters.inc(labels, "requests");
956 counters.inc(labels, "requests");
957 assert_eq!(counters.get(labels, "requests"), 2);
958
959 advance_cycle();
960 advance_cycle();
961 counters.inc(&[("flush", "cache")], "requests");
962 assert_eq!(counters.evict_stale(1), 1);
963
964 counters.inc(labels, "requests");
965 assert_eq!(counters.get(labels, "requests"), 1);
966 }
967}