s2n_quic_core/recovery/bbr/
full_pipe.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5    counter::{Counter, Saturating},
6    recovery::{
7        bandwidth,
8        bandwidth::Bandwidth,
9        bbr::{ApplicationSettings, BbrCongestionController},
10    },
11};
12use num_rational::Ratio;
13
14/// Estimator for determining if BBR has fully utilized its available bandwidth ("filled the pipe")
15#[derive(Debug, Default, Clone)]
16pub(crate) struct Estimator {
17    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.2
18    //# BBRInitFullPipe():
19    //#  BBR.filled_pipe = false
20    //#  BBR.full_bw = 0
21    //#  BBR.full_bw_count = 0
22
23    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.13
24    //# A boolean that records whether BBR estimates that it has ever
25    //# fully utilized its available bandwidth ("filled the pipe").
26    filled_pipe: bool,
27    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.13
28    //# A recent baseline BBR.max_bw to estimate if BBR has "filled the pipe" in Startup.
29    full_bw: Bandwidth,
30    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.13
31    //# The number of non-app-limited round trips without large increases in BBR.full_bw.
32    full_bw_count: Counter<u8, Saturating>,
33    /// The number of rounds where the ECN CE markings exceed ECN_THRESH
34    ecn_ce_rounds: Counter<u8, Saturating>,
35}
36
37impl Estimator {
38    /// Returns true if BBR estimates that is has ever fully utilized its available bandwidth
39    #[inline]
40    pub fn filled_pipe(&self) -> bool {
41        self.filled_pipe
42    }
43
44    /// Called on each new BBR round
45    #[inline]
46    pub fn on_round_start(
47        &mut self,
48        rate_sample: bandwidth::RateSample,
49        max_bw: Bandwidth,
50        ecn_ce_count_too_high: bool,
51    ) {
52        if self.filled_pipe {
53            return;
54        }
55
56        self.filled_pipe = self.bandwidth_plateaued(rate_sample, max_bw)
57            || self.excessive_explicit_congestion(ecn_ce_count_too_high);
58    }
59
60    /// Called on each new loss round
61    ///
62    /// Excessive inflight is checked at the end of a loss round, not a regular round, as done
63    /// in tcp_bbr2.c/bbr2_check_loss_too_high_in_startup
64    ///
65    /// See https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2133
66    #[inline]
67    pub fn on_loss_round_start(
68        &mut self,
69        rate_sample: bandwidth::RateSample,
70        loss_bursts_in_round: u8,
71        max_datagram_size: u16,
72        app_settings: &ApplicationSettings,
73    ) {
74        if self.filled_pipe {
75            return;
76        }
77
78        self.filled_pipe = self.excessive_inflight(
79            rate_sample,
80            loss_bursts_in_round,
81            max_datagram_size,
82            app_settings,
83        );
84    }
85
86    /// Determines if the rate of increase of bandwidth has decreased enough to estimate the
87    /// available bandwidth has been fully utilized.
88    ///
89    /// Based on bbr_check_full_bw_reached in tcp_bbr2.c
90    #[inline]
91    fn bandwidth_plateaued(
92        &mut self,
93        rate_sample: bandwidth::RateSample,
94        max_bw: Bandwidth,
95    ) -> bool {
96        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.2
97        //# BBRCheckStartupFullBandwidth():
98        //#   if BBR.filled_pipe or
99        //#     !BBR.round_start or rs.is_app_limited
100        //#    return  /* no need to check for a full pipe now */
101        //#   if (BBR.max_bw >= BBR.full_bw * 1.25)  /* still growing? */
102        //#     BBR.full_bw = BBR.max_bw    /* record new baseline level */
103        //#     BBR.full_bw_count = 0
104        //#   return
105        //#   BBR.full_bw_count++ /* another round w/o much growth */
106        //#   if (BBR.full_bw_count >= 3)
107        //#     BBR.filled_pipe = true
108
109        //# If BBR notices that there are several (three) rounds where attempts to double
110        //# the delivery rate actually result in little increase (less than 25 percent),
111        //# then it estimates that it has reached BBR.max_bw, sets BBR.filled_pipe to true,
112        //# exits Startup and enters Drain.
113        const DELIVERY_RATE_INCREASE: Ratio<u64> = Ratio::new_raw(5, 4); // 1.25
114        const BANDWIDTH_PLATEAU_ROUND_COUNT: u8 = 3;
115
116        if rate_sample.is_app_limited {
117            //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.2
118            //# Once per round trip, upon an ACK that acknowledges new data, and when
119            //# the delivery rate sample is not application-limited (see [draft-
120            //# cheng-iccrg-delivery-rate-estimation]), BBR runs the "full pipe" estimator
121            return false;
122        }
123
124        if max_bw >= self.full_bw * DELIVERY_RATE_INCREASE {
125            // still growing?
126            self.full_bw = max_bw; // record new baseline level
127            self.full_bw_count = Counter::default(); // restart the count
128            return false;
129        }
130
131        /* another round w/o much growth */
132        self.full_bw_count += 1;
133
134        // Bandwidth has plateaued if the number of rounds without much growth
135        // reaches `BANDWIDTH_PLATEAU_ROUND_COUNT`
136        self.full_bw_count >= BANDWIDTH_PLATEAU_ROUND_COUNT
137    }
138
139    /// Determines if inflight has been too high (due to either loss or ECN markings) and enough
140    /// distinct loss bursts have been observed to estimate the available bandwidth has been fully utilized.
141    #[inline]
142    fn excessive_inflight(
143        &mut self,
144        rate_sample: bandwidth::RateSample,
145        loss_bursts_in_round: u8,
146        max_datagram_size: u16,
147        app_settings: &ApplicationSettings,
148    ) -> bool {
149        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.3
150        //# A second method BBR uses for estimating the bottleneck is full is by looking at sustained
151        //# packet losses Specifically for a case where the following criteria are all met:
152        //#
153        //#    *  The connection has been in fast recovery for at least one full round trip.
154        //#    *  The loss rate over the time scale of a single full round trip exceeds BBRLossThresh (2%).
155
156        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.3
157        //= type=exception
158        //= reason=Chromium and Linux TCP BBRv2 both use 8 lost bursts in a round trip
159        //#    *  There are at least BBRStartupFullLossCnt=3 discontiguous sequence ranges lost in that round trip.
160
161        // See https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2325-L2329
162        // and https://source.chromium.org/chromium/chromium/src/+/main:net/third_party/quiche/src/quiche/quic/core/quic_protocol_flags_list.h;l=135;bpv=1;bpt=0
163        const STARTUP_FULL_LOSS_COUNT: u8 = 8;
164
165        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.3
166        //= type=exception
167        //= reason=The BBRv2 RFC reference to "fast recovery" here is more applicable to TCP.
168        //#    *  The connection has been in fast recovery for at least one full round trip.
169
170        // The BBRv2 RFC reference to "fast recovery" here seems more applicable to TCP, rather than
171        // QUIC. Instead, we just consider the loss rate and number of loss bursts over the round trip.
172        // This is more in line with the Chromium BBRv2 implementation.
173        // See: https://source.chromium.org/chromium/chromium/src/+/main:net/third_party/quiche/src/quiche/quic/core/congestion_control/bbr2_startup.cc;l=104
174
175        if loss_bursts_in_round < STARTUP_FULL_LOSS_COUNT {
176            // is_inflight_too_high returns true when ECN CE markings exceed the threshold, even
177            // if the loss burst count is below the threshold. During startup, excessive ECN CE
178            // is separately checked over multiple rounds, so return immediately if we have
179            // not seen enough loss bursts. This follows the Linux TCP BBRv2 implementation
180            // See https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2150
181            return false;
182        }
183
184        BbrCongestionController::is_inflight_too_high(
185            rate_sample,
186            max_datagram_size,
187            loss_bursts_in_round,
188            STARTUP_FULL_LOSS_COUNT,
189            app_settings,
190        )
191    }
192
193    /// Determines if enough consecutive rounds of explicit congestion have been encountered that we
194    /// can estimate the available bandwidth has been fully utilized.
195    ///
196    /// Based on bbr2_check_ecn_too_high_in_startup from https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L1372
197    fn excessive_explicit_congestion(&mut self, ecn_ce_count_too_high: bool) -> bool {
198        // Startup is exited if the number of consecutive round trips with ECN CE markings above
199        // the ECN_THRESH exceed this value
200        // Value from https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2334
201        const STARTUP_FULL_ECN_COUNT: u8 = 2;
202
203        if ecn_ce_count_too_high {
204            self.ecn_ce_rounds += 1;
205        } else {
206            self.ecn_ce_rounds = Counter::default();
207        }
208
209        self.ecn_ce_rounds >= STARTUP_FULL_ECN_COUNT
210    }
211
212    #[cfg(test)]
213    pub fn set_filled_pipe_for_test(&mut self, filled_pipe: bool) {
214        self.filled_pipe = filled_pipe;
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::{
222        path::MINIMUM_MAX_DATAGRAM_SIZE,
223        recovery::{bandwidth::RateSample, bbr::full_pipe},
224    };
225    use core::time::Duration;
226
227    #[test]
228    fn bandwidth_plateau() {
229        let mut fp_estimator = full_pipe::Estimator::default();
230        let rate_sample = RateSample::default();
231        let mut max_bw = Bandwidth::new(1000, Duration::from_secs(1));
232        fp_estimator.on_round_start(rate_sample, max_bw, false);
233
234        // Grow at 25% over 3 rounds
235        max_bw = max_bw * Ratio::new(5, 4); // 5/4 = 125%
236        for _ in 0..3 {
237            fp_estimator.on_round_start(rate_sample, max_bw, false);
238        }
239        // The pipe has not been filled yet since we have continued to grow bandwidth
240        assert!(!fp_estimator.filled_pipe());
241
242        // One more round with 24% growth, not growing fast enough to continue
243        max_bw = max_bw * Ratio::new(31, 25); // 31/25 = 124%
244        fp_estimator.on_round_start(rate_sample, max_bw, false);
245        // The pipe is considered full
246        assert!(fp_estimator.filled_pipe());
247    }
248
249    #[test]
250    fn bandwidth_plateau_app_limited() {
251        let mut fp_estimator = full_pipe::Estimator::default();
252        let rate_sample = RateSample {
253            is_app_limited: true,
254            ..Default::default()
255        };
256        let max_bw = Bandwidth::new(1000, Duration::from_secs(1));
257
258        // No growth, but app limited
259        for _ in 0..3 {
260            fp_estimator.on_round_start(rate_sample, max_bw, false);
261        }
262
263        // The pipe has not been filled yet since we were app limited
264        assert!(!fp_estimator.filled_pipe());
265    }
266
267    #[test]
268    fn excessive_inflight_due_to_loss() {
269        let mut fp_estimator = full_pipe::Estimator::default();
270        let rate_sample = RateSample {
271            // Set app_limited to true to ignore bandwidth plateau check
272            is_app_limited: true,
273            // More than 2% bytes lost
274            bytes_in_flight: 1000,
275            lost_bytes: 21,
276            ..Default::default()
277        };
278
279        // Only 7 loss bursts, not enough to be considered excessive loss
280        fp_estimator.on_loss_round_start(
281            rate_sample,
282            7,
283            MINIMUM_MAX_DATAGRAM_SIZE,
284            &Default::default(),
285        );
286        // The pipe has not been filled yet since there were only 2 loss bursts
287        assert!(!fp_estimator.filled_pipe());
288
289        // 3 loss bursts, enough to be considered excessive loss
290        fp_estimator.on_loss_round_start(
291            rate_sample,
292            8,
293            MINIMUM_MAX_DATAGRAM_SIZE,
294            &Default::default(),
295        );
296        // The pipe has been filled due to loss
297        assert!(fp_estimator.filled_pipe());
298    }
299
300    #[test]
301    fn excessive_inflight_due_to_ecn_ce() {
302        let mut fp_estimator = full_pipe::Estimator::default();
303        let rate_sample = RateSample {
304            // Set app_limited to true to ignore bandwidth plateau check
305            is_app_limited: true,
306            // >= ECN_THRESH (50%) of packets had ECN CE markings
307            ecn_ce_count: 5,
308            delivered_bytes: 9 * MINIMUM_MAX_DATAGRAM_SIZE as u64,
309            ..Default::default()
310        };
311
312        // Only 7 loss bursts, not enough to be considered excessive loss
313        fp_estimator.on_loss_round_start(
314            rate_sample,
315            7,
316            MINIMUM_MAX_DATAGRAM_SIZE,
317            &Default::default(),
318        );
319
320        fp_estimator.on_loss_round_start(
321            rate_sample,
322            8,
323            MINIMUM_MAX_DATAGRAM_SIZE,
324            &Default::default(),
325        );
326        // The pipe has been filled due to ECN
327        assert!(fp_estimator.filled_pipe());
328    }
329
330    #[test]
331    fn excessive_inflight_loss_rate_too_low() {
332        let mut fp_estimator = full_pipe::Estimator::default();
333        let rate_sample = RateSample {
334            // Set app_limited to true to ignore bandwidth plateau check
335            is_app_limited: true,
336            // 2% bytes lost, just below the threshold to be considered excessive
337            bytes_in_flight: 1000,
338            lost_bytes: 2,
339            ..Default::default()
340        };
341        // 8 loss bursts, enough to be considered excessive loss
342        fp_estimator.on_loss_round_start(
343            rate_sample,
344            8,
345            MINIMUM_MAX_DATAGRAM_SIZE,
346            &Default::default(),
347        );
348        // The pipe has not been filled yet since the loss rate was not high enough
349        assert!(!fp_estimator.filled_pipe());
350    }
351
352    #[test]
353    fn excessive_explicit_congestion() {
354        let mut fp_estimator = full_pipe::Estimator::default();
355        let rate_sample = RateSample {
356            // Set app_limited to true to ignore bandwidth plateau check
357            is_app_limited: true,
358            ..Default::default()
359        };
360
361        let max_bw = Bandwidth::new(1000, Duration::from_secs(1));
362
363        fp_estimator.on_round_start(rate_sample, max_bw, true);
364        // The pipe has not been filled yet since there was only one round with high ECN CE markings
365        assert!(!fp_estimator.filled_pipe());
366
367        fp_estimator.on_round_start(rate_sample, max_bw, false);
368        fp_estimator.on_round_start(rate_sample, max_bw, true);
369        // The pipe has not been filled yet since the low ecn rate sample reset the count,
370        // ie the high ecn rate samples were not contiguous
371        assert!(!fp_estimator.filled_pipe());
372
373        fp_estimator.on_round_start(rate_sample, max_bw, true);
374        // After two consecutive rounds of high ECN markings, the pipe is full
375        assert!(fp_estimator.filled_pipe());
376    }
377}