rtc_interceptor/gcc/
arrival_group.rs1use crate::rtpfb::acknowledgement::PacketReport;
4use std::time::{Duration, Instant};
5
6pub const DEFAULT_BURST_INTERVAL: Duration = Duration::from_millis(5);
12
13#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct ArrivalGroup {
16 pub first_departure: Instant,
18 pub departure: Instant,
20 pub arrival: Duration,
22 pub packets: usize,
24 pub size: usize,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq)]
34pub struct InterGroupDelay {
35 pub delta_ms: f64,
37 pub at: Instant,
39 pub size: usize,
41}
42
43#[derive(Debug, Clone)]
52pub struct ArrivalGroupAccumulator {
53 burst_interval: Duration,
54 current: Option<ArrivalGroup>,
55 previous: Option<ArrivalGroup>,
56}
57
58impl Default for ArrivalGroupAccumulator {
59 fn default() -> Self {
60 Self::new(DEFAULT_BURST_INTERVAL)
61 }
62}
63
64impl ArrivalGroupAccumulator {
65 pub fn new(burst_interval: Duration) -> Self {
67 Self {
68 burst_interval,
69 current: None,
70 previous: None,
71 }
72 }
73
74 pub fn accumulate(&mut self, report: &PacketReport) -> Option<InterGroupDelay> {
79 let arrival = report.arrival?;
80 if !report.arrived {
81 return None;
82 }
83
84 let Some(current) = self.current.as_mut() else {
85 self.current = Some(ArrivalGroup {
86 first_departure: report.departure,
87 departure: report.departure,
88 arrival,
89 packets: 1,
90 size: report.size,
91 });
92 return None;
93 };
94
95 if report
97 .departure
98 .saturating_duration_since(current.first_departure)
99 <= self.burst_interval
100 {
101 current.departure = current.departure.max(report.departure);
102 current.arrival = current.arrival.max(arrival);
103 current.packets += 1;
104 current.size += report.size;
105 return None;
106 }
107
108 let closed = *current;
110 self.current = Some(ArrivalGroup {
111 first_departure: report.departure,
112 departure: report.departure,
113 arrival,
114 packets: 1,
115 size: report.size,
116 });
117
118 let measurement = self.previous.map(|previous| gradient(&previous, &closed));
119 self.previous = Some(closed);
120 measurement
121 }
122
123 pub fn flush(&mut self) -> Option<InterGroupDelay> {
129 let closed = self.current.take()?;
130 let measurement = self.previous.map(|previous| gradient(&previous, &closed));
131 self.previous = Some(closed);
132 measurement
133 }
134}
135
136fn gradient(previous: &ArrivalGroup, current: &ArrivalGroup) -> InterGroupDelay {
138 let arrival_delta = current.arrival.as_secs_f64() - previous.arrival.as_secs_f64();
139 let departure_delta = current
140 .departure
141 .saturating_duration_since(previous.departure)
142 .as_secs_f64();
143
144 InterGroupDelay {
145 delta_ms: (arrival_delta - departure_delta) * 1_000.0,
146 at: current.departure,
147 size: current.size,
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use rtcp::transport_feedbacks::cc_feedback_report::Ecn;
155
156 fn report(departure: Instant, arrival_ms: u64, size: usize) -> PacketReport {
157 PacketReport {
158 ssrc: 1,
159 id: 0,
160 rtp_sequence_number: 0,
161 is_twcc: true,
162 twcc_sequence_number: 0,
163 size,
164 arrived: true,
165 departure,
166 arrival: Some(Duration::from_millis(arrival_ms)),
167 ecn: Ecn::default(),
168 }
169 }
170
171 #[test]
173 fn packets_sent_together_form_one_group() {
174 let epoch = Instant::now();
175 let mut accumulator = ArrivalGroupAccumulator::default();
176
177 for offset in [0, 1, 2, 3, 4] {
178 assert_eq!(
179 None,
180 accumulator.accumulate(&report(epoch + Duration::from_millis(offset), 100, 1200)),
181 "nothing is emitted until a group closes"
182 );
183 }
184
185 assert_eq!(
187 None,
188 accumulator.accumulate(&report(epoch + Duration::from_millis(20), 120, 1200))
189 );
190 let group = accumulator
191 .flush()
192 .expect("the second group closes against the first");
193 assert_eq!(1200, group.size, "the second group holds one packet");
194 }
195
196 #[test]
199 fn a_path_that_does_not_queue_has_a_zero_gradient() {
200 let epoch = Instant::now();
201 let mut accumulator = ArrivalGroupAccumulator::default();
202 let mut gradients = Vec::new();
203
204 for burst in 0..5u64 {
206 let departure = epoch + Duration::from_millis(burst * 20);
207 if let Some(delay) = accumulate_and_flush(&mut accumulator, departure, 100 + burst * 20)
208 {
209 gradients.push(delay.delta_ms);
210 }
211 }
212
213 assert!(
214 gradients.iter().all(|delta| delta.abs() < 1e-6),
215 "a non-queueing path must measure zero delay gradient: {gradients:?}"
216 );
217 }
218
219 #[test]
221 fn a_growing_queue_has_a_positive_gradient() {
222 let epoch = Instant::now();
223 let mut accumulator = ArrivalGroupAccumulator::default();
224 let mut gradients = Vec::new();
225
226 for burst in 0..5u64 {
228 let departure = epoch + Duration::from_millis(burst * 20);
229 if let Some(delay) = accumulate_and_flush(&mut accumulator, departure, 100 + burst * 30)
230 {
231 gradients.push(delay.delta_ms);
232 }
233 }
234
235 assert!(!gradients.is_empty(), "no gradients were produced");
236 assert!(
237 gradients.iter().all(|delta| (*delta - 10.0).abs() < 1e-6),
238 "each group should measure 10 ms of added delay: {gradients:?}"
239 );
240 }
241
242 #[test]
244 fn a_draining_queue_has_a_negative_gradient() {
245 let epoch = Instant::now();
246 let mut accumulator = ArrivalGroupAccumulator::default();
247 let mut gradients = Vec::new();
248
249 for burst in 0..5u64 {
250 let departure = epoch + Duration::from_millis(burst * 20);
251 if let Some(delay) = accumulate_and_flush(&mut accumulator, departure, 200 + burst * 15)
252 {
253 gradients.push(delay.delta_ms);
254 }
255 }
256
257 assert!(
258 gradients.iter().all(|delta| *delta < 0.0),
259 "a draining queue must measure negative: {gradients:?}"
260 );
261 }
262
263 #[test]
265 fn lost_packets_are_not_measured() {
266 let epoch = Instant::now();
267 let mut accumulator = ArrivalGroupAccumulator::default();
268
269 let mut lost = report(epoch, 0, 1200);
270 lost.arrived = false;
271 lost.arrival = None;
272
273 assert_eq!(None, accumulator.accumulate(&lost));
274 assert_eq!(
275 None,
276 accumulator.flush(),
277 "a lost packet must not open a group"
278 );
279 }
280
281 fn accumulate_and_flush(
283 accumulator: &mut ArrivalGroupAccumulator,
284 departure: Instant,
285 arrival_ms: u64,
286 ) -> Option<InterGroupDelay> {
287 accumulator.accumulate(&report(departure, arrival_ms, 1200))
288 }
289}