Skip to main content

trustformers_debug/
ring_buffer.rs

1//! Lock-free single-producer single-consumer (SPSC) ring buffer.
2//!
3//! Uses power-of-2 capacity so that modulo operations reduce to bitwise AND.
4//! The implementation is based on two atomic indices (`head` for writes, `tail`
5//! for reads) with `Acquire`/`Release` ordering — the same well-known pattern
6//! used by LMAX Disruptor and many embedded real-time systems.
7//!
8//! # Concurrency model
9//!
10//! `LockFreeRingBuffer` is **SPSC** — exactly **one** producer and **one**
11//! consumer thread at a time.  The `Send + Sync` implementations are
12//! deliberately provided because the buffer is safe to move across threads;
13//! it is the caller's responsibility to ensure only one thread pushes and
14//! one thread pops concurrently.
15//!
16//! # Example
17//!
18//! ```
19//! use trustformers_debug::ring_buffer::LockFreeRingBuffer;
20//! use std::sync::Arc;
21//! use std::thread;
22//!
23//! let buf: Arc<LockFreeRingBuffer<u64>> = Arc::new(LockFreeRingBuffer::new(16));
24//! let producer = Arc::clone(&buf);
25//! let consumer = Arc::clone(&buf);
26//!
27//! let t = thread::spawn(move || {
28//!     for i in 0..8_u64 {
29//!         while producer.push(i).is_err() {}
30//!     }
31//! });
32//!
33//! t.join().unwrap();
34//! for i in 0..8_u64 {
35//!     assert_eq!(consumer.pop(), Some(i));
36//! }
37//! ```
38
39use std::cell::UnsafeCell;
40use std::mem::MaybeUninit;
41use std::sync::atomic::{AtomicUsize, Ordering};
42
43// ─────────────────────────────────────────────────────────────
44// Error type
45// ─────────────────────────────────────────────────────────────
46
47/// Errors that can arise from ring-buffer operations.
48#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
49pub enum RingBufferError {
50    /// The buffer is full; the pushed item was not stored.
51    #[error("ring buffer is full (capacity {capacity})")]
52    Full {
53        /// Capacity of the buffer.
54        capacity: usize,
55    },
56    /// The requested capacity is zero.
57    #[error("ring buffer capacity must be at least 1")]
58    ZeroCapacity,
59}
60
61// ─────────────────────────────────────────────────────────────
62// LockFreeRingBuffer
63// ─────────────────────────────────────────────────────────────
64
65/// Lock-free SPSC ring buffer with power-of-2 capacity.
66///
67/// Internally stores items in a fixed-size boxed slice of
68/// `UnsafeCell<MaybeUninit<T>>`.  Two atomic counters (`head` for the write
69/// position and `tail` for the read position) are advanced using
70/// `Acquire`/`Release` ordering so that item writes are visible to the reader
71/// thread before the head index update becomes visible.
72///
73/// # Capacity rounding
74///
75/// The requested capacity is rounded **up** to the next power of two so that
76/// `index & mask` can replace `index % capacity`.
77///
78/// # Example
79///
80/// ```
81/// use trustformers_debug::ring_buffer::LockFreeRingBuffer;
82///
83/// let buf = LockFreeRingBuffer::<i32>::new(6);
84/// // Rounded up to the next power of 2 → 8
85/// assert_eq!(buf.capacity(), 8);
86///
87/// assert!(buf.push(1).is_ok());
88/// assert_eq!(buf.pop(), Some(1));
89/// assert_eq!(buf.pop(), None);
90/// ```
91pub struct LockFreeRingBuffer<T: Copy + Send + 'static> {
92    buffer: Box<[UnsafeCell<MaybeUninit<T>>]>,
93    capacity: usize,
94    mask: usize,
95    /// Write cursor — points to the next slot to fill.
96    head: AtomicUsize,
97    /// Read cursor — points to the next slot to drain.
98    tail: AtomicUsize,
99}
100
101// SAFETY: `LockFreeRingBuffer` is safe to send across threads; it is the
102// caller's responsibility to uphold the SPSC invariant (one producer, one
103// consumer).
104unsafe impl<T: Copy + Send + 'static> Send for LockFreeRingBuffer<T> {}
105// SAFETY: Internal mutation is guarded by atomic ordering; the buffer does
106// not expose mutable references to its internals.
107unsafe impl<T: Copy + Send + 'static> Sync for LockFreeRingBuffer<T> {}
108
109impl<T: Copy + Send + 'static> LockFreeRingBuffer<T> {
110    /// Creates a new buffer whose actual capacity is the smallest power of 2
111    /// that is `>= capacity`.
112    ///
113    /// # Panics
114    ///
115    /// Panics if `capacity` is 0.
116    ///
117    /// # Example
118    ///
119    /// ```
120    /// use trustformers_debug::ring_buffer::LockFreeRingBuffer;
121    /// let buf = LockFreeRingBuffer::<u8>::new(10);
122    /// assert_eq!(buf.capacity(), 16); // rounded up to next power of 2
123    /// ```
124    pub fn new(capacity: usize) -> Self {
125        assert!(
126            capacity > 0,
127            "LockFreeRingBuffer capacity must be at least 1"
128        );
129        let actual = capacity.next_power_of_two();
130        // Build the backing store as a boxed slice of uninitialised cells.
131        let buffer: Box<[UnsafeCell<MaybeUninit<T>>]> =
132            (0..actual).map(|_| UnsafeCell::new(MaybeUninit::uninit())).collect();
133        Self {
134            buffer,
135            capacity: actual,
136            mask: actual - 1,
137            head: AtomicUsize::new(0),
138            tail: AtomicUsize::new(0),
139        }
140    }
141
142    /// Attempts to push `item` into the buffer.
143    ///
144    /// Returns `Err(RingBufferError::Full { … })` if the buffer is full.
145    ///
146    /// # Example
147    ///
148    /// ```
149    /// use trustformers_debug::ring_buffer::{LockFreeRingBuffer, RingBufferError};
150    ///
151    /// let buf = LockFreeRingBuffer::<u8>::new(2);
152    /// assert!(buf.push(10).is_ok());
153    /// assert!(buf.push(20).is_ok());
154    /// let err = buf.push(30).unwrap_err();
155    /// assert!(matches!(err, RingBufferError::Full { .. }));
156    /// ```
157    pub fn push(&self, item: T) -> Result<(), RingBufferError> {
158        let head = self.head.load(Ordering::Relaxed);
159        let tail = self.tail.load(Ordering::Acquire);
160
161        if head.wrapping_sub(tail) >= self.capacity {
162            return Err(RingBufferError::Full {
163                capacity: self.capacity,
164            });
165        }
166
167        let slot = head & self.mask;
168        // SAFETY: `slot` is within `[0, capacity)`.  The producer owns this
169        // slot exclusively because `head - tail < capacity` guarantees the
170        // consumer has not yet reached it.
171        unsafe {
172            (*self.buffer[slot].get()).write(item);
173        }
174
175        // Release ordering: ensures the write above is visible to the reader
176        // thread before the head update.
177        self.head.store(head.wrapping_add(1), Ordering::Release);
178        Ok(())
179    }
180
181    /// Attempts to pop an item from the buffer.
182    ///
183    /// Returns `None` if the buffer is empty.
184    ///
185    /// # Example
186    ///
187    /// ```
188    /// use trustformers_debug::ring_buffer::LockFreeRingBuffer;
189    ///
190    /// let buf = LockFreeRingBuffer::<u8>::new(4);
191    /// assert_eq!(buf.pop(), None);
192    /// buf.push(99).unwrap();
193    /// assert_eq!(buf.pop(), Some(99));
194    /// assert_eq!(buf.pop(), None);
195    /// ```
196    pub fn pop(&self) -> Option<T> {
197        let tail = self.tail.load(Ordering::Relaxed);
198        let head = self.head.load(Ordering::Acquire);
199
200        if tail == head {
201            return None;
202        }
203
204        let slot = tail & self.mask;
205        // SAFETY: `slot` is within `[0, capacity)`.  The consumer owns this
206        // slot because `tail < head` guarantees the producer has already
207        // written to it.
208        let item = unsafe { (*self.buffer[slot].get()).assume_init_read() };
209
210        // Release ordering: ensures the read above completes before the tail
211        // update is visible to the producer.
212        self.tail.store(tail.wrapping_add(1), Ordering::Release);
213        Some(item)
214    }
215
216    /// Returns the number of items currently held in the buffer.
217    ///
218    /// Note: this is a point-in-time snapshot; the value may change
219    /// concurrently.
220    pub fn len(&self) -> usize {
221        let head = self.head.load(Ordering::Acquire);
222        let tail = self.tail.load(Ordering::Acquire);
223        head.wrapping_sub(tail)
224    }
225
226    /// Returns `true` if the buffer currently holds no items.
227    pub fn is_empty(&self) -> bool {
228        self.len() == 0
229    }
230
231    /// Returns the (rounded-up) capacity.
232    pub fn capacity(&self) -> usize {
233        self.capacity
234    }
235}
236
237// ─────────────────────────────────────────────────────────────
238// StatisticsWindow – a plain Vec-backed sliding window
239// ─────────────────────────────────────────────────────────────
240
241/// A simple sliding-window buffer that retains the most-recent `capacity`
242/// values and exposes statistical helpers.
243///
244/// Unlike `LockFreeRingBuffer` this type is **single-threaded** and designed
245/// for convenience, not throughput.
246///
247/// # Type bound
248///
249/// `T: Copy + Into<f64>` so that integer and floating-point scalars can all be
250/// treated uniformly.
251///
252/// # Example
253/// ```
254/// use trustformers_debug::ring_buffer::StatisticsWindow;
255/// let mut w = StatisticsWindow::new(4);
256/// w.push(1u32);
257/// w.push(2u32);
258/// w.push(3u32);
259/// assert_eq!(w.mean(), Some(2.0));
260/// ```
261pub struct StatisticsWindow<T: Copy + Into<f64>> {
262    buf: Vec<T>,
263    capacity: usize,
264    /// Head position in the circular backing vec.
265    head: usize,
266    len: usize,
267}
268
269impl<T: Copy + Into<f64>> StatisticsWindow<T> {
270    /// Create a new window that retains at most `capacity` values.
271    ///
272    /// Panics if `capacity == 0`.
273    pub fn new(capacity: usize) -> Self {
274        assert!(capacity > 0, "StatisticsWindow capacity must be >= 1");
275        Self {
276            buf: Vec::with_capacity(capacity),
277            capacity,
278            head: 0,
279            len: 0,
280        }
281    }
282
283    /// Push a value; if the window is full the oldest value is evicted.
284    pub fn push(&mut self, value: T) {
285        if self.len < self.capacity {
286            self.buf.push(value);
287            self.len += 1;
288        } else {
289            self.buf[self.head] = value;
290            self.head = (self.head + 1) % self.capacity;
291        }
292    }
293
294    /// Number of values currently stored.
295    pub fn len(&self) -> usize {
296        self.len
297    }
298
299    /// Returns `true` when no values are stored.
300    pub fn is_empty(&self) -> bool {
301        self.len == 0
302    }
303
304    /// Iterate over the window contents in insertion order (oldest first).
305    pub fn iter_ordered(&self) -> impl Iterator<Item = T> + '_ {
306        // When not yet full: plain slice from 0..len.
307        // When full: ring starting at head.
308        let (start, count) = if self.len < self.capacity {
309            (0, self.len)
310        } else {
311            (self.head, self.capacity)
312        };
313        (0..count).map(move |i| self.buf[(start + i) % self.capacity])
314    }
315
316    /// Snapshot of all current values as a `Vec<f64>`.
317    fn as_f64_vec(&self) -> Vec<f64> {
318        self.iter_ordered().map(|v| v.into()).collect()
319    }
320
321    /// Mean of the current window contents.
322    pub fn mean(&self) -> Option<f64> {
323        if self.is_empty() {
324            return None;
325        }
326        let vals = self.as_f64_vec();
327        Some(vals.iter().sum::<f64>() / vals.len() as f64)
328    }
329
330    /// Population standard deviation of the current window.
331    ///
332    /// Returns `None` when fewer than 2 values are stored.
333    pub fn std_dev(&self) -> Option<f64> {
334        if self.len < 2 {
335            return None;
336        }
337        let mean = self.mean()?;
338        let vals = self.as_f64_vec();
339        let variance =
340            vals.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / (vals.len() - 1) as f64;
341        Some(variance.sqrt())
342    }
343
344    /// Minimum value in the current window.
345    pub fn min(&self) -> Option<T> {
346        if self.is_empty() {
347            return None;
348        }
349        // We compare as f64 because T may not implement Ord.
350        let mut best = self.buf[0];
351        let mut best_f: f64 = best.into();
352        for v in self.iter_ordered() {
353            let vf: f64 = v.into();
354            if vf < best_f {
355                best = v;
356                best_f = vf;
357            }
358        }
359        Some(best)
360    }
361
362    /// Maximum value in the current window.
363    pub fn max(&self) -> Option<T> {
364        if self.is_empty() {
365            return None;
366        }
367        let mut best = self.buf[0];
368        let mut best_f: f64 = best.into();
369        for v in self.iter_ordered() {
370            let vf: f64 = v.into();
371            if vf > best_f {
372                best = v;
373                best_f = vf;
374            }
375        }
376        Some(best)
377    }
378
379    /// Approximate the `p`-th percentile (0.0–100.0) via a sorted copy.
380    ///
381    /// Uses nearest-rank method.
382    pub fn percentile(&self, p: f64) -> Option<f64> {
383        if self.is_empty() {
384            return None;
385        }
386        let mut sorted = self.as_f64_vec();
387        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
388        let p_clamped = p.clamp(0.0, 100.0);
389        let rank = (p_clamped / 100.0 * (sorted.len() - 1) as f64).round() as usize;
390        Some(sorted[rank.min(sorted.len() - 1)])
391    }
392
393    /// Mean of the last `window` values (or all values if fewer available).
394    pub fn windowed_mean(&self, window: usize) -> Option<f64> {
395        if self.is_empty() || window == 0 {
396            return None;
397        }
398        let total = self.len;
399        let n = window.min(total);
400        // Collect last `n` values (most-recent end of ordered sequence).
401        let vals: Vec<f64> = self.iter_ordered().skip(total - n).map(|v| v.into()).collect();
402        if vals.is_empty() {
403            return None;
404        }
405        Some(vals.iter().sum::<f64>() / vals.len() as f64)
406    }
407}
408
409// ─────────────────────────────────────────────────────────────
410// TimestampedValue & TimestampedRingBuffer
411// ─────────────────────────────────────────────────────────────
412
413/// A value paired with a nanosecond-resolution timestamp.
414#[derive(Debug, Clone, Copy)]
415pub struct TimestampedValue<T: Copy> {
416    pub value: T,
417    /// Nanoseconds since an arbitrary epoch (caller-defined, e.g. Unix epoch).
418    pub timestamp_ns: u64,
419}
420
421/// An SPSC ring buffer that stores [`TimestampedValue`] entries and exposes
422/// time-range queries and throughput estimation.
423///
424/// Internally backed by a `StatisticsWindow<f64>` for the values and a
425/// separate `Vec`-based circular buffer for the full `TimestampedValue` records.
426#[derive(Debug)]
427pub struct TimestampedRingBuffer<T: Copy> {
428    /// Circular backing store (index = 0 means slot 0 in the array).
429    buf: Vec<TimestampedValue<T>>,
430    capacity: usize,
431    head: usize,
432    len: usize,
433}
434
435impl<T: Copy> TimestampedRingBuffer<T> {
436    /// Create a new buffer with the given capacity.
437    ///
438    /// Panics if `capacity == 0`.
439    pub fn new(capacity: usize) -> Self {
440        assert!(capacity > 0, "TimestampedRingBuffer capacity must be >= 1");
441        // We can't initialise MaybeUninit here without unsafe, so we use a
442        // sentinel-free approach: track length explicitly.
443        // Backing store is initialised lazily via push.
444        Self {
445            buf: Vec::with_capacity(capacity),
446            capacity,
447            head: 0,
448            len: 0,
449        }
450    }
451
452    /// Push a new `(value, timestamp_ns)` pair.
453    ///
454    /// If the buffer is full the oldest entry is evicted.
455    pub fn push_now(&mut self, value: T, time_ns: u64) {
456        let entry = TimestampedValue {
457            value,
458            timestamp_ns: time_ns,
459        };
460        if self.len < self.capacity {
461            self.buf.push(entry);
462            self.len += 1;
463        } else {
464            self.buf[self.head] = entry;
465            self.head = (self.head + 1) % self.capacity;
466        }
467    }
468
469    /// Number of entries currently stored.
470    pub fn len(&self) -> usize {
471        self.len
472    }
473
474    /// Returns `true` when empty.
475    pub fn is_empty(&self) -> bool {
476        self.len == 0
477    }
478
479    /// Iterate over all stored entries in insertion order (oldest first).
480    pub fn iter_ordered(&self) -> impl Iterator<Item = TimestampedValue<T>> + '_ {
481        let (start, count) = if self.len < self.capacity {
482            (0, self.len)
483        } else {
484            (self.head, self.capacity)
485        };
486        (0..count).map(move |i| self.buf[(start + i) % self.capacity])
487    }
488
489    /// Estimate throughput as events per second over the entire stored window.
490    ///
491    /// Returns 0.0 when fewer than 2 entries are stored or the time span is
492    /// zero.
493    pub fn rate_per_sec(&self) -> f64 {
494        if self.len < 2 {
495            return 0.0;
496        }
497        let oldest = self.oldest_timestamp().unwrap_or(0);
498        let newest = self.newest_timestamp().unwrap_or(0);
499        let span_ns = newest.saturating_sub(oldest);
500        if span_ns == 0 {
501            return 0.0;
502        }
503        (self.len as f64 - 1.0) / (span_ns as f64 * 1e-9)
504    }
505
506    /// Return all values whose timestamps fall in `[start_ns, end_ns]`
507    /// (inclusive on both ends).
508    pub fn values_in_range(&self, start_ns: u64, end_ns: u64) -> Vec<T> {
509        self.iter_ordered()
510            .filter(|e| e.timestamp_ns >= start_ns && e.timestamp_ns <= end_ns)
511            .map(|e| e.value)
512            .collect()
513    }
514
515    /// The timestamp of the oldest retained entry.
516    pub fn oldest_timestamp(&self) -> Option<u64> {
517        self.iter_ordered().next().map(|e| e.timestamp_ns)
518    }
519
520    /// The timestamp of the most-recently added entry.
521    pub fn newest_timestamp(&self) -> Option<u64> {
522        self.iter_ordered().last().map(|e| e.timestamp_ns)
523    }
524}
525
526// ─────────────────────────────────────────────────────────────
527// Tests
528// ─────────────────────────────────────────────────────────────
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533    use std::sync::Arc;
534    use std::thread;
535
536    #[test]
537    fn test_capacity_rounds_up_to_power_of_two() {
538        let buf = LockFreeRingBuffer::<u8>::new(5);
539        assert_eq!(buf.capacity(), 8);
540
541        let buf2 = LockFreeRingBuffer::<u8>::new(8);
542        assert_eq!(buf2.capacity(), 8);
543
544        let buf3 = LockFreeRingBuffer::<u8>::new(9);
545        assert_eq!(buf3.capacity(), 16);
546    }
547
548    #[test]
549    fn test_push_and_pop_basic() {
550        let buf = LockFreeRingBuffer::<u32>::new(4);
551        assert_eq!(buf.pop(), None);
552        buf.push(1).unwrap();
553        buf.push(2).unwrap();
554        buf.push(3).unwrap();
555        assert_eq!(buf.pop(), Some(1));
556        assert_eq!(buf.pop(), Some(2));
557        assert_eq!(buf.pop(), Some(3));
558        assert_eq!(buf.pop(), None);
559    }
560
561    #[test]
562    fn test_full_buffer_returns_error() {
563        let buf = LockFreeRingBuffer::<u32>::new(2);
564        buf.push(10).unwrap();
565        buf.push(20).unwrap();
566        let err = buf.push(30).unwrap_err();
567        assert!(matches!(err, RingBufferError::Full { capacity: 2 }));
568    }
569
570    #[test]
571    fn test_len_and_is_empty() {
572        let buf = LockFreeRingBuffer::<u8>::new(4);
573        assert!(buf.is_empty());
574        assert_eq!(buf.len(), 0);
575        buf.push(1).unwrap();
576        assert!(!buf.is_empty());
577        assert_eq!(buf.len(), 1);
578        buf.push(2).unwrap();
579        assert_eq!(buf.len(), 2);
580        buf.pop();
581        assert_eq!(buf.len(), 1);
582    }
583
584    #[test]
585    fn test_wrap_around() {
586        let buf = LockFreeRingBuffer::<u32>::new(4);
587        // Fill
588        buf.push(1).unwrap();
589        buf.push(2).unwrap();
590        buf.push(3).unwrap();
591        buf.push(4).unwrap();
592        // Drain half
593        assert_eq!(buf.pop(), Some(1));
594        assert_eq!(buf.pop(), Some(2));
595        // Push two more — exercises wrap-around
596        buf.push(5).unwrap();
597        buf.push(6).unwrap();
598        assert_eq!(buf.pop(), Some(3));
599        assert_eq!(buf.pop(), Some(4));
600        assert_eq!(buf.pop(), Some(5));
601        assert_eq!(buf.pop(), Some(6));
602        assert_eq!(buf.pop(), None);
603    }
604
605    #[test]
606    fn test_concurrent_spsc() {
607        let buf: Arc<LockFreeRingBuffer<u64>> = Arc::new(LockFreeRingBuffer::new(64));
608        let producer = Arc::clone(&buf);
609        let consumer = Arc::clone(&buf);
610
611        const N: u64 = 1000;
612
613        let producer_thread = thread::spawn(move || {
614            let mut sent = 0u64;
615            while sent < N {
616                if producer.push(sent).is_ok() {
617                    sent += 1;
618                }
619            }
620        });
621
622        let consumer_thread = thread::spawn(move || {
623            let mut received = Vec::with_capacity(N as usize);
624            while received.len() < N as usize {
625                if let Some(v) = consumer.pop() {
626                    received.push(v);
627                }
628            }
629            received
630        });
631
632        producer_thread.join().unwrap();
633        let received = consumer_thread.join().unwrap();
634
635        assert_eq!(received.len(), N as usize);
636        for (i, &v) in received.iter().enumerate() {
637            assert_eq!(v, i as u64);
638        }
639    }
640
641    #[test]
642    fn test_capacity_one() {
643        let buf = LockFreeRingBuffer::<u8>::new(1);
644        assert_eq!(buf.capacity(), 1);
645        buf.push(42).unwrap();
646        assert!(buf.push(99).is_err());
647        assert_eq!(buf.pop(), Some(42));
648        assert_eq!(buf.pop(), None);
649    }
650
651    #[test]
652    fn test_f32_elements() {
653        let buf = LockFreeRingBuffer::<f32>::new(8);
654        buf.push(1.5_f32).unwrap();
655        buf.push(2.5_f32).unwrap();
656        assert!((buf.pop().unwrap() - 1.5).abs() < 1e-6);
657        assert!((buf.pop().unwrap() - 2.5).abs() < 1e-6);
658    }
659
660    #[test]
661    fn test_concurrent_ping_pong() {
662        // Producer and consumer ping-pong many small bursts
663        let buf: Arc<LockFreeRingBuffer<u32>> = Arc::new(LockFreeRingBuffer::new(32));
664        let p = Arc::clone(&buf);
665        let c = Arc::clone(&buf);
666
667        const ITERS: u32 = 2_000;
668
669        let prod = thread::spawn(move || {
670            for i in 0..ITERS {
671                while p.push(i).is_err() {
672                    thread::yield_now();
673                }
674            }
675        });
676
677        let cons = thread::spawn(move || {
678            let mut count = 0u32;
679            while count < ITERS {
680                if c.pop().is_some() {
681                    count += 1;
682                }
683            }
684            count
685        });
686
687        prod.join().unwrap();
688        assert_eq!(cons.join().unwrap(), ITERS);
689    }
690
691    #[test]
692    fn test_multiple_wrap_arounds() {
693        let buf = LockFreeRingBuffer::<u64>::new(4);
694        for round in 0..10u64 {
695            for i in 0..4u64 {
696                buf.push(round * 4 + i).unwrap();
697            }
698            for i in 0..4u64 {
699                assert_eq!(buf.pop(), Some(round * 4 + i));
700            }
701        }
702    }
703
704    // ── StatisticsWindow ────────────────────────────────────────────────────
705
706    #[test]
707    fn test_statistics_window_mean_basic() {
708        let mut w = StatisticsWindow::new(8);
709        w.push(1u32);
710        w.push(2u32);
711        w.push(3u32);
712        let m = w.mean().unwrap();
713        assert!((m - 2.0).abs() < 1e-9, "mean={}", m);
714    }
715
716    #[test]
717    fn test_statistics_window_empty_mean_returns_none() {
718        let w: StatisticsWindow<u32> = StatisticsWindow::new(4);
719        assert!(w.mean().is_none());
720    }
721
722    #[test]
723    fn test_statistics_window_eviction() {
724        // Capacity 3: after 4 pushes oldest (1) is evicted.
725        let mut w = StatisticsWindow::new(3);
726        w.push(1u32);
727        w.push(2u32);
728        w.push(3u32);
729        w.push(4u32); // evicts 1
730        assert_eq!(w.len(), 3);
731        let vals: Vec<u32> = w.iter_ordered().collect();
732        assert_eq!(vals, vec![2, 3, 4]);
733    }
734
735    #[test]
736    fn test_statistics_window_std_dev_constant() {
737        let mut w = StatisticsWindow::new(5);
738        for _ in 0..5 {
739            w.push(7u32);
740        }
741        let s = w.std_dev().unwrap();
742        assert!(s < 1e-9, "std of constant values should be 0, got {}", s);
743    }
744
745    #[test]
746    fn test_statistics_window_std_dev_two_values() {
747        let mut w = StatisticsWindow::new(4);
748        w.push(0u32);
749        w.push(4u32);
750        // Sample std: sqrt(((0-2)^2 + (4-2)^2) / 1) = sqrt(8) ≈ 2.828
751        let s = w.std_dev().unwrap();
752        assert!((s - (8.0_f64).sqrt()).abs() < 1e-6, "std={}", s);
753    }
754
755    #[test]
756    fn test_statistics_window_min_max() {
757        let mut w = StatisticsWindow::new(8);
758        w.push(5u32);
759        w.push(2u32);
760        w.push(9u32);
761        w.push(1u32);
762        assert_eq!(w.min(), Some(1u32));
763        assert_eq!(w.max(), Some(9u32));
764    }
765
766    #[test]
767    fn test_statistics_window_min_max_empty() {
768        let w: StatisticsWindow<u32> = StatisticsWindow::new(4);
769        assert!(w.min().is_none());
770        assert!(w.max().is_none());
771    }
772
773    #[test]
774    fn test_statistics_window_percentile_median() {
775        let mut w = StatisticsWindow::new(10);
776        for i in 1u32..=9 {
777            w.push(i);
778        }
779        // Sorted: 1..9, median = 5th element (idx 4) = 5.
780        let p50 = w.percentile(50.0).unwrap();
781        assert!((p50 - 5.0).abs() < 1.5, "p50={}", p50);
782    }
783
784    #[test]
785    fn test_statistics_window_windowed_mean_last_n() {
786        let mut w = StatisticsWindow::new(10);
787        for i in 1u32..=10 {
788            w.push(i);
789        }
790        // Last 3 values: 8, 9, 10 → mean = 9.
791        let wm = w.windowed_mean(3).unwrap();
792        assert!((wm - 9.0).abs() < 1e-9, "windowed_mean={}", wm);
793    }
794
795    #[test]
796    fn test_statistics_window_windowed_mean_larger_than_len() {
797        let mut w = StatisticsWindow::new(10);
798        w.push(2u32);
799        w.push(4u32);
800        // window=5 but only 2 values → falls back to all.
801        let wm = w.windowed_mean(5).unwrap();
802        assert!((wm - 3.0).abs() < 1e-9, "windowed_mean={}", wm);
803    }
804
805    #[test]
806    fn test_statistics_window_windowed_mean_zero_window() {
807        let mut w = StatisticsWindow::new(4);
808        w.push(1u32);
809        assert!(w.windowed_mean(0).is_none());
810    }
811
812    // ── TimestampedRingBuffer ───────────────────────────────────────────────
813
814    #[test]
815    fn test_timestamped_ring_buffer_basic_push_and_len() {
816        let mut tb = TimestampedRingBuffer::<u32>::new(4);
817        assert!(tb.is_empty());
818        tb.push_now(10, 1_000_000);
819        tb.push_now(20, 2_000_000);
820        assert_eq!(tb.len(), 2);
821    }
822
823    #[test]
824    fn test_timestamped_ring_buffer_eviction() {
825        let mut tb = TimestampedRingBuffer::<u32>::new(2);
826        tb.push_now(1, 100);
827        tb.push_now(2, 200);
828        tb.push_now(3, 300); // evicts first
829        assert_eq!(tb.len(), 2);
830        let vals: Vec<u32> = tb.iter_ordered().map(|e| e.value).collect();
831        assert_eq!(vals, vec![2, 3]);
832    }
833
834    #[test]
835    fn test_timestamped_oldest_newest() {
836        let mut tb = TimestampedRingBuffer::<u32>::new(4);
837        tb.push_now(0u32, 100);
838        tb.push_now(1u32, 200);
839        tb.push_now(2u32, 300);
840        assert_eq!(tb.oldest_timestamp(), Some(100));
841        assert_eq!(tb.newest_timestamp(), Some(300));
842    }
843
844    #[test]
845    fn test_timestamped_oldest_newest_empty() {
846        let tb: TimestampedRingBuffer<u32> = TimestampedRingBuffer::new(4);
847        assert!(tb.oldest_timestamp().is_none());
848        assert!(tb.newest_timestamp().is_none());
849    }
850
851    #[test]
852    fn test_timestamped_rate_per_sec() {
853        let mut tb = TimestampedRingBuffer::<u32>::new(4);
854        // 3 events over 2 seconds → rate = 2 / 2s = 1.0 events/s.
855        // (rate = (n-1) / elapsed_s)
856        tb.push_now(0, 0);
857        tb.push_now(1, 1_000_000_000); // 1 s
858        tb.push_now(2, 2_000_000_000); // 2 s
859        let rate = tb.rate_per_sec();
860        assert!((rate - 1.0).abs() < 0.01, "rate={}", rate);
861    }
862
863    #[test]
864    fn test_timestamped_rate_single_entry_is_zero() {
865        let mut tb = TimestampedRingBuffer::<u32>::new(4);
866        tb.push_now(1, 1_000_000_000);
867        assert_eq!(tb.rate_per_sec(), 0.0);
868    }
869
870    #[test]
871    fn test_timestamped_values_in_range() {
872        let mut tb = TimestampedRingBuffer::<u32>::new(8);
873        for i in 0..8u32 {
874            tb.push_now(i, i as u64 * 100);
875        }
876        // Range 200..=500 → timestamps 200, 300, 400, 500 → values 2,3,4,5.
877        let vals = tb.values_in_range(200, 500);
878        assert_eq!(vals, vec![2, 3, 4, 5]);
879    }
880
881    #[test]
882    fn test_timestamped_values_in_range_empty_result() {
883        let mut tb = TimestampedRingBuffer::<u32>::new(4);
884        tb.push_now(1, 100);
885        tb.push_now(2, 200);
886        let vals = tb.values_in_range(500, 600);
887        assert!(vals.is_empty());
888    }
889}