autd3_rs_core/link/
stats.rs1use 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}