Skip to main content

fast_telemetry/metric/dynamic/
counter_set.rs

1//! Runtime-labeled grouped counters for dynamic dimensions.
2
3use 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            // SAFETY: callers compute indexes from checked counter indexes and
65            // row offsets derived from the shard mask.
66            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                    // SAFETY: row_offset derives from a shard index masked by
157                    // the owning metric, and counters <= stride by construction.
158                    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/// A reusable handle to one dynamic-label grouped counter series.
250///
251/// Resolve this once with [`DynamicCounterSet::get_or_create`] or
252/// [`DynamicCounterSet::series`], then use index-based methods on hot paths.
253#[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    /// Increment one named counter by 1.
263    #[inline]
264    pub fn inc(&self, counter: &str) {
265        self.add(counter, 1);
266    }
267
268    /// Add `value` to one named counter.
269    #[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    /// Add `value` to one pre-resolved counter index.
278    #[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    /// Add named counter values in one grouped call.
285    #[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    /// Add pre-resolved `(counter_idx, value)` updates in one grouped call.
297    #[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    /// Add one value for every counter in index order.
304    #[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    /// Return the current total for one named counter.
311    #[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    /// Return the current total for one pre-resolved counter index.
320    #[inline]
321    pub fn get_index(&self, counter_idx: usize) -> isize {
322        self.series.sum(counter_idx)
323    }
324
325    /// Return all current values in counter index order.
326    #[inline]
327    pub fn values(&self) -> Vec<isize> {
328        self.series.values()
329    }
330
331    /// Return the index for `counter`, if it exists in this grouped set.
332    #[inline]
333    pub fn counter_index(&self, counter: &str) -> Option<usize> {
334        self.indexes.get(counter).copied()
335    }
336
337    /// Return counter names in index order.
338    #[inline]
339    pub fn counter_names(&self) -> &[String] {
340        &self.names
341    }
342
343    /// Check if this series handle has been evicted.
344    ///
345    /// If true, writes go to a detached series that is no longer exported.
346    /// Callers holding long-lived handles can check this and re-resolve via
347    /// [`DynamicCounterSet::series`] if needed.
348    #[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
359/// Runtime-label grouped counters.
360///
361/// Use `DynamicCounterSet` when label values are discovered at runtime and one
362/// logical operation updates several related counters for the same label set.
363/// Resolve and cache a [`DynamicCounterSetSeries`] once per active label set,
364/// then use pre-resolved counter indexes on the hot path.
365///
366/// ```
367/// use fast_telemetry::DynamicCounterSet;
368///
369/// let counters = DynamicCounterSet::with_shards(
370///     4,
371///     ["requests", "bytes", "errors"],
372/// );
373/// let requests = counters.counter_index("requests").unwrap();
374/// let bytes = counters.counter_index("bytes").unwrap();
375///
376/// let series = counters.get_or_create(&[
377///     ("org", "eden"),
378///     ("endpoint", "ingest"),
379/// ]);
380/// series.add_index_values(&[(requests, 1), (bytes, 4096)]);
381///
382/// assert_eq!(series.get("requests"), 1);
383/// assert_eq!(series.get("bytes"), 4096);
384/// ```
385pub 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    /// Creates a new dynamic grouped counter with a default shard count.
399    ///
400    /// Prefer [`DynamicCounterSet::with_shards`] in services that already know
401    /// the desired shard count.
402    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    /// Creates a new dynamic grouped counter with `shard_count` shards.
411    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    /// Creates a new dynamic grouped counter with a series cardinality cap.
420    ///
421    /// When the number of unique label sets approximately reaches
422    /// `max_series`, new label sets are redirected into a single overflow
423    /// series (`__ft_overflow=true`). A `max_series` of 0 disables the cap.
424    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    /// Resolve a reusable grouped series handle for `labels`.
448    pub fn series(&self, labels: &[(&str, &str)]) -> DynamicCounterSetSeries {
449        self.get_or_create(labels)
450    }
451
452    /// Resolve a reusable grouped series handle for `labels`.
453    pub fn get_or_create(&self, labels: &[(&str, &str)]) -> DynamicCounterSetSeries {
454        self.series_handle(self.cached_or_create_series(labels))
455    }
456
457    /// Increment one named counter for the series identified by `labels`.
458    #[inline]
459    pub fn inc(&self, labels: &[(&str, &str)], counter: &str) {
460        self.add(labels, counter, 1);
461    }
462
463    /// Add `value` to one named counter for the series identified by `labels`.
464    #[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    /// Add `value` to one pre-resolved counter index for `labels`.
473    #[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    /// Add named counter values in one grouped call.
481    #[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    /// Add pre-resolved `(counter_idx, value)` updates in one grouped call.
494    #[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    /// Add one value for every counter in index order.
502    #[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    /// Get one named counter for `labels`.
510    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    /// Get one pre-resolved counter index for `labels`.
518    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    /// Get all values for `labels` in counter index order.
533    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    /// Sums all counters across all dynamic series.
544    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    /// Return a cumulative snapshot of all dynamic label sets.
555    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    /// Return a snapshot of all dynamic label sets and reset counters to zero.
567    ///
568    /// Writes that occur concurrently with this method may be attributed to the
569    /// next snapshot window rather than the current one. No counts are lost.
570    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    /// Reset one counter index for all dynamic label sets and return its total.
582    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    /// Iterate all counter values without cloning label sets.
600    ///
601    /// Calls `f(counter_name, labels, value)` for every counter in every
602    /// dynamic label set. The callback runs while a shard read lock is held; it
603    /// must be fast and must not call back into this metric.
604    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    /// Returns counter names in index order.
620    #[inline]
621    pub fn counter_names(&self) -> &[String] {
622        &self.names
623    }
624
625    /// Return the index for `counter`, if it exists.
626    #[inline]
627    pub fn counter_index(&self, counter: &str) -> Option<usize> {
628        self.indexes.get(counter).copied()
629    }
630
631    /// Returns the current number of distinct dynamic label sets.
632    pub fn cardinality(&self) -> usize {
633        self.index_shards
634            .iter()
635            .map(|shard| shard.read().len())
636            .sum()
637    }
638
639    /// Returns the number of records routed to the overflow bucket.
640    pub fn overflow_count(&self) -> u64 {
641        self.overflow_count.load(Ordering::Relaxed)
642    }
643
644    /// Evict series that haven't been accessed for `max_staleness` cycles.
645    ///
646    /// Protected series (Arc::strong_count > 1) are never evicted because a
647    /// caller still holds a [`DynamicCounterSetSeries`] handle.
648    #[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}