Skip to main content

autd3_rs_core/link/
stats.rs

1use std::sync::Arc;
2use std::sync::atomic::{AtomicU64, Ordering};
3
4#[derive(Debug, Clone, Default)]
5pub struct LinkStats {
6    stale_cycles: Arc<AtomicU64>,
7    lost_cycles: Arc<AtomicU64>,
8    phase_excursions: Arc<AtomicU64>,
9    worst_phase_deviation_ns: Arc<AtomicU64>,
10    exchanges: Arc<AtomicU64>,
11    exchange_ns_total: Arc<AtomicU64>,
12    worst_exchange_ns: Arc<AtomicU64>,
13}
14
15impl LinkStats {
16    #[must_use]
17    pub fn stale_cycles(&self) -> u64 {
18        self.stale_cycles.load(Ordering::Acquire)
19    }
20
21    #[must_use]
22    pub fn lost_cycles(&self) -> u64 {
23        self.lost_cycles.load(Ordering::Acquire)
24    }
25
26    #[must_use]
27    pub fn phase_excursions(&self) -> u64 {
28        self.phase_excursions.load(Ordering::Acquire)
29    }
30
31    #[must_use]
32    pub fn worst_phase_deviation_ns(&self) -> u64 {
33        self.worst_phase_deviation_ns.load(Ordering::Acquire)
34    }
35
36    #[must_use]
37    pub fn exchanges(&self) -> u64 {
38        self.exchanges.load(Ordering::Acquire)
39    }
40
41    #[must_use]
42    pub fn worst_exchange_ns(&self) -> u64 {
43        self.worst_exchange_ns.load(Ordering::Acquire)
44    }
45
46    #[must_use]
47    pub fn mean_exchange_ns(&self) -> u64 {
48        let total = self.exchange_ns_total.load(Ordering::Acquire);
49        let exchanges = self.exchanges.load(Ordering::Acquire);
50        if exchanges == 0 {
51            return 0;
52        }
53        total / exchanges
54    }
55
56    pub fn record_exchange(&self, elapsed_ns: u64) {
57        self.worst_exchange_ns
58            .fetch_max(elapsed_ns, Ordering::Relaxed);
59        self.exchanges.fetch_add(1, Ordering::Relaxed);
60        self.exchange_ns_total
61            .fetch_add(elapsed_ns, Ordering::Release);
62    }
63
64    pub fn record_stale_cycle(&self) {
65        self.stale_cycles.fetch_add(1, Ordering::Relaxed);
66    }
67
68    pub fn record_lost_cycle(&self) {
69        self.stale_cycles.fetch_add(1, Ordering::Relaxed);
70        self.lost_cycles.fetch_add(1, Ordering::Release);
71    }
72
73    pub fn record_phase_excursion(&self, deviation_ns: u64) {
74        self.phase_excursions.fetch_add(1, Ordering::Relaxed);
75        self.worst_phase_deviation_ns
76            .fetch_max(deviation_ns, Ordering::Relaxed);
77    }
78
79    pub fn add_stale_cycles(&self, count: u64) {
80        self.stale_cycles.fetch_add(count, Ordering::Relaxed);
81    }
82
83    pub fn add_lost_cycles(&self, count: u64) {
84        self.lost_cycles.fetch_add(count, Ordering::Relaxed);
85    }
86
87    pub fn add_phase_excursions(&self, count: u64, worst_deviation_ns: u64) {
88        self.phase_excursions.fetch_add(count, Ordering::Relaxed);
89        self.worst_phase_deviation_ns
90            .fetch_max(worst_deviation_ns, Ordering::Relaxed);
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use std::sync::atomic::AtomicBool;
97
98    use super::*;
99
100    #[test]
101    fn link_stats_counters() {
102        let stats = LinkStats::default();
103        let observer = stats.clone();
104        stats.record_stale_cycle();
105        stats.record_lost_cycle();
106        assert_eq!(observer.stale_cycles(), 2);
107        assert_eq!(observer.lost_cycles(), 1);
108    }
109
110    #[test]
111    fn exchange_times_keep_the_mean_and_the_worst() {
112        let stats = LinkStats::default();
113        let observer = stats.clone();
114        assert_eq!(observer.mean_exchange_ns(), 0, "no division by zero");
115        stats.record_exchange(100_000);
116        stats.record_exchange(300_000);
117        stats.record_exchange(200_000);
118        assert_eq!(observer.exchanges(), 3);
119        assert_eq!(observer.mean_exchange_ns(), 200_000);
120        assert_eq!(observer.worst_exchange_ns(), 300_000);
121    }
122
123    #[test]
124    fn phase_excursions_keep_the_worst_deviation() {
125        let stats = LinkStats::default();
126        let observer = stats.clone();
127        stats.record_phase_excursion(1_000);
128        stats.record_phase_excursion(300);
129        stats.record_phase_excursion(2_500);
130        assert_eq!(observer.phase_excursions(), 3);
131        assert_eq!(observer.worst_phase_deviation_ns(), 2_500);
132    }
133
134    #[test]
135    fn counters_can_be_advanced_in_bulk() {
136        let stats = LinkStats::default();
137        stats.add_stale_cycles(5);
138        stats.add_lost_cycles(2);
139        stats.add_phase_excursions(7, 900);
140        stats.add_phase_excursions(1, 100);
141        assert_eq!(stats.stale_cycles(), 5);
142        assert_eq!(stats.lost_cycles(), 2);
143        assert_eq!(stats.phase_excursions(), 8);
144        assert_eq!(stats.worst_phase_deviation_ns(), 900);
145    }
146
147    #[test]
148    fn the_mean_never_exceeds_the_worst_sample_while_a_writer_is_running() {
149        const SAMPLE_NS: u64 = 1_000_000_000;
150
151        let stats = LinkStats::default();
152        let observer = stats.clone();
153        let stop = Arc::new(AtomicBool::new(false));
154        let writer_stop = stop.clone();
155        let writer = std::thread::spawn(move || {
156            while !writer_stop.load(Ordering::Acquire) {
157                stats.record_exchange(SAMPLE_NS);
158            }
159        });
160        for _ in 0..100_000 {
161            let mean = observer.mean_exchange_ns();
162            assert!(
163                mean <= SAMPLE_NS,
164                "the mean must stay within the samples, got {mean}"
165            );
166        }
167        stop.store(true, Ordering::Release);
168        writer.join().unwrap();
169    }
170}