// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// Gain factor for ECN CE mark ratio samples
// Value from https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2290
const ECN_ALPHA_GAIN: f64 = 1.0 / 16.0;

// The maximum tolerated ratio of packets containing ECN CE markings
// Value from https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2306
const ECN_THRESH: f64 = 0.5;

// On ECN CE markings, cut inflight_lo to (1 - ECN_FACTOR * ecn_alpha)
// Value from https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2301
pub(super) const ECN_FACTOR: f64 = 0.33;

#[derive(Clone, Debug)]
pub(crate) struct State {
    /// The amount of explicit congestion experienced packets in the current round trip
    ce_count_in_round: u64,
    /// The amount of bytes delivered in the current round trip
    round_start_delivered_bytes: u64,
    /// Weighted average ratio of ECN CE marked packets with `ECN_ALPHA_GAIN` applied
    alpha: f64,
    /// True if the ECN CE count over the current round trip was too high
    ce_too_high: bool,
}

impl Default for State {
    fn default() -> Self {
        Self {
            ce_count_in_round: 0,
            round_start_delivered_bytes: 0,
            alpha: 1.0,
            ce_too_high: false,
        }
    }
}

impl State {
    /// Called on each new BBR round
    #[inline]
    pub(super) fn on_round_start(&mut self, delivered_bytes: u64, max_datagram_size: u16) {
        let delivered_bytes_in_round = delivered_bytes - self.round_start_delivered_bytes;
        // update alpha
        if delivered_bytes_in_round > 0 {
            let ce_ratio = ce_ratio(
                self.ce_count_in_round,
                delivered_bytes_in_round,
                max_datagram_size,
            );
            self.alpha = calculate_alpha(self.alpha, ce_ratio);
            self.ce_too_high = is_ce_too_high(ce_ratio);
        }

        self.round_start_delivered_bytes = delivered_bytes;
        self.ce_count_in_round = 0;
    }

    /// Called each time explicit congestion is recorded
    #[inline]
    pub(super) fn on_explicit_congestion(&mut self, ce_count: u64) {
        self.ce_count_in_round += ce_count;
    }

    /// Returns true if the ECN CE ratio over the latest round was too high
    #[inline]
    pub(super) fn is_ce_too_high_in_round(&self) -> bool {
        self.ce_too_high
    }

    /// Returns the ECN alpha value
    #[inline]
    pub(super) fn alpha(&self) -> f64 {
        self.alpha
    }
}

/// Calculate the ratio of ECN CE marked bytes to overall delivered bytes
#[inline]
pub(super) fn ce_ratio(ecn_ce_count: u64, delivered_bytes: u64, max_datagram_size: u16) -> f64 {
    // Estimate the number of bytes experiencing explicit congestion by multiplying
    // the ecn_ce_count by max_datagram_size
    let ecn_ce_bytes = ecn_ce_count.saturating_mul(max_datagram_size as u64) as f64;
    ecn_ce_bytes / delivered_bytes as f64
}

/// True if the given ECN `ce_ratio` exceeds the BBR ECN threshold
#[inline]
pub(super) fn is_ce_too_high(ce_ratio: f64) -> bool {
    ce_ratio > ECN_THRESH
}

/// Calculates the new ECN alpha value
///
/// Based on `bbr2_update_ecn_alpha` from the Linux TCP BBRv2 implementation
/// See https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L1392
#[inline]
fn calculate_alpha(alpha: f64, ce_ratio: f64) -> f64 {
    ((1.0 - ECN_ALPHA_GAIN) * alpha + ECN_ALPHA_GAIN * ce_ratio).min(1.0)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{assert_delta, path::MINIMUM_MTU, recovery::bbr::ecn};

    #[test]
    fn on_round_start() {
        let mut state = ecn::State::default();
        assert_delta!(1.0, state.alpha(), 0.0001);

        let delivered_bytes = 1000;
        state.on_round_start(delivered_bytes, MINIMUM_MTU);

        // No ECN CE yet and alpha is currently at the initial value of 1,
        // so alpha is just 1 - alpha gain
        assert_delta!(1.0 - ECN_ALPHA_GAIN, state.alpha(), 0.0001);
        assert!(!state.is_ce_too_high_in_round());
        assert_eq!(delivered_bytes, state.round_start_delivered_bytes);

        state.on_explicit_congestion(10);
        assert_eq!(10, state.ce_count_in_round);

        let alpha = state.alpha();
        let prev_delivered = delivered_bytes;
        // 20 packets delivered, 10 of which were ECN CE marked
        let delivered_bytes = prev_delivered + 20 * MINIMUM_MTU as u64;

        state.on_round_start(delivered_bytes, MINIMUM_MTU);

        assert_delta!(
            (1.0 - ECN_ALPHA_GAIN) * alpha + ECN_ALPHA_GAIN * 0.5,
            state.alpha(),
            0.0001
        );
        // ECN ce count is not above the 50% `ECN_THRESH`
        assert!(!state.is_ce_too_high_in_round());
        assert_eq!(delivered_bytes, state.round_start_delivered_bytes);
        // ce count is reset
        assert_eq!(0, state.ce_count_in_round);

        state.on_explicit_congestion(11);
        assert_eq!(11, state.ce_count_in_round);

        let alpha = state.alpha();
        let prev_delivered = delivered_bytes;
        let delivered_bytes = prev_delivered + 20 * MINIMUM_MTU as u64;

        // 20 packets delivered, 11 of which were ECN CE marked
        state.on_round_start(delivered_bytes, MINIMUM_MTU);

        assert_delta!(
            (1.0 - ECN_ALPHA_GAIN) * alpha + ECN_ALPHA_GAIN * 11.0 / 20.0,
            state.alpha(),
            0.0001
        );
        // ECN ce count is above the 50% `ECN_THRESH`
        assert!(state.is_ce_too_high_in_round());
        assert_eq!(delivered_bytes, state.round_start_delivered_bytes);
        assert_eq!(0, state.ce_count_in_round);
    }
}


// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use crate::{
    counter::{Counter, Saturating},
    recovery::{bandwidth, bandwidth::Bandwidth, bbr::BbrCongestionController},
};
use num_rational::Ratio;

/// Estimator for determining if BBR has fully utilized its available bandwidth ("filled the pipe")
#[derive(Debug, Default, Clone)]
pub(crate) struct Estimator {
    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.2
    //# BBRInitFullPipe():
    //#  BBR.filled_pipe = false
    //#  BBR.full_bw = 0
    //#  BBR.full_bw_count = 0

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.13
    //# A boolean that records whether BBR estimates that it has ever
    //# fully utilized its available bandwidth ("filled the pipe").
    filled_pipe: bool,
    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.13
    //# A recent baseline BBR.max_bw to estimate if BBR has "filled the pipe" in Startup.
    full_bw: Bandwidth,
    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.13
    //# The number of non-app-limited round trips without large increases in BBR.full_bw.
    full_bw_count: Counter<u8, Saturating>,
    /// The number of rounds where the ECN CE markings exceed ECN_THRESH
    ecn_ce_rounds: Counter<u8, Saturating>,
}

impl Estimator {
    /// Returns true if BBR estimates that is has ever fully utilized its available bandwidth
    #[inline]
    pub fn filled_pipe(&self) -> bool {
        self.filled_pipe
    }

    /// Called on each new BBR round
    #[inline]
    pub fn on_round_start(
        &mut self,
        rate_sample: bandwidth::RateSample,
        max_bw: Bandwidth,
        ecn_ce_count_too_high: bool,
    ) {
        if self.filled_pipe {
            return;
        }

        self.filled_pipe = self.bandwidth_plateaued(rate_sample, max_bw)
            || self.excessive_explicit_congestion(ecn_ce_count_too_high);
    }

    /// Called on each new loss round
    ///
    /// Excessive inflight is checked at the end of a loss round, not a regular round, as done
    /// in tcp_bbr2.c/bbr2_check_loss_too_high_in_startup
    ///
    /// See https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2133
    #[inline]
    pub fn on_loss_round_start(
        &mut self,
        rate_sample: bandwidth::RateSample,
        loss_bursts_in_round: u8,
        max_datagram_size: u16,
    ) {
        if self.filled_pipe {
            return;
        }

        self.filled_pipe =
            self.excessive_inflight(rate_sample, loss_bursts_in_round, max_datagram_size);
    }

    /// Determines if the rate of increase of bandwidth has decreased enough to estimate the
    /// available bandwidth has been fully utilized.
    ///
    /// Based on bbr_check_full_bw_reached in tcp_bbr2.c
    #[inline]
    fn bandwidth_plateaued(
        &mut self,
        rate_sample: bandwidth::RateSample,
        max_bw: Bandwidth,
    ) -> bool {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.2
        //# BBRCheckStartupFullBandwidth():
        //#   if BBR.filled_pipe or
        //#     !BBR.round_start or rs.is_app_limited
        //#    return  /* no need to check for a full pipe now */
        //#   if (BBR.max_bw >= BBR.full_bw * 1.25)  /* still growing? */
        //#     BBR.full_bw = BBR.max_bw    /* record new baseline level */
        //#     BBR.full_bw_count = 0
        //#   return
        //#   BBR.full_bw_count++ /* another round w/o much growth */
        //#   if (BBR.full_bw_count >= 3)
        //#     BBR.filled_pipe = true

        //# If BBR notices that there are several (three) rounds where attempts to double
        //# the delivery rate actually result in little increase (less than 25 percent),
        //# then it estimates that it has reached BBR.max_bw, sets BBR.filled_pipe to true,
        //# exits Startup and enters Drain.
        const DELIVERY_RATE_INCREASE: Ratio<u64> = Ratio::new_raw(5, 4); // 1.25
        const BANDWIDTH_PLATEAU_ROUND_COUNT: u8 = 3;

        if rate_sample.is_app_limited {
            //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.2
            //# Once per round trip, upon an ACK that acknowledges new data, and when
            //# the delivery rate sample is not application-limited (see [draft-
            //# cheng-iccrg-delivery-rate-estimation]), BBR runs the "full pipe" estimator
            return false;
        }

        if max_bw >= self.full_bw * DELIVERY_RATE_INCREASE {
            // still growing?
            self.full_bw = max_bw; // record new baseline level
            self.full_bw_count = Counter::default(); // restart the count
            return false;
        }

        /* another round w/o much growth */
        self.full_bw_count += 1;

        // Bandwidth has plateaued if the number of rounds without much growth
        // reaches `BANDWIDTH_PLATEAU_ROUND_COUNT`
        self.full_bw_count >= BANDWIDTH_PLATEAU_ROUND_COUNT
    }

