rtc_interceptor/gcc/
threshold.rs1use std::time::{Duration, Instant};
4
5pub const DEFAULT_INITIAL_MS: f64 = 12.5;
7
8#[derive(Debug, Clone, Copy)]
22pub struct AdaptiveThreshold {
23 value_ms: f64,
25 increase_gain: f64,
27 decrease_gain: f64,
29 last_update: Option<Instant>,
31}
32
33impl Default for AdaptiveThreshold {
34 fn default() -> Self {
35 Self {
36 value_ms: DEFAULT_INITIAL_MS,
37 increase_gain: 0.01,
38 decrease_gain: 0.00018,
39 last_update: None,
40 }
41 }
42}
43
44impl AdaptiveThreshold {
45 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn value_ms(&self) -> f64 {
52 self.value_ms
53 }
54
55 pub fn update(&mut self, now: Instant, estimate_ms: f64) -> f64 {
60 let elapsed = match self.last_update {
61 Some(last) => now.saturating_duration_since(last),
62 None => {
63 self.last_update = Some(now);
64 return self.value_ms;
65 }
66 };
67 self.last_update = Some(now);
68
69 let magnitude = estimate_ms.abs();
70
71 if magnitude > self.value_ms + 15.0 {
75 return self.value_ms;
76 }
77
78 let gain = if magnitude > self.value_ms {
79 self.increase_gain
80 } else {
81 self.decrease_gain
82 };
83
84 let elapsed_ms = elapsed.as_secs_f64() * 1_000.0;
87 let step = gain * (magnitude - self.value_ms) * elapsed_ms.min(100.0);
88 self.value_ms = (self.value_ms + step).clamp(6.0, 600.0);
89
90 self.value_ms
91 }
92
93 pub fn since_update(&self, now: Instant) -> Option<Duration> {
95 self.last_update
96 .map(|last| now.saturating_duration_since(last))
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn it_starts_at_the_drafts_value() {
106 assert_eq!(DEFAULT_INITIAL_MS, AdaptiveThreshold::new().value_ms());
107 }
108
109 #[test]
112 fn a_trend_outside_the_threshold_raises_it() {
113 let epoch = Instant::now();
114 let mut threshold = AdaptiveThreshold::new();
115 threshold.update(epoch, 20.0);
116
117 for step in 1..=50u64 {
118 threshold.update(epoch + Duration::from_millis(step * 20), 20.0);
119 }
120
121 assert!(
122 threshold.value_ms() > DEFAULT_INITIAL_MS,
123 "threshold should have risen, got {}",
124 threshold.value_ms()
125 );
126 }
127
128 #[test]
130 fn a_trend_inside_the_threshold_lowers_it() {
131 let epoch = Instant::now();
132 let mut threshold = AdaptiveThreshold::new();
133 threshold.update(epoch, 0.0);
134
135 for step in 1..=500u64 {
136 threshold.update(epoch + Duration::from_millis(step * 20), 0.0);
137 }
138
139 assert!(
140 threshold.value_ms() < DEFAULT_INITIAL_MS,
141 "threshold should have fallen, got {}",
142 threshold.value_ms()
143 );
144 }
145
146 #[test]
149 fn it_rises_faster_than_it_falls() {
150 let epoch = Instant::now();
151
152 let mut rising = AdaptiveThreshold::new();
153 rising.update(epoch, 25.0);
154 let mut falling = AdaptiveThreshold::new();
155 falling.update(epoch, 0.0);
156
157 for step in 1..=25u64 {
158 let at = epoch + Duration::from_millis(step * 20);
159 rising.update(at, 25.0);
160 falling.update(at, 0.0);
161 }
162
163 let rose = rising.value_ms() - DEFAULT_INITIAL_MS;
164 let fell = DEFAULT_INITIAL_MS - falling.value_ms();
165 assert!(
166 rose > fell,
167 "K_u must exceed K_d: rose by {rose}, fell by {fell}"
168 );
169 }
170
171 #[test]
174 fn an_outlier_does_not_move_it() {
175 let epoch = Instant::now();
176 let mut threshold = AdaptiveThreshold::new();
177 threshold.update(epoch, 0.0);
178
179 let before = threshold.value_ms();
180 threshold.update(epoch + Duration::from_millis(20), 5_000.0);
181
182 assert_eq!(
183 before,
184 threshold.value_ms(),
185 "an absurd sample must be ignored, not absorbed"
186 );
187 }
188
189 #[test]
192 fn it_stays_within_bounds() {
193 let epoch = Instant::now();
194
195 let mut low = AdaptiveThreshold::new();
196 low.update(epoch, 0.0);
197 for step in 1..=100_000u64 {
198 low.update(epoch + Duration::from_millis(step * 20), 0.0);
199 }
200 assert!(low.value_ms() >= 6.0, "floor breached: {}", low.value_ms());
201
202 let mut high = AdaptiveThreshold::new();
203 high.update(epoch, 0.0);
204 for step in 1..=100_000u64 {
205 let target = high.value_ms() + 1.0;
207 high.update(epoch + Duration::from_millis(step * 20), target);
208 }
209 assert!(
210 high.value_ms() <= 600.0,
211 "ceiling breached: {}",
212 high.value_ms()
213 );
214 }
215}