prns_runtime/manifold/
throughput.rs1use crate::engine::InstantMillis;
4use crate::interfaces::TransferRates;
5
6const CONSIDERED_IDLE_AFTER_MS: u64 = 2_000;
7const SAMPLE_WINDOW: usize = 8;
8
9struct ActiveRate {
10 last_ms: u64,
11 seen: bool,
12 samples: [u32; SAMPLE_WINDOW],
13 filled: usize,
15 head: usize,
17}
18
19impl ActiveRate {
20 const fn new() -> Self {
21 Self {
22 last_ms: 0,
23 seen: false,
24 samples: [0; SAMPLE_WINDOW],
25 filled: 0,
26 head: 0,
27 }
28 }
29
30 fn record(&mut self, now: InstantMillis, bytes: u64) {
31 let now = now.0;
32 let dt = if self.seen {
33 now.saturating_sub(self.last_ms)
34 .clamp(1, CONSIDERED_IDLE_AFTER_MS)
35 } else {
36 CONSIDERED_IDLE_AFTER_MS
37 };
38 let sample = u32::try_from(bytes.saturating_mul(8_000) / dt).unwrap_or(u32::MAX);
39 self.samples[self.head] = sample;
40 self.head = (self.head + 1) % SAMPLE_WINDOW;
41 self.filled = (self.filled + 1).min(SAMPLE_WINDOW);
42 self.seen = true;
43 self.last_ms = now;
44 }
45
46 fn rate(&self) -> u32 {
48 if self.filled == 0 {
49 return 0;
50 }
51 let sum: u64 = self.samples[..self.filled]
52 .iter()
53 .map(|&sample| u64::from(sample))
54 .sum();
55 u32::try_from(sum / self.filled as u64).unwrap_or(u32::MAX)
56 }
57}
58
59pub struct ThroughputLedger {
60 rx: ActiveRate,
61 tx: ActiveRate,
62}
63
64impl Default for ThroughputLedger {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl ThroughputLedger {
71 #[must_use]
72 pub const fn new() -> Self {
73 Self {
74 rx: ActiveRate::new(),
75 tx: ActiveRate::new(),
76 }
77 }
78
79 pub fn record_rx(&mut self, now: InstantMillis, bytes: u64) {
80 self.rx.record(now, bytes);
81 }
82
83 pub fn record_tx(&mut self, now: InstantMillis, bytes: u64) {
84 self.tx.record(now, bytes);
85 }
86
87 pub fn rates(&self) -> TransferRates {
88 TransferRates {
89 rx_bps: self.rx.rate(),
90 tx_bps: self.tx.rate(),
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn rate_is_the_active_transfer_rate_not_a_wall_clock_average() {
101 let mut ledger = ThroughputLedger::new();
102 ledger.record_rx(InstantMillis(0), 1_000);
103 ledger.record_rx(InstantMillis(100), 1_000);
104 assert_eq!(ledger.rates().rx_bps, 42_000);
105 }
106
107 #[test]
108 fn a_quick_burst_reads_its_real_rate_not_diluted_by_surrounding_idle() {
109 let mut ledger = ThroughputLedger::new();
110 let mut t = 1_000;
111 ledger.record_tx(InstantMillis(t), 1_500);
112 for _ in 0..9 {
113 t += 10;
114 ledger.record_tx(InstantMillis(t), 1_500);
115 }
116 assert_eq!(ledger.rates().tx_bps, 1_200_000);
117 }
118
119 #[test]
120 fn the_rate_is_the_mean_of_the_recent_samples() {
121 let mut ledger = ThroughputLedger::new();
122 ledger.record_rx(InstantMillis(0), 1_000);
123 ledger.record_rx(InstantMillis(100), 1_000);
124 ledger.record_rx(InstantMillis(300), 1_000);
125 assert_eq!(ledger.rates().rx_bps, 41_333);
126 }
127
128 #[test]
129 fn the_rate_is_held_between_bursts_not_dropped_to_zero() {
130 let mut ledger = ThroughputLedger::new();
131 ledger.record_rx(InstantMillis(0), 1_000);
132 ledger.record_rx(InstantMillis(100), 1_000);
133 assert_eq!(ledger.rates().rx_bps, 42_000);
134 }
135
136 #[test]
137 fn an_event_after_an_idle_gap_samples_the_floor_not_the_smear() {
138 let mut ledger = ThroughputLedger::new();
139 ledger.record_tx(InstantMillis(0), 1_000);
140 ledger.record_tx(InstantMillis(100), 1_000);
141 ledger.record_tx(InstantMillis(5_000), 1_000);
142 assert_eq!(ledger.rates().tx_bps, 29_333);
143 ledger.record_tx(InstantMillis(5_100), 1_000);
144 assert_eq!(ledger.rates().tx_bps, 42_000);
145 }
146
147 #[test]
148 fn sparse_lone_frames_read_a_nonzero_floor() {
149 let mut ledger = ThroughputLedger::new();
150 ledger.record_tx(InstantMillis(1_000), 167);
151 ledger.record_tx(InstantMillis(181_000), 167);
152 ledger.record_tx(InstantMillis(361_000), 167);
153 assert_eq!(ledger.rates().tx_bps, 668);
154 assert_eq!(ledger.rates().rx_bps, 0, "nothing was ever received");
155 }
156}