    /// Determines if inflight has been too high (due to either loss or ECN markings) and enough
    /// distinct loss bursts have been observed to estimate the available bandwidth has been fully utilized.
    #[inline]
    fn excessive_inflight(
        &mut self,
        rate_sample: bandwidth::RateSample,
        loss_bursts_in_round: u8,
        max_datagram_size: u16,
    ) -> bool {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.3
        //# A second method BBR uses for estimating the bottleneck is full is by looking at sustained
        //# packet losses Specifically for a case where the following criteria are all met:
        //#
        //#    *  The connection has been in fast recovery for at least one full round trip.
        //#    *  The loss rate over the time scale of a single full round trip exceeds BBRLossThresh (2%).

        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.3
        //= type=exception
        //= reason=Chromium and Linux TCP BBRv2 both use 8 lost bursts in a round trip
        //#    *  There are at least BBRStartupFullLossCnt=3 discontiguous sequence ranges lost in that round trip.

        // See https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2325-L2329
        // 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
        const STARTUP_FULL_LOSS_COUNT: u8 = 8;

        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.3
        //= type=exception
        //= reason=The BBRv2 RFC reference to "fast recovery" here is more applicable to TCP.
        //#    *  The connection has been in fast recovery for at least one full round trip.

        // The BBRv2 RFC reference to "fast recovery" here seems more applicable to TCP, rather than
        // QUIC. Instead, we just consider the loss rate and number of loss bursts over the round trip.
        // This is more in line with the Chromium BBRv2 implementation.
        // See: https://source.chromium.org/chromium/chromium/src/+/main:net/third_party/quiche/src/quiche/quic/core/congestion_control/bbr2_startup.cc;l=104

        if loss_bursts_in_round < STARTUP_FULL_LOSS_COUNT {
            // is_inflight_too_high returns true when ECN CE markings exceed the threshold, even
            // if the loss burst count is below the threshold. During startup, excessive ECN CE
            // is separately checked over multiple rounds, so return immediately if we have
            // not seen enough loss bursts. This follows the Linux TCP BBRv2 implementation
            // See https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2150
            return false;
        }

        BbrCongestionController::is_inflight_too_high(
            rate_sample,
            max_datagram_size,
            loss_bursts_in_round,
            STARTUP_FULL_LOSS_COUNT,
        )
    }

    /// Determines if enough consecutive rounds of explicit congestion have been encountered that we
    /// can estimate the available bandwidth has been fully utilized.
    ///
    /// Based on bbr2_check_ecn_too_high_in_startup from https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L1372
    fn excessive_explicit_congestion(&mut self, ecn_ce_count_too_high: bool) -> bool {
        // Startup is exited if the number of consecutive round trips with ECN CE markings above
        // the ECN_THRESH exceed this value
        // Value from https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2334
        const STARTUP_FULL_ECN_COUNT: u8 = 2;

        if ecn_ce_count_too_high {
            self.ecn_ce_rounds += 1;
        } else {
            self.ecn_ce_rounds = Counter::default();
        }

        self.ecn_ce_rounds >= STARTUP_FULL_ECN_COUNT
    }

    #[cfg(test)]
    pub fn set_filled_pipe_for_test(&mut self, filled_pipe: bool) {
        self.filled_pipe = filled_pipe;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        path::MINIMUM_MTU,
        recovery::{bandwidth::RateSample, bbr::full_pipe},
    };
    use core::time::Duration;

    #[test]
    fn bandwidth_plateau() {
        let mut fp_estimator = full_pipe::Estimator::default();
        let rate_sample = RateSample::default();
        let mut max_bw = Bandwidth::new(1000, Duration::from_secs(1));
        fp_estimator.on_round_start(rate_sample, max_bw, false);

        // Grow at 25% over 3 rounds
        max_bw = max_bw * Ratio::new(5, 4); // 5/4 = 125%
        for _ in 0..3 {
            fp_estimator.on_round_start(rate_sample, max_bw, false);
        }
        // The pipe has not been filled yet since we have continued to grow bandwidth
        assert!(!fp_estimator.filled_pipe());

        // One more round with 24% growth, not growing fast enough to continue
        max_bw = max_bw * Ratio::new(31, 25); // 31/25 = 124%
        fp_estimator.on_round_start(rate_sample, max_bw, false);
        // The pipe is considered full
        assert!(fp_estimator.filled_pipe());
    }

    #[test]
    fn bandwidth_plateau_app_limited() {
        let mut fp_estimator = full_pipe::Estimator::default();
        let rate_sample = RateSample {
            is_app_limited: true,
            ..Default::default()
        };
        let max_bw = Bandwidth::new(1000, Duration::from_secs(1));

        // No growth, but app limited
        for _ in 0..3 {
            fp_estimator.on_round_start(rate_sample, max_bw, false);
        }

        // The pipe has not been filled yet since we were app limited
        assert!(!fp_estimator.filled_pipe());
    }

    #[test]
    fn excessive_inflight_due_to_loss() {
        let mut fp_estimator = full_pipe::Estimator::default();
        let rate_sample = RateSample {
            // Set app_limited to true to ignore bandwidth plateau check
            is_app_limited: true,
            // More than 2% bytes lost
            bytes_in_flight: 1000,
            lost_bytes: 21,
            ..Default::default()
        };

        // Only 7 loss bursts, not enough to be considered excessive loss
        fp_estimator.on_loss_round_start(rate_sample, 7, MINIMUM_MTU);
        // The pipe has not been filled yet since there were only 2 loss bursts
        assert!(!fp_estimator.filled_pipe());

        // 3 loss bursts, enough to be considered excessive loss
        fp_estimator.on_loss_round_start(rate_sample, 8, MINIMUM_MTU);
        // The pipe has been filled due to loss
        assert!(fp_estimator.filled_pipe());
    }

    #[test]
    fn excessive_inflight_due_to_ecn_ce() {
        let mut fp_estimator = full_pipe::Estimator::default();
        let rate_sample = RateSample {
            // Set app_limited to true to ignore bandwidth plateau check
            is_app_limited: true,
            // >= ECN_THRESH (50%) of packets had ECN CE markings
            ecn_ce_count: 5,
            delivered_bytes: 9 * MINIMUM_MTU as u64,
            ..Default::default()
        };

        // Only 7 loss bursts, not enough to be considered excessive loss
        fp_estimator.on_loss_round_start(rate_sample, 7, MINIMUM_MTU);

        fp_estimator.on_loss_round_start(rate_sample, 8, MINIMUM_MTU);
        // The pipe has been filled due to ECN
        assert!(fp_estimator.filled_pipe());
    }

    #[test]
    fn excessive_inflight_loss_rate_too_low() {
        let mut fp_estimator = full_pipe::Estimator::default();
        let rate_sample = RateSample {
            // Set app_limited to true to ignore bandwidth plateau check
            is_app_limited: true,
            // 2% bytes lost, just below the threshold to be considered excessive
            bytes_in_flight: 1000,
            lost_bytes: 2,
            ..Default::default()
        };
        // 8 loss bursts, enough to be considered excessive loss
        fp_estimator.on_loss_round_start(rate_sample, 8, MINIMUM_MTU);
        // The pipe has not been filled yet since the loss rate was not high enough
        assert!(!fp_estimator.filled_pipe());
    }

    #[test]
    fn excessive_explicit_congestion() {
        let mut fp_estimator = full_pipe::Estimator::default();
        let rate_sample = RateSample {
            // Set app_limited to true to ignore bandwidth plateau check
            is_app_limited: true,
            ..Default::default()
        };

        let max_bw = Bandwidth::new(1000, Duration::from_secs(1));

        fp_estimator.on_round_start(rate_sample, max_bw, true);
        // The pipe has not been filled yet since there was only one round with high ECN CE markings
        assert!(!fp_estimator.filled_pipe());

        fp_estimator.on_round_start(rate_sample, max_bw, false);
        fp_estimator.on_round_start(rate_sample, max_bw, true);
        // The pipe has not been filled yet since the low ecn rate sample reset the count,
        // ie the high ecn rate samples were not contiguous
        assert!(!fp_estimator.filled_pipe());

        fp_estimator.on_round_start(rate_sample, max_bw, true);
        // After two consecutive rounds of high ECN markings, the pipe is full
        assert!(fp_estimator.filled_pipe());
    }
}


// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use crate::{
    counter::{Counter, Saturating},
    recovery::{
        bandwidth::Bandwidth,
        bbr::{BbrCongestionController, State},
        congestion_controller::Publisher,
        pacing::{INITIAL_INTERVAL, MINIMUM_PACING_RTT},
        MAX_BURST_PACKETS,
    },
    time::{Duration, Timestamp},
};
use num_rational::Ratio;

/// A packet pacer that returns departure times that evenly distribute bursts of packets over time
#[derive(Clone, Debug)]
pub struct Pacer {
    // The capacity of the current departure time slot
    capacity: Counter<u32, Saturating>,
    // The time the next packet should be transmitted
    next_packet_departure_time: Option<Timestamp>,
    // The current pacing rate for a BBR flow, which controls inter-packet spacing
    pacing_rate: Bandwidth,
    // The maximum size of a data aggregate scheduled and transmitted together
    send_quantum: usize,
}

impl Pacer {
    pub(super) fn new(max_datagram_size: u16) -> Self {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.2
        //# BBRInitPacingRate():
        //#   nominal_bandwidth = InitialCwnd / (SRTT ? SRTT : 1ms)
        //# BBR.pacing_rate =  BBRStartupPacingGain * nominal_bandwidth
        let initial_cwnd = BbrCongestionController::initial_window(max_datagram_size);
        let nominal_bandwidth = Bandwidth::new(initial_cwnd as u64, Duration::from_millis(1));
        let pacing_rate =
            Self::bandwidth_to_pacing_rate(nominal_bandwidth, State::Startup.pacing_gain());

        Self {
            capacity: Default::default(),
            next_packet_departure_time: None,
            pacing_rate,
            send_quantum: Self::max_send_quantum(max_datagram_size),
        }
    }

    /// Called when each packet has been written
    #[inline]
    pub fn on_packet_sent(&mut self, now: Timestamp, bytes_sent: usize, rtt: Duration) {
        if rtt < MINIMUM_PACING_RTT {
            return;
        }

        if self.capacity == 0 {
            if let Some(next_packet_departure_time) = self.next_packet_departure_time {
                self.next_packet_departure_time =
                    Some((next_packet_departure_time + self.interval()).max(now));
            } else {
                self.next_packet_departure_time = Some(now + INITIAL_INTERVAL);
            }
            self.capacity = Counter::new(self.send_quantum as u32);
        }

        self.capacity -= bytes_sent as u32;
    }

    /// Initialize the pacing rate with the given rtt and cwnd
    #[inline]
    pub(super) fn initialize_pacing_rate<Pub: Publisher>(
        &mut self,
        cwnd: u32,
        rtt: Duration,
        gain: Ratio<u64>,
        publisher: &mut Pub,
    ) {
        let bw = Bandwidth::new(cwnd as u64, rtt);

        let rate = Self::bandwidth_to_pacing_rate(bw, gain);
        self.pacing_rate = rate;
        publisher.on_pacing_rate_updated(rate, self.send_quantum as u32, gain);
    }

    /// Sets the pacing rate used for determining the earliest departure time
    #[inline]
    pub(super) fn set_pacing_rate<Pub: Publisher>(
        &mut self,
        bw: Bandwidth,
        gain: Ratio<u64>,
        filled_pipe: bool,
        publisher: &mut Pub,
    ) {
        let rate = Self::bandwidth_to_pacing_rate(bw, gain);

        if filled_pipe || rate > self.pacing_rate {
            self.pacing_rate = rate;
            publisher.on_pacing_rate_updated(rate, self.send_quantum as u32, gain);
        }
    }

    /// Sets the maximum size of data aggregate scheduled and transmitted together
    #[inline]
    pub(super) fn set_send_quantum(&mut self, max_datagram_size: u16) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.3
        //# if (BBR.pacing_rate < 1.2 Mbps)
        //#   floor = 1 * SMSS
        //# else
        //#   floor = 2 * SMSS
        //# BBR.send_quantum = min(BBR.pacing_rate * 1ms, 64KBytes)
        //# BBR.send_quantum = max(BBR.send_quantum, floor)

        // 1.2 Mbps
        const SEND_QUANTUM_THRESHOLD: Bandwidth =
            Bandwidth::new(1_200_000 / 8, Duration::from_secs(1));

        let floor = if self.pacing_rate < SEND_QUANTUM_THRESHOLD {
            max_datagram_size
        } else {
            max_datagram_size * 2
        } as usize;

