Skip to main content

laminar_core/xdp/
stats.rs

1//! XDP statistics.
2
3use std::sync::atomic::{AtomicU64, Ordering};
4
5/// Statistics collected by the XDP program.
6#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
7pub struct XdpStats {
8    /// Packets dropped (invalid protocol, malformed).
9    pub dropped: u64,
10    /// Packets passed to kernel stack.
11    pub passed: u64,
12    /// Packets redirected to specific CPUs.
13    pub redirected: u64,
14    /// Invalid packets (too short, bad header).
15    pub invalid: u64,
16    /// Total bytes processed.
17    pub bytes_processed: u64,
18}
19
20impl XdpStats {
21    /// Returns the total number of packets processed.
22    #[must_use]
23    pub const fn total_packets(&self) -> u64 {
24        self.dropped + self.passed + self.redirected + self.invalid
25    }
26
27    /// Returns the drop rate as a percentage (0.0 to 100.0).
28    #[must_use]
29    #[allow(clippy::cast_precision_loss)]
30    pub fn drop_rate(&self) -> f64 {
31        let total = self.total_packets();
32        if total == 0 {
33            return 0.0;
34        }
35        (self.dropped as f64 / total as f64) * 100.0
36    }
37
38    /// Returns the redirect rate as a percentage (0.0 to 100.0).
39    #[must_use]
40    #[allow(clippy::cast_precision_loss)]
41    pub fn redirect_rate(&self) -> f64 {
42        let total = self.total_packets();
43        if total == 0 {
44            return 0.0;
45        }
46        (self.redirected as f64 / total as f64) * 100.0
47    }
48
49    /// Merges stats from another instance.
50    pub fn merge(&mut self, other: &XdpStats) {
51        self.dropped += other.dropped;
52        self.passed += other.passed;
53        self.redirected += other.redirected;
54        self.invalid += other.invalid;
55        self.bytes_processed += other.bytes_processed;
56    }
57}
58
59impl std::fmt::Display for XdpStats {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(
62            f,
63            "XdpStats {{ dropped: {}, passed: {}, redirected: {}, invalid: {}, bytes: {} }}",
64            self.dropped, self.passed, self.redirected, self.invalid, self.bytes_processed
65        )
66    }
67}
68
69/// Atomic version of XDP stats for concurrent updates.
70#[derive(Debug, Default)]
71#[allow(dead_code)]
72pub(crate) struct AtomicXdpStats {
73    dropped: AtomicU64,
74    passed: AtomicU64,
75    redirected: AtomicU64,
76    invalid: AtomicU64,
77    bytes_processed: AtomicU64,
78}
79
80#[allow(dead_code)]
81impl AtomicXdpStats {
82    /// Creates new atomic stats.
83    #[must_use]
84    pub const fn new() -> Self {
85        Self {
86            dropped: AtomicU64::new(0),
87            passed: AtomicU64::new(0),
88            redirected: AtomicU64::new(0),
89            invalid: AtomicU64::new(0),
90            bytes_processed: AtomicU64::new(0),
91        }
92    }
93
94    /// Increments the dropped counter.
95    pub fn inc_dropped(&self) {
96        self.dropped.fetch_add(1, Ordering::Relaxed);
97    }
98
99    /// Increments the passed counter.
100    pub fn inc_passed(&self) {
101        self.passed.fetch_add(1, Ordering::Relaxed);
102    }
103
104    /// Increments the redirected counter.
105    pub fn inc_redirected(&self) {
106        self.redirected.fetch_add(1, Ordering::Relaxed);
107    }
108
109    /// Increments the invalid counter.
110    pub fn inc_invalid(&self) {
111        self.invalid.fetch_add(1, Ordering::Relaxed);
112    }
113
114    /// Adds to the bytes processed counter.
115    pub fn add_bytes(&self, bytes: u64) {
116        self.bytes_processed.fetch_add(bytes, Ordering::Relaxed);
117    }
118
119    /// Returns a snapshot of the current stats.
120    #[must_use]
121    pub fn snapshot(&self) -> XdpStats {
122        XdpStats {
123            dropped: self.dropped.load(Ordering::Relaxed),
124            passed: self.passed.load(Ordering::Relaxed),
125            redirected: self.redirected.load(Ordering::Relaxed),
126            invalid: self.invalid.load(Ordering::Relaxed),
127            bytes_processed: self.bytes_processed.load(Ordering::Relaxed),
128        }
129    }
130
131    /// Resets all counters to zero.
132    pub fn reset(&self) {
133        self.dropped.store(0, Ordering::Relaxed);
134        self.passed.store(0, Ordering::Relaxed);
135        self.redirected.store(0, Ordering::Relaxed);
136        self.invalid.store(0, Ordering::Relaxed);
137        self.bytes_processed.store(0, Ordering::Relaxed);
138    }
139}
140
141/// Per-CPU statistics.
142#[derive(Debug, Clone)]
143#[allow(dead_code)]
144pub(crate) struct PerCpuStats {
145    /// Stats per CPU.
146    pub cpu_stats: Vec<XdpStats>,
147}
148
149#[allow(dead_code)]
150impl PerCpuStats {
151    /// Creates new per-CPU stats for the given number of CPUs.
152    #[must_use]
153    pub fn new(num_cpus: usize) -> Self {
154        Self {
155            cpu_stats: vec![XdpStats::default(); num_cpus],
156        }
157    }
158
159    /// Returns aggregate stats across all CPUs.
160    #[must_use]
161    pub fn aggregate(&self) -> XdpStats {
162        let mut total = XdpStats::default();
163        for stats in &self.cpu_stats {
164            total.merge(stats);
165        }
166        total
167    }
168
169    /// Returns stats for a specific CPU.
170    #[must_use]
171    pub fn get(&self, cpu: usize) -> Option<&XdpStats> {
172        self.cpu_stats.get(cpu)
173    }
174
175    /// Returns the number of CPUs.
176    #[must_use]
177    pub fn num_cpus(&self) -> usize {
178        self.cpu_stats.len()
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_xdp_stats_default() {
188        let stats = XdpStats::default();
189        assert_eq!(stats.dropped, 0);
190        assert_eq!(stats.total_packets(), 0);
191    }
192
193    #[test]
194    fn test_xdp_stats_total() {
195        let stats = XdpStats {
196            dropped: 10,
197            passed: 20,
198            redirected: 30,
199            invalid: 5,
200            bytes_processed: 1000,
201        };
202        assert_eq!(stats.total_packets(), 65);
203    }
204
205    #[test]
206    fn test_xdp_stats_rates() {
207        let stats = XdpStats {
208            dropped: 25,
209            passed: 50,
210            redirected: 25,
211            invalid: 0,
212            bytes_processed: 0,
213        };
214        assert!((stats.drop_rate() - 25.0).abs() < 0.01);
215        assert!((stats.redirect_rate() - 25.0).abs() < 0.01);
216    }
217
218    #[test]
219    fn test_xdp_stats_rates_zero_total() {
220        let stats = XdpStats::default();
221        assert!(stats.drop_rate().abs() < f64::EPSILON);
222        assert!(stats.redirect_rate().abs() < f64::EPSILON);
223    }
224
225    #[test]
226    fn test_xdp_stats_merge() {
227        let mut stats1 = XdpStats {
228            dropped: 10,
229            passed: 20,
230            redirected: 30,
231            invalid: 5,
232            bytes_processed: 1000,
233        };
234        let stats2 = XdpStats {
235            dropped: 5,
236            passed: 10,
237            redirected: 15,
238            invalid: 2,
239            bytes_processed: 500,
240        };
241        stats1.merge(&stats2);
242
243        assert_eq!(stats1.dropped, 15);
244        assert_eq!(stats1.passed, 30);
245        assert_eq!(stats1.redirected, 45);
246        assert_eq!(stats1.invalid, 7);
247        assert_eq!(stats1.bytes_processed, 1500);
248    }
249
250    #[test]
251    fn test_xdp_stats_display() {
252        let stats = XdpStats {
253            dropped: 1,
254            passed: 2,
255            redirected: 3,
256            invalid: 4,
257            bytes_processed: 5,
258        };
259        let display = format!("{stats}");
260        assert!(display.contains("dropped: 1"));
261        assert!(display.contains("redirected: 3"));
262    }
263
264    #[test]
265    fn test_atomic_stats() {
266        let stats = AtomicXdpStats::new();
267
268        stats.inc_dropped();
269        stats.inc_dropped();
270        stats.inc_passed();
271        stats.inc_redirected();
272        stats.inc_invalid();
273        stats.add_bytes(100);
274
275        let snapshot = stats.snapshot();
276        assert_eq!(snapshot.dropped, 2);
277        assert_eq!(snapshot.passed, 1);
278        assert_eq!(snapshot.redirected, 1);
279        assert_eq!(snapshot.invalid, 1);
280        assert_eq!(snapshot.bytes_processed, 100);
281    }
282
283    #[test]
284    fn test_atomic_stats_reset() {
285        let stats = AtomicXdpStats::new();
286        stats.inc_dropped();
287        stats.reset();
288        assert_eq!(stats.snapshot().dropped, 0);
289    }
290
291    #[test]
292    fn test_per_cpu_stats() {
293        let mut per_cpu = PerCpuStats::new(4);
294
295        per_cpu.cpu_stats[0].dropped = 10;
296        per_cpu.cpu_stats[1].dropped = 20;
297        per_cpu.cpu_stats[2].redirected = 30;
298        per_cpu.cpu_stats[3].passed = 40;
299
300        let aggregate = per_cpu.aggregate();
301        assert_eq!(aggregate.dropped, 30);
302        assert_eq!(aggregate.redirected, 30);
303        assert_eq!(aggregate.passed, 40);
304
305        assert_eq!(per_cpu.get(0).unwrap().dropped, 10);
306        assert!(per_cpu.get(10).is_none());
307        assert_eq!(per_cpu.num_cpus(), 4);
308    }
309}