Skip to main content

quiche/recovery/gcongestion/bbr2/
probe_bw.rs

1// Copyright (c) 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// Copyright (C) 2023, Cloudflare, Inc.
6// All rights reserved.
7//
8// Redistribution and use in source and binary forms, with or without
9// modification, are permitted provided that the following conditions are
10// met:
11//
12//     * Redistributions of source code must retain the above copyright notice,
13//       this list of conditions and the following disclaimer.
14//
15//     * Redistributions in binary form must reproduce the above copyright
16//       notice, this list of conditions and the following disclaimer in the
17//       documentation and/or other materials provided with the distribution.
18//
19// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
20// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
21// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
22// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
23// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
24// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
25// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31use std::ops::Add;
32use std::time::Duration;
33use std::time::Instant;
34
35use crate::recovery::gcongestion::bbr2::Params;
36use crate::recovery::gcongestion::Acked;
37use crate::recovery::gcongestion::Lost;
38use crate::recovery::RecoveryStats;
39
40use super::mode::Cycle;
41use super::mode::CyclePhase;
42use super::mode::Mode;
43use super::mode::ModeImpl;
44use super::network_model::BBRv2NetworkModel;
45use super::network_model::DEFAULT_MSS;
46use super::BBRv2CongestionEvent;
47use super::BwLoMode;
48use super::Limits;
49
50#[derive(Debug)]
51pub(super) struct ProbeBW {
52    pub(super) model: BBRv2NetworkModel,
53    pub(super) cycle: Cycle,
54}
55
56#[derive(PartialEq, PartialOrd)]
57enum AdaptUpperBoundsResult {
58    AdaptedOk,
59    AdaptedProbedTooHigh,
60    NotAdaptedInflightHighNotSet,
61    NotAdaptedInvalidSample,
62}
63
64impl ModeImpl for ProbeBW {
65    #[cfg(feature = "qlog")]
66    fn state_str(&self) -> &'static str {
67        match self.cycle.phase {
68            CyclePhase::NotStarted => unreachable!(),
69            CyclePhase::Up => "bbr_probe_bw_up",
70            CyclePhase::Down => "bbr_probe_bw_down",
71            CyclePhase::Cruise => "bbr_probe_bw_cruise",
72            CyclePhase::Refill => "bbr_probe_bw_refill",
73        }
74    }
75
76    fn enter(
77        &mut self, now: Instant,
78        _congestion_event: Option<&BBRv2CongestionEvent>, params: &Params,
79    ) {
80        self.cycle.start_time = now;
81
82        match self.cycle.phase {
83            CyclePhase::NotStarted => {
84                // First time entering PROBE_BW. Start a new probing cycle.
85                self.enter_probe_down(false, false, now, params)
86            },
87            CyclePhase::Cruise => self.enter_probe_cruise(now),
88            CyclePhase::Refill =>
89                self.enter_probe_refill(self.cycle.probe_up_rounds, now),
90            CyclePhase::Up | CyclePhase::Down => {},
91        }
92    }
93
94    fn on_congestion_event(
95        mut self, prior_in_flight: usize, event_time: Instant, _: &[Acked],
96        _: &[Lost], congestion_event: &mut BBRv2CongestionEvent,
97        target_bytes_inflight: usize, params: &Params,
98        _recovery_stats: &mut RecoveryStats, _cwnd: usize,
99    ) -> Mode {
100        if congestion_event.end_of_round_trip {
101            if self.cycle.start_time != event_time {
102                self.cycle.rounds_since_probe += 1;
103            }
104
105            if self.cycle.phase_start_time != event_time {
106                self.cycle.rounds_in_phase += 1;
107            }
108        }
109
110        let mut switch_to_probe_rtt = false;
111
112        match self.cycle.phase {
113            CyclePhase::NotStarted => unreachable!(),
114            CyclePhase::Up => self.update_probe_up(
115                prior_in_flight,
116                target_bytes_inflight,
117                congestion_event,
118                params,
119            ),
120            CyclePhase::Down => {
121                self.update_probe_down(
122                    target_bytes_inflight,
123                    congestion_event,
124                    params,
125                );
126                if self.cycle.phase != CyclePhase::Down &&
127                    self.model.maybe_expire_min_rtt(congestion_event, params)
128                {
129                    switch_to_probe_rtt = true;
130                }
131            },
132            CyclePhase::Cruise => self.update_probe_cruise(
133                target_bytes_inflight,
134                congestion_event,
135                params,
136            ),
137            CyclePhase::Refill => self.update_probe_refill(
138                target_bytes_inflight,
139                congestion_event,
140                params,
141            ),
142        }
143
144        // Do not need to set the gains if switching to PROBE_RTT, they will be
145        // set when `ProbeRTT::enter` is called.
146        if !switch_to_probe_rtt {
147            self.model
148                .set_pacing_gain(self.cycle.phase.pacing_gain(params));
149            self.model.set_cwnd_gain(self.cycle.phase.cwnd_gain(params));
150        }
151
152        if switch_to_probe_rtt {
153            self.into_probe_rtt(event_time, Some(congestion_event), params)
154        } else {
155            Mode::ProbeBW(self)
156        }
157    }
158
159    fn get_cwnd_limits(&self, params: &Params) -> Limits<usize> {
160        if self.cycle.phase == CyclePhase::Cruise {
161            let limit = self
162                .model
163                .inflight_lo()
164                .min(self.model.inflight_hi_with_headroom(params));
165            return Limits::no_greater_than(limit);
166        }
167
168        if self.cycle.phase == CyclePhase::Up &&
169            params.probe_up_ignore_inflight_hi
170        {
171            // Similar to STARTUP.
172            return Limits::no_greater_than(self.model.inflight_lo());
173        }
174
175        Limits::no_greater_than(
176            self.model.inflight_lo().min(self.model.inflight_hi()),
177        )
178    }
179
180    fn is_probing_for_bandwidth(&self) -> bool {
181        self.cycle.phase == CyclePhase::Refill ||
182            self.cycle.phase == CyclePhase::Up
183    }
184
185    fn on_exit_quiescence(
186        mut self, now: Instant, quiescence_start_time: Instant, _params: &Params,
187    ) -> Mode {
188        self.model
189            .postpone_min_rtt_timestamp(now - quiescence_start_time);
190        Mode::ProbeBW(self)
191    }
192
193    fn leave(
194        &mut self, _now: Instant,
195        _congestion_event: Option<&BBRv2CongestionEvent>,
196    ) {
197    }
198}
199
200impl ProbeBW {
201    fn enter_probe_down(
202        &mut self, probed_too_high: bool, stopped_risky_probe: bool,
203        now: Instant, params: &Params,
204    ) {
205        let cycle = &mut self.cycle;
206        cycle.last_cycle_probed_too_high = probed_too_high;
207        cycle.last_cycle_stopped_risky_probe = stopped_risky_probe;
208
209        cycle.phase = CyclePhase::Down;
210        cycle.start_time = now;
211        cycle.phase_start_time = now;
212        cycle.rounds_in_phase = 0;
213
214        if params.bw_lo_mode != BwLoMode::Default {
215            // Clear `bandwidth_lo` if it was set in PROBE_UP because losses in
216            // that phase should not change it permanently.
217            // A valid REFILL value for `bandwidth_lo` is quickly rediscovered.
218            self.model.clear_bandwidth_lo();
219        }
220
221        // TODO(vlad): Choose a probe wait time.
222        cycle.rounds_since_probe = 0;
223        cycle.probe_wait_time = Some(
224            params.probe_bw_probe_base_duration + Duration::from_micros(500),
225        );
226
227        cycle.probe_up_bytes = None;
228        cycle.probe_up_app_limited_since_inflight_hi_limited = false;
229        cycle.has_advanced_max_bw = false;
230        self.model.restart_round_early();
231    }
232
233    fn enter_probe_cruise(&mut self, now: Instant) {
234        if self.cycle.phase == CyclePhase::Down {
235            self.exit_probe_down();
236        }
237
238        let cycle = &mut self.cycle;
239
240        self.model.cap_inflight_lo(self.model.inflight_hi());
241        cycle.phase = CyclePhase::Cruise;
242        cycle.phase_start_time = now;
243        cycle.rounds_in_phase = 0;
244        cycle.is_sample_from_probing = false;
245    }
246
247    fn enter_probe_refill(&mut self, probe_up_rounds: usize, now: Instant) {
248        if self.cycle.phase == CyclePhase::Down {
249            self.exit_probe_down();
250        }
251
252        let cycle = &mut self.cycle;
253
254        cycle.phase = CyclePhase::Refill;
255        cycle.phase_start_time = now;
256        cycle.rounds_in_phase = 0;
257
258        cycle.is_sample_from_probing = false;
259        cycle.last_cycle_stopped_risky_probe = false;
260
261        self.model.clear_bandwidth_lo();
262        self.model.clear_inflight_lo();
263        cycle.probe_up_rounds = probe_up_rounds;
264        cycle.probe_up_acked = 0;
265        self.model.restart_round_early();
266    }
267
268    fn enter_probe_up(&mut self, now: Instant, cwnd: usize) {
269        let cycle = &mut self.cycle;
270
271        cycle.phase = CyclePhase::Up;
272        cycle.phase_start_time = now;
273        cycle.rounds_in_phase = 0;
274        cycle.is_sample_from_probing = true;
275        self.raise_inflight_high_slope(cwnd);
276        self.model.restart_round_early();
277    }
278
279    fn exit_probe_down(&mut self) {
280        if !self.cycle.has_advanced_max_bw {
281            self.model.advance_max_bandwidth_filter();
282            self.cycle.has_advanced_max_bw = true;
283        }
284    }
285
286    fn update_probe_down(
287        &mut self, target_bytes_inflight: usize,
288        congestion_event: &BBRv2CongestionEvent, params: &Params,
289    ) {
290        if self.cycle.rounds_in_phase == 1 && congestion_event.end_of_round_trip {
291            self.cycle.is_sample_from_probing = false;
292
293            if !congestion_event.last_packet_send_state.is_app_limited {
294                self.model.advance_max_bandwidth_filter();
295                self.cycle.has_advanced_max_bw = true;
296            }
297
298            if self.cycle.last_cycle_stopped_risky_probe &&
299                !self.cycle.last_cycle_probed_too_high
300            {
301                self.enter_probe_refill(0, congestion_event.event_time);
302                return;
303            }
304        }
305
306        self.maybe_adapt_upper_bounds(
307            target_bytes_inflight,
308            congestion_event,
309            params,
310        );
311
312        if self.is_time_to_probe_bandwidth(
313            target_bytes_inflight,
314            congestion_event,
315            params,
316        ) {
317            self.enter_probe_refill(0, congestion_event.event_time);
318            return;
319        }
320
321        // This exit condition is experimental code from Google quiche which
322        // diverges from the RFC. Use `disable_probe_down_early_exit` to
323        // override the behavior.
324        if self.has_stayed_long_enough_in_probe_down(congestion_event, params) {
325            self.enter_probe_cruise(congestion_event.event_time);
326            return;
327        }
328
329        let inflight_with_headroom = self.model.inflight_hi_with_headroom(params);
330        let bytes_in_flight = congestion_event.bytes_in_flight;
331
332        if bytes_in_flight > inflight_with_headroom {
333            // Stay in PROBE_DOWN.
334            return;
335        }
336
337        // Transition to PROBE_CRUISE iff we've drained to target.
338        let bdp = self.model.bdp0();
339
340        if bytes_in_flight < bdp {
341            self.enter_probe_cruise(congestion_event.event_time);
342        }
343    }
344
345    fn update_probe_cruise(
346        &mut self, target_bytes_inflight: usize,
347        congestion_event: &BBRv2CongestionEvent, params: &Params,
348    ) {
349        self.maybe_adapt_upper_bounds(
350            target_bytes_inflight,
351            congestion_event,
352            params,
353        );
354
355        if self.is_time_to_probe_bandwidth(
356            target_bytes_inflight,
357            congestion_event,
358            params,
359        ) {
360            self.enter_probe_refill(0, congestion_event.event_time);
361        }
362    }
363
364    fn update_probe_refill(
365        &mut self, target_bytes_inflight: usize,
366        congestion_event: &BBRv2CongestionEvent, params: &Params,
367    ) {
368        self.maybe_adapt_upper_bounds(
369            target_bytes_inflight,
370            congestion_event,
371            params,
372        );
373
374        if self.cycle.rounds_in_phase > 0 && congestion_event.end_of_round_trip {
375            self.enter_probe_up(
376                congestion_event.event_time,
377                congestion_event.prior_cwnd,
378            );
379        }
380    }
381
382    fn update_probe_up(
383        &mut self, prior_in_flight: usize, target_bytes_inflight: usize,
384        congestion_event: &BBRv2CongestionEvent, params: &Params,
385    ) {
386        if self.maybe_adapt_upper_bounds(
387            target_bytes_inflight,
388            congestion_event,
389            params,
390        ) == AdaptUpperBoundsResult::AdaptedProbedTooHigh
391        {
392            self.enter_probe_down(
393                true,
394                false,
395                congestion_event.event_time,
396                params,
397            );
398            return;
399        }
400
401        self.probe_inflight_high_upward(congestion_event, params);
402
403        let mut is_risky = false;
404        let mut is_queuing = false;
405        if self.cycle.last_cycle_probed_too_high &&
406            prior_in_flight >= self.model.inflight_hi()
407        {
408            is_risky = true;
409        } else if self.cycle.rounds_in_phase > 0 {
410            if params.max_probe_up_queue_rounds > 0 {
411                if congestion_event.end_of_round_trip {
412                    self.model
413                        .check_persistent_queue(params.full_bw_threshold, params);
414                    if self.model.rounds_with_queueing() >=
415                        params.max_probe_up_queue_rounds
416                    {
417                        is_queuing = true;
418                    }
419                }
420            } else {
421                let mut queuing_threshold_extra_bytes =
422                    self.model.queueing_threshold_extra_bytes();
423                if params.add_ack_height_to_queueing_threshold {
424                    queuing_threshold_extra_bytes += self.model.max_ack_height();
425                }
426                let queuing_threshold = (params.full_bw_threshold *
427                    self.model.bdp0() as f32)
428                    as usize +
429                    queuing_threshold_extra_bytes;
430
431                is_queuing =
432                    congestion_event.bytes_in_flight >= queuing_threshold;
433            }
434        }
435
436        if is_risky || is_queuing {
437            self.enter_probe_down(
438                false,
439                is_risky,
440                congestion_event.event_time,
441                params,
442            );
443        }
444    }
445
446    fn is_time_to_probe_bandwidth(
447        &self, target_bytes_inflight: usize,
448        congestion_event: &BBRv2CongestionEvent, params: &Params,
449    ) -> bool {
450        if self.has_cycle_lasted(
451            self.cycle.probe_wait_time.unwrap(),
452            congestion_event,
453        ) {
454            return true;
455        }
456
457        if self.is_time_to_probe_for_reno_coexistence(
458            target_bytes_inflight,
459            1.0,
460            congestion_event,
461            params,
462        ) {
463            return true;
464        }
465
466        false
467    }
468
469    fn maybe_adapt_upper_bounds(
470        &mut self, target_bytes_inflight: usize,
471        congestion_event: &BBRv2CongestionEvent, params: &Params,
472    ) -> AdaptUpperBoundsResult {
473        let send_state = congestion_event.last_packet_send_state;
474
475        if !send_state.is_valid {
476            return AdaptUpperBoundsResult::NotAdaptedInvalidSample;
477        }
478
479        // TODO(vlad): use BytesInFlight?
480        let mut inflight_at_send = send_state.bytes_in_flight;
481        if params.use_bytes_delivered_for_inflight_hi {
482            inflight_at_send = self.model.total_bytes_acked() -
483                congestion_event.last_packet_send_state.total_bytes_acked;
484        }
485
486        if self.cycle.is_sample_from_probing {
487            if self.model.is_inflight_too_high(
488                congestion_event,
489                params.probe_bw_full_loss_count,
490                params,
491            ) {
492                self.cycle.is_sample_from_probing = false;
493                if !send_state.is_app_limited ||
494                    params.max_probe_up_queue_rounds > 0
495                {
496                    let inflight_target = (target_bytes_inflight as f32 *
497                        (1.0 - params.beta))
498                        as usize;
499
500                    let mut new_inflight_hi =
501                        inflight_at_send.max(inflight_target);
502
503                    if params.limit_inflight_hi_by_max_delivered {
504                        new_inflight_hi = self
505                            .model
506                            .max_bytes_delivered_in_round()
507                            .max(new_inflight_hi);
508                    }
509
510                    self.model.set_inflight_hi(new_inflight_hi);
511                }
512                return AdaptUpperBoundsResult::AdaptedProbedTooHigh;
513            }
514            return AdaptUpperBoundsResult::AdaptedOk;
515        }
516
517        if self.model.inflight_hi() == self.model.inflight_hi_default() {
518            return AdaptUpperBoundsResult::NotAdaptedInflightHighNotSet;
519        }
520
521        // Raise the upper bound for inflight.
522        if inflight_at_send > self.model.inflight_hi() {
523            self.model.set_inflight_hi(inflight_at_send);
524        }
525
526        AdaptUpperBoundsResult::AdaptedOk
527    }
528
529    fn has_cycle_lasted(
530        &self, duration: Duration, congestion_event: &BBRv2CongestionEvent,
531    ) -> bool {
532        (congestion_event.event_time - self.cycle.start_time) > duration
533    }
534
535    fn has_phase_lasted(
536        &self, duration: Duration, congestion_event: &BBRv2CongestionEvent,
537    ) -> bool {
538        (congestion_event.event_time - self.cycle.phase_start_time) > duration
539    }
540
541    fn is_time_to_probe_for_reno_coexistence(
542        &self, target_bytes_inflight: usize, probe_wait_fraction: f64,
543        _congestion_event: &BBRv2CongestionEvent, params: &Params,
544    ) -> bool {
545        if !params.enable_reno_coexistence {
546            return false;
547        }
548
549        let mut rounds = params.probe_bw_probe_max_rounds;
550        if params.probe_bw_probe_reno_gain > 0.0 {
551            let reno_rounds = (params.probe_bw_probe_reno_gain *
552                target_bytes_inflight as f32 /
553                DEFAULT_MSS as f32) as usize;
554            rounds = reno_rounds.min(rounds);
555        }
556
557        self.cycle.rounds_since_probe >=
558            (rounds as f64 * probe_wait_fraction) as usize
559    }
560
561    // Used to prevent a BBR2 flow from staying in PROBE_DOWN for too
562    // long, as seen in some multi-sender simulator tests.
563    //
564    // This is experimental code from Google quiche and diverges from the RFC.
565    // Use `disable_probe_down_early_exit` to override the behavior.
566    // - RFC: https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-02.html#name-probebw_down
567    // - Google quiche: https://github.com/google/quiche/blob/b370e7a/quiche/quic/core/congestion_control/bbr2_probe_bw.cc#L142
568    fn has_stayed_long_enough_in_probe_down(
569        &self, congestion_event: &BBRv2CongestionEvent, params: &Params,
570    ) -> bool {
571        if params.disable_probe_down_early_exit {
572            return false;
573        }
574
575        // Stay in PROBE_DOWN for at most the time of a min rtt, as it is done
576        // in BBRv1.
577        self.has_phase_lasted(self.model.min_rtt(), congestion_event)
578    }
579
580    fn raise_inflight_high_slope(&mut self, cwnd: usize) {
581        let growth_this_round = 1usize << self.cycle.probe_up_rounds;
582        // Capping rounds at 30 limits `growth_this_round` to 1G and keeps
583        // `probe_up_bytes` near one MSS. This grows `inflight_hi` by about one
584        // packet per ACK.
585        self.cycle.probe_up_rounds = self.cycle.probe_up_rounds.add(1).min(30);
586        let probe_up_bytes = cwnd / growth_this_round;
587        self.cycle.probe_up_bytes = Some(probe_up_bytes.max(DEFAULT_MSS));
588    }
589
590    fn probe_inflight_high_upward(
591        &mut self, congestion_event: &BBRv2CongestionEvent, params: &Params,
592    ) {
593        if params.probe_up_ignore_inflight_hi {
594            // When inflight_hi is disabled in PROBE_UP, it increases when
595            // the number of bytes delivered in a round is larger inflight_hi.
596            return;
597        } else {
598            // TODO(vlad): probe_up_simplify_inflight_hi?
599            if congestion_event.prior_bytes_in_flight <
600                congestion_event.prior_cwnd
601            {
602                // Not fully utilizing cwnd, so can't safely grow.
603                return;
604            }
605
606            if congestion_event.prior_cwnd < self.model.inflight_hi() {
607                // Not fully using inflight_hi, so don't grow it.
608                return;
609            }
610
611            self.cycle.probe_up_acked += congestion_event.bytes_acked;
612        }
613
614        if let Some(probe_up_bytes) = self.cycle.probe_up_bytes {
615            if self.cycle.probe_up_acked >= probe_up_bytes {
616                let delta = self.cycle.probe_up_acked / probe_up_bytes;
617                // probe_up_acked is set to the remainder mod probe_up_bytes;
618                // probe_up_acked >= delta * probe_up_bytes so this
619                // can't underflow.
620                self.cycle.probe_up_acked -= delta * probe_up_bytes;
621                let new_inflight_hi =
622                    self.model.inflight_hi() + delta * DEFAULT_MSS;
623                if new_inflight_hi > self.model.inflight_hi() {
624                    self.model.set_inflight_hi(new_inflight_hi);
625                }
626            }
627        }
628
629        if congestion_event.end_of_round_trip {
630            self.raise_inflight_high_slope(congestion_event.prior_cwnd);
631        }
632    }
633
634    fn into_probe_rtt(
635        mut self, now: Instant, congestion_event: Option<&BBRv2CongestionEvent>,
636        params: &Params,
637    ) -> Mode {
638        self.leave(now, congestion_event);
639        let mut next_mode = Mode::probe_rtt(self.model, self.cycle);
640        next_mode.enter(now, congestion_event, params);
641        next_mode
642    }
643}
644
645#[cfg(test)]
646mod tests {
647    use rstest::rstest;
648
649    use super::*;
650    use crate::recovery::gcongestion::bbr2::SendTimeState;
651    use crate::recovery::gcongestion::bbr2::DEFAULT_PARAMS;
652
653    #[rstest]
654    fn probe_upward(#[values(100, 10_000, 65_536, 300_000)] step: usize) {
655        let test_event =
656            |probe_bw: &ProbeBW, bytes_acked: usize, end_of_round_trip: bool| {
657                BBRv2CongestionEvent {
658                    event_time: probe_bw.cycle.start_time,
659                    prior_cwnd: probe_bw.model.inflight_hi(),
660                    prior_bytes_in_flight: probe_bw.model.inflight_hi(),
661                    bytes_in_flight: 0,
662                    bytes_acked,
663                    bytes_lost: 0,
664                    end_of_round_trip,
665                    is_probing_for_bandwidth: true,
666                    sample_max_bandwidth: None,
667                    sample_min_rtt: None,
668                    last_packet_send_state: SendTimeState::default(),
669                }
670            };
671
672        let do_probe_up = |probe_bw: &mut ProbeBW,
673                           params: &Params,
674                           total_bytes: usize| {
675            let mut remaining = total_bytes;
676            loop {
677                let congestion_event =
678                    test_event(probe_bw, step.min(remaining), false);
679
680                probe_bw.probe_inflight_high_upward(&congestion_event, params);
681
682                if remaining > step {
683                    remaining -= step;
684                } else {
685                    break;
686                }
687            }
688        };
689
690        let params = &DEFAULT_PARAMS;
691        let model = BBRv2NetworkModel::new(params, Duration::from_millis(333));
692        let cycle = Cycle::default();
693        let mut probe_bw = ProbeBW { model, cycle };
694        probe_bw.model.set_inflight_hi(100_000);
695        probe_bw.raise_inflight_high_slope(100_000);
696        assert_eq!(probe_bw.cycle.probe_up_rounds, 1);
697        assert_eq!(probe_bw.cycle.probe_up_bytes, Some(100_000));
698
699        assert_eq!(probe_bw.model.inflight_hi(), 100_000);
700        do_probe_up(&mut probe_bw, params, 1_000_000);
701        // End inflight_hi should be independent of step size.
702        assert_eq!(probe_bw.model.inflight_hi(), 113_000);
703
704        // Slope is increased at the end of round by decreasing probe_up_bytes.
705        probe_bw.probe_inflight_high_upward(
706            &test_event(&probe_bw, 10_000, true),
707            params,
708        );
709        assert_eq!(probe_bw.cycle.probe_up_rounds, 2);
710        assert_eq!(probe_bw.cycle.probe_up_bytes, Some(56500));
711
712        do_probe_up(&mut probe_bw, params, 1_000_000);
713        // End inflight_hi should be independent of step size.
714        assert_eq!(probe_bw.model.inflight_hi(), 135_100);
715
716        probe_bw.probe_inflight_high_upward(
717            &test_event(&probe_bw, 10_000, true),
718            params,
719        );
720        assert_eq!(probe_bw.cycle.probe_up_rounds, 3);
721        assert_eq!(probe_bw.cycle.probe_up_bytes, Some(33775));
722
723        do_probe_up(&mut probe_bw, params, 1_000_000);
724        // End inflight_hi should be independent of step size.
725        assert_eq!(probe_bw.model.inflight_hi(), 174_100);
726    }
727}