Skip to main content

fast_telemetry/metric/
counter.rs

1//! Thread-sharded atomic counter.
2//!
3//! Forked from https://crates.io/crates/fast-counter (MIT/Apache licensed)
4//! Originally authored by https://crates.io/users/JackThomson2
5//! Modified to use crossbeam's CachePadded for more correct cache line sizing,
6//! and to support swap operations and export operations.
7
8use crate::thread_id::thread_id;
9use crossbeam_utils::CachePadded;
10use std::fmt;
11use std::sync::atomic::{AtomicIsize, Ordering};
12
13fn make_padded_counter() -> CachePadded<AtomicIsize> {
14    CachePadded::new(AtomicIsize::new(0))
15}
16
17#[cfg(feature = "bench-tools")]
18fn make_counter_cell() -> AtomicIsize {
19    AtomicIsize::new(0)
20}
21
22/// A sharded atomic counter.
23///
24/// Shards cache-line aligned AtomicIsize values across a vector for faster
25/// updates in high contention scenarios.
26pub struct Counter {
27    cells: Vec<CachePadded<AtomicIsize>>,
28}
29
30impl fmt::Debug for Counter {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        f.debug_struct("Counter")
33            .field("sum", &self.sum())
34            .field("cells", &self.cells.len())
35            .finish()
36    }
37}
38
39impl Counter {
40    /// Creates a new Counter with at least `count` cells.
41    ///
42    /// The count is rounded up to the next power of two for fast modulo.
43    #[inline]
44    pub fn new(count: usize) -> Self {
45        let count = count.next_power_of_two();
46        Self {
47            cells: (0..count).map(|_| make_padded_counter()).collect(),
48        }
49    }
50
51    /// Adds a value to the counter using relaxed ordering.
52    #[inline]
53    pub fn add(&self, value: isize) {
54        self.add_with_ordering(value, Ordering::Relaxed)
55    }
56
57    /// Increments the counter by 1.
58    #[inline]
59    pub fn inc(&self) {
60        self.add(1)
61    }
62
63    #[inline]
64    fn add_with_thread_id(&self, thread_id: usize, value: isize, ordering: Ordering) {
65        let idx = thread_id & (self.cells.len() - 1);
66        // SAFETY: idx is always < cells.len() due to power-of-two masking
67        let cell = if cfg!(debug_assertions) {
68            self.cells.get(idx).expect("index out of bounds")
69        } else {
70            unsafe { self.cells.get_unchecked(idx) }
71        };
72        cell.fetch_add(value, ordering);
73    }
74
75    /// Adds a value to the counter with the specified ordering.
76    #[inline]
77    pub fn add_with_ordering(&self, value: isize, ordering: Ordering) {
78        self.add_with_thread_id(thread_id(), value, ordering);
79    }
80
81    /// Benchmark-only prototype for batching increments across multiple counters.
82    #[cfg(feature = "bench-tools")]
83    #[doc(hidden)]
84    #[inline]
85    pub fn inc_many(counters: &[Counter]) {
86        Self::add_many(counters, 1);
87    }
88
89    /// Benchmark-only prototype for batching additions across multiple counters.
90    #[cfg(feature = "bench-tools")]
91    #[doc(hidden)]
92    #[inline]
93    pub fn add_many(counters: &[Counter], value: isize) {
94        Self::add_many_with_ordering(counters, value, Ordering::Relaxed);
95    }
96
97    /// Benchmark-only prototype for batching additions across multiple counters.
98    #[cfg(feature = "bench-tools")]
99    #[doc(hidden)]
100    #[inline]
101    pub fn add_many_with_ordering(counters: &[Counter], value: isize, ordering: Ordering) {
102        let thread_id = thread_id();
103        for counter in counters {
104            counter.add_with_thread_id(thread_id, value, ordering);
105        }
106    }
107
108    /// Returns the sum of all shards using relaxed ordering.
109    ///
110    /// # Eventual Consistency
111    ///
112    /// Due to sharding, this may be slightly inaccurate under heavy concurrent
113    /// modification - writes to already-summed shards won't be reflected until
114    /// the next call. The total is eventually consistent.
115    #[inline]
116    pub fn sum(&self) -> isize {
117        self.sum_with_ordering(Ordering::Relaxed)
118    }
119
120    /// Returns the sum of all shards with the specified ordering.
121    #[inline]
122    pub fn sum_with_ordering(&self, ordering: Ordering) -> isize {
123        self.cells.iter().map(|c| c.load(ordering)).sum()
124    }
125
126    /// Resets all shards to zero and returns the previous sum.
127    ///
128    /// Useful for delta-style metrics export.
129    ///
130    /// # Eventual Consistency
131    ///
132    /// Writes that occur concurrently with `swap()` may be attributed to the
133    /// next window rather than the current one. This is because shards are
134    /// swapped sequentially - a write landing on an already-swapped shard
135    /// will be picked up by the next `swap()` call. No counts are lost; they
136    /// simply shift to the next export window. For telemetry purposes with
137    /// multi-second export intervals, this timing skew is negligible.
138    #[inline]
139    pub fn swap(&self) -> isize {
140        self.cells
141            .iter()
142            .map(|c| c.swap(0, Ordering::Relaxed))
143            .sum()
144    }
145}
146
147/// Benchmark-only prototype for grouping related counters in one sharded layout.
148#[cfg(feature = "bench-tools")]
149#[doc(hidden)]
150pub struct CounterSet {
151    cells: Vec<AtomicIsize>,
152    counters: usize,
153    stride: usize,
154    shard_mask: usize,
155}
156
157#[cfg(feature = "bench-tools")]
158impl CounterSet {
159    /// Creates a grouped counter set with `counters` counters per shard.
160    ///
161    /// Shards are padded by row instead of by cell, so related counters updated
162    /// by the same thread sit contiguously while adjacent shard rows remain
163    /// separated enough to avoid false sharing.
164    pub fn new(shards: usize, counters: usize) -> Self {
165        assert!(counters >= 1, "counters must be >= 1");
166        let shards = shards.next_power_of_two();
167        let cells_per_padded_counter =
168            std::mem::size_of::<CachePadded<AtomicIsize>>() / std::mem::size_of::<AtomicIsize>();
169        let row_padding = cells_per_padded_counter.max(1);
170        let stride = counters.div_ceil(row_padding) * row_padding;
171        let cells = (0..(shards * stride))
172            .map(|_| make_counter_cell())
173            .collect();
174
175        Self {
176            cells,
177            counters,
178            stride,
179            shard_mask: shards - 1,
180        }
181    }
182
183    /// Returns the number of counters in each shard row.
184    #[inline]
185    pub fn len(&self) -> usize {
186        self.counters
187    }
188
189    /// Returns true if there are no counters in the set.
190    #[inline]
191    pub fn is_empty(&self) -> bool {
192        self.counters == 0
193    }
194
195    #[inline]
196    fn current_shard_offset(&self) -> usize {
197        (thread_id() & self.shard_mask) * self.stride
198    }
199
200    #[inline]
201    fn cell_at(&self, index: usize) -> &AtomicIsize {
202        if cfg!(debug_assertions) {
203            self.cells.get(index).expect("index out of bounds")
204        } else {
205            // SAFETY: callers compute indexes from checked counter indexes and
206            // row offsets derived from the shard mask.
207            unsafe { self.cells.get_unchecked(index) }
208        }
209    }
210
211    /// Increments one counter in the current thread's shard row.
212    #[inline]
213    pub fn inc(&self, counter_idx: usize) {
214        self.add(counter_idx, 1);
215    }
216
217    /// Adds a value to one counter in the current thread's shard row.
218    #[inline]
219    pub fn add(&self, counter_idx: usize, value: isize) {
220        assert!(counter_idx < self.counters, "counter index out of bounds");
221        let offset = self.current_shard_offset();
222        self.cell_at(offset + counter_idx)
223            .fetch_add(value, Ordering::Relaxed);
224    }
225
226    /// Increments all counters in the current thread's shard row.
227    #[inline]
228    pub fn inc_all(&self) {
229        self.add_all(1);
230    }
231
232    /// Adds the same value to all counters in the current thread's shard row.
233    #[inline(always)]
234    pub fn add_all(&self, value: isize) {
235        let offset = self.current_shard_offset();
236        let row = if cfg!(debug_assertions) {
237            self.cells
238                .get(offset..offset + self.counters)
239                .expect("row index out of bounds")
240        } else {
241            // SAFETY: current_shard_offset derives from shard_mask and stride,
242            // and counters <= stride by construction.
243            unsafe { std::slice::from_raw_parts(self.cells.as_ptr().add(offset), self.counters) }
244        };
245        for cell in row {
246            cell.fetch_add(value, Ordering::Relaxed);
247        }
248    }
249
250    /// Adds the same value to selected counters in the current thread's shard row.
251    #[inline]
252    pub fn add_indices(&self, indexes: &[usize], value: isize) {
253        let offset = self.current_shard_offset();
254        if cfg!(debug_assertions) {
255            for index in indexes {
256                assert!(*index < self.counters, "counter index out of bounds");
257            }
258        }
259        for index in indexes {
260            self.cell_at(offset + *index)
261                .fetch_add(value, Ordering::Relaxed);
262        }
263    }
264
265    /// Adds one value per selected counter in the current thread's shard row.
266    #[inline]
267    pub fn add_index_values(&self, updates: &[(usize, isize)]) {
268        let offset = self.current_shard_offset();
269        if cfg!(debug_assertions) {
270            for (index, _) in updates {
271                assert!(*index < self.counters, "counter index out of bounds");
272            }
273        }
274        for (index, value) in updates {
275            self.cell_at(offset + *index)
276                .fetch_add(*value, Ordering::Relaxed);
277        }
278    }
279
280    /// Adds the same value to all counters in the current thread's shard row.
281    #[inline]
282    pub fn add_all_indexed(&self, value: isize) {
283        let offset = self.current_shard_offset();
284        for counter_idx in 0..self.counters {
285            self.cell_at(offset + counter_idx)
286                .fetch_add(value, Ordering::Relaxed);
287        }
288    }
289
290    /// Adds one value per counter in the current thread's shard row.
291    #[inline(always)]
292    pub fn add_values(&self, values: &[isize]) {
293        assert_eq!(values.len(), self.counters, "values must match counters");
294        let offset = self.current_shard_offset();
295        match values {
296            [a] => {
297                self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
298            }
299            [a, b] => {
300                self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
301                self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
302            }
303            [a, b, c] => {
304                self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
305                self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
306                self.cell_at(offset + 2).fetch_add(*c, Ordering::Relaxed);
307            }
308            [a, b, c, d] => {
309                self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
310                self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
311                self.cell_at(offset + 2).fetch_add(*c, Ordering::Relaxed);
312                self.cell_at(offset + 3).fetch_add(*d, Ordering::Relaxed);
313            }
314            [a, b, c, d, e] => {
315                self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
316                self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
317                self.cell_at(offset + 2).fetch_add(*c, Ordering::Relaxed);
318                self.cell_at(offset + 3).fetch_add(*d, Ordering::Relaxed);
319                self.cell_at(offset + 4).fetch_add(*e, Ordering::Relaxed);
320            }
321            [a, b, c, d, e, f] => {
322                self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
323                self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
324                self.cell_at(offset + 2).fetch_add(*c, Ordering::Relaxed);
325                self.cell_at(offset + 3).fetch_add(*d, Ordering::Relaxed);
326                self.cell_at(offset + 4).fetch_add(*e, Ordering::Relaxed);
327                self.cell_at(offset + 5).fetch_add(*f, Ordering::Relaxed);
328            }
329            [a, b, c, d, e, f, g] => {
330                self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
331                self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
332                self.cell_at(offset + 2).fetch_add(*c, Ordering::Relaxed);
333                self.cell_at(offset + 3).fetch_add(*d, Ordering::Relaxed);
334                self.cell_at(offset + 4).fetch_add(*e, Ordering::Relaxed);
335                self.cell_at(offset + 5).fetch_add(*f, Ordering::Relaxed);
336                self.cell_at(offset + 6).fetch_add(*g, Ordering::Relaxed);
337            }
338            [a, b, c, d, e, f, g, h] => {
339                self.cell_at(offset).fetch_add(*a, Ordering::Relaxed);
340                self.cell_at(offset + 1).fetch_add(*b, Ordering::Relaxed);
341                self.cell_at(offset + 2).fetch_add(*c, Ordering::Relaxed);
342                self.cell_at(offset + 3).fetch_add(*d, Ordering::Relaxed);
343                self.cell_at(offset + 4).fetch_add(*e, Ordering::Relaxed);
344                self.cell_at(offset + 5).fetch_add(*f, Ordering::Relaxed);
345                self.cell_at(offset + 6).fetch_add(*g, Ordering::Relaxed);
346                self.cell_at(offset + 7).fetch_add(*h, Ordering::Relaxed);
347            }
348            values => {
349                let row = if cfg!(debug_assertions) {
350                    self.cells
351                        .get(offset..offset + self.counters)
352                        .expect("row index out of bounds")
353                } else {
354                    // SAFETY: current_shard_offset derives from shard_mask and
355                    // stride, and counters <= stride by construction.
356                    unsafe {
357                        std::slice::from_raw_parts(self.cells.as_ptr().add(offset), self.counters)
358                    }
359                };
360                for (cell, value) in row.iter().zip(values.iter().copied()) {
361                    cell.fetch_add(value, Ordering::Relaxed);
362                }
363            }
364        }
365    }
366
367    /// Returns the sum for one counter across all shards.
368    #[inline]
369    pub fn sum(&self, counter_idx: usize) -> isize {
370        assert!(counter_idx < self.counters, "counter index out of bounds");
371        let shards = self.cells.len() / self.stride;
372        (0..shards)
373            .map(|shard| {
374                self.cell_at((shard * self.stride) + counter_idx)
375                    .load(Ordering::Relaxed)
376            })
377            .sum()
378    }
379
380    /// Returns the sum of all counters across all shards.
381    #[inline]
382    pub fn sum_all(&self) -> isize {
383        let shards = self.cells.len() / self.stride;
384        let mut total = 0isize;
385        for shard in 0..shards {
386            let offset = shard * self.stride;
387            for counter_idx in 0..self.counters {
388                total += self.cell_at(offset + counter_idx).load(Ordering::Relaxed);
389            }
390        }
391        total
392    }
393}
394
395/// Benchmark-only prototype for buffering related counter updates locally.
396#[cfg(feature = "bench-tools")]
397#[doc(hidden)]
398pub struct CounterSetBuffer<'a> {
399    counters: &'a CounterSet,
400    deltas: Vec<isize>,
401    all_delta: isize,
402    ops_since_flush: usize,
403    flush_every: usize,
404    op_dirty: bool,
405}
406
407#[cfg(feature = "bench-tools")]
408impl<'a> CounterSetBuffer<'a> {
409    /// Creates a local buffer for a grouped counter set.
410    #[inline]
411    pub fn new(counters: &'a CounterSet, flush_every: usize) -> Self {
412        assert!(flush_every >= 1, "flush_every must be >= 1");
413        Self {
414            counters,
415            deltas: vec![0; counters.len()],
416            all_delta: 0,
417            ops_since_flush: 0,
418            flush_every,
419            op_dirty: false,
420        }
421    }
422
423    /// Increments one buffered counter.
424    #[inline]
425    pub fn inc(&mut self, counter_idx: usize) {
426        self.add(counter_idx, 1);
427    }
428
429    /// Adds a value to one buffered counter.
430    #[inline]
431    pub fn add(&mut self, counter_idx: usize, value: isize) {
432        assert!(
433            counter_idx < self.deltas.len(),
434            "counter index out of bounds"
435        );
436        self.deltas[counter_idx] += value;
437        self.op_dirty = true;
438    }
439
440    /// Increments every buffered counter and marks one logical operation complete.
441    #[inline(always)]
442    pub fn inc_all(&mut self) {
443        self.all_delta += 1;
444        self.finish_group_op();
445    }
446
447    /// Adds the same value to every buffered counter and marks one logical operation complete.
448    #[inline(always)]
449    pub fn add_all(&mut self, value: isize) {
450        self.all_delta += value;
451        self.finish_group_op();
452    }
453
454    /// Adds one value per buffered counter and marks one logical operation complete.
455    #[inline]
456    pub fn add_values(&mut self, values: &[isize]) {
457        assert_eq!(
458            values.len(),
459            self.deltas.len(),
460            "values must match counters"
461        );
462        for (delta, value) in self.deltas.iter_mut().zip(values.iter().copied()) {
463            *delta += value;
464        }
465        self.finish_group_op();
466    }
467
468    /// Marks the current logical operation complete.
469    #[inline]
470    pub fn finish_op(&mut self) {
471        if !self.op_dirty {
472            return;
473        }
474
475        self.op_dirty = false;
476        self.finish_group_op();
477    }
478
479    #[inline(always)]
480    fn finish_group_op(&mut self) {
481        self.ops_since_flush += 1;
482        if self.ops_since_flush >= self.flush_every {
483            self.flush();
484        }
485    }
486
487    /// Flushes buffered deltas to the backing grouped counter set.
488    #[inline]
489    pub fn flush(&mut self) {
490        if self.ops_since_flush == 0 && !self.op_dirty {
491            return;
492        }
493
494        if self.all_delta != 0 {
495            self.counters.add_all(self.all_delta);
496            self.all_delta = 0;
497        }
498        if self.deltas.iter().any(|delta| *delta != 0) {
499            self.counters.add_values(&self.deltas);
500            self.deltas.fill(0);
501        }
502        self.ops_since_flush = 0;
503        self.op_dirty = false;
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510
511    #[test]
512    fn basic_test() {
513        let counter = Counter::new(1);
514        counter.add(1);
515        assert_eq!(counter.sum(), 1);
516    }
517
518    #[test]
519    fn increment_multiple_times() {
520        let counter = Counter::new(1);
521        counter.add(1);
522        counter.add(1);
523        counter.add(1);
524        assert_eq!(counter.sum(), 3);
525    }
526
527    #[test]
528    fn test_inc() {
529        let counter = Counter::new(4);
530        counter.inc();
531        counter.inc();
532        assert_eq!(counter.sum(), 2);
533    }
534
535    #[test]
536    fn test_swap() {
537        let counter = Counter::new(4);
538        counter.add(100);
539        let val = counter.swap();
540        assert_eq!(val, 100);
541        assert_eq!(counter.sum(), 0);
542    }
543
544    #[test]
545    fn two_threads_incrementing_concurrently() {
546        let counter = Counter::new(2);
547
548        std::thread::scope(|s| {
549            for _ in 0..2 {
550                s.spawn(|| {
551                    counter.add(1);
552                });
553            }
554        });
555
556        assert_eq!(counter.sum(), 2);
557    }
558
559    #[test]
560    fn multiple_threads_incrementing_many_times() {
561        const WRITE_COUNT: isize = 1_000_000;
562        const THREAD_COUNT: isize = 8;
563
564        let counter = Counter::new(THREAD_COUNT as usize);
565
566        std::thread::scope(|s| {
567            for _ in 0..THREAD_COUNT {
568                s.spawn(|| {
569                    for _ in 0..WRITE_COUNT {
570                        counter.add(1);
571                    }
572                });
573            }
574        });
575
576        assert_eq!(counter.sum(), THREAD_COUNT * WRITE_COUNT);
577    }
578
579    #[test]
580    fn debug_format() {
581        let counter = Counter::new(8);
582        counter.add(42);
583        let debug = format!("{counter:?}");
584        assert!(debug.contains("sum: 42"));
585        assert!(debug.contains("cells: 8"));
586    }
587
588    #[cfg(feature = "bench-tools")]
589    #[test]
590    fn inc_many_updates_all_counters() {
591        let counters = vec![Counter::new(4), Counter::new(4), Counter::new(4)];
592
593        Counter::inc_many(&counters);
594        Counter::add_many(&counters, 2);
595
596        for counter in &counters {
597            assert_eq!(counter.sum(), 3);
598        }
599    }
600
601    #[cfg(feature = "bench-tools")]
602    #[test]
603    fn counter_set_updates_grouped_counters() {
604        let counters = CounterSet::new(4, 3);
605
606        counters.inc(0);
607        counters.add(1, 2);
608        counters.inc_all();
609        counters.add_values(&[2, 3, 4]);
610        counters.add_indices(&[0, 2], 1);
611        counters.add_index_values(&[(1, 2), (2, 3)]);
612
613        assert_eq!(counters.len(), 3);
614        assert!(!counters.is_empty());
615        assert_eq!(counters.sum(0), 5);
616        assert_eq!(counters.sum(1), 8);
617        assert_eq!(counters.sum(2), 9);
618        assert_eq!(counters.sum_all(), 22);
619    }
620
621    #[cfg(feature = "bench-tools")]
622    #[test]
623    fn counter_set_buffer_flushes_grouped_and_individual_updates() {
624        let counters = CounterSet::new(4, 3);
625
626        {
627            let mut buffer = CounterSetBuffer::new(&counters, 2);
628
629            buffer.inc(0);
630            buffer.add(1, 2);
631            buffer.finish_op();
632            assert_eq!(counters.sum_all(), 0);
633
634            buffer.inc_all();
635            assert_eq!(counters.sum(0), 2);
636            assert_eq!(counters.sum(1), 3);
637            assert_eq!(counters.sum(2), 1);
638
639            buffer.add(2, 4);
640            buffer.finish_op();
641            buffer.flush();
642        }
643
644        assert_eq!(counters.sum(0), 2);
645        assert_eq!(counters.sum(1), 3);
646        assert_eq!(counters.sum(2), 5);
647        assert_eq!(counters.sum_all(), 10);
648    }
649}