// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 use crate::{ recovery::{ bandwidth::Bandwidth, bbr::{ ecn::ECN_FACTOR, windowed_filter::{MinRttWindowedFilter, WindowedMaxFilter}, BETA, }, }, time::Timestamp, }; use core::time::Duration; //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.9.2 //# The data volume model parameters together estimate both the volume of in-flight data required to //# reach the full bandwidth available to the flow (BBR.max_inflight), and the maximum volume of //# data that is consistent with the queue pressure objective (cwnd). #[derive(Clone, Debug)] pub(crate) struct Model { //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.9.2 //# The windowed minimum round-trip time sample measured over the last MinRTTFilterLen = 10 seconds. min_rtt_filter: MinRttWindowedFilter, //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.9.2 //# A volume of data that is the estimate of the recent degree of aggregation in the network path. extra_acked_filter: WindowedMaxFilter, //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.12 //# the start of the time interval for estimating the excess amount of data acknowledged due to aggregation effects. extra_acked_interval_start: Option, //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.12 //# the volume of data marked as delivered since BBR.extra_acked_interval_start. extra_acked_delivered: u64, //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.9.2 //# Analogous to BBR.bw_hi, the long-term maximum volume of in-flight data that the algorithm //# estimates will produce acceptable queue pressure, based on signals in the current or //# previous bandwidth probing cycle, as measured by loss. inflight_hi: u64, //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.9.2 //# Analogous to BBR.bw_lo, the short-term maximum volume of in-flight data that the algorithm //# estimates is safe for matching the current network path delivery process, based on any loss //# signals in the current bandwidth probing cycle. inflight_lo: u64, } impl Model { /// Constructs a new `data_volume::Model` pub fn new() -> Self { //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.12 //# The window length of the BBR.ExtraACKedFilter max filter window: //# 10 (in units of packet-timed round trips). const EXTRA_ACKED_FILTER_LEN: u64 = 10; Self { min_rtt_filter: MinRttWindowedFilter::new(), extra_acked_filter: WindowedMaxFilter::new(EXTRA_ACKED_FILTER_LEN), extra_acked_interval_start: None, extra_acked_delivered: 0, inflight_hi: u64::MAX, inflight_lo: u64::MAX, } } /// The windowed maximum recent estimate in bytes of the degree of aggregation in the path pub fn extra_acked(&self) -> u64 { self.extra_acked_filter.value().unwrap_or(0) } /// The windowed minimum round trip time pub fn min_rtt(&self) -> Option { self.min_rtt_filter.min_rtt() } /// The long-term maximum volume of in-flight data that the algorithm /// estimates will produce acceptable queue pressure pub fn inflight_hi(&self) -> u64 { self.inflight_hi } /// The short-term maximum volume of in-flight data that the algorithm /// estimates is safe for matching the current network path delivery process pub fn inflight_lo(&self) -> u64 { self.inflight_lo } /// True if the probe RTT has expired and is due for a refresh by entering the ProbeRTT state pub fn probe_rtt_expired(&self) -> bool { self.min_rtt_filter.probe_rtt_expired() } /// Overrides the last updated time for Min Probe RTT to ensure ProbeRTT is not entered until /// the next PROBE_RTT_INTERVAL. /// /// Called immediately after ProbeRTT period ends or after resuming from idle pub fn schedule_next_probe_rtt(&mut self, now: Timestamp) { self.min_rtt_filter.schedule_next_probe_rtt(now) } /// Update the min_rtt estimate with the given `rtt` pub fn update_min_rtt(&mut self, rtt: Duration, now: Timestamp) { self.min_rtt_filter.update(rtt, now) } /// Update the ack aggregation estimate pub fn update_ack_aggregation( &mut self, bw: Bandwidth, bytes_acknowledged: usize, cwnd: u32, round_count: u64, now: Timestamp, ) { //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.5.5 //# BBRUpdateACKAggregation(): //# /* Find excess ACKed beyond expected amount over this interval */ //# interval = (Now() - BBR.extra_acked_interval_start) //# expected_delivered = BBR.bw * interval //# /* Reset interval if ACK rate is below expected rate: */ //# if (BBR.extra_acked_delivered <= expected_delivered) //# BBR.extra_acked_delivered = 0 //# BBR.extra_acked_interval_start = Now() //# expected_delivered = 0 //# BBR.extra_acked_delivered += rs.newly_acked //# extra = BBR.extra_acked_delivered - expected_delivered //# extra = min(extra, cwnd) //# BBR.extra_acked = //# update_windowed_max_filter( //# filter=BBR.ExtraACKedFilter, //# value=extra, //# time=BBR.round_count, //# window_length=BBRExtraAckedFilterLen) let mut expected_delivered = 0; if let Some(extra_acked_interval_start) = self.extra_acked_interval_start { // Find excess ACKed beyond expected amount over this interval let interval = now - extra_acked_interval_start; expected_delivered = bw * interval; } // Reset interval if ACK rate is below expected rate if self.extra_acked_delivered <= expected_delivered || self.extra_acked_interval_start.is_none() { self.extra_acked_delivered = 0; self.extra_acked_interval_start = Some(now); expected_delivered = 0; } self.extra_acked_delivered += bytes_acknowledged as u64; let extra = (self.extra_acked_delivered - expected_delivered).min(cwnd as u64); self.extra_acked_filter.update(extra, round_count); } /// Updates `inflight_hi` with the given `inflight_hi` pub fn update_upper_bound(&mut self, inflight_hi: u64) { self.inflight_hi = inflight_hi; } /// Updates `inflight_lo` if there is loss or ECN in the round pub fn update_lower_bound( &mut self, cwnd: u32, inflight_latest: u64, loss_in_round: bool, ecn_in_round: bool, ecn_alpha: f64, ) { if !loss_in_round && !ecn_in_round { return; } //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.5.6.3 //# if (BBR.inflight_lo == Infinity) //# BBR.inflight_lo = cwnd if self.inflight_lo == u64::MAX { self.inflight_lo = cwnd as u64; } // Update inflight_lo to the lower of the values determined when loss_in_round or ecn_in_round // Based on https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L1618 let ecn_inflight_lo = if ecn_in_round { ((1.0 - (ecn_alpha * ECN_FACTOR)) * self.inflight_lo as f64) as u64 } else { u64::MAX }; let loss_inflight_lo = if loss_in_round { //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.5.6.3 //# BBR.inflight_lo = max(BBR.inflight_latest, //# BBRBeta * BBR.infligh_lo) inflight_latest.max((BETA * self.inflight_lo).to_integer()) } else { u64::MAX }; self.inflight_lo = loss_inflight_lo.min(ecn_inflight_lo); } /// Resets `inflight_lo` to its initial value pub fn reset_lower_bound(&mut self) { //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.5.6.3 //# BBR.inflight_lo = Infinity self.inflight_lo = u64::MAX } /// Sets the `extra_acked_interval_start` to the given `timestamp` pub fn set_extra_acked_interval_start(&mut self, timestamp: Timestamp) { self.extra_acked_interval_start = Some(timestamp); } #[cfg(test)] pub fn set_extra_acked_for_test(&mut self, sample: u64, round_count: u64) { self.extra_acked_filter.update(sample, round_count); } #[cfg(test)] pub fn set_inflight_lo_for_test(&mut self, inflight_lo: u64) { self.inflight_lo = inflight_lo; } #[cfg(test)] pub fn extra_acked_interval_start(&self) -> Option { self.extra_acked_interval_start } #[cfg(test)] pub fn next_probe_rtt(&self) -> Option { self.min_rtt_filter.next_probe_rtt() } } #[cfg(test)] mod tests { use super::*; use crate::time::{Clock, NoopClock}; #[test] fn new() { let model = Model::new(); assert_eq!(0, model.extra_acked()); assert_eq!(None, model.min_rtt()); assert_eq!(u64::MAX, model.inflight_hi()); assert_eq!(u64::MAX, model.inflight_lo()); } #[test] fn update_ack_aggregation() { let now = NoopClock.get_time(); let mut model = Model::new(); let now = now + Duration::from_millis(200); let bw = Bandwidth::new(1500, Duration::from_secs(1)); // The first call to update_ack_aggregation starts a new ack aggregation epoch model.update_ack_aggregation(bw, 1600, 12000, 0, now); assert_eq!(1600, model.extra_acked()); assert_eq!(Some(now), model.extra_acked_interval_start); assert_eq!(1600, model.extra_acked_delivered); let now = now + Duration::from_secs(1); model.update_ack_aggregation(bw, 1600, 12000, 1, now); // The BW sample indicates 1500 bytes should be ACKed over the interval, but instead 1600 were, // meaning the extra acked amount is 100 bytes. This is added to the initial 1600 extra acked // amount to arrive at 1700 bytes. assert_eq!(1700, model.extra_acked()); let now = now + Duration::from_secs(1); // Even more extra data is acked, but since the cwnd is lower than the extra amount, that // value is used as the extra acked (1600 bytes). 1700 remains the max extra acked. model.update_ack_aggregation(bw, 1700, 1600, 2, now); assert_eq!(1700, model.extra_acked()); } #[test] fn update_lower_bound() { let mut model = Model::new(); model.update_lower_bound(1000, 100, true, false, 1.0); // We didn't have a valid inflight_lo value yet, and the given inflight_latest is lower than cwnd * BETA, // so inflight_lo is set to cwnd * BETA assert_eq!((BETA * 1000).to_integer(), model.inflight_lo()); let inflight_latest = 1500; model.update_lower_bound(1000, inflight_latest, true, false, 1.0); // The new sample is higher than inflight_lo, so update inflight_lo assert_eq!(inflight_latest, model.inflight_lo()); let ecn_alpha = 4.0 / 5.0; model.update_lower_bound(1000, inflight_latest, false, true, ecn_alpha); // There was ecn_in_round, so lower inflight_lo according to ecn_alpha // (1 - 4/5 * .33) * 1500 = 1104 assert_eq!(1104, model.inflight_lo()); // Resetting the lower bound sets inflight_lo to u64::MAX model.reset_lower_bound(); assert_eq!(u64::MAX, model.inflight_lo()); // There is both ECN and Loss in the round, but the Loss reduced value is lower, so use the Loss value model.update_lower_bound(1500, 100, true, true, ecn_alpha); assert_eq!(1050, model.inflight_lo()); model.reset_lower_bound(); // There is both ECN and Loss in the round, but the Loss reduced value is higher, so use the ECN value model.update_lower_bound(1500, 1200, true, true, ecn_alpha); assert_eq!(1104, model.inflight_lo()); } }