Skip to content
File

Blob: firmware/vendor/str0m/src/bwe/loss_controller.rs

rust1552 lines
1use std::cmp::max;
2use std::cmp::min;
3use std::time::{Duration, Instant};
4 
5use super::macros::log_inherent_loss;
6use super::macros::log_loss_based_bitrate_estimate;
7use super::macros::log_loss_bw_limit_in_window;
8use crate::rtp_::TwccSendRecord;
9use crate::{Bitrate, DataSize};
10 
11use super::time::{TimeDelta, Timestamp};
12 
13/// Loss controller based on libWebRTC's `LossBasedBweV2`.
14///
15/// ## Overview
16///
17/// The estimator attempts to estimate the inherent loss of the link using Maximum Likelihood
18/// Estimation of an assumed Bernoulli distribution. This allows it to distinguish congestion
19/// induced loss from this inherent loss.
20///
21/// The controller integrates with ALR (Application Limited Region) detection and link capacity
22/// tracking. When in ALR, it uses proven link capacity from successful ALR probes as an upper
23/// bound on estimates, preventing overestimation in application-limited scenarios. When
24/// transitioning into or out of ALR, the controller resets its observation window to avoid
25/// mixing traffic patterns from different network utilization regimes.
26///
27/// The estimate is bounded by the output of the delay-based estimator, meaning this controller
28/// can only reduce estimates (acting as a safety cap), not increase them.
29///
30///
31/// Ref:
32/// * https://webrtc.googlesource.com/src/+/refs/heads/main/modules/congestion_controller/goog_cc/loss_based_bwe_v2.cc
33/// * https://webrtc.googlesource.com/src/+/refs/heads/main/modules/congestion_controller/goog_cc/loss_based_bwe_v2.h
34pub struct LossController {
35 /// Configuration for the controller.
36 config: Config,
37 
38 /// The current state of the controller.
39 state: LossControllerState,
40 
41 /// Staging ground for observations while they are being constructed.
42 partial_observation: PartialObservation,
43 
44 /// The last packet sent in the most recent observation.
45 last_send_time_most_recent_observation: Timestamp,
46 
47 // Observation window
48 /// Forever growing counter of observations. Observation::id derives from this.
49 num_observations: u64,
50 /// Window of observations.
51 observations: Box<[Observation]>,
52 /// Temporal weights, used to weight observations by recency. Same size as `observations`.
53 temporal_weights: Box<[f64]>,
54 /// Upper bound temporal weights, used to weight observations by recency. Same size as `observations`.
55 instant_upper_bound_temporal_weights: Box<[f64]>,
56 
57 /// Precomputed instantaneous upper bound on bandwidth estimate.
58 cached_instant_upper_bound: Option<Bitrate>,
59 /// Last time we reduced the estimate.
60 last_time_estimate_reduced: Timestamp,
61 
62 /// When we started recovering after being loss limited last time.
63 /// While in this window the bandwidth estimate is bounded by `bandwidth_limit_in_current_window`.
64 recovering_after_loss_timestamp: Timestamp,
65 /// Upper bound on estimate while in recovery window.
66 bandwidth_limit_in_current_window: Bitrate,
67 
68 /// The current estimate
69 current_estimate: ChannelParameters,
70 
71 /// The min bitrate we will emit as an estimate.
72 min_bitrate: Bitrate,
73 /// The max bitrate we will emit as an estimate.
74 max_bitrate: Bitrate,
75 
76 /// The most recent acknowledged bitrate derived from TWCC.
77 acknowledged_bitrate: Bitrate,
78 
79 /// The most recent estimated bitrate from the delay based estimator.
80 delay_based_estimate: Bitrate,
81 
82 /// HOLD mechanism state - prevents immediate ramp-up after loss
83 last_hold_info: HoldInfo,
84 
85 /// ALR start time (None if not in ALR)
86 alr_start_time: Option<Instant>,
87 
88 /// Link capacity estimate from probes during ALR
89 link_capacity_estimate: Option<Bitrate>,
90 
91 /// Previous ALR state to detect transitions
92 was_in_alr: bool,
93}
94 
95/// State of the Loss Controller
96#[derive(Debug, PartialEq, Clone, Copy)]
97pub enum LossControllerState {
98 /// LossController is in increasing state
99 Increasing,
100 /// LossController is in decreasing state
101 Decreasing,
102 /// LossController is in relaying the estimate of the delay controller
103 DelayBased,
104}
105 
106pub trait PacketResult {
107 /// When the packet was sent
108 fn local_send_time(&self) -> Instant;
109 /// Size of the packet payload
110 fn size(&self) -> DataSize;
111 
112 /// Whether this packet was lost or not.
113 fn lost(&self) -> bool;
114}
115 
116impl LossController {
117 pub fn new() -> LossController {
118 let config = Config::default();
119 
120 let mut controller = LossController {
121 state: LossControllerState::DelayBased,
122 partial_observation: PartialObservation::new(),
123 last_send_time_most_recent_observation: Timestamp::DistantFuture,
124 observations: vec![Observation::DUMMY; config.observation_window_size]
125 .into_boxed_slice(),
126 num_observations: 0,
127 temporal_weights: vec![0_f64; config.observation_window_size].into_boxed_slice(),
128 instant_upper_bound_temporal_weights: vec![0_f64; config.observation_window_size]
129 .into_boxed_slice(),
130 cached_instant_upper_bound: None,
131 last_time_estimate_reduced: Timestamp::DistantPast,
132 recovering_after_loss_timestamp: Timestamp::DistantPast,
133 bandwidth_limit_in_current_window: Bitrate::MAX,
134 
135 current_estimate: ChannelParameters::new(config.initial_inherent_loss_estimate),
136 
137 min_bitrate: Bitrate::kbps(1),
138 max_bitrate: Bitrate::INFINITY,
139 
140 // review usage from here on after
141 acknowledged_bitrate: Bitrate::INFINITY,
142 delay_based_estimate: Bitrate::INFINITY,
143 
144 last_hold_info: HoldInfo::default(),
145 
146 alr_start_time: None,
147 link_capacity_estimate: None,
148 was_in_alr: false,
149 
150 config,
151 };
152 
153 // Initialize weights
154 {
155 let this = &mut controller;
156 for i in 0..this.config.observation_window_size {
157 let val = f64::powi(this.config.temporal_weight_factor, i as i32);
158 this.temporal_weights[i] = val;
159 let val = f64::powi(
160 this.config.instant_upper_bound_temporal_weight_factor,
161 i as i32,
162 );
163 this.instant_upper_bound_temporal_weights[i] = val;
164 }
165 };
166 
167 controller
168 }
169 
170 /// Override the current bandwidth estimate.
171 pub fn set_bandwidth_estimate(&mut self, bandwidth_estimate: Bitrate) {
172 self.current_estimate.loss_limited_bandwidth = bandwidth_estimate;
173 }
174 
175 /// Update the acknowledged bitrate based on TWCC feedback.
176 pub fn set_acknowledged_bitrate(&mut self, acknowledged_bitrate: Bitrate) {
177 self.acknowledged_bitrate = acknowledged_bitrate;
178 }
179 
180 /// Set ALR start time from the ALR detector.
181 pub fn set_alr_start_time(&mut self, alr_start: Option<Instant>) {
182 let was_in_alr = self.was_in_alr;
183 let is_in_alr = alr_start.is_some();
184 
185 // Detect ALR state transition
186 if was_in_alr != is_in_alr {
187 // Reset observations on ALR transition to avoid mixing
188 // ALR and non-ALR traffic in the same observation window
189 self.reset_observations();
190 trace!(
191 "LossController: ALR state changed (was: {}, now: {}), observations reset",
192 was_in_alr, is_in_alr
193 );
194 }
195 
196 self.alr_start_time = alr_start;
197 self.was_in_alr = is_in_alr;
198 }
199 
200 /// Set link capacity estimate from successful ALR probes.
201 pub fn set_link_capacity_estimate(&mut self, capacity: Option<Bitrate>) {
202 self.link_capacity_estimate = capacity;
203 }
204 
205 /// Check if currently in ALR state
206 fn is_in_alr(&self) -> bool {
207 self.alr_start_time.is_some()
208 }
209 
210 /// Reset all observations.
211 ///
212 /// Called when ALR state transitions to avoid mixing traffic patterns
213 /// from different network utilization regimes.
214 fn reset_observations(&mut self) {
215 self.partial_observation = PartialObservation::new();
216 self.last_send_time_most_recent_observation = Timestamp::DistantFuture;
217 
218 // Clear observation window
219 for observation in self.observations.iter_mut() {
220 *observation = Observation::DUMMY;
221 }
222 
223 // Reset cached values that depend on observations
224 self.cached_instant_upper_bound = None;
225 }
226 
227 /// Update the estimate using TWCC feedback from the network.
228 /// After this [`loss_based_result`] returns the latest estimate.
229 pub fn update_bandwidth_estimate(
230 &mut self,
231 packet_results: &[impl PacketResult],
232 delay_based_estimated: Bitrate,
233 ) {
234 self.delay_based_estimate = delay_based_estimated;
235 
236 if packet_results.is_empty() {
237 debug!("packet results is empty");
238 return;
239 }
240 
241 if !self.maybe_add_observation(packet_results) {
242 return;
243 }
244 
245 if !self.current_estimate.loss_limited_bandwidth.is_valid() {
246 warn!("estimator must be initialized before use");
247 return;
248 }
249 
250 let mut best_candidate = self.current_estimate;
251 let mut objective_max = f64::MIN;
252 
253 for candidate in self.get_candidates().iter_mut() {
254 self.newtons_method_update(candidate);
255 
256 let candidate_objective = self.get_objective(candidate);
257 if candidate_objective > objective_max {
258 objective_max = candidate_objective;
259 best_candidate = *candidate;
260 }
261 }
262 
263 if best_candidate.loss_limited_bandwidth < self.current_estimate.loss_limited_bandwidth {
264 self.last_time_estimate_reduced = self.last_send_time_most_recent_observation;
265 }
266 
267 // do not increase the estimate if the average loss is greater than current inherent loss
268 if self.average_reported_loss_ratio() > best_candidate.inherent_loss
269 && self
270 .config
271 .not_increase_if_inherent_loss_less_than_average_loss
272 && self.current_estimate.loss_limited_bandwidth < best_candidate.loss_limited_bandwidth
273 {
274 best_candidate.loss_limited_bandwidth = self.current_estimate.loss_limited_bandwidth;
275 }
276 
277 if self.is_bandwidth_limited_due_to_loss() {
278 // Bound the estimate increase if:
279 // 1. The estimate has been increased for less than
280 // `delayed_increase_window` ago, and
281 // 2. The best candidate is greater than bandwidth_limit_in_current_window.
282 
283 if self.recovering_after_loss_timestamp.is_exact()
284 && self.recovering_after_loss_timestamp + self.config.delayed_increase_window
285 > self.last_send_time_most_recent_observation
286 && best_candidate.loss_limited_bandwidth > self.bandwidth_limit_in_current_window
287 {
288 best_candidate.loss_limited_bandwidth = self.bandwidth_limit_in_current_window;
289 }
290 
291 let increase_when_loss_limited =
292 self.is_estimate_increasing_when_loss_limited(best_candidate);
293 
294 if increase_when_loss_limited && self.acknowledged_bitrate.is_valid() {
295 // Choose rampup factor based on whether we're in HOLD region (WebRTC lines 270-281)
296 let rampup_factor = if self.last_hold_info.rate.is_valid()
297 && self.acknowledged_bitrate
298 < self.last_hold_info.rate * self.config.bandwidth_rampup_hold_threshold
299 {
300 self.config.bandwidth_rampup_upper_bound_factor_in_hold // 1.2
301 } else {
302 self.config.bandwidth_rampup_upper_bound_factor // 1.5
303 };
304 
305 best_candidate.loss_limited_bandwidth =
306 self.current_estimate.loss_limited_bandwidth.max(
307 best_candidate
308 .loss_limited_bandwidth
309 .min(self.acknowledged_bitrate * rampup_factor),
310 );
311 
312 // WebRTC lines 282-290: Ensure at least 1 bps increase when transitioning from Decreasing
313 if self.state == LossControllerState::Decreasing
314 && best_candidate.loss_limited_bandwidth
315 == self.current_estimate.loss_limited_bandwidth
316 {
317 best_candidate.loss_limited_bandwidth =
318 self.current_estimate.loss_limited_bandwidth + Bitrate::bps(1);
319 }
320 }
321 }
322 
323 let loss_limited_bandwidth = best_candidate.loss_limited_bandwidth;
324 
325 // HOLD check (WebRTC lines 321-334): If in Decreasing state and HOLD timer active, cap at HOLD rate
326 if self.state == LossControllerState::Decreasing
327 && self.last_hold_info.timestamp > self.last_send_time_most_recent_observation
328 && loss_limited_bandwidth < self.delay_based_estimate
329 {
330 // During HOLD period, cap estimate at HOLD rate
331 self.current_estimate = best_candidate;
332 self.current_estimate.loss_limited_bandwidth =
333 loss_limited_bandwidth.min(self.last_hold_info.rate);
334 log_inherent_loss!(self.current_estimate.inherent_loss);
335 log_loss_based_bitrate_estimate!(self.current_estimate.loss_limited_bandwidth.as_f64());
336 return;
337 }
338 
339 // State transitions with HOLD mechanism (WebRTC lines 336-378)
340 let new_state = if self.is_estimate_increasing_when_loss_limited(best_candidate)
341 && loss_limited_bandwidth < delay_based_estimated
342 && loss_limited_bandwidth < self.max_bitrate
343 {
344 LossControllerState::Increasing
345 } else if loss_limited_bandwidth < self.delay_based_estimate
346 && loss_limited_bandwidth < self.max_bitrate
347 {
348 // Entering Decreasing state - set HOLD info
349 if self.state != LossControllerState::Decreasing
350 && self.config.hold_duration_factor > 0.0
351 {
352 const MAX_HOLD_DURATION: Duration = Duration::from_secs(60);
353 self.last_hold_info = HoldInfo {
354 timestamp: self.last_send_time_most_recent_observation
355 + self.last_hold_info.duration,
356 duration: MAX_HOLD_DURATION.min(Duration::from_secs_f64(
357 self.last_hold_info.duration.as_secs_f64()
358 * self.config.hold_duration_factor,
359 )),
360 rate: loss_limited_bandwidth,
361 };
362 }
363 LossControllerState::Decreasing
364 } else {
365 // Reset HOLD info when returning to DelayBased
366 self.last_hold_info = HoldInfo {
367 timestamp: Timestamp::DistantPast,
368 duration: Duration::from_millis(300),
369 rate: Bitrate::INFINITY,
370 };
371 LossControllerState::DelayBased
372 };
373 self.set_state(new_state);
374 
375 self.current_estimate = best_candidate;
376 log_inherent_loss!(self.current_estimate.inherent_loss);
377 log_loss_based_bitrate_estimate!(self.current_estimate.loss_limited_bandwidth.as_f64());
378 
379 const CONGESTION_CONTROLLER_MIN_BITRATE: Bitrate = Bitrate::kbps(5);
380 const CONF_MAX_INCREASE_FACTOR: f64 = 1.3;
381 
382 if self.is_bandwidth_limited_due_to_loss()
383 && (!self.recovering_after_loss_timestamp.is_exact()
384 || self.recovering_after_loss_timestamp + self.config.delayed_increase_window
385 < self.last_send_time_most_recent_observation)
386 {
387 self.bandwidth_limit_in_current_window = CONGESTION_CONTROLLER_MIN_BITRATE
388 .max(loss_limited_bandwidth * CONF_MAX_INCREASE_FACTOR);
389 
390 self.recovering_after_loss_timestamp = self.last_send_time_most_recent_observation;
391 log_loss_bw_limit_in_window!(self.bandwidth_limit_in_current_window.as_f64());
392 }
393 }
394 
395 // TODO: Determine if we want to integrate these two with the rest of the system.
396 #[cfg(test)]
397 pub fn set_max_bitrate(&mut self, max_bitrate: Bitrate) {
398 self.max_bitrate = max_bitrate;
399 }
400 
401 #[cfg(test)]
402 pub fn set_min_bitrate(&mut self, min_bitrate: Bitrate) {
403 self.min_bitrate = min_bitrate;
404 }
405 
406 pub fn loss_based_result(&self) -> LossBasedBweResult {
407 let mut result = LossBasedBweResult {
408 bandwidth_estimate: self.current_estimate.loss_limited_bandwidth.as_valid(),
409 state: self.state,
410 };
411 
412 if self.num_observations == 0 {
413 return result;
414 }
415 
416 let Some(loss_limited_bandwidth) = self.current_estimate.loss_limited_bandwidth.as_valid()
417 else {
418 return result;
419 };
420 let instant_upper_bound = self.get_instant_upper_bound();
421 
422 if self.delay_based_estimate.is_valid() {
423 result.bandwidth_estimate = Some(
424 loss_limited_bandwidth
425 .min(self.delay_based_estimate)
426 .min(instant_upper_bound),
427 )
428 } else {
429 result.bandwidth_estimate = Some(loss_limited_bandwidth.min(instant_upper_bound))
430 }
431 
432 result
433 }
434 
435 fn maybe_add_observation(&mut self, packet_results: &[impl PacketResult]) -> bool {
436 let Some(summary) = PacketResultsSummary::from(packet_results) else {
437 return false;
438 };
439 
440 let last_send_time = Timestamp::from(summary.last_send_time);
441 
442 self.partial_observation.update(summary);
443 
444 if !self.last_send_time_most_recent_observation.is_exact() {
445 self.last_send_time_most_recent_observation = last_send_time;
446 }
447 
448 let observation_duration = last_send_time - self.last_send_time_most_recent_observation;
449 
450 if observation_duration <= Duration::ZERO {
451 return false;
452 }
453 
454 // decide if we can accept the partial observation as complete
455 if observation_duration <= self.config.observation_duration_lower_bound {
456 return false;
457 }
458 
459 self.last_send_time_most_recent_observation = last_send_time;
460 
461 let observation = {
462 let id = self.num_observations;
463 self.num_observations += 1;
464 
465 Observation {
466 num_packets: self.partial_observation.num_packets,
467 size: self.partial_observation.size,
468 num_lost_packets: self.partial_observation.num_lost_packets,
469 lost_size: self.partial_observation.lost_size,
470 num_received_packets: self.partial_observation.num_packets
471 - self.partial_observation.num_lost_packets,
472 sending_rate: self.partial_observation.size / observation_duration,
473 id,
474 is_initialized: true,
475 }
476 };
477 
478 // save our complete observation
479 self.observations[observation.id as usize % self.config.observation_window_size] =
480 observation;
481 
482 // renew the partial observation
483 self.partial_observation = PartialObservation::new();
484 
485 // calculate upper bound
486 self.cached_instant_upper_bound = Some(self.calculate_instant_upper_bound());
487 
488 true
489 }
490 
491 fn get_candidates(&self) -> Vec<ChannelParameters> {
492 let mut bandwidths = vec![];
493 
494 let current = self.current_estimate.loss_limited_bandwidth;
495 
496 for factor in self.config.candidate_factor.iter() {
497 bandwidths.push(factor * current.as_f64());
498 }
499 
500 if self.delay_based_estimate.is_valid()
501 && self.config.append_delay_based_estimate_candidate
502 && self.delay_based_estimate > current
503 {
504 bandwidths.push(self.delay_based_estimate.as_f64());
505 }
506 
507 let candidate_bandwidth_upper_bound = self.get_candidate_bandwidth_upper_bound().as_f64();
508 
509 if self.config.append_acknowledged_rate_candidate && self.acknowledged_bitrate.is_valid() {
510 bandwidths.push(
511 (self.acknowledged_bitrate * self.config.bandwidth_backoff_lower_bound_factor)
512 .as_f64(),
513 );
514 }
515 
516 if self.config.append_delay_based_estimate_candidate
517 && self.delay_based_estimate.is_valid()
518 && self.delay_based_estimate > current
519 {
520 bandwidths.push(
521 (self.delay_based_estimate * self.config.bandwidth_backoff_lower_bound_factor)
522 .as_f64(),
523 );
524 }
525 
526 let mut candidates = Vec::with_capacity(bandwidths.len());
527 
528 for bandwidth in bandwidths.iter_mut() {
529 let mut candidate = self.current_estimate;
530 candidate.loss_limited_bandwidth = if self.config.trendline_integration_enabled {
531 bandwidth.min(candidate_bandwidth_upper_bound).into()
532 } else {
533 bandwidth
534 .min(
535 self.current_estimate
536 .loss_limited_bandwidth
537 .as_f64()
538 .max(candidate_bandwidth_upper_bound),
539 )
540 .into()
541 };
542 candidate.inherent_loss = self.get_feasible_inherent_loss(&candidate);
543 candidates.push(candidate);
544 }
545 
546 candidates
547 }
548 
549 fn newtons_method_update(&self, channel_parameters: &mut ChannelParameters) {
550 if self.num_observations == 0 {
551 return;
552 }
553 
554 for _ in 0..self.config.newton_iterations {
555 let derivatives = self.get_derivatives(channel_parameters);
556 channel_parameters.inherent_loss -=
557 self.config.newton_step_size * (derivatives.0 / derivatives.1);
558 channel_parameters.inherent_loss = self.get_feasible_inherent_loss(channel_parameters);
559 }
560 }
561 
562 fn get_derivatives(&self, channel_prameters: &ChannelParameters) -> (f64, f64) {
563 let mut derivatives: (f64, f64) = (0.0, 0.0);
564 
565 for observation in self.observations.iter() {
566 if !observation.is_initialized {
567 continue;
568 }
569 
570 let loss_probability = self.get_loss_probability(
571 channel_prameters.inherent_loss,
572 channel_prameters.loss_limited_bandwidth,
573 observation.sending_rate,
574 );
575 
576 let index = (self.num_observations - 1) - observation.id;
577 let temporal_weight = self.temporal_weights[index as usize];
578 
579 if self.config.use_byte_loss_ratio {
580 derivatives.0 += temporal_weight
581 * ((observation.lost_size.as_kb() / loss_probability)
582 - ((observation.size - observation.lost_size).as_kb()
583 / (1.0 - loss_probability)));
584 
585 derivatives.1 -= temporal_weight
586 * ((observation.lost_size.as_kb() / f64::powi(loss_probability, 2))
587 + ((observation.size - observation.lost_size).as_kb()
588 / f64::powi(1.0 - loss_probability, 2)));
589 } else {
590 derivatives.0 += temporal_weight
591 * ((observation.num_lost_packets as f64 / loss_probability)
592 - (observation.num_received_packets as f64 / (1.0 - loss_probability)));
593 
594 derivatives.1 -= temporal_weight
595 * ((observation.num_lost_packets as f64 / f64::powi(loss_probability, 2))
596 + (observation.num_received_packets as f64
597 / f64::powi(1.0 - loss_probability, 2)));
598 }
599 }
600 
601 // Clamp second derivative to safe value if invalid due to floating-point edge cases
602 // (infinity, denormals, extreme values from pathological TWCC feedback)
603 if !derivatives.1.is_sign_negative() || derivatives.1 == 0.0 || derivatives.1.is_nan() {
604 derivatives.1 = -1.0e-6;
605 debug!(
606 "Second derivative clamped to safe value due to invalid result: was {:?}",
607 derivatives.1
608 );
609 }
610 
611 derivatives
612 }
613 
614 fn get_loss_probability(
615 &self,
616 inherent_loss: f64,
617 loss_limited_bandwidth: Bitrate,
618 sending_rate: Bitrate,
619 ) -> f64 {
620 let inherent_loss = inherent_loss.clamp(0.0, 1.0);
621 
622 // maybe warn if sending rate or loss limited bandwidth are not finite
623 
624 let mut loss_probability = inherent_loss;
625 if sending_rate.is_valid()
626 && loss_limited_bandwidth.is_valid()
627 && sending_rate > loss_limited_bandwidth
628 {
629 loss_probability += (1.0 - inherent_loss)
630 * ((sending_rate - loss_limited_bandwidth).as_f64() / sending_rate.as_f64());
631 }
632 
633 loss_probability.clamp(1.0e-6, 1.0 - 1.0e-6)
634 }
635 
636 fn get_objective(&self, candidate: &ChannelParameters) -> f64 {
637 let mut objective = 0.0;
638 let high_bandwidth_bias = self.get_high_bandwidth_bias(candidate.loss_limited_bandwidth);
639 
640 for observation in self.observations.iter() {
641 if !observation.is_initialized {
642 continue;
643 }
644 
645 let loss_probability = self.get_loss_probability(
646 candidate.inherent_loss,
647 candidate.loss_limited_bandwidth,
648 observation.sending_rate,
649 );
650 
651 let index = (self.num_observations - 1) - observation.id;
652 let temporal_weight = self.temporal_weights[index as usize];
653 
654 if self.config.use_byte_loss_ratio {
655 objective += temporal_weight
656 * ((observation.lost_size.as_kb() / 1000.0) * f64::ln(loss_probability)
657 + ((observation.size - observation.lost_size).as_kb() / 1000.0)
658 * f64::ln(1.0 - loss_probability));
659 objective +=
660 temporal_weight * high_bandwidth_bias * observation.size.as_kb() / 1000.0;
661 } else {
662 objective += temporal_weight
663 * (observation.num_lost_packets as f64 * f64::ln(loss_probability)
664 + (observation.num_received_packets as f64
665 * f64::ln(1.0 - loss_probability)));
666 
667 objective += temporal_weight * high_bandwidth_bias * observation.num_packets as f64;
668 }
669 }
670 
671 objective
672 }
673 
674 fn is_estimate_increasing_when_loss_limited(&self, candidate: ChannelParameters) -> bool {
675 if !self.is_bandwidth_limited_due_to_loss() {
676 return false;
677 }
678 
679 let current = self.current_estimate.loss_limited_bandwidth;
680 let candidate = candidate.loss_limited_bandwidth;
681 
682 if current < candidate {
683 return true;
684 }
685 
686 current == candidate && self.state == LossControllerState::Increasing
687 }
688 
689 fn is_bandwidth_limited_due_to_loss(&self) -> bool {
690 self.state != LossControllerState::DelayBased
691 }
692 
693 fn get_candidate_bandwidth_upper_bound(&self) -> Bitrate {
694 let mut upper_bound = self.max_bitrate;
695 
696 // When in ALR and we have a link capacity estimate from probes,
697 // use it as the upper bound. This prevents estimating beyond proven capacity.
698 if self.is_in_alr() {
699 if let Some(capacity) = self.link_capacity_estimate {
700 if capacity.is_valid() {
701 upper_bound = upper_bound.min(capacity);
702 }
703 }
704 }
705 
706 if self.is_bandwidth_limited_due_to_loss()
707 && self.bandwidth_limit_in_current_window.is_valid()
708 {
709 upper_bound = self.bandwidth_limit_in_current_window.min(upper_bound);
710 }
711 
712 upper_bound = self.get_instant_upper_bound().min(upper_bound);
713 if self.delay_based_estimate.is_valid() {
714 upper_bound = upper_bound.min(self.delay_based_estimate);
715 }
716 
717 if !self.acknowledged_bitrate.is_valid() {
718 return upper_bound;
719 }
720 
721 if self.config.rampup_acceleration_max_factor > Duration::ZERO
722 && self.last_send_time_most_recent_observation.is_exact()
723 && self.last_time_estimate_reduced.is_exact()
724 {
725 let delta = (self.last_send_time_most_recent_observation
726 - self.last_time_estimate_reduced)
727 .max(TimeDelta::ZERO);
728 let time_since_bw_reduced = self
729 .config
730 .rampup_acceleration_maxout_time
731 .as_secs_f64()
732 .min(delta.as_secs_f64());
733 
734 let rampup_acceleration = self.config.rampup_acceleration_max_factor.as_secs_f64()
735 * time_since_bw_reduced
736 / self.config.rampup_acceleration_maxout_time.as_secs_f64();
737 
738 upper_bound = upper_bound + (self.acknowledged_bitrate * rampup_acceleration);
739 }
740 
741 upper_bound
742 }
743 
744 fn set_state(&mut self, state: LossControllerState) {
745 if state != self.state {
746 debug!(
747 "Changing loss controller state: {:?} -> {:?}",
748 self.state, state
749 );
750 }
751 self.state = state;
752 }
753 
754 fn get_high_bandwidth_bias(&self, bandwidth: Bitrate) -> f64 {
755 if !bandwidth.is_valid() {
756 return 0.0;
757 }
758 
759 let average_reported_loss_ratio = self.average_reported_loss_ratio();
760 
761 self.adjust_bias_factor(
762 average_reported_loss_ratio,
763 self.config.higher_bandwidth_bias_factor,
764 ) * bandwidth.as_f64()
765 + self.adjust_bias_factor(
766 average_reported_loss_ratio,
767 self.config.higher_log_bandwidth_bias_factor,
768 ) * f64::ln(1.0 + bandwidth.as_f64())
769 }
770 
771 fn adjust_bias_factor(&self, loss_rate: f64, bias_factor: f64) -> f64 {
772 let diff = self.config.threshold_of_high_bandwidth_preference - loss_rate;
773 bias_factor * (diff / self.config.bandwidth_preference_smoothing_factor + diff.abs())
774 }
775 
776 fn calculate_instant_upper_bound(&self) -> Bitrate {
777 // this requires someone to set the max bitrate from outside
778 let mut instant_limit = self.max_bitrate;
779 
780 let average_reported_loss_ratio = self.average_reported_loss_ratio();
781 
782 if average_reported_loss_ratio > self.config.instant_upper_bound_loss_offset {
783 instant_limit = self.config.instant_upper_bound_bandwidth_balance
784 / (average_reported_loss_ratio - self.config.instant_upper_bound_loss_offset);
785 
786 if average_reported_loss_ratio > self.config.high_loss_rate_threshold {
787 let limit = self.config.bandwidth_cap_at_high_loss_rate
788 - self.config.slope_of_bwe_high_loss_function * average_reported_loss_ratio;
789 
790 instant_limit = limit.max(self.min_bitrate);
791 }
792 }
793 
794 instant_limit
795 }
796 
797 fn get_instant_upper_bound(&self) -> Bitrate {
798 self.cached_instant_upper_bound
799 .as_valid()
800 .unwrap_or(self.max_bitrate)
801 }
802 
803 fn average_reported_loss_ratio(&self) -> f64 {
804 let mut total = 0_f64;
805 let mut lost = 0_f64;
806 
807 for observation in self.observations.iter() {
808 if !observation.is_initialized {
809 continue;
810 }
811 
812 let index = (self.num_observations - 1) - observation.id;
813 
814 let instant_temporal_weight = self.instant_upper_bound_temporal_weights[index as usize];
815 
816 if self.config.use_byte_loss_ratio {
817 total += instant_temporal_weight * observation.size.as_bytes_f64();
818 lost += instant_temporal_weight * observation.lost_size.as_bytes_f64();
819 } else {
820 total += instant_temporal_weight * observation.num_packets as f64;
821 lost += instant_temporal_weight * observation.num_lost_packets as f64;
822 }
823 }
824 
825 if total == 0_f64 {
826 return 0.0;
827 }
828 
829 lost / total
830 }
831 
832 fn get_feasible_inherent_loss(&self, channel_parameters: &ChannelParameters) -> f64 {
833 channel_parameters
834 .inherent_loss
835 .max(self.config.inherent_loss_lower_bound)
836 .min(
837 self.get_inherent_loss_upper_bound(Some(channel_parameters.loss_limited_bandwidth)),
838 )
839 }
840 
841 fn get_inherent_loss_upper_bound(&self, bandwidth: Option<Bitrate>) -> f64 {
842 let Some(bandwidth) = bandwidth else {
843 return 1.0;
844 };
845 
846 if bandwidth == Bitrate::ZERO {
847 return 1.0;
848 }
849 
850 let inherent_loss_upper_bound = self.config.inherent_loss_upper_bound_offset
851 + self
852 .config
853 .inherent_loss_upper_bound_bandwidth_balance
854 .as_f64()
855 / bandwidth.as_f64();
856 
857 inherent_loss_upper_bound.min(1.0)
858 }
859}
860 
861struct Config {
862 observation_window_size: usize, // minimum is 2
863 observation_duration_lower_bound: Duration,
864 trendline_integration_enabled: bool,
865 temporal_weight_factor: f64,
866 instant_upper_bound_temporal_weight_factor: f64,
867 instant_upper_bound_loss_offset: f64,
868 instant_upper_bound_bandwidth_balance: Bitrate,
869 high_loss_rate_threshold: f64,
870 slope_of_bwe_high_loss_function: Bitrate,
871 bandwidth_cap_at_high_loss_rate: Bitrate,
872 initial_inherent_loss_estimate: f64,
873 inherent_loss_upper_bound_offset: f64,
874 inherent_loss_upper_bound_bandwidth_balance: Bitrate,
875 inherent_loss_lower_bound: f64,
876 newton_iterations: usize,
877 newton_step_size: f64,
878 not_increase_if_inherent_loss_less_than_average_loss: bool,
879 delayed_increase_window: Duration,
880 bandwidth_rampup_upper_bound_factor: f64,
881 candidate_factor: [f64; 3],
882 append_acknowledged_rate_candidate: bool,
883 append_delay_based_estimate_candidate: bool,
884 bandwidth_backoff_lower_bound_factor: f64,
885 rampup_acceleration_maxout_time: Duration,
886 rampup_acceleration_max_factor: Duration,
887 higher_bandwidth_bias_factor: f64,
888 higher_log_bandwidth_bias_factor: f64,
889 threshold_of_high_bandwidth_preference: f64,
890 bandwidth_preference_smoothing_factor: f64,
891 use_byte_loss_ratio: bool,
892 hold_duration_factor: f64,
893 bandwidth_rampup_hold_threshold: f64,
894 bandwidth_rampup_upper_bound_factor_in_hold: f64,
895}
896 
897#[derive(Debug)]
898struct PacketResultsSummary {
899 num_packets: u64,
900 num_lost_packets: u64,
901 total_size: DataSize,
902 lost_size: DataSize,
903 first_send_time: Instant,
904 last_send_time: Instant,
905}
906 
907impl PacketResultsSummary {
908 pub fn new(first_send_time: Instant, last_send_time: Instant) -> PacketResultsSummary {
909 PacketResultsSummary {
910 num_packets: 0,
911 num_lost_packets: 0,
912 total_size: DataSize::ZERO,
913 lost_size: DataSize::ZERO,
914 last_send_time,
915 first_send_time,
916 }
917 }
918 
919 pub fn from(records: &[impl PacketResult]) -> Option<PacketResultsSummary> {
920 let first = records.first()?;
921 
922 let mut summary =
923 PacketResultsSummary::new(first.local_send_time(), first.local_send_time());
924 for record in records {
925 let lost: u64 = record.lost().into();
926 let size = record.size();
927 
928 summary.num_packets += 1;
929 summary.total_size += size;
930 summary.lost_size += size * lost;
931 summary.num_lost_packets += lost;
932 summary.first_send_time = min(summary.first_send_time, record.local_send_time());
933 summary.last_send_time = max(summary.last_send_time, record.local_send_time());
934 }
935 
936 Some(summary)
937 }
938}
939 
940#[derive(Debug, Clone, Copy)]
941struct Observation {
942 num_packets: u64,
943 size: DataSize,
944 num_lost_packets: u64,
945 lost_size: DataSize,
946 num_received_packets: u64,
947 sending_rate: Bitrate,
948 id: u64,
949 is_initialized: bool,
950}
951 
952impl Observation {
953 pub const DUMMY: Self = Self {
954 num_packets: 0,
955 size: DataSize::ZERO,
956 num_lost_packets: 0,
957 lost_size: DataSize::ZERO,
958 num_received_packets: 0,
959 sending_rate: Bitrate::NEG_INFINITY,
960 id: 0,
961 is_initialized: false,
962 };
963}
964 
965struct PartialObservation {
966 num_packets: u64,
967 num_lost_packets: u64,
968 size: DataSize,
969 lost_size: DataSize,
970}
971 
972impl PartialObservation {
973 pub fn new() -> PartialObservation {
974 PartialObservation {
975 num_packets: 0,
976 num_lost_packets: 0,
977 size: DataSize::ZERO,
978 lost_size: DataSize::ZERO,
979 }
980 }
981 
982 pub fn update(&mut self, summary: PacketResultsSummary) {
983 self.num_packets += summary.num_packets;
984 self.num_lost_packets += summary.num_lost_packets;
985 self.size += summary.total_size;
986 self.lost_size += summary.lost_size;
987 }
988}
989 
990/// An estimate derived from some candidate.
991#[derive(Debug, Clone, Copy)]
992struct ChannelParameters {
993 /// The estimated inherent loss
994 inherent_loss: f64,
995 /// The estimated bandwidth
996 loss_limited_bandwidth: Bitrate,
997}
998 
999impl ChannelParameters {
1000 pub fn new(inherent_loss: f64) -> ChannelParameters {
1001 ChannelParameters {
1002 inherent_loss,
1003 loss_limited_bandwidth: Bitrate::NEG_INFINITY,
1004 }
1005 }
1006}
1007 
1008/// HOLD mechanism info - prevents immediate ramp-up after loss detection
1009#[derive(Debug, Clone, Copy)]
1010struct HoldInfo {
1011 timestamp: Timestamp,
1012 duration: Duration,
1013 rate: Bitrate,
1014}
1015 
1016impl Default for HoldInfo {
1017 fn default() -> Self {
1018 const INIT_HOLD_DURATION: Duration = Duration::from_millis(300);
1019 Self {
1020 timestamp: Timestamp::DistantPast,
1021 duration: INIT_HOLD_DURATION,
1022 rate: Bitrate::INFINITY,
1023 }
1024 }
1025}
1026 
1027trait AsValid<T> {
1028 fn as_valid(&self) -> Option<T>;
1029}
1030 
1031impl AsValid<Bitrate> for Option<Bitrate> {
1032 fn as_valid(&self) -> Option<Bitrate> {
1033 if let Some(bitrate) = self {
1034 if bitrate.as_f64().is_finite() {
1035 return Some(*bitrate);
1036 }
1037 }
1038 None
1039 }
1040}
1041 
1042#[derive(Debug)]
1043pub struct LossBasedBweResult {
1044 pub bandwidth_estimate: Option<Bitrate>,
1045 pub state: LossControllerState,
1046}
1047 
1048impl Default for Config {
1049 fn default() -> Self {
1050 Self {
1051 observation_window_size: 15,
1052 observation_duration_lower_bound: Duration::from_millis(250),
1053 trendline_integration_enabled: false,
1054 temporal_weight_factor: 0.9,
1055 instant_upper_bound_temporal_weight_factor: 0.9,
1056 instant_upper_bound_loss_offset: 0.05,
1057 instant_upper_bound_bandwidth_balance: Bitrate::kbps(100),
1058 high_loss_rate_threshold: 1.0,
1059 slope_of_bwe_high_loss_function: Bitrate::kbps(1000),
1060 bandwidth_cap_at_high_loss_rate: Bitrate::kbps(500),
1061 initial_inherent_loss_estimate: 0.01,
1062 inherent_loss_upper_bound_offset: 0.05,
1063 inherent_loss_upper_bound_bandwidth_balance: Bitrate::kbps(100),
1064 inherent_loss_lower_bound: 1.0e-3,
1065 newton_iterations: 1,
1066 newton_step_size: 0.75,
1067 not_increase_if_inherent_loss_less_than_average_loss: true,
1068 delayed_increase_window: Duration::from_millis(300),
1069 bandwidth_rampup_upper_bound_factor: 1.5,
1070 candidate_factor: [1.02, 1.0, 0.95],
1071 append_acknowledged_rate_candidate: true,
1072 append_delay_based_estimate_candidate: true,
1073 bandwidth_backoff_lower_bound_factor: 1.0,
1074 rampup_acceleration_maxout_time: Duration::from_secs(60),
1075 rampup_acceleration_max_factor: Duration::from_secs(60),
1076 higher_bandwidth_bias_factor: 0.0002,
1077 higher_log_bandwidth_bias_factor: 0.02,
1078 threshold_of_high_bandwidth_preference: 0.2,
1079 bandwidth_preference_smoothing_factor: 0.002,
1080 use_byte_loss_ratio: true,
1081 hold_duration_factor: 2.0,
1082 bandwidth_rampup_hold_threshold: 1.3,
1083 bandwidth_rampup_upper_bound_factor_in_hold: 1.2,
1084 }
1085 }
1086}
1087 
1088impl PacketResult for &TwccSendRecord {
1089 fn local_send_time(&self) -> Instant {
1090 (*self).local_send_time()
1091 }
1092 
1093 fn size(&self) -> DataSize {
1094 (*self).size().into()
1095 }
1096 
1097 fn lost(&self) -> bool {
1098 (*self).remote_recv_time().is_none()
1099 }
1100}
1101 
1102#[cfg(test)]
1103mod test {
1104 use std::time::Instant;
1105 
1106 use fastrand::Rng;
1107 use systemstat::Duration;
1108 
1109 use super::{Bitrate, DataSize, LossBasedBweResult, LossController, LossControllerState};
1110 struct PacketResult {
1111 local_send_time: Instant,
1112 size: DataSize,
1113 lost: bool,
1114 }
1115 
1116 impl super::PacketResult for PacketResult {
1117 fn local_send_time(&self) -> Instant {
1118 self.local_send_time
1119 }
1120 
1121 fn size(&self) -> DataSize {
1122 self.size
1123 }
1124 
1125 fn lost(&self) -> bool {
1126 self.lost
1127 }
1128 }
1129 
1130 #[test]
1131 fn no_loss() {
1132 // Test no loss, estimate should be bounded by delay based estimate
1133 let mut lbc = LossController::new();
1134 lbc.set_min_bitrate(Bitrate::from(50_000)); // 50 kbps
1135 lbc.set_max_bitrate(Bitrate::from(1_000_000_000)); // 1 Gbps
1136 
1137 let acknowledged_bitrate = Bitrate::from(1_000_000); // 1 Mbps
1138 lbc.set_acknowledged_bitrate(acknowledged_bitrate);
1139 lbc.set_bandwidth_estimate(Bitrate::from(1_250_000)); // 1.25Mbps
1140 
1141 let mut pkt_builder = PacketBuilder::new(Instant::now()).num_packets(26);
1142 
1143 // A single observation at 1Mbps
1144 let result = pkt_builder.build_packets();
1145 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1146 pkt_builder = pkt_builder.forward_time(Duration::from_millis(250));
1147 
1148 // A single observation at 1Mbps
1149 let result = pkt_builder.build_packets();
1150 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1151 
1152 let LossBasedBweResult {
1153 bandwidth_estimate,
1154 state,
1155 } = lbc.loss_based_result();
1156 
1157 assert_eq!(
1158 bandwidth_estimate,
1159 Some(Bitrate::bps(1_500_000)),
1160 "Estimate should increase to delay based estimate, but not further"
1161 );
1162 assert_eq!(state, LossControllerState::DelayBased);
1163 }
1164 
1165 #[test]
1166 fn stable_loss() {
1167 // Test stable loss at 5% which should be ignored by the loss controller
1168 let mut lbc = LossController::new();
1169 lbc.set_min_bitrate(Bitrate::from(50_000)); // 50 kbps
1170 lbc.set_max_bitrate(Bitrate::from(1_000_000_000)); // 1 Gbps
1171 
1172 let acknowledged_bitrate = Bitrate::from(1_000_000); // 1 Mbps
1173 lbc.set_acknowledged_bitrate(acknowledged_bitrate);
1174 lbc.set_bandwidth_estimate(Bitrate::from(1_250_000)); // 1.25Mbps
1175 
1176 let mut pkt_builder = PacketBuilder::new(Instant::now())
1177 .with_loss(0.05)
1178 .num_packets(26);
1179 
1180 // It takes a while for the maximum likelihood estimation to react to the inherent loss
1181 // this is why we need quite a few observations before the estimate increases to the delay
1182 // based bound
1183 // 40 observations(10 seconds) at 1Mbps
1184 for _ in 0..40 {
1185 let result = pkt_builder.build_packets();
1186 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1187 pkt_builder = pkt_builder.forward_time(Duration::from_millis(250));
1188 }
1189 
1190 let LossBasedBweResult {
1191 bandwidth_estimate,
1192 state,
1193 } = lbc.loss_based_result();
1194 
1195 assert!(
1196 state == LossControllerState::DelayBased || state == LossControllerState::Increasing,
1197 "With stable inherent loss, should be in DelayBased or Increasing state, got {:?}",
1198 state
1199 );
1200 
1201 // Note: The loss controller returns loss_limited_bandwidth even in DelayBased state.
1202 // The BWE integration layer (SendSideBandwidthEstimator::last_estimate) is responsible
1203 // for using the delay-based estimate when state is DelayBased.
1204 // This matches WebRTC's LossBasedBweV2 behavior (see loss_based_bwe_v2.cc:379).
1205 assert!(
1206 bandwidth_estimate.is_some(),
1207 "Loss controller should return an estimate even in DelayBased state"
1208 );
1209 }
1210 
1211 #[test]
1212 fn stable_loss_with_loss_spike() {
1213 // Test stable loss at 5% which should be ignored by the loss controller, followed by a
1214 // loss spike which should cause the estimate to dip
1215 let mut lbc = LossController::new();
1216 lbc.set_min_bitrate(Bitrate::from(50_000)); // 50 kbps
1217 lbc.set_max_bitrate(Bitrate::from(1_000_000_000)); // 1 Gbps
1218 
1219 let acknowledged_bitrate = Bitrate::from(1_000_000); // 1 Mbps
1220 lbc.set_acknowledged_bitrate(acknowledged_bitrate);
1221 lbc.set_bandwidth_estimate(Bitrate::from(1_250_000)); // 1.25Mbps
1222 
1223 let mut pkt_builder = PacketBuilder::new(Instant::now())
1224 .with_loss(0.05)
1225 .num_packets(26);
1226 
1227 // It takes a while for the maximum likelihood estimation to react to the inherent loss
1228 // this is why we need quite a few observations before the estimate increases to the delay
1229 // based bound and is stable.
1230 // 40 observations(10 seconds) at 1Mbps
1231 for _ in 0..40 {
1232 let result = pkt_builder.build_packets();
1233 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1234 pkt_builder = pkt_builder.forward_time(Duration::from_millis(250));
1235 }
1236 
1237 pkt_builder = pkt_builder.with_loss(0.9);
1238 // Loss spike(1second at 90% loss)
1239 for _ in 0..4 {
1240 let result = pkt_builder.build_packets();
1241 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1242 
1243 pkt_builder = pkt_builder.forward_time(Duration::from_millis(250));
1244 }
1245 
1246 let LossBasedBweResult {
1247 bandwidth_estimate,
1248 state,
1249 } = lbc.loss_based_result();
1250 
1251 let estimate = bandwidth_estimate.expect("Should have an estimate");
1252 assert!(
1253 estimate < Bitrate::bps(500_000),
1254 "A loss spike should result in a reduced estimate, estimate was {estimate}"
1255 );
1256 assert_eq!(state, LossControllerState::Decreasing);
1257 }
1258 
1259 #[test]
1260 fn loss_spike_recovery() {
1261 // Test stable loss at 5% which should be ignored by the loss controller, followed by a
1262 // loss spike which should cause the estimate to dip
1263 let mut lbc = LossController::new();
1264 lbc.set_min_bitrate(Bitrate::from(50_000)); // 50 kbps
1265 lbc.set_max_bitrate(Bitrate::from(1_000_000_000)); // 1 Gbps
1266 
1267 let acknowledged_bitrate = Bitrate::from(1_000_000); // 1 Mbps
1268 lbc.set_acknowledged_bitrate(acknowledged_bitrate);
1269 lbc.set_bandwidth_estimate(Bitrate::from(1_250_000)); // 1.25Mbps
1270 
1271 let mut pkt_builder = PacketBuilder::new(Instant::now())
1272 .with_loss(0.05)
1273 .num_packets(26);
1274 
1275 // It takes a while for the maximum likelihood estimation to react to the inherent loss
1276 // this is why we need quite a few observations before the estimate increases to the delay
1277 // based bound and is stable.
1278 // 40 observations(10 seconds) at 1Mbps
1279 for _ in 0..40 {
1280 let result = pkt_builder.build_packets();
1281 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1282 pkt_builder = pkt_builder.forward_time(Duration::from_millis(250));
1283 }
1284 
1285 // Loss spike
1286 pkt_builder = pkt_builder.with_loss(0.9);
1287 let result = pkt_builder.build_packets();
1288 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1289 pkt_builder = pkt_builder.forward_time(Duration::from_millis(250));
1290 
1291 pkt_builder = pkt_builder.with_loss(0.05);
1292 // Set loss back to 5% and gradually ramp up the bitrate
1293 for i in 0..40 {
1294 pkt_builder = pkt_builder.num_packets(6 + i / 2);
1295 
1296 let result = pkt_builder.build_packets();
1297 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1298 
1299 pkt_builder = pkt_builder.forward_time(Duration::from_millis(250));
1300 }
1301 
1302 let LossBasedBweResult { state, .. } = lbc.loss_based_result();
1303 
1304 // Note: The loss controller returns loss_limited_bandwidth even in DelayBased state.
1305 // After recovery, it may be in Increasing or DelayBased state depending on dynamics.
1306 // The BWE integration layer uses the delay-based estimate when state is DelayBased.
1307 assert!(
1308 state == LossControllerState::DelayBased || state == LossControllerState::Increasing,
1309 "After recovery from loss spike, should be in DelayBased or Increasing state, got {:?}",
1310 state
1311 );
1312 }
1313 
1314 #[test]
1315 fn stable_loss_gradual_overuse() {
1316 // Test stable loss at 5% which should be ignored by the loss controller, followed by
1317 // a gradual increase in loss as we overuse the capacity
1318 let mut lbc = LossController::new();
1319 lbc.set_min_bitrate(Bitrate::from(50_000)); // 50 kbps
1320 lbc.set_max_bitrate(Bitrate::from(1_000_000_000)); // 1 Gbps
1321 
1322 let acknowledged_bitrate = Bitrate::from(1_000_000); // 1 Mbps
1323 lbc.set_acknowledged_bitrate(acknowledged_bitrate);
1324 lbc.set_bandwidth_estimate(Bitrate::from(1_250_000)); // 1.25Mbps
1325 
1326 let mut pkt_builder = PacketBuilder::new(Instant::now())
1327 .with_loss(0.05)
1328 .num_packets(26);
1329 
1330 // It takes a while for the maximum likelihood estimation to react to the inherent loss
1331 // this is why we need quite a few observations before the estimate increases to the delay
1332 // based bound
1333 // 40 observations(10 seconds) at 1Mbps
1334 for _ in 0..40 {
1335 let result = pkt_builder.build_packets();
1336 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1337 
1338 pkt_builder = pkt_builder.forward_time(Duration::from_millis(250));
1339 }
1340 
1341 // Gradual increase
1342 for inc in 0..10 {
1343 pkt_builder = pkt_builder.with_loss(0.05 + (inc as f64 / 10.0));
1344 
1345 for _ in 0..4 {
1346 let result = pkt_builder.build_packets();
1347 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1348 
1349 pkt_builder = pkt_builder.forward_time(Duration::from_millis(250));
1350 }
1351 }
1352 
1353 let LossBasedBweResult {
1354 bandwidth_estimate,
1355 state,
1356 } = lbc.loss_based_result();
1357 
1358 let estimate = bandwidth_estimate.expect("Should have an estimate");
1359 assert!(
1360 estimate < Bitrate::bps(1_000_000),
1361 "A gradual overuse should result in a lowered estimate"
1362 );
1363 assert_eq!(state, LossControllerState::Decreasing);
1364 }
1365 
1366 #[test]
1367 fn test_loss_limited_window() {
1368 let mut lbc = LossController::new();
1369 lbc.set_min_bitrate(Bitrate::kbps(50));
1370 lbc.set_max_bitrate(Bitrate::gbps(1));
1371 
1372 let acknowledged_bitrate = Bitrate::mbps(1); // 1 Mbps
1373 lbc.set_acknowledged_bitrate(acknowledged_bitrate);
1374 lbc.set_bandwidth_estimate(Bitrate::kbps(1_250)); // 1.25Mbps
1375 
1376 let mut pkt_builder = PacketBuilder::new(Instant::now()).num_packets(25);
1377 
1378 {
1379 // Initial observation with no loss
1380 let result = pkt_builder.build_packets();
1381 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1382 pkt_builder = pkt_builder.forward_time(Duration::from_millis(250));
1383 }
1384 
1385 let loss_limited = {
1386 // loss spike observation at 50%
1387 pkt_builder = pkt_builder.with_loss(0.5);
1388 let result = pkt_builder.build_packets();
1389 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1390 pkt_builder = pkt_builder.forward_time(Duration::from_millis(250));
1391 
1392 let LossBasedBweResult {
1393 bandwidth_estimate,
1394 state,
1395 } = lbc.loss_based_result();
1396 
1397 let estimate = bandwidth_estimate.expect("Should have an estimate");
1398 assert!(
1399 estimate < Bitrate::kbps(600),
1400 "A loss spike should've caused a significant drop in estimate, got {}",
1401 estimate
1402 );
1403 assert_eq!(state, LossControllerState::Decreasing);
1404 
1405 estimate
1406 };
1407 
1408 {
1409 // Recovery observation at 0% loss
1410 pkt_builder = pkt_builder.with_loss(0.0);
1411 let result = pkt_builder.build_packets();
1412 // Lower acknowledged bitrate to simulate reacting to estimate due to spike
1413 lbc.set_acknowledged_bitrate(Bitrate::kbps(300));
1414 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1415 pkt_builder = pkt_builder.forward_time(Duration::from_millis(250));
1416 
1417 let LossBasedBweResult {
1418 bandwidth_estimate,
1419 state,
1420 } = lbc.loss_based_result();
1421 
1422 let estimate = bandwidth_estimate.expect("Should have an estimate");
1423 assert!(
1424 estimate > loss_limited && estimate <= Bitrate::mbps(1),
1425 "During the recovery window after a loss spike the estimate should increase, but be bounded. loss_limited={}, estimate={}, expected <= 1 Mbps",
1426 loss_limited,
1427 estimate
1428 );
1429 assert_eq!(state, LossControllerState::Decreasing);
1430 }
1431 
1432 {
1433 // Another recovery observation at 0% loss, outside of the limit window
1434 pkt_builder = pkt_builder.num_packets(80);
1435 let result = pkt_builder.build_packets();
1436 lbc.set_acknowledged_bitrate(Bitrate::mbps(1));
1437 lbc.update_bandwidth_estimate(&result, Bitrate::bps(1_500_000));
1438 
1439 let LossBasedBweResult {
1440 bandwidth_estimate,
1441 state,
1442 } = lbc.loss_based_result();
1443 
1444 let estimate = bandwidth_estimate.expect("Should have an estimate");
1445 assert!(
1446 estimate == Bitrate::bps(1_000_000),
1447 "Eventually the estimate should recover but still remain bounded until the average loss caused by spike ages out"
1448 );
1449 assert_eq!(state, LossControllerState::Decreasing);
1450 }
1451 }
1452 
1453 struct PacketBuilder {
1454 now: Instant,
1455 rng: Rng,
1456 loss_rate: f64,
1457 send_distribution: LogNormalDistribution,
1458 recv_distribution: LogNormalDistribution,
1459 num_packets: u32,
1460 packet_size: DataSize,
1461 }
1462 
1463 impl PacketBuilder {
1464 fn new(now: Instant) -> Self {
1465 Self {
1466 now,
1467 rng: Rng::with_seed(34791910),
1468 loss_rate: 0.0,
1469 send_distribution: LogNormalDistribution {
1470 mean: 0.05,
1471 std_dev: 1.0,
1472 },
1473 recv_distribution: LogNormalDistribution {
1474 mean: 4.0,
1475 std_dev: 10.0,
1476 },
1477 num_packets: 10,
1478 packet_size: DataSize::bytes(1200),
1479 }
1480 }
1481 
1482 fn forward_time(mut self, by: Duration) -> Self {
1483 self.now += by;
1484 self
1485 }
1486 
1487 fn with_loss(mut self, loss_rate: f64) -> Self {
1488 self.loss_rate = loss_rate;
1489 self
1490 }
1491 
1492 fn num_packets(mut self, packets: u32) -> Self {
1493 self.num_packets = packets;
1494 self
1495 }
1496 
1497 fn build_packets(&mut self) -> Vec<PacketResult> {
1498 let mut last_send_time = self.now;
1499 let mut last_recv_time = self.now;
1500 let mut result: Vec<PacketResult> = Vec::with_capacity(self.num_packets as usize);
1501 
1502 for _ in 0..self.num_packets {
1503 let lost = self.rng.f64() <= self.loss_rate;
1504 let first_send_time = last_send_time
1505 + Duration::from_secs_f64(
1506 self.send_distribution.sample(&mut self.rng) / 1000.0,
1507 );
1508 let recv_time = last_recv_time
1509 + Duration::from_secs_f64(
1510 self.recv_distribution.sample(&mut self.rng) / 1000.0,
1511 );
1512 
1513 result.push(PacketResult {
1514 local_send_time: first_send_time,
1515 size: self.packet_size,
1516 lost,
1517 });
1518 
1519 last_send_time = first_send_time;
1520 if !lost {
1521 last_recv_time = recv_time;
1522 }
1523 }
1524 
1525 result
1526 }
1527 }
1528 
1529 struct LogNormalDistribution {
1530 mean: f64,
1531 std_dev: f64,
1532 }
1533 
1534 impl LogNormalDistribution {
1535 fn sample(&self, rng: &mut Rng) -> f64 {
1536 let normal = normal_distribution(rng);
1537 let location =
1538 (self.mean.powi(2) / (self.mean.powi(2) + self.std_dev.powi(2)).sqrt()).ln();
1539 let scale = (1.0 + (self.std_dev / self.mean).powi(2)).ln().sqrt();
1540 
1541 (location + scale * normal).exp()
1542 }
1543 }
1544 
1545 fn normal_distribution(rng: &mut Rng) -> f64 {
1546 let u1 = rng.f64();
1547 let u2 = rng.f64();
1548 
1549 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
1550 }
1551}