        let send_quantum = (self.pacing_rate * Duration::from_millis(1)) as usize;
        self.send_quantum = send_quantum
            .max(floor)
            .min(Self::max_send_quantum(max_datagram_size));
    }

    /// Returns the earliest time that a packet may be transmitted.
    ///
    /// If the time is in the past or is `None`, the packet should be transmitted immediately.
    pub(super) fn earliest_departure_time(&self) -> Option<Timestamp> {
        self.next_packet_departure_time
    }

    /// Returns the maximum size of data aggregate scheduled and transmitted together
    pub(super) fn send_quantum(&self) -> usize {
        self.send_quantum
    }

    /// Returns the maximum value for send_quantum
    #[inline]
    fn max_send_quantum(max_datagram_size: u16) -> usize {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.3
        //= type=exception
        //= reason=QUIC recommends limiting bursts to the initial congestion window
        //# BBR.send_quantum = min(BBR.pacing_rate * 1ms, 64KBytes)
        MAX_BURST_PACKETS as usize * max_datagram_size as usize
    }

    // Recalculate the interval between bursts of paced packets
    #[inline]
    fn interval(&self) -> Duration {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.2
        //# BBR.next_departure_time = max(Now(), BBR.next_departure_time)
        //# packet.departure_time = BBR.next_departure_time
        //# pacing_delay = packet.size / BBR.pacing_rate

        self.send_quantum as u64 / self.pacing_rate
    }

    // Calculate the pacing rate based on the given bandwidth, pacing gain, and the pacing margin
    #[inline]
    fn bandwidth_to_pacing_rate(bw: Bandwidth, gain: Ratio<u64>) -> Bandwidth {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.5
        //# The static discount factor of 1% used to scale BBR.bw to produce BBR.pacing_rate.
        const PACING_MARGIN_PERCENT: u64 = 1;
        const PACING_RATIO: Ratio<u64> = Ratio::new_raw(100 - PACING_MARGIN_PERCENT, 100);

        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.2
        //# BBRSetPacingRateWithGain(pacing_gain):
        //#   rate = pacing_gain * bw * (100 - BBRPacingMarginPercent) / 100
        //#   if (BBR.filled_pipe || rate > BBR.pacing_rate)
        //#     BBR.pacing_rate = rate
        bw * gain * PACING_RATIO
    }

    #[cfg(test)]
    pub fn set_send_quantum_for_test(&mut self, send_quantum: usize) {
        self.send_quantum = send_quantum
    }

    #[cfg(test)]
    pub fn pacing_rate(&self) -> Bandwidth {
        self.pacing_rate
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        event, path,
        path::MINIMUM_MTU,
        recovery::{
            bandwidth::Bandwidth,
            bbr::{pacing::Pacer, State, State::Startup},
            congestion_controller::PathPublisher,
            pacing::INITIAL_INTERVAL,
        },
        time::{Clock, NoopClock},
    };
    use core::time::Duration;
    use num_rational::Ratio;

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.2
    //= type=test
    // BBRInitPacingRate():
    //     nominal_bandwidth = InitialCwnd / (SRTT ? SRTT : 1ms)
    //     BBR.pacing_rate =  BBRStartupPacingGain * nominal_bandwidth
    #[test]
    fn new() {
        // nominal_bandwidth = 12_000 / 1ms = ~83.3333nanos/byte
        // pacing_rate = 2.77 * 83.333nanos/byte * 99% = ~30.388nanos/byte

        let pacer = Pacer::new(MINIMUM_MTU);

        assert_eq!(
            Bandwidth::new(1000, Duration::from_nanos(30388)),
            pacer.pacing_rate
        );
    }

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.3
    //= type=test
    //# BBR.send_quantum = min(BBR.pacing_rate * 1ms, 64KBytes)
    #[test]
    fn max_send_quantum() {
        // BBR specifies a maximum send_quantum of 64KB, but since s2n-quic has a MAX_BURST_PACKETS
        // of 10 and 10 * MINIMUM_MTU is less than 64KB, this limit will always be higher than the
        // limit s2n-quic imposes. This test ensures that this remains true if MAX_BURST_PACKETS is
        // increased.
        assert_eq!(Pacer::max_send_quantum(MINIMUM_MTU), 12_000);
        assert!(Pacer::max_send_quantum(MINIMUM_MTU) < 64_000);
    }

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.2
    //= type=test
    //# BBRSetPacingRateWithGain(pacing_gain):
    //#   rate = pacing_gain * bw * (100 - BBRPacingMarginPercent) / 100
    //#   if (BBR.filled_pipe || rate > BBR.pacing_rate)
    //#     BBR.pacing_rate = rate
    #[test]
    fn set_pacing_rate() {
        let mut pacer = Pacer::new(MINIMUM_MTU);
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        let bandwidth = Bandwidth::new(1000, Duration::from_millis(1));
        pacer.set_pacing_rate(bandwidth, Ratio::new(5, 4), true, &mut publisher);

        // pacing rate = pacing_gain * bw * (100 - BBRPacingMarginPercent) / 100
        //             = 1.25 * 1000bytes/ms * 99/100
        //             = 1237.5bytes/ms
        assert_eq!(
            Bandwidth::new(12375, Duration::from_millis(10)),
            pacer.pacing_rate
        );
    }

    #[test]
    fn initialize_pacing_rate() {
        let mut pacer = Pacer::new(MINIMUM_MTU);
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());

        // pacing_rate = 14_000/100ms * 2.77 * .99 = 383_922 bytes/sec
        pacer.initialize_pacing_rate(
            14_000,
            Duration::from_millis(100),
            Startup.pacing_gain(),
            &mut publisher,
        );
        assert_eq!(
            Bandwidth::new(383_922, Duration::from_secs(1)),
            pacer.pacing_rate
        );
    }

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.3
    //= type=test
    //# if (BBR.pacing_rate < 1.2 Mbps)
    //#   floor = 1 * SMSS
    //# else
    //#   floor = 2 * SMSS
    //# BBR.send_quantum = min(BBR.pacing_rate * 1ms, 64KBytes)
    //# BBR.send_quantum = max(BBR.send_quantum, floor)
    #[test]
    fn set_send_quantum() {
        let mut pacer = Pacer::new(MINIMUM_MTU);
        // pacing_rate < 1.2 Mbps, floor = MINIMUM_MTU
        pacer.pacing_rate = Bandwidth::new(1_100_000 / 8, Duration::from_secs(1));
        pacer.set_send_quantum(MINIMUM_MTU);
        // pacing_Rate * 1ms = 137 bytes
        // send_quantum = min(137, 12_000) = 137
        // send_quantum = max(137, MINIMUM_MTU) = MINIMUM_MTU
        assert_eq!(MINIMUM_MTU as usize, pacer.send_quantum);

        // pacing_rate = 1.2 Mbps, floor = 2 * MINIMUM_MTU
        pacer.pacing_rate = Bandwidth::new(1_200_000 / 8, Duration::from_secs(1));
        pacer.set_send_quantum(MINIMUM_MTU);
        // pacing_Rate * 1ms = 150 bytes
        // send_quantum = min(150, 12_000) = 150
        // send_quantum = max(150, 2 * MINIMUM_MTU) = 2 * MINIMUM_MTU
        assert_eq!(2 * MINIMUM_MTU as usize, pacer.send_quantum);

        // pacing_rate = 10.0 MBps, floor = 2 * MINIMUM_MTU
        pacer.pacing_rate = Bandwidth::new(10_000_000, Duration::from_secs(1));
        pacer.set_send_quantum(MINIMUM_MTU);
        // pacing_Rate * 1ms = 10000 bytes
        // send_quantum = min(10000, 12_000) = 10000
        // send_quantum = max(10000, 2 * MINIMUM_MTU) = 10000
        assert_eq!(10000, pacer.send_quantum);

        // pacing_rate = 100.0 MBps, floor = 2 * MINIMUM_MTU
        pacer.pacing_rate = Bandwidth::new(100_000_000, Duration::from_secs(1));
        pacer.set_send_quantum(MINIMUM_MTU);
        // pacing_Rate * 1ms = 100000 bytes
        // send_quantum = min(100000, 12_000) = 12_000
        // send_quantum = max(12_000, 2 * MINIMUM_MTU) = 12_000
        assert_eq!(12_000, pacer.send_quantum);
    }

    #[test]
    fn test_one_rtt() {
        let mut pacer = Pacer::new(MINIMUM_MTU);
        let now = NoopClock.get_time();
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());

        let rtt = Duration::from_millis(100);
        let bw = Bandwidth::new(100_000, rtt);

        pacer.set_pacing_rate(bw, State::Startup.pacing_gain(), true, &mut publisher);

        let bytes_to_send = pacer.pacing_rate * rtt;

        // Send one packet to move beyond the initial interval
        pacer.on_packet_sent(now, MINIMUM_MTU as usize, rtt);
        assert_eq!(
            Some(now + INITIAL_INTERVAL),
            pacer.earliest_departure_time()
        );

        let mut sent_bytes = 0;
        let now = now + INITIAL_INTERVAL;
        while sent_bytes < bytes_to_send {
            // Confirm the current departure time is less than 1 rtt
            assert!(pacer
                .earliest_departure_time()
                .map_or(true, |departure_time| departure_time < now + rtt));
            pacer.on_packet_sent(now, MINIMUM_MTU as usize, rtt);
            sent_bytes += MINIMUM_MTU as u64;
        }
        assert!(pacer
            .earliest_departure_time()
            .unwrap()
            .has_elapsed(now + rtt));
    }
}


// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use crate::{
    counter::{Counter, Saturating},
    event,
    event::IntoEvent,
    random,
    recovery::{
        bandwidth::RateSample,
        bbr,
        bbr::{congestion, data_rate, data_volume, round, BbrCongestionController},
        congestion_controller::Publisher,
    },
    time::Timestamp,
};
use core::{convert::TryInto, time::Duration};
use num_rational::Ratio;
use num_traits::One;

const MAX_BW_PROBE_UP_ROUNDS: u8 = 30;

/// Max number of packet-timed rounds to wait before probing for bandwidth
const MAX_BW_PROBE_ROUNDS: u8 = 63;

/// The number of discontiguous bursts of loss required before inflight_hi is lowered
/// Value from:
/// https://source.chromium.org/chromium/chromium/src/+/main:net/third_party/quiche/src/quiche/quic/core/quic_protocol_flags_list.h;l=139;bpv=1;bpt=0
pub(super) const PROBE_BW_FULL_LOSS_COUNT: u8 = 2;

/// Cwnd gain used in the Probe BW state
///
/// This value is defined in the table in
/// https://www.ietf.org/archive/id/draft-cardwell-iccrg-bbr-congestion-control-02.html#section-4.6.1
pub(crate) const CWND_GAIN: Ratio<u64> = Ratio::new_raw(2, 1);

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3
//# a BBR flow in ProbeBW mode cycles through the four
//# Probe bw states - DOWN, CRUISE, REFILL, and UP
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CyclePhase {
    /// Send slower than the network is delivering data, to reduce the amount of data in flight
    Down,
    /// Send at the same rate the network is delivering data
    Cruise,
    /// Try to fully utilize the network bottleneck without creating any significant queue pressure
    Refill,
    /// Probe for possible increases in available bandwidth
    Up,
}

