umsh_ulcp/
stats.rs

1//! Traffic counters for the `PROP_STAT_*` properties.
2//!
3//! [`StatsLedger`] is the one place a device tallies what its radio did.
4//! It sits in a `static` and is written by whichever parts of the stack
5//! are in a position to see each event exactly once — on a firmware that
6//! is the radio multiplexer for frames on the air, the PHY runner for
7//! receptions the demodulator rejected, and the device node's pump for
8//! what the MAC decided — and it is read by the ULCP session, by the
9//! device's own display, and by anything else that wants the same
10//! numbers rather than its own.
11//!
12//! # Every producer adds
13//!
14//! Each counter is a pair of `u32`s: a `raw` tally that only ever grows
15//! (wrapping, never resetting) and a `base` that a host's write moves.
16//! What a host reads is `raw - base`, and clearing a counter is
17//! `base = raw`.
18//!
19//! Zeroing `raw` instead would be wrong twice over. Two of the producers
20//! mirror tallies the ledger does not own — the MAC's counters and the
21//! PHY's — so a zeroed cell would be overwritten by the next mirror pass
22//! and the clear would visibly bounce back; and a cell another task is
23//! concurrently `fetch_add`-ing cannot be zeroed without losing whatever
24//! landed in between. Mirroring producers therefore feed the *difference*
25//! since their last pass, which makes every write to the ledger an
26//! addition and leaves the base as the only thing a reset touches.
27//!
28//! Counters wrap rather than saturate. Wrapping arithmetic is what makes
29//! `raw - base` right across the boundary; a saturating counter would
30//! pin at the maximum and never move again, which is worse than starting
31//! over.
32
33use core::sync::atomic::{AtomicU32, Ordering};
34
35use crate::ids::prop;
36
37/// The counters a device keeps.
38///
39/// The discriminants are the ledger's slot indices and are not on the
40/// wire; [`Counter::property`] gives the identifier that is.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum Counter {
43    /// Frames that reached the air, from any client of the radio.
44    TxPackets,
45    /// Transmit attempts the channel-activity check held back.
46    TxChannelBusy,
47    /// Receptions off the air that are UMSH packets.
48    RxPackets,
49    /// Receptions the radio rejected on CRC.
50    RxBadCrc,
51    /// Receptions that passed CRC and are not UMSH packets.
52    RxNonUmsh,
53    /// Receptions the device's own node acted on.
54    RxAccepted,
55    /// Receptions this node chose to repeat.
56    Forwarded,
57    /// Receptions declined under the operator's forwarding policy.
58    ForwardDropped,
59    /// Queued repeats dropped after overhearing the destination's ack.
60    ForwardCancelled,
61}
62
63/// How many counters a ledger holds.
64pub const COUNTERS: usize = 9;
65
66impl Counter {
67    /// Every counter, in identifier order.
68    pub const ALL: [Counter; COUNTERS] = [
69        Counter::TxPackets,
70        Counter::TxChannelBusy,
71        Counter::RxPackets,
72        Counter::RxBadCrc,
73        Counter::RxNonUmsh,
74        Counter::RxAccepted,
75        Counter::Forwarded,
76        Counter::ForwardDropped,
77        Counter::ForwardCancelled,
78    ];
79
80    /// The property identifier this counter is read and cleared through.
81    pub const fn property(self) -> u32 {
82        match self {
83            Counter::TxPackets => prop::STAT_TX_PACKETS,
84            Counter::TxChannelBusy => prop::STAT_TX_CHANNEL_BUSY,
85            Counter::RxPackets => prop::STAT_RX_PACKETS,
86            Counter::RxBadCrc => prop::STAT_RX_BAD_CRC,
87            Counter::RxNonUmsh => prop::STAT_RX_NON_UMSH,
88            Counter::RxAccepted => prop::STAT_RX_ACCEPTED,
89            Counter::Forwarded => prop::STAT_FORWARDED,
90            Counter::ForwardDropped => prop::STAT_FORWARD_DROPPED,
91            Counter::ForwardCancelled => prop::STAT_FORWARD_CANCELLED,
92        }
93    }
94
95    /// The counter a property identifier names, if it names one.
96    pub const fn from_property(key: u32) -> Option<Counter> {
97        Some(match key {
98            prop::STAT_TX_PACKETS => Counter::TxPackets,
99            prop::STAT_TX_CHANNEL_BUSY => Counter::TxChannelBusy,
100            prop::STAT_RX_PACKETS => Counter::RxPackets,
101            prop::STAT_RX_BAD_CRC => Counter::RxBadCrc,
102            prop::STAT_RX_NON_UMSH => Counter::RxNonUmsh,
103            prop::STAT_RX_ACCEPTED => Counter::RxAccepted,
104            prop::STAT_FORWARDED => Counter::Forwarded,
105            prop::STAT_FORWARD_DROPPED => Counter::ForwardDropped,
106            prop::STAT_FORWARD_CANCELLED => Counter::ForwardCancelled,
107            _ => return None,
108        })
109    }
110
111    /// Whether this counter can only be answered by a device running a
112    /// node of its own.
113    ///
114    /// The four that come from the MAC. A session with nothing behind it
115    /// does not have these properties at all, rather than reporting a
116    /// zero that reads as "this repeater has never repeated anything".
117    pub const fn needs_node(self) -> bool {
118        matches!(
119            self,
120            Counter::RxAccepted
121                | Counter::Forwarded
122                | Counter::ForwardDropped
123                | Counter::ForwardCancelled
124        )
125    }
126}
127
128/// One counter: what has happened, and where the host last started
129/// counting from.
130struct Cell {
131    raw: AtomicU32,
132    base: AtomicU32,
133}
134
135impl Cell {
136    const fn new() -> Self {
137        Self {
138            raw: AtomicU32::new(0),
139            base: AtomicU32::new(0),
140        }
141    }
142}
143
144/// The shared traffic ledger.
145///
146/// Lock-free and `const`-constructible, so a board declares one `static`
147/// and hands out `&'static` references to every producer and reader.
148/// Ordering is `Relaxed` throughout: each counter is independent, no
149/// reader is deciding anything from the ordering between two of them,
150/// and the alternative buys a fence on the radio's hot path to make
151/// diagnostics agree about an instant nobody is looking at.
152pub struct StatsLedger {
153    cells: [Cell; COUNTERS],
154}
155
156impl StatsLedger {
157    pub const fn new() -> Self {
158        Self {
159            cells: [const { Cell::new() }; COUNTERS],
160        }
161    }
162
163    /// Record one event.
164    pub fn bump(&self, counter: Counter) {
165        self.add(counter, 1);
166    }
167
168    /// Record `n` events. Adding zero is free and is the ordinary case
169    /// for a mirroring producer whose source has not moved.
170    pub fn add(&self, counter: Counter, n: u32) {
171        if n != 0 {
172            self.cells[counter as usize]
173                .raw
174                .fetch_add(n, Ordering::Relaxed);
175        }
176    }
177
178    /// What a host reads: events since boot, or since it last cleared
179    /// this counter.
180    pub fn get(&self, counter: Counter) -> u32 {
181        let cell = &self.cells[counter as usize];
182        cell.raw
183            .load(Ordering::Relaxed)
184            .wrapping_sub(cell.base.load(Ordering::Relaxed))
185    }
186
187    /// The underlying tally, ignoring any clear. Nothing on the wire
188    /// reports this; it is here so a device that wants a since-boot view
189    /// of its own has one.
190    pub fn raw(&self, counter: Counter) -> u32 {
191        self.cells[counter as usize].raw.load(Ordering::Relaxed)
192    }
193
194    /// Start this counter over from here.
195    ///
196    /// Clearing several counters is several of these, so a frame landing
197    /// mid-sweep is counted before one and after another. Nothing reads
198    /// them closely enough for that to matter, and the alternative is a
199    /// lock on the radio's hot path.
200    pub fn reset(&self, counter: Counter) {
201        let cell = &self.cells[counter as usize];
202        cell.base
203            .store(cell.raw.load(Ordering::Relaxed), Ordering::Relaxed);
204    }
205}
206
207impl Default for StatsLedger {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213impl core::fmt::Debug for StatsLedger {
214    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
215        f.debug_struct("StatsLedger")
216            .field("tx_packets", &self.get(Counter::TxPackets))
217            .field("rx_packets", &self.get(Counter::RxPackets))
218            .finish_non_exhaustive()
219    }
220}
221
222/// A mirror of a monotone tally kept somewhere else.
223///
224/// Producers that copy someone else's counters — the MAC's, the PHY's —
225/// hold one of these per source field and feed the ledger the difference
226/// since the last pass, so the ledger only ever sees additions. A source
227/// that goes backwards (a counter reconstructed, a peripheral restarted)
228/// contributes nothing rather than a nonsensical jump.
229#[derive(Clone, Copy, Debug, Default)]
230pub struct Mirror {
231    last: u32,
232}
233
234impl Mirror {
235    pub const fn new() -> Self {
236        Self { last: 0 }
237    }
238
239    /// Fold the source's current value in, returning what was added.
240    pub fn advance(&mut self, ledger: &StatsLedger, counter: Counter, now: u32) -> u32 {
241        let delta = now.saturating_sub(self.last);
242        self.last = now;
243        ledger.add(counter, delta);
244        delta
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn properties_round_trip() {
254        for counter in Counter::ALL {
255            assert_eq!(Counter::from_property(counter.property()), Some(counter));
256        }
257        assert_eq!(Counter::from_property(prop::PHY_DUTY_NOW), None);
258    }
259
260    #[test]
261    fn counting_and_clearing() {
262        let ledger = StatsLedger::new();
263        assert_eq!(ledger.get(Counter::TxPackets), 0);
264
265        ledger.bump(Counter::TxPackets);
266        ledger.add(Counter::TxPackets, 4);
267        assert_eq!(ledger.get(Counter::TxPackets), 5);
268        // Counters are independent.
269        assert_eq!(ledger.get(Counter::RxPackets), 0);
270
271        ledger.reset(Counter::TxPackets);
272        assert_eq!(ledger.get(Counter::TxPackets), 0);
273        // The underlying tally is untouched, and counting resumes from
274        // the clear rather than from boot.
275        assert_eq!(ledger.raw(Counter::TxPackets), 5);
276        ledger.bump(Counter::TxPackets);
277        assert_eq!(ledger.get(Counter::TxPackets), 1);
278    }
279
280    #[test]
281    fn reported_value_survives_the_wrap() {
282        let ledger = StatsLedger::new();
283        ledger.add(Counter::RxPackets, u32::MAX - 1);
284        ledger.reset(Counter::RxPackets);
285        // Three more events take the raw tally across the boundary; the
286        // reported value is still three.
287        ledger.add(Counter::RxPackets, 3);
288        assert_eq!(ledger.raw(Counter::RxPackets), 1);
289        assert_eq!(ledger.get(Counter::RxPackets), 3);
290    }
291
292    #[test]
293    fn mirrors_feed_the_difference() {
294        let ledger = StatsLedger::new();
295        let mut mirror = Mirror::new();
296
297        // A source that is already nonzero when the mirror starts
298        // contributes its whole value once, and nothing again until it
299        // moves.
300        assert_eq!(mirror.advance(&ledger, Counter::Forwarded, 7), 7);
301        assert_eq!(mirror.advance(&ledger, Counter::Forwarded, 7), 0);
302        assert_eq!(mirror.advance(&ledger, Counter::Forwarded, 9), 2);
303        assert_eq!(ledger.get(Counter::Forwarded), 9);
304
305        // A clear holds even though the source keeps climbing.
306        ledger.reset(Counter::Forwarded);
307        mirror.advance(&ledger, Counter::Forwarded, 9);
308        assert_eq!(ledger.get(Counter::Forwarded), 0);
309        mirror.advance(&ledger, Counter::Forwarded, 11);
310        assert_eq!(ledger.get(Counter::Forwarded), 2);
311
312        // A source that goes backwards contributes nothing rather than
313        // dragging the ledger with it.
314        mirror.advance(&ledger, Counter::Forwarded, 0);
315        assert_eq!(ledger.get(Counter::Forwarded), 2);
316        mirror.advance(&ledger, Counter::Forwarded, 1);
317        assert_eq!(ledger.get(Counter::Forwarded), 3);
318    }
319
320    #[test]
321    fn only_the_mac_sourced_counters_need_a_node() {
322        let needs: Vec<Counter> = Counter::ALL
323            .into_iter()
324            .filter(|c| c.needs_node())
325            .collect();
326        assert_eq!(
327            needs,
328            [
329                Counter::RxAccepted,
330                Counter::Forwarded,
331                Counter::ForwardDropped,
332                Counter::ForwardCancelled
333            ]
334        );
335    }
336}