Skip to content
File

Blob: firmware/vendor/str0m/src/bwe/alr_detector/detector.rs

rust345 lines
1use std::time::Instant;
2 
3use super::budget::IntervalBudget;
4use crate::rtp_::{Bitrate, DataSize};
5 
6/// Application Limited Region detector.
7///
8/// Detects when we're sending significantly less than network capacity,
9/// using IntervalBudget to handle bursty encoder traffic over 500ms windows.
10///
11/// This is critical for real-world usage where encoders produce bursty traffic
12/// (keyframes, scene changes, temporal layers, etc.) The IntervalBudget smooths
13/// these bursts over a 500ms window to detect sustained application-limited state.
14///
15/// ## ALR State Transitions
16///
17/// - **Enter ALR**: When budget ratio > 0.80 (sustained low usage for 500ms)
18/// - **Exit ALR**: When budget ratio < 0.50 (increased usage)
19/// - The 30% hysteresis gap prevents rapid state flapping
20#[derive(Debug)]
21pub struct AlrDetector {
22 /// Budget tracker with 500ms window
23 budget: IntervalBudget,
24 /// Current ALR state
25 state: AlrState,
26 /// Last time we sent bytes
27 last_send_time: Option<Instant>,
28 
29 // Configuration matching libWebRTC defaults
30 /// Target send rate as fraction of estimate (0.65 = 65%)
31 bandwidth_usage_ratio: f64,
32 /// Budget ratio threshold to enter ALR (0.80 = 80%)
33 start_budget_level_ratio: f64,
34 /// Budget ratio threshold to exit ALR (0.50 = 50%)
35 stop_budget_level_ratio: f64,
36}
37 
38/// ALR state for the detector.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40enum AlrState {
41 /// Not currently in Application Limited Region
42 NotInAlr,
43 /// In Application Limited Region since the given instant
44 InAlr(Instant),
45}
46 
47impl AlrState {
48 fn alr_start_time(&self) -> Option<Instant> {
49 match self {
50 AlrState::InAlr(start) => Some(*start),
51 AlrState::NotInAlr => None,
52 }
53 }
54}
55 
56impl AlrDetector {
57 /// Create a new ALR detector with libWebRTC default configuration.
58 pub fn new() -> Self {
59 Self {
60 // Start with zero bitrate, will be updated via set_estimated_bitrate()
61 // Use can_build_up_underuse=true for ALR detection
62 budget: IntervalBudget::new(Bitrate::ZERO, true),
63 state: AlrState::NotInAlr,
64 last_send_time: None,
65 
66 // libWebRTC defaults from alr_detector.cc:30-49
67 bandwidth_usage_ratio: 0.65,
68 start_budget_level_ratio: 0.80,
69 stop_budget_level_ratio: 0.50,
70 }
71 }
72 
73 /// Update with bytes sent.
74 ///
75 /// Should be called for every media packet sent (excluding padding and probes).
76 /// This updates the IntervalBudget and checks for ALR state transitions.
77 pub fn on_bytes_sent(&mut self, bytes: DataSize, now: Instant) {
78 // First call - just record time
79 let Some(last) = self.last_send_time else {
80 self.last_send_time = Some(now);
81 return;
82 };
83 
84 // saturating_duration_since protects against time going backwards
85 let delta = now.saturating_duration_since(last);
86 self.last_send_time = Some(now);
87 
88 // Update budget based on time and usage
89 self.budget.use_budget(bytes);
90 self.budget.increase_budget(delta);
91 
92 // Check for state transitions with hysteresis
93 let ratio = self.budget.budget_ratio();
94 
95 match self.state {
96 AlrState::NotInAlr => {
97 if ratio > self.start_budget_level_ratio {
98 // Enter ALR: budget accumulated because we're not sending much
99 self.state = AlrState::InAlr(now);
100 trace!(
101 "ALR: Entered ALR state (ratio={:.3} > threshold={:.3})",
102 ratio, self.start_budget_level_ratio
103 );
104 }
105 }
106 AlrState::InAlr(_) => {
107 if ratio < self.stop_budget_level_ratio {
108 // Exit ALR: budget depleted because we're sending more
109 self.state = AlrState::NotInAlr;
110 trace!(
111 "ALR: Exited ALR state (ratio={:.3} < threshold={:.3})",
112 ratio, self.stop_budget_level_ratio
113 );
114 }
115 }
116 }
117 }
118 
119 /// Update the BWE estimate.
120 ///
121 /// This adjusts the IntervalBudget's target rate to 65% of the estimate.
122 /// Should be called whenever the bandwidth estimate changes.
123 pub fn set_estimated_bitrate(&mut self, estimate: Bitrate) {
124 let target = estimate * self.bandwidth_usage_ratio;
125 self.budget.set_target_rate(target);
126 }
127 
128 /// Get the time when ALR started, if currently in ALR.
129 ///
130 /// Returns `Some(Instant)` if in ALR, `None` otherwise.
131 /// Used by ProbeControl to determine if periodic ALR probes should be sent.
132 pub fn alr_start_time(&self) -> Option<Instant> {
133 self.state.alr_start_time()
134 }
135 
136 #[cfg(test)]
137 pub fn budget_ratio(&self) -> f64 {
138 self.budget.budget_ratio()
139 }
140}
141 
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use std::time::Duration;
146 
147 #[test]
148 fn test_alr_not_detected_initially() {
149 let alr = AlrDetector::new();
150 assert!(alr.alr_start_time().is_none());
151 }
152 
153 #[test]
154 fn test_alr_enter_with_low_usage() {
155 let mut alr = AlrDetector::new();
156 let now = Instant::now();
157 
158 // Set estimate to 1 Mbps (target will be 650 kbps)
159 alr.set_estimated_bitrate(Bitrate::mbps(1));
160 
161 // Send very little data (way below target)
162 // Over 500ms, we should accumulate budget
163 for i in 0..50 {
164 let t = now + Duration::from_millis(i * 10);
165 // Send 100 bytes every 10ms = 80 kbps (way below 650 kbps target)
166 alr.on_bytes_sent(DataSize::bytes(100), t);
167 }
168 
169 // Should enter ALR
170 assert!(alr.alr_start_time().is_some());
171 assert!(alr.budget_ratio() > 0.80);
172 }
173 
174 #[test]
175 fn test_alr_exit_with_high_usage() {
176 let mut alr = AlrDetector::new();
177 let now = Instant::now();
178 
179 alr.set_estimated_bitrate(Bitrate::mbps(1));
180 
181 // Enter ALR with low usage
182 for i in 0..50 {
183 let t = now + Duration::from_millis(i * 10);
184 alr.on_bytes_sent(DataSize::bytes(100), t);
185 }
186 assert!(alr.alr_start_time().is_some());
187 
188 // Now send at high rate to exit ALR
189 let start_exit = now + Duration::from_millis(500);
190 for i in 0..50 {
191 let t = start_exit + Duration::from_millis(i * 10);
192 // Send 10,000 bytes every 10ms = 8 Mbps (way above 650 kbps target)
193 alr.on_bytes_sent(DataSize::bytes(10_000), t);
194 }
195 
196 // Should exit ALR
197 assert!(alr.alr_start_time().is_none());
198 assert!(alr.budget_ratio() < 0.50);
199 }
200 
201 #[test]
202 fn test_alr_hysteresis() {
203 let mut alr = AlrDetector::new();
204 let now = Instant::now();
205 
206 alr.set_estimated_bitrate(Bitrate::mbps(1));
207 
208 // Send at exactly 65% (the target) - should not enter ALR
209 for i in 0..100 {
210 let t = now + Duration::from_millis(i * 10);
211 // 65% of 1 Mbps = 650 kbps = 812.5 bytes per 10ms
212 alr.on_bytes_sent(DataSize::bytes(812), t);
213 }
214 
215 // Budget ratio should be near 0, not trigger ALR entry (needs > 0.80)
216 assert!(alr.alr_start_time().is_none());
217 let ratio = alr.budget_ratio();
218 assert!(ratio < 0.80, "ratio={}", ratio);
219 assert!(ratio > -0.20, "ratio={}", ratio); // Some tolerance
220 }
221 
222 #[test]
223 fn test_alr_handles_bursts() {
224 let mut alr = AlrDetector::new();
225 let now = Instant::now();
226 
227 alr.set_estimated_bitrate(Bitrate::mbps(1));
228 
229 // Simulate bursty encoder: alternate between large and small frames
230 let mut time = now;
231 for i in 0..100 {
232 time = time + Duration::from_millis(33); // ~30fps
233 
234 if i % 10 == 0 {
235 // Keyframe: 10x normal size
236 alr.on_bytes_sent(DataSize::bytes(50_000), time);
237 } else {
238 // Normal frame: small
239 alr.on_bytes_sent(DataSize::bytes(5_000), time);
240 }
241 }
242 
243 // Average is about 9.5 KB per frame = 285 KB/s = 2.28 Mbps
244 // This is above the 650 kbps target, so should NOT be in ALR
245 // The IntervalBudget's 500ms window should smooth out the bursts
246 assert!(alr.alr_start_time().is_none());
247 }
248 
249 #[test]
250 fn test_alr_estimate_change() {
251 let mut alr = AlrDetector::new();
252 let now = Instant::now();
253 
254 // Start with 1 Mbps estimate (target = 650 kbps)
255 alr.set_estimated_bitrate(Bitrate::mbps(1));
256 
257 // Send at 200 kbps (way below 650 kbps target) to accumulate budget quickly
258 for i in 0..100 {
259 let t = now + Duration::from_millis(i * 10);
260 alr.on_bytes_sent(DataSize::bytes(250), t); // 200 kbps
261 }
262 
263 // Should enter ALR after sending well below target
264 assert!(alr.alr_start_time().is_some());
265 
266 // Now BWE drops to 300 kbps (target becomes 195 kbps)
267 alr.set_estimated_bitrate(Bitrate::kbps(300));
268 
269 // Continue sending at 250 kbps - now above target (195 kbps)
270 let continue_time = now + Duration::from_millis(1000);
271 for i in 0..100 {
272 let t = continue_time + Duration::from_millis(i * 10);
273 alr.on_bytes_sent(DataSize::bytes(312), t); // 250 kbps
274 }
275 
276 // Should exit ALR (now sending above target)
277 assert!(alr.alr_start_time().is_none());
278 }
279 
280 #[test]
281 fn test_alr_should_not_trigger_when_sending_at_target_rate() {
282 // This test verifies that ALR doesn't incorrectly trigger when sending
283 // at the target rate, even with small time advances between packets.
284 let mut alr = AlrDetector::new();
285 let now = Instant::now();
286 
287 // Set estimate to 8.25 Mbps (target will be 5.36 Mbps at 65%)
288 let estimate = Bitrate::kbps(8250);
289 alr.set_estimated_bitrate(estimate);
290 
291 // Send at exactly 8.25 Mbps with realistic packet timing
292 // Packet: 1150 bytes
293 // Interval: 1150 bytes × 8 bits / 8,250,000 bps = 1.115ms
294 let packet_size = DataSize::bytes(1150);
295 let packet_interval = Duration::from_micros(1115); // 1.115ms
296 
297 // Send 100 packets over ~111ms
298 for i in 0..100 {
299 let t = now + packet_interval * i;
300 alr.on_bytes_sent(packet_size, t);
301 }
302 
303 // Should NOT be in ALR - we're sending at 8.25 Mbps, which is above
304 // the target threshold of 5.36 Mbps (65% of estimate)
305 assert!(
306 alr.alr_start_time().is_none(),
307 "ALR should not trigger when sending at target rate. Budget ratio: {:.3}",
308 alr.budget_ratio()
309 );
310 }
311 
312 #[test]
313 fn test_alr_handles_spurious_small_time_advances() {
314 // This test verifies that ALR works correctly even when time advances
315 // in very small increments (like the test harness's forced 0.2ms advances).
316 let mut alr = AlrDetector::new();
317 let now = Instant::now();
318 
319 // Set estimate to 8.25 Mbps
320 let estimate = Bitrate::kbps(8250);
321 alr.set_estimated_bitrate(estimate);
322 
323 // Simulate sending 1150 byte packets at 8.25 Mbps
324 // But with spurious 0.2ms time advances between on_bytes_sent calls
325 let packet_size = DataSize::bytes(1150);
326 let spurious_advance = Duration::from_micros(200); // 0.2ms
327 
328 let mut time = now;
329 // Send packets - but accumulate proper time even if called frequently
330 for _ in 0..100 {
331 alr.on_bytes_sent(packet_size, time);
332 time += spurious_advance; // Small spurious advance
333 }
334 
335 // With 100 packets over 20ms (100 × 0.2ms), we've sent:
336 // 115,000 bytes in 20ms = 46 Mbps
337 // This is WAY above target, so ALR should NOT trigger
338 assert!(
339 alr.alr_start_time().is_none(),
340 "ALR should not trigger with spurious small advances when sending fast. Budget ratio: {:.3}",
341 alr.budget_ratio()
342 );
343 }
344}