impl CyclePhase {
    /// The dynamic gain factor used to scale BBR.bw to produce BBR.pacing_rate
    pub fn pacing_gain(&self) -> Ratio<u64> {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.1
        //# In the ProbeBW_DOWN phase of the cycle, a BBR flow pursues the deceleration tactic,
        //# to try to send slower than the network is delivering data, to reduce the amount of data
        //# in flight, with all of the standard motivations for the deceleration tactic (discussed
        //# in "State Machine Tactics", above). It does this by switching to a BBR.pacing_gain of
        //# 0.9, sending at 90% of BBR.bw.
        const DOWN_PACING_GAIN: Ratio<u64> = Ratio::new_raw(9, 10);
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.4
        //# After ProbeBW_REFILL refills the pipe, ProbeBW_UP probes for possible increases in
        //# available bandwidth by using a BBR.pacing_gain of 1.25, sending faster than the current
        //# estimated available bandwidth.
        const UP_PACING_GAIN: Ratio<u64> = Ratio::new_raw(5, 4);
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.3
        //# During ProbeBW_REFILL BBR uses a BBR.pacing_gain of 1.0, to send at a rate that
        //# matches the current estimated available bandwidth

        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.2
        //# In the ProbeBW_CRUISE phase of the cycle, a BBR flow pursues the "cruising" tactic
        //# (discussed in "State Machine Tactics", above), attempting to send at the same rate
        //# the network is delivering data. It tries to match the sending rate to the flow's
        //# current available bandwidth, to try to achieve high utilization of the available
        //# bandwidth without increasing queue pressure. It does this by switching to a
        //# pacing_gain of 1.0, sending at 100% of BBR.bw.
        const CRUISE_REFILL_PACING_GAIN: Ratio<u64> = Ratio::new_raw(1, 1);

        match self {
            CyclePhase::Down => DOWN_PACING_GAIN,
            CyclePhase::Cruise | CyclePhase::Refill => CRUISE_REFILL_PACING_GAIN,
            CyclePhase::Up => UP_PACING_GAIN,
        }
    }

    /// Transition to the given `new_phase`
    fn transition_to<Pub: Publisher>(&mut self, new_phase: CyclePhase, publisher: &mut Pub) {
        if cfg!(debug_assertions) {
            match new_phase {
                CyclePhase::Down => assert_eq!(*self, CyclePhase::Up),
                CyclePhase::Cruise => assert_eq!(*self, CyclePhase::Down),
                CyclePhase::Refill => {
                    assert!(*self == CyclePhase::Down || *self == CyclePhase::Cruise)
                }
                CyclePhase::Up => assert_eq!(*self, CyclePhase::Refill),
            }
        }

        publisher.on_bbr_state_changed(new_phase.into_event());

        *self = new_phase;
    }
}

impl IntoEvent<event::builder::BbrState> for CyclePhase {
    #[inline]
    fn into_event(self) -> event::builder::BbrState {
        use event::builder::BbrState;
        match self {
            CyclePhase::Down => BbrState::ProbeBwDown,
            CyclePhase::Cruise => BbrState::ProbeBwCruise,
            CyclePhase::Refill => BbrState::ProbeBwRefill,
            CyclePhase::Up => BbrState::ProbeBwUp,
        }
    }
}

/// How the incoming ACK stream relates to our bandwidth probing
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum AckPhase {
    /// not probing; not getting probe feedback
    Init,
    /// stopped probing; still getting feedback
    ProbeStopping,
    /// sending at est. bw to fill pipe
    Refilling,
    /// inflight rising to probe bw
    ProbeStarting,
    /// getting feedback from bw probing
    ProbeFeedback,
}

impl AckPhase {
    /// Transition to the given `new_phase`
    fn transition_to(&mut self, new_phase: AckPhase) {
        if cfg!(debug_assertions) {
            match new_phase {
                AckPhase::ProbeStopping => {
                    assert!(
                        *self == AckPhase::Init
                            || *self == AckPhase::ProbeStarting
                            || *self == AckPhase::ProbeFeedback
                    )
                }
                AckPhase::Refilling => {
                    assert!(*self == AckPhase::Init || *self == AckPhase::ProbeStopping)
                }
                AckPhase::ProbeStarting => assert_eq!(*self, AckPhase::Refilling),
                AckPhase::ProbeFeedback => assert_eq!(*self, AckPhase::ProbeStarting),
                AckPhase::Init => assert_eq!(*self, AckPhase::ProbeStopping),
            }
        }

        *self = new_phase;
    }
}

#[derive(Clone, Debug)]
pub(crate) struct State {
    /// The current mode for deciding how fast to send
    cycle_phase: CyclePhase,
    /// How the incoming ACK stream relates to our bandwidth probing
    ack_phase: AckPhase,
    /// A random duration to wait until probing for bandwidth
    bw_probe_wait: Duration,
    /// Packet-timed rounds since probed bw
    rounds_since_bw_probe: Counter<u8, Saturating>,
    /// Bytes delivered per inflight_hi increment
    bw_probe_up_cnt: u32,
    /// Bytes ACKed since inflight_hi increment
    bw_probe_up_acks: u32,
    /// cwnd-limited rounds in PROBE_UP
    bw_probe_up_rounds: u8,
    /// Time of this cycle phase start
    cycle_start_timestamp: Option<Timestamp>,
}

impl State {
    /// Constructs new `probe_bw::State`
    fn new() -> Self {
        Self {
            cycle_phase: CyclePhase::Up,
            ack_phase: AckPhase::Init,
            bw_probe_wait: Duration::ZERO,
            rounds_since_bw_probe: Counter::default(),
            bw_probe_up_cnt: u32::MAX,
            bw_probe_up_acks: 0,
            bw_probe_up_rounds: 0,
            cycle_start_timestamp: None,
        }
    }

    /// Returns the current `probe_bw::CyclePhase`
    pub fn cycle_phase(&self) -> CyclePhase {
        self.cycle_phase
    }

    pub fn on_round_start(&mut self) {
        self.rounds_since_bw_probe += 1;
    }

    /// Returns true if enough time has passed to transition the cycle phase
    pub fn is_time_to_probe_bw(
        &self,
        target_inflight: u32,
        max_data_size: u16,
        now: Timestamp,
    ) -> bool {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.5.3
        //# BBRCheckTimeToProbeBW()
        //#   if (BBRHasElapsedInPhase(BBR.bw_probe_wait) ||
        //#       BBRIsRenoCoexistenceProbeTime())
        //#     BBRStartProbeBW_REFILL()
        //#     return true
        //#   return false

        debug_assert!(
            self.cycle_phase == CyclePhase::Down || self.cycle_phase == CyclePhase::Cruise
        );

        if self.has_elapsed_in_phase(self.bw_probe_wait, now)
            || self.is_reno_coexistence_probe_time(target_inflight, max_data_size)
        {
            return true;
        }
        false
    }

    /// Probe for possible increases in bandwidth
    fn probe_inflight_hi_upward(
        &mut self,
        bytes_acknowledged: usize,
        data_volume_model: &mut data_volume::Model,
        cwnd: u32,
        max_data_size: u16,
        round_start: bool,
    ) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.6
        //# BBRProbeInflightHiUpward()
        //#   if (!is_cwnd_limited or cwnd < BBR.inflight_hi)
        //#       return  /* not fully using inflight_hi, so don't grow it */
        //#   BBR.bw_probe_up_acks += rs.newly_acked
        //#   if (BBR.bw_probe_up_acks >= BBR.probe_up_cnt)
        //#      delta = BBR.bw_probe_up_acks / BBR.probe_up_cnt
        //#      BBR.bw_probe_up_acks -= delta * BBR.bw_probe_up_cnt
        //#      BBR.inflight_hi += delta
        //#   if (BBR.round_start)
        //#      BBRRaiseInflightHiSlope()

        // is_cwnd_limited and cwnd < BBR.inflight_hi is checked upstream

