Skip to main content

entrenar/monitor/inference/collector/
ring.rs

1//! RingCollector - Vec-based ring buffer for real-time
2
3use super::super::path::DecisionPath;
4use super::super::trace::DecisionTrace;
5use super::traits::TraceCollector;
6
7/// Ring buffer collector with fixed capacity
8///
9/// Target: <100ns per trace
10///
11/// # Features
12/// - O(1) push operation
13/// - Overwrites oldest entries when full
14/// - No unsafe code
15///
16/// # Example
17///
18/// ```ignore
19/// use entrenar::monitor::inference::{RingCollector, LinearPath};
20///
21/// let mut collector = RingCollector::<LinearPath, 64>::new();
22/// collector.record(trace);
23/// let recent = collector.recent(10);
24/// ```
25pub struct RingCollector<P: DecisionPath, const N: usize> {
26    buffer: Vec<DecisionTrace<P>>,
27    head: usize,
28}
29
30impl<P: DecisionPath, const N: usize> RingCollector<P, N> {
31    /// Create a new ring collector
32    pub fn new() -> Self {
33        Self { buffer: Vec::with_capacity(N), head: 0 }
34    }
35
36    /// Get the most recent n traces (or all if n > count)
37    pub fn recent(&self, n: usize) -> Vec<&DecisionTrace<P>> {
38        let take = n.min(self.buffer.len());
39        let mut result = Vec::with_capacity(take);
40
41        for i in 0..take {
42            let idx = if self.buffer.len() < N {
43                // Not yet wrapped
44                self.buffer.len() - 1 - i
45            } else {
46                // Wrapped: head points to next write, so head-1 is most recent
47                (self.head + N - 1 - i) % N
48            };
49            result.push(&self.buffer[idx]);
50        }
51
52        result
53    }
54
55    /// Get all traces in order (oldest first)
56    pub fn all(&self) -> Vec<&DecisionTrace<P>> {
57        let mut result = Vec::with_capacity(self.buffer.len());
58
59        if self.buffer.is_empty() {
60            return result;
61        }
62
63        if self.buffer.len() < N {
64            // Not yet wrapped - just iterate in order
65            for trace in &self.buffer {
66                result.push(trace);
67            }
68        } else {
69            // Wrapped: head is the oldest
70            for i in 0..N {
71                let idx = (self.head + i) % N;
72                result.push(&self.buffer[idx]);
73            }
74        }
75
76        result
77    }
78
79    /// Get the last trace if any
80    pub fn last(&self) -> Option<&DecisionTrace<P>> {
81        if self.buffer.is_empty() {
82            return None;
83        }
84        if self.buffer.len() < N {
85            self.buffer.last()
86        } else {
87            let idx = (self.head + N - 1) % N;
88            Some(&self.buffer[idx])
89        }
90    }
91
92    /// Clear all traces
93    pub fn clear(&mut self) {
94        self.buffer.clear();
95        self.head = 0;
96    }
97
98    /// Capacity of the ring buffer
99    pub const fn capacity(&self) -> usize {
100        N
101    }
102}
103
104impl<P: DecisionPath, const N: usize> TraceCollector<P> for RingCollector<P, N> {
105    fn record(&mut self, trace: DecisionTrace<P>) {
106        if self.buffer.len() < N {
107            // Buffer not yet full, just push
108            self.buffer.push(trace);
109        } else {
110            // Buffer full, overwrite oldest
111            self.buffer[self.head] = trace;
112            self.head = (self.head + 1) % N;
113        }
114    }
115
116    fn flush(&mut self) -> std::io::Result<()> {
117        // Ring buffer doesn't need flushing
118        Ok(())
119    }
120
121    fn len(&self) -> usize {
122        self.buffer.len()
123    }
124}
125
126impl<P: DecisionPath, const N: usize> Default for RingCollector<P, N> {
127    fn default() -> Self {
128        Self::new()
129    }
130}