rtc_interceptor/gcc/
loss.rs1use std::time::{Duration, Instant};
4
5pub const DEFAULT_LOW_LOSS: f64 = 0.02;
7
8pub const DEFAULT_HIGH_LOSS: f64 = 0.10;
10
11pub const DEFAULT_LOSS_INTERVAL: Duration = Duration::from_millis(200);
13
14#[derive(Debug, Clone)]
33pub struct LossController {
34 target: f64,
36 min: f64,
37 max: f64,
38 low: f64,
39 high: f64,
40 interval: Duration,
41 average_loss: Option<f64>,
43 last_change: Option<Instant>,
44}
45
46impl LossController {
47 pub fn new(initial: f64, min: f64, max: f64) -> Self {
49 Self {
50 target: initial.clamp(min, max),
51 min,
52 max,
53 low: DEFAULT_LOW_LOSS,
54 high: DEFAULT_HIGH_LOSS,
55 interval: DEFAULT_LOSS_INTERVAL,
56 average_loss: None,
57 last_change: None,
58 }
59 }
60
61 pub fn target(&self) -> f64 {
63 self.target
64 }
65
66 pub fn average_loss(&self) -> Option<f64> {
68 self.average_loss
69 }
70
71 pub fn update(&mut self, now: Instant, lost: usize, total: usize) -> f64 {
73 if total == 0 {
74 return self.target;
75 }
76
77 let sample = lost as f64 / total as f64;
78 let average = match self.average_loss {
81 Some(previous) => 0.8 * previous + 0.2 * sample,
82 None => sample,
83 };
84 self.average_loss = Some(average);
85
86 if let Some(last) = self.last_change
87 && now.saturating_duration_since(last) < self.interval
88 {
89 return self.target;
90 }
91
92 if average.max(sample) < self.low {
93 self.target = (self.target * 1.05).clamp(self.min, self.max);
95 self.last_change = Some(now);
96 } else if average.min(sample) > self.high {
97 self.target = (self.target * (1.0 - 0.5 * average)).clamp(self.min, self.max);
99 self.last_change = Some(now);
100 }
101 self.target
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 const MIN: f64 = 100_000.0;
113 const MAX: f64 = 10_000_000.0;
114
115 fn controller() -> LossController {
116 LossController::new(1_000_000.0, MIN, MAX)
117 }
118
119 #[test]
122 fn heavy_loss_lowers_the_target_on_its_own() {
123 let epoch = Instant::now();
124 let mut controller = controller();
125 let before = controller.target();
126
127 let mut at = epoch;
128 for _ in 0..10 {
129 at += DEFAULT_LOSS_INTERVAL;
130 controller.update(at, 20, 100);
131 }
132
133 assert!(
134 controller.target() < before,
135 "20% loss must lower the target: {before} → {}",
136 controller.target()
137 );
138 }
139
140 #[test]
142 fn a_clean_path_raises_the_target() {
143 let epoch = Instant::now();
144 let mut controller = controller();
145 let before = controller.target();
146
147 let mut at = epoch;
148 for _ in 0..10 {
149 at += DEFAULT_LOSS_INTERVAL;
150 controller.update(at, 0, 100);
151 }
152
153 assert!(
154 controller.target() > before,
155 "a lossless path should be probed: {before} → {}",
156 controller.target()
157 );
158 }
159
160 #[test]
163 fn moderate_loss_changes_nothing() {
164 let epoch = Instant::now();
165 let mut controller = controller();
166 let before = controller.target();
167
168 let mut at = epoch;
169 for _ in 0..20 {
170 at += DEFAULT_LOSS_INTERVAL;
171 controller.update(at, 5, 100);
173 }
174
175 assert_eq!(
176 before,
177 controller.target(),
178 "loss inside the band must not move the target"
179 );
180 }
181
182 #[test]
184 fn the_backoff_is_proportional_to_the_loss() {
185 let epoch = Instant::now();
186 let mut mild = controller();
187 let mut severe = controller();
188
189 let mut at = epoch;
190 for _ in 0..5 {
191 at += DEFAULT_LOSS_INTERVAL;
192 mild.update(at, 12, 100);
193 severe.update(at, 50, 100);
194 }
195
196 assert!(
197 severe.target() < mild.target(),
198 "50% loss should back off further than 12%: {} vs {}",
199 severe.target(),
200 mild.target()
201 );
202 }
203
204 #[test]
206 fn empty_feedback_changes_nothing() {
207 let epoch = Instant::now();
208 let mut controller = controller();
209 let before = controller.target();
210
211 assert_eq!(before, controller.update(epoch, 0, 0));
212 assert_eq!(None, controller.average_loss());
213 }
214
215 #[test]
218 fn the_target_stays_within_configured_bounds() {
219 let epoch = Instant::now();
220 let mut controller = LossController::new(MIN, MIN, 400_000.0);
221
222 let mut at = epoch;
223 for _ in 0..500 {
224 at += DEFAULT_LOSS_INTERVAL;
225 controller.update(at, 0, 100);
226 }
227 assert!(
228 controller.target() <= 400_000.0,
229 "ceiling breached: {}",
230 controller.target()
231 );
232
233 for _ in 0..500 {
234 at += DEFAULT_LOSS_INTERVAL;
235 controller.update(at, 90, 100);
236 }
237 assert!(
238 controller.target() >= MIN,
239 "floor breached: {}",
240 controller.target()
241 );
242 }
243}