        self.bw_probe_up_acks += bytes_acknowledged as u32;
        // Increase inflight_hi by the number of bw_probe_up_cnt bytes within bw_probe_up_acks
        if self.bw_probe_up_acks >= self.bw_probe_up_cnt {
            let delta = self.bw_probe_up_acks / self.bw_probe_up_cnt;
            self.bw_probe_up_acks -= delta * self.bw_probe_up_cnt;
            let inflight_hi =
                data_volume_model.inflight_hi() + (delta as u64 * max_data_size as u64);
            data_volume_model.update_upper_bound(inflight_hi);
        }
        if round_start {
            self.raise_inflight_hi_slope(cwnd, max_data_size);
        }
    }

    /// Raise inflight_hi slope if appropriate
    fn raise_inflight_hi_slope(&mut self, cwnd: u32, max_data_size: u16) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.6
        //# BBRRaiseInflightHiSlope():
        //#   growth_this_round = 1MSS << BBR.bw_probe_up_rounds
        //#   BBR.bw_probe_up_rounds = min(BBR.bw_probe_up_rounds + 1, 30)
        //#   BBR.probe_up_cnt = max(cwnd / growth_this_round, 1)

        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.4
        //# BBR takes an approach where the additive increase to BBR.inflight_hi
        //# exponentially doubles each round trip
        let growth_this_round = 1 << self.bw_probe_up_rounds;
        // The MAX_BW_PROBE_UP_ROUNDS (30) number below means `growth_this_round` is capped at 1G
        // and the lower bound of `bw_probe_up_cnt` is (practically) 1 mss, at this speed inflight_hi
        // grows by approximately 1 packet per packet acked.
        self.bw_probe_up_rounds = (self.bw_probe_up_rounds + 1).min(MAX_BW_PROBE_UP_ROUNDS);
        self.bw_probe_up_cnt = (cwnd / growth_this_round).max(max_data_size as u32);
    }

    /// True if the given `interval` duration has elapsed since the current cycle phase began
    fn has_elapsed_in_phase(&self, interval: Duration, now: Timestamp) -> bool {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.6
        //# BBRHasElapsedInPhase(interval)
        //#   return Now() > BBR.cycle_stamp + interval

        self.cycle_start_timestamp
            .map_or(false, |cycle_stamp| now > cycle_stamp + interval)
    }

    /// Bandwidth probing can cause loss. To help coexistence with loss-based
    /// congestion control we spread out our probing in a Reno-conscious way. Due to
    /// the shape of the Reno sawtooth, the time required between loss epochs for an
    /// idealized Reno flow is a number of round trips that is the BDP of that
    /// flow. We count packet-timed round trips directly, since measured RTT can
    /// vary widely, and Reno is driven by packet-timed round trips.
    fn is_reno_coexistence_probe_time(&self, target_inflight: u32, max_data_size: u16) -> bool {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.5.3
        //# BBRIsRenoCoexistenceProbeTime()
        //#   reno_rounds = BBRTargetInflight()
        //#   rounds = min(reno_rounds, 63)
        //#   return BBR.rounds_since_bw_probe >= rounds

        let reno_rounds = target_inflight / max_data_size as u32;
        let rounds = reno_rounds
            .try_into()
            .unwrap_or(u8::MAX)
            .min(MAX_BW_PROBE_ROUNDS);
        self.rounds_since_bw_probe >= rounds
    }

    /// Start the `Cruise` cycle phase
    fn start_cruise<Pub: Publisher>(&mut self, publisher: &mut Pub) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.6
        //# BBRStartProbeBW_CRUISE()
        //#   BBR.state = ProbeBW_CRUISE

        self.cycle_phase
            .transition_to(CyclePhase::Cruise, publisher);
    }

    /// Start the `Up` cycle phase
    fn start_up<Pub: Publisher>(
        &mut self,
        round_counter: &mut round::Counter,
        delivered_bytes: u64,
        cwnd: u32,
        max_data_size: u16,
        now: Timestamp,
        publisher: &mut Pub,
    ) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.6
        //# BBRStartProbeBW_UP()
        //#   BBR.ack_phase = ACKS_PROBE_STARTING
        //#   BBRStartRound()
        //#   BBR.cycle_stamp = Now() /* start wall clock */
        //#   BBR.state = ProbeBW_UP
        //#   BBRRaiseInflightHiSlope()

        self.ack_phase.transition_to(AckPhase::ProbeStarting);
        round_counter.set_round_end(delivered_bytes);
        self.cycle_start_timestamp = Some(now);
        self.cycle_phase.transition_to(CyclePhase::Up, publisher);
        self.raise_inflight_hi_slope(cwnd, max_data_size);
    }

    /// Start the `Refill` cycle phase
    fn start_refill<Pub: Publisher>(
        &mut self,
        data_volume_model: &mut data_volume::Model,
        data_rate_model: &mut data_rate::Model,
        round_counter: &mut round::Counter,
        delivered_bytes: u64,
        publisher: &mut Pub,
    ) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.6
        //# BBRStartProbeBW_REFILL()
        //# BBRResetLowerBounds()
        //#   BBR.bw_probe_up_rounds = 0
        //#   BBR.bw_probe_up_acks = 0
        //#   BBR.ack_phase = ACKS_REFILLING
        //#   BBRStartRound()
        //#   BBR.state = ProbeBW_REFILL

        data_volume_model.reset_lower_bound();
        data_rate_model.reset_lower_bound();
        self.bw_probe_up_rounds = 0;
        self.bw_probe_up_acks = 0;
        self.ack_phase.transition_to(AckPhase::Refilling);
        round_counter.set_round_end(delivered_bytes);
        self.cycle_phase
            .transition_to(CyclePhase::Refill, publisher);
    }

    /// Start the `Down` cycle phase
    fn start_down<Pub: Publisher>(
        &mut self,
        congestion_state: &mut congestion::State,
        round_counter: &mut round::Counter,
        delivered_bytes: u64,
        random_generator: &mut dyn random::Generator,
        now: Timestamp,
        publisher: &mut Pub,
    ) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.6
        //# BBRStartProbeBW_DOWN()
        //#   BBRResetCongestionSignals()
        //#   BBR.probe_up_cnt = Infinity /* not growing inflight_hi */
        //#   BBRPickProbeWait()
        //#   BBR.cycle_stamp = Now()  /* start wall clock */
        //#   BBR.ack_phase  = ACKS_PROBE_STOPPING
        //#   BBRStartRound()
        //#   BBR.state = ProbeBW_DOWN

        congestion_state.reset();
        self.bw_probe_up_cnt = u32::MAX;
        self.pick_probe_wait(random_generator);
        self.cycle_start_timestamp = Some(now);
        self.ack_phase.transition_to(AckPhase::ProbeStopping);
        round_counter.set_round_end(delivered_bytes);
        self.cycle_phase.transition_to(CyclePhase::Down, publisher);
    }

    /// Randomly determine how long to wait before probing again
    ///
    /// Note: This uses a method for determining a number in a random range that has a very slight
    ///       bias. In practice, this bias should not result in a detectable impact to BBR performance.
    fn pick_probe_wait(&mut self, random_generator: &mut dyn random::Generator) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.5.3
        //# BBRPickProbeWait()
        //#    /* Decide random round-trip bound for wait: */
        //#     BBR.rounds_since_bw_probe =
        //#       random_int_between(0, 1); /* 0 or 1 */
        //#     /* Decide the random wall clock bound for wait: */
        //#     BBR.bw_probe_wait =
        //#       2sec + random_float_between(0.0, 1.0) /* 0..1 sec */
        self.rounds_since_bw_probe
            .set(random::gen_range_biased(random_generator, 0..=1) as u8);
        self.bw_probe_wait =
            Duration::from_millis(random::gen_range_biased(random_generator, 2000..=3000) as u64);
    }

    #[cfg(test)]
    pub fn set_cycle_phase_for_test(&mut self, cycle_phase: CyclePhase) {
        self.cycle_phase = cycle_phase;

        match cycle_phase {
            CyclePhase::Down => self.ack_phase = AckPhase::ProbeStopping,
            CyclePhase::Refill => self.ack_phase = AckPhase::Refilling,
            CyclePhase::Up => self.ack_phase = AckPhase::ProbeStarting,
            CyclePhase::Cruise => {}
        }
    }
}

/// Methods related to the ProbeBW state
impl BbrCongestionController {
    /// Enters the `ProbeBw` state
    ///
    /// If `cruise_immediately` is true, `CyclePhase::Cruise` will be entered immediately
    /// after entering `CyclePhase::Down`
    #[inline]
    pub(super) fn enter_probe_bw<Pub: Publisher>(
        &mut self,
        cruise_immediately: bool,
        random_generator: &mut dyn random::Generator,
        now: Timestamp,
        publisher: &mut Pub,
    ) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.6
        //# BBREnterProbeBW():
        //#     BBRStartProbeBW_DOWN()

        let mut state = State::new();
        state.start_down(
            &mut self.congestion_state,
            &mut self.round_counter,
            self.bw_estimator.delivered_bytes(),
            random_generator,
            now,
            publisher,
        );

        if cruise_immediately {
            state.start_cruise(publisher);
        }

