diff --git a/Cargo.toml b/Cargo.toml index b3cd9c935..a8ad41200 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,21 @@ defmt = ["dep:defmt", "heapless/defmt-03"] "socket-dns" = ["socket", "proto-dns"] "socket-mdns" = ["socket-dns"] +# Enable Cubic TCP congestion control algorithm, and it is used as a default congestion controller. +# +# Cubic relies on double precision (`f64`) floating point operations, which may cause issues in some contexts: +# +# * Small embedded processors (such as Cortex-M0, Cortex-M1, and Cortex-M3) do not have an FPU, +# and floating point operations consume significant amounts of CPU time and Flash space. +# * Interrupt handlers should almost always avoid floating-point operations. +# * Kernel-mode code on desktop processors usually avoids FPU operations to reduce the penalty of saving and restoring FPU registers. +# +# In all these cases, `CongestionControl::Reno` is a better choice of congestion control algorithm. +"socket-tcp-cubic" = [] + +# Enable Reno TCP congestion control algorithm, and it is used as a default congestion controller. +"socket-tcp-reno" = [] + "packetmeta-id" = [] "async" = [] @@ -85,7 +100,7 @@ default = [ ] # Private features -# Features starting with "_" are considered private. They should not be enabled by +# Features starting with "_" are considered private. They should not be enabled by # other crates, and they are not considered semver-stable. "_proto-fragmentation" = [] diff --git a/README.md b/README.md index 74fa161e9..9642fdc09 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ There are 3 supported mediums. hop-by-hop option. #### 6LoWPAN - + * Implementation of [RFC6282](https://tools.ietf.org/rfc/rfc6282.txt). * Fragmentation is supported, as defined in [RFC4944](https://tools.ietf.org/rfc/rfc4944.txt). * UDP header compression/decompression is supported. @@ -229,8 +229,8 @@ They can be set in two ways: - Via Cargo features: enable a feature like `-`. `name` must be in lowercase and use dashes instead of underscores. For example. `iface-max-addr-count-3`. Only a selection of values is available, check `Cargo.toml` for the list. -- Via environment variables at build time: set the variable named `SMOLTCP_`. For example -`SMOLTCP_IFACE_MAX_ADDR_COUNT=3 cargo build`. You can also set them in the `[env]` section of `.cargo/config.toml`. +- Via environment variables at build time: set the variable named `SMOLTCP_`. For example +`SMOLTCP_IFACE_MAX_ADDR_COUNT=3 cargo build`. You can also set them in the `[env]` section of `.cargo/config.toml`. Any value can be set, unlike with Cargo features. Environment variables take precedence over Cargo features. If two Cargo features are enabled for the same setting diff --git a/src/socket/tcp.rs b/src/socket/tcp.rs index d7b85ab42..75b47d065 100644 --- a/src/socket/tcp.rs +++ b/src/socket/tcp.rs @@ -17,6 +17,8 @@ use crate::wire::{ TCP_HEADER_LEN, }; +mod congestion; + macro_rules! tcp_trace { ($($arg:expr),*) => (net_log!(trace, $($arg),*)); } @@ -390,6 +392,19 @@ impl Display for Tuple { } } +/// A congestion control algorithm. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CongestionControl { + None, + + #[cfg(feature = "socket-tcp-reno")] + Reno, + + #[cfg(feature = "socket-tcp-cubic")] + Cubic, +} + /// A Transmission Control Protocol socket. /// /// A TCP socket may passively listen for connections or actively connect to another endpoint. @@ -464,6 +479,9 @@ pub struct Socket<'a> { /// Nagle's Algorithm enabled. nagle: bool, + /// The congestion control algorithm. + congestion_controller: congestion::AnyController, + #[cfg(feature = "async")] rx_waker: WakerRegistration, #[cfg(feature = "async")] @@ -522,6 +540,7 @@ impl<'a> Socket<'a> { ack_delay_timer: AckDelayTimer::Idle, challenge_ack_timer: Instant::from_secs(0), nagle: true, + congestion_controller: congestion::AnyController::new(), #[cfg(feature = "async")] rx_waker: WakerRegistration::new(), @@ -530,6 +549,54 @@ impl<'a> Socket<'a> { } } + /// Set an algorithm for congestion control. + /// + /// `CongestionControl::None` indicates that no congestion control is applied. + /// Options `CongestionControl::Cubic` and `CongestionControl::Reno` are also available. + /// To use Reno and Cubic, please enable the `socket-tcp-reno` and `socket-tcp-cubic` features + /// in the `smoltcp` crate, respectively. + /// + /// `CongestionControl::Reno` is a classic congestion control algorithm valued for its simplicity. + /// Despite having a lower algorithmic complexity than `Cubic`, + /// it is less efficient in terms of bandwidth usage. + /// + /// `CongestionControl::Cubic` represents a modern congestion control algorithm designed to + /// be more efficient and fair compared to `CongestionControl::Reno`. + /// It is the default choice for Linux, Windows, and macOS. + /// `CongestionControl::Cubic` relies on double precision (`f64`) floating point operations, which may cause issues in some contexts: + /// * Small embedded processors (such as Cortex-M0, Cortex-M1, and Cortex-M3) do not have an FPU, and floating point operations consume significant amounts of CPU time and Flash space. + /// * Interrupt handlers should almost always avoid floating-point operations. + /// * Kernel-mode code on desktop processors usually avoids FPU operations to reduce the penalty of saving and restoring FPU registers. + /// In all these cases, `CongestionControl::Reno` is a better choice of congestion control algorithm. + pub fn set_congestion_control(&mut self, congestion_control: CongestionControl) { + use congestion::*; + + self.congestion_controller = match congestion_control { + CongestionControl::None => AnyController::None(no_control::NoControl), + + #[cfg(feature = "socket-tcp-reno")] + CongestionControl::Reno => AnyController::Reno(reno::Reno::new()), + + #[cfg(feature = "socket-tcp-cubic")] + CongestionControl::Cubic => AnyController::Cubic(cubic::Cubic::new()), + } + } + + /// Return the current congestion control algorithm. + pub fn congestion_control(&self) -> CongestionControl { + use congestion::*; + + match self.congestion_controller { + AnyController::None(_) => CongestionControl::None, + + #[cfg(feature = "socket-tcp-reno")] + AnyController::Reno(_) => CongestionControl::Reno, + + #[cfg(feature = "socket-tcp-cubic")] + AnyController::Cubic(_) => CongestionControl::Cubic, + } + } + /// Register a waker for receive operations. /// /// The waker is woken on state changes that might affect the return value @@ -1593,6 +1660,9 @@ impl<'a> Socket<'a> { } self.rtte.on_ack(cx.now(), ack_number); + self.congestion_controller + .inner_mut() + .on_ack(cx.now(), ack_len, &self.rtte); } } @@ -1636,6 +1706,9 @@ impl<'a> Socket<'a> { tcp_trace!("received SYNACK with zero MSS, ignoring"); return None; } + self.congestion_controller + .inner_mut() + .set_mss(max_seg_size as usize); self.remote_mss = max_seg_size as usize } @@ -1681,6 +1754,9 @@ impl<'a> Socket<'a> { return None; } self.remote_mss = max_seg_size as usize; + self.congestion_controller + .inner_mut() + .set_mss(self.remote_mss); } self.remote_seq_no = repr.seq_number + 1; @@ -1795,6 +1871,10 @@ impl<'a> Socket<'a> { let is_window_update = new_remote_win_len != self.remote_win_len; self.remote_win_len = new_remote_win_len; + self.congestion_controller + .inner_mut() + .set_remote_window(new_remote_win_len); + if ack_len > 0 { // Dequeue acknowledged octets. debug_assert!(self.tx_buffer.len() >= ack_len); @@ -1831,6 +1911,11 @@ impl<'a> Socket<'a> { // Increment duplicate ACK count self.local_rx_dup_acks = self.local_rx_dup_acks.saturating_add(1); + // Inform congestion controller of duplicate ACK + self.congestion_controller + .inner_mut() + .on_duplicate_ack(cx.now()); + net_debug!( "received duplicate ACK for seq {} (duplicate nr {}{})", ack_number, @@ -1995,6 +2080,9 @@ impl<'a> Socket<'a> { 0 }; + // Compare max_send with the congestion window. + let max_send = max_send.min(self.congestion_controller.inner().window()); + // Can we send at least 1 octet? let mut can_send = max_send != 0; // Can we send at least 1 full segment? @@ -2072,6 +2160,10 @@ impl<'a> Socket<'a> { self.remote_last_ts = Some(cx.now()); } + self.congestion_controller + .inner_mut() + .pre_transmit(cx.now()); + // Check if any state needs to be changed because of a timer. if self.timed_out(cx.now()) { // If a timeout expires, we should abort the connection. @@ -2095,6 +2187,11 @@ impl<'a> Socket<'a> { // Inform RTTE, so that it can avoid bogus measurements. self.rtte.on_retransmit(); + + // Inform the congestion controller that we're retransmitting. + self.congestion_controller + .inner_mut() + .on_retransmit(cx.now()); } } @@ -2315,6 +2412,9 @@ impl<'a> Socket<'a> { if repr.segment_len() > 0 { self.rtte .on_send(cx.now(), repr.seq_number + repr.segment_len()); + self.congestion_controller + .inner_mut() + .post_transmit(cx.now(), repr.segment_len()); } if !self.seq_to_transmit(cx) && repr.segment_len() > 0 { @@ -7309,4 +7409,24 @@ mod test { assert_eq!(r.retransmission_timeout(), Duration::from_millis(rto)); } } + + #[test] + fn test_set_get_congestion_control() { + let mut s = socket_established(); + + #[cfg(feature = "socket-tcp-reno")] + { + s.set_congestion_control(CongestionControl::Reno); + assert_eq!(s.congestion_control(), CongestionControl::Reno); + } + + #[cfg(feature = "socket-tcp-cubic")] + { + s.set_congestion_control(CongestionControl::Cubic); + assert_eq!(s.congestion_control(), CongestionControl::Cubic); + } + + s.set_congestion_control(CongestionControl::None); + assert_eq!(s.congestion_control(), CongestionControl::None); + } } diff --git a/src/socket/tcp/congestion.rs b/src/socket/tcp/congestion.rs new file mode 100644 index 000000000..c904f214f --- /dev/null +++ b/src/socket/tcp/congestion.rs @@ -0,0 +1,101 @@ +use crate::time::Instant; + +use super::RttEstimator; + +pub(super) mod no_control; + +#[cfg(feature = "socket-tcp-cubic")] +pub(super) mod cubic; + +#[cfg(feature = "socket-tcp-reno")] +pub(super) mod reno; + +#[allow(unused_variables)] +pub(super) trait Controller { + /// Returns the number of bytes that can be sent. + fn window(&self) -> usize; + + /// Set the remote window size. + fn set_remote_window(&mut self, remote_window: usize) {} + + fn on_ack(&mut self, now: Instant, len: usize, rtt: &RttEstimator) {} + + fn on_retransmit(&mut self, now: Instant) {} + + fn on_duplicate_ack(&mut self, now: Instant) {} + + fn pre_transmit(&mut self, now: Instant) {} + + fn post_transmit(&mut self, now: Instant, len: usize) {} + + /// Set the maximum segment size. + fn set_mss(&mut self, mss: usize) {} +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub(super) enum AnyController { + None(no_control::NoControl), + + #[cfg(feature = "socket-tcp-reno")] + Reno(reno::Reno), + + #[cfg(feature = "socket-tcp-cubic")] + Cubic(cubic::Cubic), +} + +impl AnyController { + /// Create a new congestion controller. + /// `AnyController::new()` selects the best congestion controller based on the features. + /// + /// - If `socket-tcp-cubic` feature is enabled, it will use `Cubic`. + /// - If `socket-tcp-reno` feature is enabled, it will use `Reno`. + /// - If both `socket-tcp-cubic` and `socket-tcp-reno` features are enabled, it will use `Cubic`. + /// - `Cubic` is more efficient regarding throughput. + /// - `Reno` is more conservative and is suitable for low-power devices. + /// - If no congestion controller is available, it will use `NoControl`. + /// + /// Users can also select a congestion controller manually by [`super::Socket::set_congestion_control()`] + /// method at run-time. + #[allow(unreachable_code)] + #[inline] + pub fn new() -> Self { + #[cfg(feature = "socket-tcp-cubic")] + { + return AnyController::Cubic(cubic::Cubic::new()); + } + + #[cfg(feature = "socket-tcp-reno")] + { + return AnyController::Reno(reno::Reno::new()); + } + + AnyController::None(no_control::NoControl) + } + + #[inline] + pub fn inner_mut(&mut self) -> &mut dyn Controller { + match self { + AnyController::None(n) => n, + + #[cfg(feature = "socket-tcp-reno")] + AnyController::Reno(r) => r, + + #[cfg(feature = "socket-tcp-cubic")] + AnyController::Cubic(c) => c, + } + } + + #[inline] + pub fn inner(&self) -> &dyn Controller { + match self { + AnyController::None(n) => n, + + #[cfg(feature = "socket-tcp-reno")] + AnyController::Reno(r) => r, + + #[cfg(feature = "socket-tcp-cubic")] + AnyController::Cubic(c) => c, + } + } +} diff --git a/src/socket/tcp/congestion/cubic.rs b/src/socket/tcp/congestion/cubic.rs new file mode 100644 index 000000000..c048aaf86 --- /dev/null +++ b/src/socket/tcp/congestion/cubic.rs @@ -0,0 +1,312 @@ +use crate::time::Instant; + +use super::Controller; + +// Constants for the Cubic congestion control algorithm. +// See RFC 8312. +const BETA_CUBIC: f64 = 0.7; +const C: f64 = 0.4; + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Cubic { + cwnd: usize, // Congestion window + min_cwnd: usize, // The minimum size of congestion window + w_max: usize, // Window size just before congestion + recovery_start: Option, + rwnd: usize, // Remote window + last_update: Instant, + ssthresh: usize, +} + +impl Cubic { + pub fn new() -> Cubic { + Cubic { + cwnd: 1024 * 2, + min_cwnd: 1024 * 2, + w_max: 1024 * 2, + recovery_start: None, + rwnd: 64 * 1024, + last_update: Instant::from_millis(0), + ssthresh: usize::MAX, + } + } +} + +impl Controller for Cubic { + fn window(&self) -> usize { + self.cwnd + } + + fn on_retransmit(&mut self, now: Instant) { + self.w_max = self.cwnd; + self.ssthresh = self.cwnd >> 1; + self.recovery_start = Some(now); + } + + fn on_duplicate_ack(&mut self, now: Instant) { + self.w_max = self.cwnd; + self.ssthresh = self.cwnd >> 1; + self.recovery_start = Some(now); + } + + fn set_remote_window(&mut self, remote_window: usize) { + if self.rwnd < remote_window { + self.rwnd = remote_window; + } + } + + fn on_ack(&mut self, _now: Instant, len: usize, _rtt: &crate::socket::tcp::RttEstimator) { + // Slow start. + if self.cwnd < self.ssthresh { + self.cwnd = self + .cwnd + .saturating_add(len) + .min(self.rwnd) + .max(self.min_cwnd); + } + } + + fn pre_transmit(&mut self, now: Instant) { + let Some(recovery_start) = self.recovery_start else { + self.recovery_start = Some(now); + return; + }; + + let now_millis = now.total_millis(); + + // If the last update was less than 100ms ago, don't update the congestion window. + if self.last_update > recovery_start && now_millis - self.last_update.total_millis() < 100 { + return; + } + + // Elapsed time since the start of the recovery phase. + let t = now_millis - recovery_start.total_millis(); + if t < 0 { + return; + } + + // K = (w_max * (1 - beta) / C)^(1/3) + let k3 = ((self.w_max as f64) * (1.0 - BETA_CUBIC)) / C; + let k = if let Some(k) = cube_root(k3) { + k + } else { + return; + }; + + // cwnd = C(T - K)^3 + w_max + let s = t as f64 / 1000.0 - k; + let s = s * s * s; + let cwnd = C * s + self.w_max as f64; + + self.last_update = now; + + self.cwnd = (cwnd as usize).max(self.min_cwnd).min(self.rwnd); + } + + fn set_mss(&mut self, mss: usize) { + self.min_cwnd = mss; + } +} + +#[inline] +fn abs(a: f64) -> f64 { + if a < 0.0 { + -a + } else { + a + } +} + +/// Calculate cube root by using the Newton-Raphson method. +fn cube_root(a: f64) -> Option { + if a <= 0.0 { + return None; + } + + let (tolerance, init) = if a < 1_000.0 { + (1.0, 8.879040017426005) // cube_root(700.0) + } else if a < 1_000_000.0 { + (5.0, 88.79040017426004) // cube_root(700_000.0) + } else if a < 1_000_000_000.0 { + (50.0, 887.9040017426004) // cube_root(700_000_000.0) + } else if a < 1_000_000_000_000.0 { + (500.0, 8879.040017426003) // cube_root(700_000_000_000.0) + } else if a < 1_000_000_000_000_000.0 { + (5000.0, 88790.40017426001) // cube_root(700_000_000_000.0) + } else { + (50000.0, 887904.0017426) // cube_root(700_000_000_000_000.0) + }; + + let mut x = init; // initial value + let mut n = 20; // The maximum iteration + loop { + let next_x = (2.0 * x + a / (x * x)) / 3.0; + if abs(next_x - x) < tolerance { + return Some(next_x); + } + x = next_x; + + if n == 0 { + return Some(next_x); + } + + n -= 1; + } +} + +#[cfg(test)] +mod test { + use crate::{socket::tcp::RttEstimator, time::Instant}; + + use super::*; + + #[test] + fn test_cubic() { + let remote_window = 64 * 1024 * 1024; + let now = Instant::from_millis(0); + + for i in 0..10 { + for j in 0..9 { + let mut cubic = Cubic::new(); + // Set remote window. + cubic.set_remote_window(remote_window); + + cubic.set_mss(1480); + + if i & 1 == 0 { + cubic.on_retransmit(now); + } else { + cubic.on_duplicate_ack(now); + } + + cubic.pre_transmit(now); + + let mut n = i; + for _ in 0..j { + n *= i; + } + + let elapsed = Instant::from_millis(n); + cubic.pre_transmit(elapsed); + + let cwnd = cubic.window(); + println!("Cubic: elapsed = {}, cwnd = {}", elapsed, cwnd); + + assert!(cwnd >= cubic.min_cwnd); + assert!(cubic.window() <= remote_window); + } + } + } + + #[test] + fn cubic_time_inversion() { + let mut cubic = Cubic::new(); + + let t1 = Instant::from_micros(0); + let t2 = Instant::from_micros(i64::MAX); + + cubic.on_retransmit(t2); + cubic.pre_transmit(t1); + + let cwnd = cubic.window(); + println!("Cubic:time_inversion: cwnd: {}, cubic: {cubic:?}", cwnd); + + assert!(cwnd >= cubic.min_cwnd); + assert!(cwnd <= cubic.rwnd); + } + + #[test] + fn cubic_long_elapsed_time() { + let mut cubic = Cubic::new(); + + let t1 = Instant::from_millis(0); + let t2 = Instant::from_micros(i64::MAX); + + cubic.on_retransmit(t1); + cubic.pre_transmit(t2); + + let cwnd = cubic.window(); + println!("Cubic:long_elapsed_time: cwnd: {}", cwnd); + + assert!(cwnd >= cubic.min_cwnd); + assert!(cwnd <= cubic.rwnd); + } + + #[test] + fn cubic_last_update() { + let mut cubic = Cubic::new(); + + let t1 = Instant::from_millis(0); + let t2 = Instant::from_millis(100); + let t3 = Instant::from_millis(199); + let t4 = Instant::from_millis(20000); + + cubic.on_retransmit(t1); + + cubic.pre_transmit(t2); + let cwnd2 = cubic.window(); + + cubic.pre_transmit(t3); + let cwnd3 = cubic.window(); + + cubic.pre_transmit(t4); + let cwnd4 = cubic.window(); + + println!( + "Cubic:last_update: cwnd2: {}, cwnd3: {}, cwnd4: {}", + cwnd2, cwnd3, cwnd4 + ); + + assert_eq!(cwnd2, cwnd3); + assert_ne!(cwnd2, cwnd4); + } + + #[test] + fn cubic_slow_start() { + let mut cubic = Cubic::new(); + + let t1 = Instant::from_micros(0); + + let cwnd = cubic.window(); + let ack_len = 1024; + + cubic.on_ack(t1, ack_len, &RttEstimator::default()); + + assert!(cubic.window() > cwnd); + + for i in 1..1000 { + let t2 = Instant::from_micros(i); + cubic.on_ack(t2, ack_len * 100, &RttEstimator::default()); + assert!(cubic.window() <= cubic.rwnd); + } + + let t3 = Instant::from_micros(2000); + + let cwnd = cubic.window(); + cubic.on_retransmit(t3); + assert_eq!(cwnd >> 1, cubic.ssthresh); + } + + #[test] + fn cubic_pre_transmit() { + let mut cubic = Cubic::new(); + cubic.pre_transmit(Instant::from_micros(2000)); + } + + #[test] + fn test_cube_root() { + for n in (1..1000000).step_by(99) { + let a = n as f64; + let a = a * a * a; + let result = cube_root(a); + println!("cube_root({a}) = {}", result.unwrap()); + } + } + + #[test] + #[should_panic] + fn cube_root_zero() { + cube_root(0.0).unwrap(); + } +} diff --git a/src/socket/tcp/congestion/no_control.rs b/src/socket/tcp/congestion/no_control.rs new file mode 100644 index 000000000..9ca4be3de --- /dev/null +++ b/src/socket/tcp/congestion/no_control.rs @@ -0,0 +1,11 @@ +use super::Controller; + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct NoControl; + +impl Controller for NoControl { + fn window(&self) -> usize { + usize::MAX + } +} diff --git a/src/socket/tcp/congestion/reno.rs b/src/socket/tcp/congestion/reno.rs new file mode 100644 index 000000000..8c0295487 --- /dev/null +++ b/src/socket/tcp/congestion/reno.rs @@ -0,0 +1,130 @@ +use crate::{socket::tcp::RttEstimator, time::Instant}; + +use super::Controller; + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Reno { + cwnd: usize, + min_cwnd: usize, + ssthresh: usize, + rwnd: usize, +} + +impl Reno { + pub fn new() -> Self { + Reno { + cwnd: 1024 * 2, + min_cwnd: 1024 * 2, + ssthresh: usize::MAX, + rwnd: 64 * 1024, + } + } +} + +impl Controller for Reno { + fn window(&self) -> usize { + self.cwnd + } + + fn on_ack(&mut self, _now: Instant, len: usize, _rtt: &RttEstimator) { + let len = if self.cwnd < self.ssthresh { + // Slow start. + len + } else { + self.ssthresh = self.cwnd; + self.min_cwnd + }; + + self.cwnd = self + .cwnd + .saturating_add(len) + .min(self.rwnd) + .max(self.min_cwnd); + } + + fn on_duplicate_ack(&mut self, _now: Instant) { + self.ssthresh = (self.cwnd >> 1).max(self.min_cwnd); + } + + fn on_retransmit(&mut self, _now: Instant) { + self.cwnd = (self.cwnd >> 1).max(self.min_cwnd); + } + + fn set_mss(&mut self, mss: usize) { + self.min_cwnd = mss; + } + + fn set_remote_window(&mut self, remote_window: usize) { + if self.rwnd < remote_window { + self.rwnd = remote_window; + } + } +} + +#[cfg(test)] +mod test { + use crate::time::Instant; + + use super::*; + + #[test] + fn test_reno() { + let remote_window = 64 * 1024; + let now = Instant::from_millis(0); + + for i in 0..10 { + for j in 0..9 { + let mut reno = Reno::new(); + reno.set_mss(1480); + + // Set remote window. + reno.set_remote_window(remote_window); + + reno.on_ack(now, 4096, &RttEstimator::default()); + + let mut n = i; + for _ in 0..j { + n *= i; + } + + if i & 1 == 0 { + reno.on_retransmit(now); + } else { + reno.on_duplicate_ack(now); + } + + let elapsed = Instant::from_millis(1000); + reno.on_ack(elapsed, n, &RttEstimator::default()); + + let cwnd = reno.window(); + println!("Reno: elapsed = {}, cwnd = {}", elapsed, cwnd); + + assert!(cwnd >= reno.min_cwnd); + assert!(reno.window() <= remote_window); + } + } + } + + #[test] + fn reno_min_cwnd() { + let remote_window = 64 * 1024; + let now = Instant::from_millis(0); + + let mut reno = Reno::new(); + reno.set_remote_window(remote_window); + + for _ in 0..100 { + reno.on_retransmit(now); + assert!(reno.window() >= reno.min_cwnd); + } + } + + #[test] + fn reno_set_rwnd() { + let mut reno = Reno::new(); + reno.set_remote_window(64 * 1024 * 1024); + + println!("{reno:?}"); + } +}