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