        // New BBR state requires updating the model
        self.try_fast_path = false;
        self.state
            .transition_to(bbr::State::ProbeBw(state), publisher);
    }

    /// Transition the current Probe BW cycle phase if necessary
    #[inline]
    pub(super) fn update_probe_bw_cycle_phase<Pub: Publisher>(
        &mut self,
        random_generator: &mut dyn random::Generator,
        now: Timestamp,
        publisher: &mut Pub,
    ) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.6
        //# BBRUpdateProbeBWCyclePhase():
        //#    if (!BBR.filled_pipe)
        //#      return  /* only handling steady-state behavior here */
        //#    BBRAdaptUpperBounds()
        //#    if (!IsInAProbeBWState())
        //#      return /* only handling ProbeBW states here: */
        //#
        //#    switch (state)
        //#
        //#    ProbeBW_DOWN:
        //#      if (BBRCheckTimeToProbeBW())
        //#        return /* already decided state transition */
        //#      if (BBRCheckTimeToCruise())
        //#        BBRStartProbeBW_CRUISE()
        //#
        //#    ProbeBW_CRUISE:
        //#      if (BBRCheckTimeToProbeBW())
        //#        return /* already decided state transition */
        //#
        //#    ProbeBW_REFILL:
        //#      /* After one round of REFILL, start UP */
        //#      if (BBR.round_start)
        //#        BBR.bw_probe_samples = 1
        //#        BBRStartProbeBW_UP()
        //#
        //#    ProbeBW_UP:
        //#      if (BBRHasElapsedInPhase(BBR.min_rtt) and
        //#          inflight > BBRInflight(BBR.max_bw, 1.25))
        //#       BBRStartProbeBW_DOWN()

        debug_assert!(
            self.full_pipe_estimator.filled_pipe(),
            "only handling steady-state behavior here"
        );

        debug_assert!(
            self.state.is_probing_bw(),
            "only handling ProbeBw states here"
        );

        let target_inflight = self.target_inflight();
        let inflight = self.inflight(self.data_rate_model.max_bw(), self.state.pacing_gain());
        let time_to_cruise = self.is_time_to_cruise(now);

        if let bbr::State::ProbeBw(ref mut probe_bw_state) = self.state {
            let prior_cycle_phase = probe_bw_state.cycle_phase();

            if self.round_counter.round_start() {
                probe_bw_state.on_round_start();
            }

            match probe_bw_state.cycle_phase() {
                CyclePhase::Down | CyclePhase::Cruise => {
                    if probe_bw_state.is_time_to_probe_bw(
                        target_inflight,
                        self.max_datagram_size,
                        now,
                    ) {
                        probe_bw_state.start_refill(
                            &mut self.data_volume_model,
                            &mut self.data_rate_model,
                            &mut self.round_counter,
                            self.bw_estimator.delivered_bytes(),
                            publisher,
                        );
                    } else if probe_bw_state.cycle_phase == CyclePhase::Down && time_to_cruise {
                        probe_bw_state.start_cruise(publisher);
                    }
                }
                CyclePhase::Refill => {
                    // After one round of Refill, start Up
                    if self.round_counter.round_start() {
                        self.bw_probe_samples = true;
                        probe_bw_state.start_up(
                            &mut self.round_counter,
                            self.bw_estimator.delivered_bytes(),
                            self.cwnd,
                            self.max_datagram_size,
                            now,
                            publisher,
                        );
                    }
                }
                CyclePhase::Up => {
                    let min_rtt = self
                        .data_volume_model
                        .min_rtt()
                        .expect("at least one RTT has passed");

                    if probe_bw_state.has_elapsed_in_phase(min_rtt, now)
                        && self.bytes_in_flight > inflight
                    {
                        probe_bw_state.start_down(
                            &mut self.congestion_state,
                            &mut self.round_counter,
                            self.bw_estimator.delivered_bytes(),
                            random_generator,
                            now,
                            publisher,
                        );
                    }
                }
            }

            if prior_cycle_phase != probe_bw_state.cycle_phase() {
                // New phase, so need to update cwnd and pacing rate
                self.try_fast_path = false;
            }
        }
    }

    /// Adapt the upper bounds lower or higher depending on the loss rate
    #[inline]
    pub(super) fn adapt_upper_bounds<Pub: Publisher>(
        &mut self,
        bytes_acknowledged: usize,
        random_generator: &mut dyn random::Generator,
        now: Timestamp,
        publisher: &mut Pub,
    ) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.6
        //# BBRAdaptUpperBounds()
        //#   if (BBR.ack_phase == ACKS_PROBE_STARTING and BBR.round_start)
        //#      /* starting to get bw probing samples */
        //#      BBR.ack_phase = ACKS_PROBE_FEEDBACK
        //#    if (BBR.ack_phase == ACKS_PROBE_STOPPING and BBR.round_start)
        //#      /* end of samples from bw probing phase */
        //#      if (IsInAProbeBWState() and !rs.is_app_limited)
        //#        BBRAdvanceMaxBwFilter()
        //#
        //#    if (!CheckInflightTooHigh())
        //#      /* Loss rate is safe. Adjust upper bounds upward. */
        //#      if (BBR.inflight_hi == Infinity or BBR.bw_hi == Infinity)
        //#        return /* no upper bounds to raise */
        //#      if (rs.tx_in_flight > BBR.inflight_hi)
        //#        BBR.inflight_hi = rs.tx_in_flight

        debug_assert!(
            self.full_pipe_estimator.filled_pipe(),
            "only handling steady-state behavior here"
        );

        let rate_sample = self.bw_estimator.rate_sample();

        // Update AckPhase once per round
        if self.round_counter.round_start() {
            self.update_ack_phase(rate_sample);
        }

        if Self::is_inflight_too_high(
            rate_sample,
            self.max_datagram_size,
            self.congestion_state.loss_bursts_in_round(),
            PROBE_BW_FULL_LOSS_COUNT,
        ) {
            if self.bw_probe_samples {
                // Inflight is too high and the sample is from bandwidth probing: lower inflight downward
                self.on_inflight_too_high(
                    rate_sample.is_app_limited,
                    rate_sample.bytes_in_flight,
                    random_generator,
                    now,
                    publisher,
                );
            }
        } else {
            // Loss rate is safe. Adjust upper bounds upward

            if self.data_volume_model.inflight_hi() == u64::MAX {
                // no upper bounds to raise
                return;
            }

            // draft-cardwell-iccrg-bbr-congestion-control-02 also considers raising bw_hi at this
            // point, but since the draft never lowers bw_hi from its initial value of Infinity, this
            // doesn't have any effect. bw_hi in the current Linux V2Alpha BBR2 branch corresponds
            // to max_hi from the draft, there is no equivalent to the bw_hi in the draft
            // TODO: Update this logic based on subsequent draft updates or consider lowering
            //       bw_hi in `on_inflight_too_high`
            if rate_sample.bytes_in_flight as u64 > self.data_volume_model.inflight_hi() {
                self.data_volume_model
                    .update_upper_bound(rate_sample.bytes_in_flight as u64);
            }

            if let bbr::State::ProbeBw(ref mut probe_bw_state) = self.state {
                if probe_bw_state.cycle_phase() == CyclePhase::Up
                    && self.cwnd_limited_in_round
                    && self.cwnd as u64 >= self.data_volume_model.inflight_hi()
                {
                    // inflight_hi is being fully utilized, so probe if we can increase it
                    probe_bw_state.probe_inflight_hi_upward(
                        bytes_acknowledged,
                        &mut self.data_volume_model,
                        self.cwnd,
                        self.max_datagram_size,
                        self.round_counter.round_start(),
                    );
                }
            }
        }
    }

    /// Update AckPhase and advance the Max BW filter if necessary
    #[inline]
    fn update_ack_phase(&mut self, rate_sample: RateSample) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.6
        //#   if (BBR.ack_phase == ACKS_PROBE_STARTING and BBR.round_start)
        //#      /* starting to get bw probing samples */
        //#      BBR.ack_phase = ACKS_PROBE_FEEDBACK
        //#    if (BBR.ack_phase == ACKS_PROBE_STOPPING and BBR.round_start)
        //#      /* end of samples from bw probing phase */
        //#      if (IsInAProbeBWState() and !rs.is_app_limited)
        //#        BBRAdvanceMaxBwFilter()

        debug_assert!(self.round_counter.round_start());

        if let bbr::State::ProbeBw(ref mut probe_bw_state) = self.state {
            match probe_bw_state.ack_phase {
                AckPhase::ProbeStarting => {
                    // starting to get bw probing samples
                    probe_bw_state
                        .ack_phase
                        .transition_to(AckPhase::ProbeFeedback);
                }
                AckPhase::ProbeStopping => {
                    // end of samples from bw probing phase
                    self.bw_probe_samples = false;
                    probe_bw_state.ack_phase.transition_to(AckPhase::Init);
                    if !rate_sample.is_app_limited {
                        self.data_rate_model.advance_max_bw_filter();
                    }
                }
                _ => {}
            }
        } else {
            self.bw_probe_samples = false;
        }
    }

    /// Called when loss indicates the current inflight amount is too high
    #[inline]
    pub(super) fn on_inflight_too_high<Pub: Publisher>(
        &mut self,
        is_app_limited: bool,
        bytes_in_flight: u32,
        random_generator: &mut dyn random::Generator,
        now: Timestamp,
        publisher: &mut Pub,
    ) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.5.6.2
        //# BBRHandleInflightTooHigh()
        //# BBR.bw_probe_samples = 0;  /* only react once per bw probe */
        //#    if (!rs.is_app_limited)
        //#      BBR.inflight_hi = max(rs.tx_in_flight,
        //#                            BBRTargetInflight() * BBRBeta))
        //#    If (BBR.state == ProbeBW_UP)
        //#      BBRStartProbeBW_DOWN()

        self.bw_probe_samples = false; // only react once per bw probe
        if !is_app_limited {
            self.data_volume_model.update_upper_bound(
                (bytes_in_flight as u64)
                    .max((bbr::BETA * self.target_inflight() as u64).to_integer()),
            )
        }

        if let bbr::State::ProbeBw(ref mut probe_bw_state) = self.state {
            if probe_bw_state.cycle_phase() == CyclePhase::Up {
                probe_bw_state.start_down(
                    &mut self.congestion_state,
                    &mut self.round_counter,
                    self.bw_estimator.delivered_bytes(),
                    random_generator,
                    now,
                    publisher,
                );
            }
        }
    }

    /// Returns true if it is time to transition from `Down` to `Cruise`
    #[inline]
    fn is_time_to_cruise(&self, now: Timestamp) -> bool {
        if let (bbr::State::ProbeBw(probe_bw_state), Some(min_rtt)) =
            (&self.state, self.data_volume_model.min_rtt())
        {
            // Chromium and Linux TCP both limit the time spent in ProbeBW_Down to min_rtt
            // See https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L1982-L1981
            //  and https://source.chromium.org/chromium/chromium/src/+/main:net/third_party/quiche/src/quiche/quic/core/congestion_control/bbr2_probe_bw.cc;l=276
            if probe_bw_state.has_elapsed_in_phase(min_rtt, now) {
                return true;
            }
        }

        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.6
        //# BBRCheckTimeToCruise())
        //#   if (inflight > BBRInflightWithHeadroom())
        //#      return false /* not enough headroom */
        //#   if (inflight <= BBRInflight(BBR.max_bw, 1.0))
        //#      return true  /* inflight <= estimated BDP */
        if self.bytes_in_flight > self.inflight_with_headroom() {
            return false; // not enough headroom
        }
        if self.bytes_in_flight <= self.inflight(self.data_rate_model.max_bw(), Ratio::one()) {
            return true; // inflight <= estimated BDP
        }
        false
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        path,
        path::MINIMUM_MTU,
        recovery::{
            bandwidth::{Bandwidth, PacketInfo},
            congestion_controller::PathPublisher,
        },
        time::{Clock, NoopClock},
    };

    #[test]
    fn pacing_gain() {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.1
        //= type=test
        //# In the ProbeBW_DOWN phase of the cycle, a BBR flow pursues the deceleration tactic,
        //# to try to send slower than the network is delivering data, to reduce the amount of data
        //# in flight, with all of the standard motivations for the deceleration tactic (discussed
        //# in "State Machine Tactics", above). It does this by switching to a BBR.pacing_gain of
        //# 0.9, sending at 90% of BBR.bw.
        assert_eq!(Ratio::new_raw(9, 10), CyclePhase::Down.pacing_gain());

        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.4
        //= type=test
        //# After ProbeBW_REFILL refills the pipe, ProbeBW_UP probes for possible increases in
        //# available bandwidth by using a BBR.pacing_gain of 1.25, sending faster than the current
        //# estimated available bandwidth.
        assert_eq!(Ratio::new_raw(5, 4), CyclePhase::Up.pacing_gain());

        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.3
        //= type=test
        //# During ProbeBW_REFILL BBR uses a BBR.pacing_gain of 1.0, to send at a rate that
        //# matches the current estimated available bandwidth
        assert_eq!(Ratio::new_raw(1, 1), CyclePhase::Refill.pacing_gain());

        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.2
        //= type=test
        //# In the ProbeBW_CRUISE phase of the cycle, a BBR flow pursues the "cruising" tactic
        //# (discussed in "State Machine Tactics", above), attempting to send at the same rate
        //# the network is delivering data. It tries to match the sending rate to the flow's
        //# current available bandwidth, to try to achieve high utilization of the available
        //# bandwidth without increasing queue pressure. It does this by switching to a
        //# pacing_gain of 1.0, sending at 100% of BBR.bw.
        assert_eq!(Ratio::new_raw(1, 1), CyclePhase::Cruise.pacing_gain());
    }

    #[test]
    fn new_probe_bw_state() {
        let state = State::new();

        assert_eq!(CyclePhase::Up, state.cycle_phase);
        assert_eq!(AckPhase::Init, state.ack_phase);
        assert_eq!(Duration::ZERO, state.bw_probe_wait);
        assert_eq!(Counter::new(0), state.rounds_since_bw_probe);
        assert_eq!(0, state.bw_probe_up_acks);
        assert_eq!(0, state.bw_probe_up_rounds);
        assert_eq!(None, state.cycle_start_timestamp);
    }

    #[test]
    fn is_time_to_probe_bw() {
        let mut state = State::new();
        let now = NoopClock.get_time();
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        state
            .cycle_phase
            .transition_to(CyclePhase::Down, &mut publisher);

        // cycle_stamp hasn't been set yet
        assert!(!state.is_time_to_probe_bw(12000, 1200, now));

        state.cycle_start_timestamp = Some(now);
        let bw_probe_wait = Duration::from_millis(500);
        state.bw_probe_wait = bw_probe_wait;
        // not ready to probe yet
        assert!(!state.is_time_to_probe_bw(12000, 1200, now + bw_probe_wait));
        // now we're ready to probe
        assert!(state.is_time_to_probe_bw(
            100,
            1200,
            now + bw_probe_wait + Duration::from_millis(1)
        ));

        state.rounds_since_bw_probe = Counter::new(10);
        // 13200 / 1200 = 11 reno rounds, not in reno coexistence probe time
        assert!(!state.is_time_to_probe_bw(13200, 1200, now));
        // 12000 / 1200 = 10 reno rounds, now we are in reno coexistence probe time
        assert!(state.is_time_to_probe_bw(12000, 1200, now));

        // At high BDPs, we probe when MAX_BW_PROBE_ROUNDS is reached
        state.rounds_since_bw_probe = Counter::new(MAX_BW_PROBE_ROUNDS - 1);
        assert!(!state.is_time_to_probe_bw(u32::MAX, 1200, now));
        state.rounds_since_bw_probe = Counter::new(MAX_BW_PROBE_ROUNDS);
        assert!(state.is_time_to_probe_bw(u32::MAX, 1200, now));
    }

    #[test]
    fn probe_inflight_hi_upward() {
        let mut state = State::new();

        let bytes_acknowledged = 2400;
        let mut data_volume_model = data_volume::Model::new();
        let cwnd = 12000;
        let max_data_size = 1200;
        let round_start = true;

        state.bw_probe_up_rounds = 3;
        data_volume_model.update_upper_bound(12000);

        state.probe_inflight_hi_upward(
            bytes_acknowledged,
            &mut data_volume_model,
            cwnd,
            max_data_size,
            round_start,
        );

        assert_eq!(bytes_acknowledged as u32, state.bw_probe_up_acks);
        assert_eq!(12000, data_volume_model.inflight_hi());
        assert_eq!(4, state.bw_probe_up_rounds);
        // bw_probe_up_cnt = cwnd (12000) / 1 << 3
        assert_eq!(cwnd / 8, state.bw_probe_up_cnt);

        let new_bytes_acknowledged = (cwnd / 8) as usize;
        state.probe_inflight_hi_upward(
            new_bytes_acknowledged,
            &mut data_volume_model,
            cwnd,
            max_data_size,
            false,
        );

        // bw_probe_up_acks = bytes_acknowledged + new_bytes_acknowledged = 3900
        // delta = 3900 / bw_probe_up_cnt  = 3900 / 1500 = 2
        // bw_probe_up_acks = bw_probe_up_acks - delta * bw_probe_up_cnt = 3900 - 2 * 1500 = 900
        assert_eq!(900, state.bw_probe_up_acks);
        // inflight_hi = inflight_hi + delta * max_data_size = 12000 + 2 * 1200 = 14400
        assert_eq!(14400, data_volume_model.inflight_hi());
        // bw_probe_up_rounds stays the same, since round_start was false
        assert_eq!(4, state.bw_probe_up_rounds);
        // bw_probe_up_cnt stays the same, since round_start was false
        assert_eq!(cwnd / 8, state.bw_probe_up_cnt);
    }

    #[test]
    fn start_cruise() {
        let mut state = State::new();
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        state
            .cycle_phase
            .transition_to(CyclePhase::Down, &mut publisher);

        state.start_cruise(&mut publisher);

        assert_eq!(CyclePhase::Cruise, state.cycle_phase());
    }

    #[test]
    fn start_up() {
        let mut state = State::new();
        let mut round_counter = round::Counter::default();
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        let delivered_bytes = 100;
        let cwnd = 12000;
        let max_data_size = 1200;
        let now = NoopClock.get_time();

        state.ack_phase = AckPhase::Refilling;
        state.cycle_phase = CyclePhase::Refill;

        state.start_up(
            &mut round_counter,
            delivered_bytes,
            cwnd,
            max_data_size,
            now,
            &mut publisher,
        );

        assert_eq!(CyclePhase::Up, state.cycle_phase());
        assert_eq!(AckPhase::ProbeStarting, state.ack_phase);
        assert_eq!(Some(now), state.cycle_start_timestamp);

        // raise_inflight_hi_slope is called
        assert_eq!(1, state.bw_probe_up_rounds);
        assert_eq!(cwnd, state.bw_probe_up_cnt);

        // verify the end of round is set to delivered_bytes
        // verify the end of round is set to delivered_bytes
        assert_round_end(round_counter, delivered_bytes);
    }

    #[test]
    fn start_refill() {
        let mut state = State::new();
        let mut round_counter = round::Counter::default();
        let delivered_bytes = 100;
        let mut data_volume_model = data_volume::Model::new();
        let mut data_rate_model = data_rate::Model::new();
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        data_volume_model.update_lower_bound(12000, 12000, true, false, 1.0);
        data_rate_model.update_lower_bound(Bandwidth::ZERO);

        state.ack_phase = AckPhase::ProbeStopping;
        state.cycle_phase = CyclePhase::Cruise;

        state.start_refill(
            &mut data_volume_model,
            &mut data_rate_model,
            &mut round_counter,
            delivered_bytes,
            &mut publisher,
        );

        assert_eq!(CyclePhase::Refill, state.cycle_phase());
        // Lower bounds are reset
        assert_eq!(u64::MAX, data_volume_model.inflight_lo());
        assert_eq!(Bandwidth::INFINITY, data_rate_model.bw_lo());

        assert_eq!(0, state.bw_probe_up_rounds);
        assert_eq!(0, state.bw_probe_up_acks);
        assert_eq!(AckPhase::Refilling, state.ack_phase);

        // verify the end of round is set to delivered_bytes
        assert_round_end(round_counter, delivered_bytes);
    }

    #[test]
    fn start_down() {
        let mut state = State::new();
        let mut congestion_state = congestion::testing::test_state();
        let mut round_counter = round::Counter::default();
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        let delivered_bytes = 100;
        let now = NoopClock.get_time();
        let random = &mut random::testing::Generator::default();

        state.cycle_phase = CyclePhase::Up;

        state.start_down(
            &mut congestion_state,
            &mut round_counter,
            delivered_bytes,
            random,
            now,
            &mut publisher,
        );

        assert_eq!(CyclePhase::Down, state.cycle_phase());
        assert_eq!(u32::MAX, state.bw_probe_up_cnt);
        assert!(state.rounds_since_bw_probe >= 0 && state.rounds_since_bw_probe <= 1);
        assert!(
            state.bw_probe_wait >= Duration::from_secs(2)
                && state.bw_probe_wait <= Duration::from_secs(3)
        );
        assert_eq!(Some(now), state.cycle_start_timestamp);
        assert_eq!(AckPhase::ProbeStopping, state.ack_phase);

        // verify congestion state is reset
        congestion::testing::assert_reset(congestion_state);

        // verify the end of round is set to delivered_bytes
        assert_round_end(round_counter, delivered_bytes);
    }

    fn assert_round_end(mut round_counter: round::Counter, expected_end: u64) {
        let now = NoopClock.get_time();
        // verify the end of round is set to delivered_bytes
        let mut packet_info = PacketInfo {
            delivered_bytes: expected_end - 1,
            delivered_time: now,
            lost_bytes: 0,
            ecn_ce_count: 0,
            first_sent_time: now,
            bytes_in_flight: 0,
            is_app_limited: false,
        };
        round_counter.on_ack(packet_info, expected_end);
        assert!(!round_counter.round_start());

        packet_info.delivered_bytes = expected_end;
        round_counter.on_ack(packet_info, expected_end);
        assert!(round_counter.round_start());
    }

    #[test]
    fn ack_phase_valid_transitions() {
        let mut ack_phase = AckPhase::Init;
        ack_phase.transition_to(AckPhase::ProbeStopping);
        assert_eq!(ack_phase, AckPhase::ProbeStopping);
        ack_phase.transition_to(AckPhase::Refilling);
        assert_eq!(ack_phase, AckPhase::Refilling);
        ack_phase.transition_to(AckPhase::ProbeStarting);
        assert_eq!(ack_phase, AckPhase::ProbeStarting);
        ack_phase.transition_to(AckPhase::ProbeFeedback);
        assert_eq!(ack_phase, AckPhase::ProbeFeedback);
        ack_phase.transition_to(AckPhase::ProbeStopping);
        assert_eq!(ack_phase, AckPhase::ProbeStopping);
        ack_phase.transition_to(AckPhase::Refilling);
        assert_eq!(ack_phase, AckPhase::Refilling);
        ack_phase.transition_to(AckPhase::ProbeStarting);
        assert_eq!(ack_phase, AckPhase::ProbeStarting);
        ack_phase.transition_to(AckPhase::ProbeStopping);
        assert_eq!(ack_phase, AckPhase::ProbeStopping);
        ack_phase.transition_to(AckPhase::Init);
        assert_eq!(ack_phase, AckPhase::Init);
        ack_phase.transition_to(AckPhase::Refilling);
        assert_eq!(ack_phase, AckPhase::Refilling);
    }

    #[test]
    fn enter_probe_bw() {
        let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
        let mut rng = random::testing::Generator::default();
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        let now = NoopClock.get_time();
        bbr.state = bbr::State::Drain;

        // cruise_immediately = false
        bbr.enter_probe_bw(false, &mut rng, now, &mut publisher);

        assert!(bbr.state.is_probing_bw());
        if let bbr::State::ProbeBw(probe_bw_state) = bbr.state {
            assert_eq!(CyclePhase::Down, probe_bw_state.cycle_phase());
        }

        assert!(!bbr.try_fast_path);

        // cruise_immediately = true
        bbr.state = bbr::State::Drain;
        bbr.enter_probe_bw(true, &mut rng, now, &mut publisher);
        assert!(bbr.state.is_probing_bw_cruise());
    }

    #[test]
    fn update_ack_phase() {
        let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
        let mut rng = random::testing::Generator::default();
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        let now = NoopClock.get_time();
        bbr.state = bbr::State::Drain;

        // cruise_immediately = false
        bbr.enter_probe_bw(false, &mut rng, now, &mut publisher);

        // Start a new round
        let packet_info = PacketInfo {
            delivered_bytes: 0,
            delivered_time: now,
            lost_bytes: 0,
            ecn_ce_count: 0,
            first_sent_time: now,
            bytes_in_flight: 3000,
            is_app_limited: false,
        };
        bbr.round_counter.on_ack(packet_info, 5000);

        bbr.bw_probe_samples = true;

        assert!(bbr.state.is_probing_bw());
        if let bbr::State::ProbeBw(ref mut probe_bw_state) = bbr.state {
            assert_eq!(probe_bw_state.ack_phase, AckPhase::ProbeStopping);
            assert_eq!(bbr.data_rate_model.cycle_count(), 0);
        }

        let rate_sample = RateSample {
            is_app_limited: false,
            ..Default::default()
        };
        bbr.update_ack_phase(rate_sample);

        // Moving from ProbeStopping to Init increments the cycle count
        if let bbr::State::ProbeBw(ref mut probe_bw_state) = bbr.state {
            assert_eq!(probe_bw_state.ack_phase, AckPhase::Init);
            assert_eq!(bbr.data_rate_model.cycle_count(), 1);
            assert!(!bbr.bw_probe_samples);
        }

        bbr.update_ack_phase(rate_sample);

        // Updating the ack phase again does not increment the cycle count
        if let bbr::State::ProbeBw(ref mut probe_bw_state) = bbr.state {
            assert_eq!(probe_bw_state.ack_phase, AckPhase::Init);
            assert_eq!(bbr.data_rate_model.cycle_count(), 1);

            // set ack phase for the next test
            probe_bw_state.ack_phase = AckPhase::ProbeStarting;
        }

        bbr.bw_probe_samples = true;
        bbr.update_ack_phase(rate_sample);

        if let bbr::State::ProbeBw(ref mut probe_bw_state) = bbr.state {
            assert_eq!(probe_bw_state.ack_phase, AckPhase::ProbeFeedback);
            assert_eq!(bbr.data_rate_model.cycle_count(), 1);
            assert!(bbr.bw_probe_samples);
        }
    }
}


// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use crate::{
    random,
    recovery::{
        bandwidth, bbr,
        bbr::{probe_rtt, round, BbrCongestionController},
        congestion_controller::Publisher,
    },
    time::{Timer, Timestamp},
};
use core::time::Duration;
use num_rational::Ratio;

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.14.2
//# A constant specifying the minimum duration for which ProbeRTT state
//# holds inflight to BBRMinPipeCwnd or fewer packets: 200 ms.
const PROBE_RTT_DURATION: Duration = Duration::from_millis(200);

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.4.4
//# BBREnterProbeRTT():
//#     BBR.state = ProbeRTT
//#     BBR.pacing_gain = 1
pub(crate) const PACING_GAIN: Ratio<u64> = Ratio::new_raw(1, 1);

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.14.2
//# A constant specifying the gain value for calculating the cwnd during ProbeRTT: 0.5
pub(crate) const CWND_GAIN: Ratio<u64> = Ratio::new_raw(1, 2);

#[derive(Clone, Debug, Default)]
pub(crate) struct State {
    timer: Timer,
    round_done: bool,
}

impl State {
    /// Keeps BBR in the `ProbeRTT` state for max of (PROBE_RTT_DURATION, 1 round)
    fn handle_probe_rtt(
        &mut self,
        bw_estimator: &mut bandwidth::Estimator,
        round_counter: &mut round::Counter,
        probe_rtt_cwnd: u32,
        bytes_in_flight: u32,
        now: Timestamp,
    ) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.4.4
        //# BBRHandleProbeRTT()
        //#     /* Ignore low rate samples during ProbeRTT: */
        //#     MarkConnectionAppLimited()
        //#     if (BBR.probe_rtt_done_stamp == 0 and
        //#         packets_in_flight <= BBRProbeRTTCwnd())
        //#       /* Wait for at least ProbeRTTDuration to elapse: */
        //#      BBR.probe_rtt_done_stamp =
        //#         Now() + ProbeRTTDuration
        //#       /* Wait for at least one round to elapse: */
        //#       BBR.probe_rtt_round_done = false
        //#       BBRStartRound()
        //#     else if (BBR.probe_rtt_done_stamp != 0)
        //#       if (BBR.round_start)
        //#         BBR.probe_rtt_round_done = true
        //#       if (BBR.probe_rtt_round_done)
        //#         BBRCheckProbeRTTDone()

        // Ignore low rate samples during ProbeRTT
        bw_estimator.on_app_limited(bytes_in_flight);

