invocation_counter/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
4
5#[derive(Debug)]
6struct Slot {
7    interval_start: AtomicU64,
8    counter: AtomicU32,
9}
10
11impl Slot {
12    fn new() -> Self {
13        Self {
14            interval_start: AtomicU64::new(0),
15            counter: AtomicU32::new(0),
16        }
17    }
18}
19
20/// A structure for tracking invocation counts over sliding time windows.
21///
22/// `InvocationCounter` implements a ring buffer-based algorithm that efficiently answers the question:
23/// "How many times has a function been called in the last X time units?" The structure divides time
24/// into fixed-size intervals and uses a circular array of slots to track counts within each interval.
25///
26/// # Time Units
27///
28/// Time units are abstract and can represent any consistent time measurement (milliseconds,
29/// seconds, ticks, etc.). The counter treats time as unsigned 64-bit integers, where larger
30/// values represent later points in time.
31///
32/// # Count Approximation
33///
34/// The counter provides an **approximate** count due to its interval-based design:
35/// - Invocations are grouped into discrete time intervals (slots)
36/// - The sliding window moves in interval-sized steps, not continuously
37/// - Recent invocations may be counted even if they fall slightly outside the exact window
38/// - Very old invocations may be excluded if their slots have been reused
39///
40/// This approximation trades perfect accuracy for significant performance and memory efficiency.
41///
42/// # Architecture
43///
44/// The counter uses a ring buffer with 2^`slot_count_exp` slots, where each slot represents a time
45/// interval of 2^`slot_size_exp` time units. The total sliding window size is therefore:
46/// `2^slot_count_exp × 2^slot_size_exp` time units.
47///
48/// Each slot contains:
49/// - `interval_start`: The start timestamp of the time interval this slot represents
50/// - `counter`: The number of invocations registered in this interval
51///
52/// As time progresses, slots are reused in a circular fashion. When a new time interval begins
53/// that maps to an already-occupied slot, the slot is reset and begins tracking the new interval.
54///
55/// # Example
56///
57/// ```rust
58/// # use invocation_counter::InvocationCounter;
59///
60/// // Create counter: 8 slots × 16 time units = 128 time unit sliding window
61/// let counter = InvocationCounter::new(3, 4);  // 2^3 slots, 2^4 time units per slot
62///
63/// counter.register(10);  // Register invocation at time 10
64/// counter.register(25);  // Register invocation at time 25
65///
66/// assert_eq!(counter.count_in(0, 26), 2);     // both in range [0, 26)
67/// assert_eq!(counter.count_in(0, 16), 1);     // only first (slot 0: times 0-15)
68/// assert_eq!(counter.count_in(16, 32), 1);    // only second (slot 1: times 16-31)
69/// assert_eq!(counter.count_in(50, 60), 0);    // out of range
70///
71/// counter.register(200);
72/// assert_eq!(counter.count_in(200 - 128, 201), 1);  // Only the last in 128-unit window
73/// ```
74#[derive(Debug)]
75pub struct InvocationCounter {
76    slots: Box<[Slot]>,
77    slot_count_exp: u8,
78    slot_size_exp: u8,
79    max_current_time: AtomicU64,
80}
81
82impl InvocationCounter {
83    /// Creates a new `InvocationCounter` with the specified configuration.
84    ///
85    /// # Arguments
86    ///
87    /// * `slot_count_exp` - Exponent for the number of slots (2^slot_count_exp slots total)
88    /// * `slot_size_exp` - Exponent for the size of each time interval (2^slot_size_exp time units per slot)
89    ///
90    /// The total sliding window size will be: `2^slot_count_exp × 2^slot_size_exp` time units.
91    ///
92    /// # Examples
93    ///
94    /// ```rust
95    /// # use invocation_counter::InvocationCounter;
96    /// // Create a counter with 8 slots (2^3), each covering 16 time units (2^4)
97    /// // Total window: 8 × 16 = 128 time units
98    /// let counter = InvocationCounter::new(3, 4);
99    /// ```
100    pub fn new(slot_count_exp: u8, slot_size_exp: u8) -> Self {
101        let slots = (0..(1 << slot_count_exp))
102            .map(|_| Slot::new())
103            .collect::<Vec<_>>()
104            .into_boxed_slice();
105
106        Self {
107            slots,
108            slot_count_exp,
109            slot_size_exp,
110            max_current_time: AtomicU64::new(0),
111        }
112    }
113
114    /// Registers an invocation at the specified time.
115    ///
116    /// This method is thread-safe. Multiple threads can call this method
117    /// concurrently without external synchronization.
118    ///
119    /// # Arguments
120    ///
121    /// * `current_time` - The timestamp when the invocation occurred
122    ///
123    /// # Examples
124    ///
125    /// ```rust
126    /// # use invocation_counter::InvocationCounter;
127    /// let counter = InvocationCounter::new(3, 4); // 8 slots × 16 units = 128-unit window
128    ///
129    /// counter.register(10);
130    /// counter.register(10); // Same interval, increments counter
131    /// counter.register(25); // Different interval, uses different slot
132    /// ```
133    pub fn register(&self, current_time: u64) {
134        let interval_start = current_time >> self.slot_size_exp;
135
136        let slot_index = interval_start % (1 << self.slot_count_exp);
137
138        let interval_start = interval_start << self.slot_size_exp;
139
140        let slot = &self.slots[slot_index as usize];
141
142        let time_in_slot = slot.interval_start.load(Ordering::Acquire);
143        if time_in_slot == interval_start {
144            slot.counter.fetch_add(1, Ordering::Relaxed);
145        } else {
146            slot.interval_start.store(interval_start, Ordering::Release);
147            slot.counter.store(1, Ordering::Release);
148        }
149
150        let current_max_time = self.max_current_time.load(Ordering::Acquire);
151        if current_max_time < current_time {
152            self.max_current_time
153                .compare_exchange_weak(
154                    current_max_time,
155                    current_time,
156                    Ordering::Release,
157                    Ordering::Relaxed,
158                )
159                .ok();
160        }
161    }
162
163    /// Returns the slot count exponent used to create this counter.
164    ///
165    /// The actual number of slots is `2^slot_count_exp()`.
166    ///
167    /// # Examples
168    ///
169    /// ```rust
170    /// # use invocation_counter::InvocationCounter;
171    /// let counter = InvocationCounter::new(3, 4); // 8 slots, each covering 16 time units
172    /// assert_eq!(counter.slot_count_exp(), 3);
173    /// assert_eq!(1 << counter.slot_count_exp(), 8); // 2^3 = 8 slots
174    /// ```
175    pub fn slot_count_exp(&self) -> u8 {
176        self.slot_count_exp
177    }
178
179    /// Returns the slot size exponent used to create this counter.
180    ///
181    /// The actual size of each time interval is `2^slot_size_exp()` time units.
182    ///
183    /// # Examples
184    ///
185    /// ```rust
186    /// # use invocation_counter::InvocationCounter;
187    /// let counter = InvocationCounter::new(3, 4); // 8 slots, each covering 16 time units
188    /// assert_eq!(counter.slot_size_exp(), 4);
189    /// assert_eq!(1 << counter.slot_size_exp(), 16); // 2^4 = 16 time units per slot
190    /// ```
191    pub fn slot_size_exp(&self) -> u8 {
192        self.slot_size_exp
193    }
194
195    /// Returns the total number of invocations within the specified time range.
196    ///
197    /// Unlike `count()` which uses a fixed sliding window, this method allows querying
198    /// invocations within any arbitrary time range defined by `start_time` and `end_time`.
199    /// The method still respects the ring buffer's current valid data range.
200    ///
201    /// # Arguments
202    ///
203    /// * `start_time` - The start time of the range to query (inclusive)
204    /// * `end_time` - The end time of the range to query (exclusive)
205    ///
206    /// # Returns
207    ///
208    /// The total number of invocations that occurred within the specified time range,
209    /// limited by the data currently available in the ring buffer.
210    ///
211    /// # Examples
212    ///
213    /// ```rust
214    /// # use invocation_counter::InvocationCounter;
215    /// let counter = InvocationCounter::new(3, 4); // 8 slots × 16 units = 128-unit window
216    ///
217    /// counter.register(10);
218    /// counter.register(25);
219    /// counter.register(100);
220    ///
221    /// assert_eq!(counter.count_in(0, 50), 2);   // First two invocations
222    /// assert_eq!(counter.count_in(20, 120), 2); // Second and third invocations
223    /// assert_eq!(counter.count_in(0, 200), 3);  // All invocations (if within ring buffer range)
224    /// ```
225    pub fn count_in(&self, start_time: u64, end_time: u64) -> u32 {
226        if start_time >= end_time {
227            return 0;
228        }
229
230        let current_max_time = self.max_current_time.load(Ordering::Acquire);
231
232        // Calculate the ring buffer's valid range (same as count method)
233        let ring_end = ((current_max_time >> self.slot_size_exp) + 1) << self.slot_size_exp;
234        let ring_start =
235            ring_end.saturating_sub((1 << self.slot_size_exp) * (1 << self.slot_count_exp));
236        let ring_buffer_range = ring_start..ring_end;
237
238        // Calculate the requested range, aligning to slot boundaries
239        // start_time is inclusive: include the slot that contains start_time
240        let asked_start = start_time >> self.slot_size_exp << self.slot_size_exp;
241        // end_time is exclusive: find the slot that would contain end_time and use its start as boundary
242        // If end_time is exactly at a slot boundary, use that boundary
243        // Otherwise, use the start of the next slot after the slot containing end_time
244        let asked_end = if end_time & ((1 << self.slot_size_exp) - 1) == 0 {
245            // end_time is exactly at slot boundary
246            end_time
247        } else {
248            // end_time is within a slot, use start of next slot
249            ((end_time >> self.slot_size_exp) + 1) << self.slot_size_exp
250        };
251        let asked_range = asked_start..asked_end;
252
253        // Find the intersection of ring buffer range and requested range
254        let valid_range = ring_buffer_range.start.max(asked_range.start)
255            ..ring_buffer_range.end.min(asked_range.end);
256
257        let mut count = 0;
258        for slot in &self.slots {
259            let time_in_slot = slot.interval_start.load(Ordering::Acquire);
260            if valid_range.contains(&time_in_slot) {
261                count += slot.counter.load(Ordering::Acquire);
262            }
263        }
264
265        count
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use std::sync::Arc;
273    use std::thread;
274
275    #[test]
276    fn test_basic_functionality() {
277        // 4 slots (2^2) * 8 time units (2^3) = 32 time units window
278        let counter = InvocationCounter::new(2, 3);
279
280        counter.register(0);
281        counter.register(1);
282        counter.register(8);
283        counter.register(16);
284
285        // All within 32-unit window from time 16
286        assert_eq!(counter.count_in(0, 16 + 1), 4);
287
288        // All outside window from time 100
289        assert_eq!(counter.count_in(100 - 32, 100 + 1), 0);
290    }
291
292    #[test]
293    fn test_count_in() {
294        // 2 slots (2^1) * 4 time units (2^2) = 8 time units window
295        // Slot 0: times 0-3 (interval_start = 0)
296        // Slot 1: times 4-7 (interval_start = 4)
297        let counter = InvocationCounter::new(1, 2);
298
299        counter.register(0);
300
301        // All registrations 0,1,2,3 go to the same slot (interval_start = 0)
302        // count_in works on slot boundaries, so any range that includes slot 0 gets all counts
303        assert_eq!(counter.count_in(0, 1), 1); // Includes slot 0
304        assert_eq!(counter.count_in(0, 4), 1); // Includes slot 0
305        assert_eq!(counter.count_in(0, 8), 1); // Includes slot 0
306        assert_eq!(counter.count_in(4, 8), 0); // Only includes slot 1 (empty)
307
308        counter.register(1);
309        // Still all in slot 0
310        assert_eq!(counter.count_in(0, 4), 2); // Includes slot 0
311        assert_eq!(counter.count_in(0, 8), 2); // Includes slot 0
312        assert_eq!(counter.count_in(4, 8), 0); // Only slot 1
313
314        counter.register(2);
315        assert_eq!(counter.count_in(0, 4), 3); // Slot 0
316        assert_eq!(counter.count_in(4, 8), 0); // Slot 1
317
318        counter.register(3);
319        assert_eq!(counter.count_in(0, 4), 4); // Slot 0
320        assert_eq!(counter.count_in(4, 8), 0); // Slot 1
321
322        // Register 4 in slot 1 (interval_start = 4)
323        counter.register(4);
324        assert_eq!(counter.count_in(0, 4), 4); // Slot 0 only (end_time=4 excludes slot 1)
325        assert_eq!(counter.count_in(4, 8), 1); // Slot 1 only
326        assert_eq!(counter.count_in(0, 8), 5); // Both slots
327
328        counter.register(5);
329        assert_eq!(counter.count_in(0, 4), 4); // Slot 0 only
330        assert_eq!(counter.count_in(4, 8), 2); // Slot 1 only
331        assert_eq!(counter.count_in(0, 8), 6); // Both slots
332
333        counter.register(6);
334        assert_eq!(counter.count_in(0, 4), 4); // Slot 0 only
335        assert_eq!(counter.count_in(4, 8), 3); // Slot 1 only
336        assert_eq!(counter.count_in(0, 8), 7); // Both slots
337
338        counter.register(7);
339        assert_eq!(counter.count_in(0, 4), 4); // Slot 0 only
340        assert_eq!(counter.count_in(4, 8), 4); // Slot 1 only
341        assert_eq!(counter.count_in(0, 8), 8); // Both slots
342        assert_eq!(counter.count_in(4, 12), 4); // Slot 1 only
343        assert_eq!(counter.count_in(12, 16), 0); // Out of range
344
345        // Register 8 - wraps to slot 0, resets it (interval_start = 8)
346        counter.register(8);
347        assert_eq!(counter.count_in(0, 4), 0); // Old slot 0 data gone
348        assert_eq!(counter.count_in(4, 8), 4); // Slot 1 unchanged
349        assert_eq!(counter.count_in(8, 12), 1); // New slot 0
350        assert_eq!(counter.count_in(4, 12), 5); // Slot 1 + new slot 0
351        assert_eq!(counter.count_in(8, 9), 1); // Within new slot 0
352        assert_eq!(counter.count_in(0, 12), 5); // Full range
353
354        counter.register(9);
355        assert_eq!(counter.count_in(0, 4), 0); // Old slot 0 still gone
356        assert_eq!(counter.count_in(4, 8), 4); // Slot 1 unchanged
357        assert_eq!(counter.count_in(8, 12), 2); // New slot 0 has 2 entries
358        assert_eq!(counter.count_in(4, 12), 6); // Slot 1 + new slot 0
359        assert_eq!(counter.count_in(8, 10), 2); // Within new slot 0
360        assert_eq!(counter.count_in(0, 16), 6); // Full range
361
362        counter.register(10);
363        assert_eq!(counter.count_in(4, 8), 4); // Slot 1 unchanged
364        assert_eq!(counter.count_in(8, 12), 3); // New slot 0 has 3 entries
365        assert_eq!(counter.count_in(4, 12), 7); // Slot 1 + new slot 0
366        assert_eq!(counter.count_in(8, 11), 3); // Within new slot 0
367        assert_eq!(counter.count_in(0, 16), 7); // Full range
368
369        counter.register(11);
370        assert_eq!(counter.count_in(4, 8), 4); // Slot 1 unchanged
371        assert_eq!(counter.count_in(8, 12), 4); // New slot 0 has 4 entries
372        assert_eq!(counter.count_in(4, 12), 8); // Slot 1 + new slot 0
373        assert_eq!(counter.count_in(8, 12), 4); // Full new slot 0
374        assert_eq!(counter.count_in(10, 12), 4); // Includes full slot 0 (slot-boundary aligned)
375        assert_eq!(counter.count_in(0, 16), 8); // Full range
376
377        // Register 12 - wraps to slot 1, resets it (interval_start = 12)
378        counter.register(12);
379        assert_eq!(counter.count_in(4, 8), 0); // Old slot 1 data gone
380        assert_eq!(counter.count_in(8, 12), 4); // Slot 0 unchanged
381        assert_eq!(counter.count_in(12, 16), 1); // New slot 1
382        assert_eq!(counter.count_in(8, 16), 5); // Slot 0 + new slot 1
383        assert_eq!(counter.count_in(0, 16), 5); // Full range
384        assert_eq!(counter.count_in(12, 13), 1); // Within new slot 1
385
386        counter.register(13);
387        assert_eq!(counter.count_in(8, 12), 4); // Slot 0 unchanged
388        assert_eq!(counter.count_in(12, 16), 2); // New slot 1 has 2 entries
389        assert_eq!(counter.count_in(8, 16), 6); // Slot 0 + new slot 1
390        assert_eq!(counter.count_in(0, 16), 6); // Full range
391        assert_eq!(counter.count_in(12, 14), 2); // Within new slot 1
392
393        counter.register(14);
394        assert_eq!(counter.count_in(8, 12), 4); // Slot 0 unchanged
395        assert_eq!(counter.count_in(12, 16), 3); // New slot 1 has 3 entries
396        assert_eq!(counter.count_in(8, 16), 7); // Slot 0 + new slot 1
397        assert_eq!(counter.count_in(0, 16), 7); // Full range
398        assert_eq!(counter.count_in(12, 15), 3); // Within new slot 1
399
400        counter.register(15);
401        assert_eq!(counter.count_in(8, 12), 4); // Slot 0 unchanged
402        assert_eq!(counter.count_in(12, 16), 4); // New slot 1 has 4 entries
403        assert_eq!(counter.count_in(8, 16), 8); // Slot 0 + new slot 1
404        assert_eq!(counter.count_in(0, 16), 8); // Full range
405        assert_eq!(counter.count_in(12, 16), 4); // Full new slot 1
406
407        // Register 16 - wraps to slot 0 again, resets it (interval_start = 16)
408        counter.register(16);
409        assert_eq!(counter.count_in(8, 12), 0); // Old slot 0 data gone
410        assert_eq!(counter.count_in(12, 16), 4); // Slot 1 unchanged
411        assert_eq!(counter.count_in(16, 20), 1); // New slot 0
412        assert_eq!(counter.count_in(12, 20), 5); // Slot 1 + new slot 0
413        assert_eq!(counter.count_in(0, 20), 5); // Full range
414        assert_eq!(counter.count_in(16, 17), 1); // Within new slot 0
415    }
416
417    #[test]
418    fn test_slot_reuse() {
419        // 4 slots (2^2) * 4 time units (2^2) = 16 time units window
420        let counter = InvocationCounter::new(2, 2);
421
422        counter.register(0); // slot 0
423        counter.register(16); // wraps to slot 0, resets counter
424
425        assert_eq!(counter.count_in(0, 17), 1); // Only recent registration
426    }
427
428    #[test]
429    fn test_concurrent_access() {
430        let num_threads = 4;
431        let registrations_per_thread = 100;
432
433        // 8 slots (2^3) * 64 time units (2^6) = 512 time units window
434        let counter = Arc::new(InvocationCounter::new(3, 6));
435
436        let handles: Vec<_> = (0..num_threads)
437            .map(|thread_id| {
438                let counter_clone = Arc::clone(&counter);
439                thread::spawn(move || {
440                    for i in 0..registrations_per_thread {
441                        counter_clone.register(thread_id as u64 * 10 + i as u64);
442                    }
443                })
444            })
445            .collect();
446
447        for handle in handles {
448            handle.join().unwrap();
449        }
450
451        let count = counter.count_in(0, 399); // Query at end of all registrations
452        assert!(count > 0);
453        assert!(count <= num_threads * registrations_per_thread);
454    }
455
456    #[test]
457    fn test_edge_cases() {
458        // 4 slots (2^2) * 8 time units (2^3) = 32 time units window
459        let counter = InvocationCounter::new(2, 3);
460
461        // Empty counter
462        assert_eq!(counter.count_in(0, 0), 0);
463
464        // Zero time
465        counter.register(0);
466        assert_eq!(counter.count_in(0, 1), 1);
467
468        // Large time values
469        let large_time = 1_000_000u64;
470        counter.register(large_time);
471        assert_eq!(counter.count_in(large_time, large_time + 1), 1);
472    }
473}