netpulse-cli 0.1.1

A zero-config, single-binary network quality monitor with percentile stats, jitter, and MTR-style traceroute
Documentation
// src/stats/ring_buffer.rs — Fixed-size circular buffer
//
// The RingBuffer stores the N most recent ProbeResults per target.
// Older samples are automatically overwritten when the buffer is full.
//
// Wrapped in Arc<Mutex<RingBuffer>> for sharing between the prober
// task and the stats engine. This is the first step — later you could
// upgrade to crossbeam's lock-free SegQueue for even lower contention.

use crate::probers::ProbeResult;
use std::collections::VecDeque;

/// A fixed-capacity circular buffer of ProbeResults.
///
/// When the buffer is full, the oldest entry is evicted.
pub struct RingBuffer {
    capacity: usize,
    inner: VecDeque<ProbeResult>,
}

impl RingBuffer {
    /// Create a new ring buffer with the given capacity.
    pub fn new(capacity: usize) -> Self {
        assert!(capacity > 0, "RingBuffer capacity must be > 0");
        Self {
            capacity,
            inner: VecDeque::with_capacity(capacity),
        }
    }

    /// Push a new probe result, evicting the oldest if at capacity.
    pub fn push(&mut self, result: ProbeResult) {
        if self.inner.len() == self.capacity {
            self.inner.pop_front();
        }
        self.inner.push_back(result);
    }

    /// Return a snapshot of all current samples as a Vec.
    /// This is a clone — callers get their own data and the lock is
    /// released as soon as the borrow ends.
    pub fn snapshot(&self) -> Vec<ProbeResult> {
        self.inner.iter().cloned().collect()
    }

    /// How many samples are currently stored.
    pub fn len(&self) -> usize {
        self.inner.len()
    }

    /// True if the buffer contains no samples.
    pub fn is_empty(&self) -> bool {
        self.inner.is_empty()
    }

    /// Current capacity.
    pub fn capacity(&self) -> usize {
        self.capacity
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Utc;

    fn make_result(rtt_us: Option<u64>, seq: u64) -> ProbeResult {
        ProbeResult {
            target: "test".to_string(),
            rtt_us,
            timestamp: Utc::now(),
            seq,
            responder_ip: None,
        }
    }

    #[test]
    fn test_capacity_eviction() {
        let mut rb = RingBuffer::new(3);
        for i in 0..5u64 {
            rb.push(make_result(Some(i * 1000), i));
        }
        // Should only hold the last 3
        assert_eq!(rb.len(), 3);
        let snap = rb.snapshot();
        assert_eq!(snap[0].seq, 2);
        assert_eq!(snap[1].seq, 3);
        assert_eq!(snap[2].seq, 4);
    }

    #[test]
    fn test_empty() {
        let rb = RingBuffer::new(10);
        assert!(rb.is_empty());
        assert_eq!(rb.len(), 0);
    }

    #[test]
    fn test_snapshot_is_clone() {
        let mut rb = RingBuffer::new(5);
        rb.push(make_result(Some(1000), 0));
        let snap = rb.snapshot();
        // Mutating original doesn't affect snapshot
        rb.push(make_result(Some(2000), 1));
        assert_eq!(snap.len(), 1);
    }
}