        if !self.timer.is_armed() && bytes_in_flight <= probe_rtt_cwnd {
            // Wait for at least ProbeRTTDuration to elapse:
            self.timer.set(now + PROBE_RTT_DURATION);
            // Wait for at least one round to elapse:
            self.round_done = false;
            round_counter.set_round_end(bw_estimator.delivered_bytes());
        } else if self.timer.is_armed() && round_counter.round_start() {
            self.round_done = true;
        }
    }

    /// Returns true if the `ProbeRtt` state is done and should be exited
    pub fn is_done(&self, now: Timestamp) -> bool {
        self.round_done && self.timer.is_expired(now)
    }
}

/// Methods related to the `ProbeRtt` state
impl BbrCongestionController {
    /// Check if it is time to start probing for RTT changes, and enter the ProbeRtt state if so
    #[inline]
    pub(super) fn check_probe_rtt<Pub: Publisher>(
        &mut self,
        random_generator: &mut dyn random::Generator,
        now: Timestamp,
        publisher: &mut Pub,
    ) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.4.4
        //# BBRCheckProbeRTT()
        //#    if (BBR.state != ProbeRTT and
        //#         BBR.probe_rtt_expired and
        //#         not BBR.idle_restart)
        //#       BBREnterProbeRTT()
        //#       BBRSaveCwnd()
        //#       BBR.probe_rtt_done_stamp = 0
        //#       BBR.ack_phase = ACKS_PROBE_STOPPING
        //#      BBRStartRound()
        //#     if (BBR.state == ProbeRTT)
        //#       BBRHandleProbeRTT()
        //#     if (rs.delivered > 0)
        //#       BBR.idle_restart = false

        // `BBR.probe_rtt_done_stamp = 0`, which is equivalent to `probe_rtt::State.timer.cancel`, is
        // not necessary, as the timer is contained with the `probe_rtt::State`, and is thus unarmed
        // whenever BBR is not in the `ProbeRTT` state

        // `BBR.ack_phase = ACKS_PROBE_STOPPING` is not performed here as `ack_phase` is only used by
        // the `ProbeBW` state, which is initialized to `ACKS_PROBE_STOPPING` every time it is
        // reentered

        if !self.state.is_probing_rtt()
            && self.data_volume_model.probe_rtt_expired()
            && !self.idle_restart
        {
            // New BBR state requires updating the model
            self.try_fast_path = false;
            self.state
                .transition_to(bbr::State::ProbeRtt(State::default()), publisher);
            self.save_cwnd();
            self.round_counter
                .set_round_end(self.bw_estimator.delivered_bytes());
        }

        let probe_rtt_cwnd = self.probe_rtt_cwnd();
        if let bbr::State::ProbeRtt(probe_rtt_state) = &mut self.state {
            probe_rtt_state.handle_probe_rtt(
                &mut self.bw_estimator,
                &mut self.round_counter,
                probe_rtt_cwnd,
                *self.bytes_in_flight,
                now,
            );
            // The RFC pseudocode exits `ProbeRTT` internal to `BBRHandleProbeRTT`, whereas this
            // code checks if the `ProbeRTT` state is ready to exit here
            if probe_rtt_state.is_done(now) {
                self.exit_probe_rtt(random_generator, now, publisher);
            }
        }

        if self.bw_estimator.rate_sample().delivered_bytes > 0 {
            self.idle_restart = false;
        }
    }

    /// Exits the `ProbeRtt` state
    #[inline]
    pub(super) fn exit_probe_rtt<Pub: Publisher>(
        &mut self,
        random_generator: &mut dyn random::Generator,
        now: Timestamp,
        publisher: &mut Pub,
    ) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.4.4
        //# BBRCheckProbeRTTDone():
        //#  if (BBR.probe_rtt_done_stamp != 0 and
        //#      Now() > BBR.probe_rtt_done_stamp)
        //#    /* schedule next ProbeRTT: */
        //#    BBR.probe_rtt_min_stamp = Now()
        //#    BBRRestoreCwnd()
        //#    BBRExitProbeRTT()

        if cfg!(debug_assertions) {
            // BBR.probe_rtt_done_stamp != 0 and Now() > BBR.probe_rtt_done_stamp should be
            // checked by calling `probe_rtt_state.is_done(now)` prior to calling `exit_probe_rtt`
            assert!(self.state.is_probing_rtt());
            if let bbr::State::ProbeRtt(probe_rtt_state) = &self.state {
                assert!(probe_rtt_state.is_done(now));
            }
        }

        // schedule next ProbeRTT:
        self.data_volume_model.schedule_next_probe_rtt(now);
        self.restore_cwnd();

        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.4.5
        //# BBRExitProbeRTT()
        //#     BBRResetLowerBounds()
        //#     if (BBR.filled_pipe)
        //#       BBRStartProbeBW_DOWN()
        //#       BBRStartProbeBW_CRUISE()
        //#     else
        //#       BBREnterStartup()

        self.data_volume_model.reset_lower_bound();
        self.data_rate_model.reset_lower_bound();

        if self.full_pipe_estimator.filled_pipe() {
            //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.4.5
            //# as an optimization, since the connection is exiting ProbeRTT, we know that infligh
            //# is already below the estimated BDP, so the connection can proceed immediately to
            //# ProbeBW_CRUISE
            let cruise_immediately = true;
            self.enter_probe_bw(cruise_immediately, random_generator, now, publisher);
        } else {
            self.enter_startup(publisher);
        }
    }

    /// Returns the congestion window that should be used during the `ProbeRTT` state
    #[inline]
    pub(super) fn probe_rtt_cwnd(&self) -> u32 {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.4.5
        //# BBRProbeRTTCwnd():
        //#    probe_rtt_cwnd = BBRBDPMultiple(BBR.bw, BBRProbeRTTCwndGain)
        //#    probe_rtt_cwnd = max(probe_rtt_cwnd, BBRMinPipeCwnd)
        //#    return probe_rtt_cwnd

        self.bdp_multiple(self.data_rate_model.bw(), probe_rtt::CWND_GAIN)
            .try_into()
            .unwrap_or(u32::MAX)
            .max(self.minimum_window())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        event, path,
        path::MINIMUM_MTU,
        recovery::{
            bandwidth::{Bandwidth, PacketInfo, RateSample},
            bbr::windowed_filter::PROBE_RTT_INTERVAL,
            congestion_controller::PathPublisher,
        },
        time::{Clock, NoopClock},
    };

    #[test]
    fn check_probe_rtt_enter_probe_rtt() {
        let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
        let now = NoopClock.get_time();
        let mut rng = random::testing::Generator::default();
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        bbr.bw_estimator.set_delivered_bytes_for_test(1000);

        bbr.data_volume_model
            .update_min_rtt(Duration::from_millis(100), now);
        let now = now + PROBE_RTT_INTERVAL;
        bbr.data_volume_model
            .update_min_rtt(Duration::from_millis(100), now);
        assert!(bbr.data_volume_model.probe_rtt_expired());
        assert!(!bbr.idle_restart);

        bbr.check_probe_rtt(&mut rng, now, &mut publisher);
        assert!(bbr.state.is_probing_rtt());
        assert!(!bbr.try_fast_path);
        assert_eq!(bbr.prior_cwnd, bbr.cwnd);
        assert_eq!(1000, bbr.round_counter.round_end());
    }

    #[test]
    fn check_probe_rtt_exit_probe_rtt() {
        let mut bbr = bbr_in_probe_rtt_ready_to_exit();
        let mut rng = random::testing::Generator::default();
        let now = NoopClock.get_time();
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        bbr.idle_restart = true;
        bbr.check_probe_rtt(&mut rng, now, &mut publisher);
        assert!(!bbr.state.is_probing_rtt());
        // No delivered bytes in the rate sample, so remain in idle restart
        assert!(bbr.idle_restart);

        // Next probe rtt is scheduled
        assert_eq!(
            Some(now + PROBE_RTT_INTERVAL),
            bbr.data_volume_model.next_probe_rtt()
        );

        let mut bbr = bbr_in_probe_rtt_ready_to_exit();
        bbr.idle_restart = true;
        bbr.bw_estimator.set_rate_sample_for_test(RateSample {
            delivered_bytes: 1000,
            ..Default::default()
        });

        bbr.check_probe_rtt(&mut rng, now, &mut publisher);
        assert!(!bbr.state.is_probing_rtt());
        // Positive elivered bytes in the rate sample, so set idle restart to false
        assert!(!bbr.idle_restart);

        // Next probe rtt is scheduled
        assert_eq!(
            Some(now + PROBE_RTT_INTERVAL),
            bbr.data_volume_model.next_probe_rtt()
        );
    }

    #[test]
    fn exit_probe_rtt() {
        let mut bbr = bbr_in_probe_rtt_ready_to_exit();
        let mut rng = random::testing::Generator::default();
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        let now = NoopClock.get_time();
        bbr.cwnd = 1000;
        bbr.prior_cwnd = 2000;
        bbr.data_volume_model.set_inflight_lo_for_test(100_000);
        bbr.data_rate_model
            .update_lower_bound(Bandwidth::new(1000, Duration::from_millis(1)));
        bbr.exit_probe_rtt(&mut rng, now, &mut publisher);

        // cwnd restored
        assert_eq!(2000, bbr.cwnd);
        // lower bounds reset
        assert_eq!(u64::MAX, bbr.data_volume_model.inflight_lo());
        assert_eq!(Bandwidth::INFINITY, bbr.data_rate_model.bw_lo());
        assert!(bbr.state.is_startup());

        // If full pipe then transition to probe bw cruise
        let mut bbr = bbr_in_probe_rtt_ready_to_exit();
        bbr.full_pipe_estimator.set_filled_pipe_for_test(true);
        bbr.exit_probe_rtt(&mut rng, now, &mut publisher);
        assert!(bbr.state.is_probing_bw_cruise());

        // Next probe rtt is scheduled
        assert_eq!(
            Some(now + PROBE_RTT_INTERVAL),
            bbr.data_volume_model.next_probe_rtt()
        );
    }

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.4.5
    //= type=test
    //# BBRProbeRTTCwnd():
    //#    probe_rtt_cwnd = BBRBDPMultiple(BBR.bw, BBRProbeRTTCwndGain)
    //#    probe_rtt_cwnd = max(probe_rtt_cwnd, BBRMinPipeCwnd)
    //#    return probe_rtt_cwnd
    #[test]
    fn probe_rtt_cwnd() {
        let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
        let now = NoopClock.get_time();

        // bdp_multiple > minimum_window
        assert_eq!(
            BbrCongestionController::initial_window(MINIMUM_MTU),
            bbr.probe_rtt_cwnd()
        );

        bbr.data_volume_model
            .update_min_rtt(Duration::from_millis(100), now);

        // bdp_multiple < minimum_window
        assert_eq!(bbr.minimum_window(), bbr.probe_rtt_cwnd());
    }

    /// Helper method to return a BBR congestion controller in the ProbeRtt
    /// but ready to exit that state
    fn bbr_in_probe_rtt_ready_to_exit() -> BbrCongestionController {
        let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
        let now = NoopClock.get_time();
        let mut probe_rtt_state = probe_rtt::State {
            timer: Default::default(),
            round_done: false,
        };
        probe_rtt_state.timer.set(now);
        probe_rtt_state.round_done = true;
        bbr.state = bbr::State::ProbeRtt(probe_rtt_state);
        bbr.round_counter.set_round_end(100);
        let packet_info = PacketInfo {
            delivered_bytes: 100,
            delivered_time: now,
            lost_bytes: 0,
            ecn_ce_count: 0,
            first_sent_time: now,
            bytes_in_flight: 0,
            is_app_limited: false,
        };
        bbr.round_counter.on_ack(packet_info, 200);
        assert!(bbr.round_counter.round_start());
        bbr.bw_estimator.set_delivered_bytes_for_test(200);
        bbr
    }
}