use std::sync::atomic::Ordering;
use std::time::Duration;
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use crate::simulation::{TimeSource, VirtualTime};
use crate::util::time_source::SharedMockTimeSource;
use super::config::{
LedbatConfig, MAX_GAIN_DIVISOR, MSS, SLOWDOWN_FREEZE_RTTS, SLOWDOWN_INTERVAL_MULTIPLIER,
SLOWDOWN_REDUCTION_FACTOR, TARGET,
};
use super::controller::LedbatController;
use super::state::CongestionState;
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)] pub struct NetworkCondition {
pub rtt: Duration,
pub jitter: Option<(f64, f64)>,
pub loss_rate: f64,
}
#[allow(dead_code)] impl NetworkCondition {
pub const LAN: Self = Self {
rtt: Duration::from_millis(1),
jitter: None,
loss_rate: 0.0,
};
pub const DATACENTER: Self = Self {
rtt: Duration::from_millis(10),
jitter: Some((0.95, 1.05)),
loss_rate: 0.0,
};
pub const CONTINENTAL: Self = Self {
rtt: Duration::from_millis(50),
jitter: Some((0.9, 1.1)),
loss_rate: 0.001,
};
pub const INTERCONTINENTAL: Self = Self {
rtt: Duration::from_millis(135),
jitter: Some((0.8, 1.2)),
loss_rate: 0.005,
};
pub const HIGH_LATENCY: Self = Self {
rtt: Duration::from_millis(250),
jitter: Some((0.9, 1.1)),
loss_rate: 0.01,
};
pub fn custom(rtt_ms: u64, jitter_pct: Option<f64>, loss_rate: f64) -> Self {
let jitter = jitter_pct.map(|pct| (1.0 - pct, 1.0 + pct));
Self {
rtt: Duration::from_millis(rtt_ms),
jitter,
loss_rate,
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct HarnessSnapshot {
pub time_nanos: u64,
pub cwnd: usize,
pub state: CongestionState,
pub periodic_slowdowns: usize,
pub flightsize: usize,
pub queuing_delay: Duration,
pub base_delay: Duration,
}
pub struct LedbatTestHarness {
time_source: VirtualTime,
controller: LedbatController<VirtualTime>,
condition: NetworkCondition,
rng: SmallRng,
epoch_nanos: u64,
}
#[allow(dead_code)] impl LedbatTestHarness {
pub fn new(config: LedbatConfig, condition: NetworkCondition, seed: u64) -> Self {
let time_source = VirtualTime::new();
let epoch_nanos = time_source.now_nanos();
let controller = LedbatController::new_with_time_source(config, time_source.clone());
Self {
time_source,
controller,
condition,
rng: SmallRng::seed_from_u64(seed),
epoch_nanos,
}
}
pub fn controller(&self) -> &LedbatController<VirtualTime> {
&self.controller
}
pub fn current_time_nanos(&self) -> u64 {
self.time_source.now_nanos() - self.epoch_nanos
}
pub fn advance_time(&mut self, duration: Duration) {
self.time_source.advance(duration);
}
fn jittered_rtt(&mut self) -> Duration {
let base_nanos = self.condition.rtt.as_nanos() as f64;
let jittered_nanos = match self.condition.jitter {
Some((min_mult, max_mult)) => {
let mult = self.rng.random_range(min_mult..=max_mult);
base_nanos * mult
}
None => base_nanos,
};
Duration::from_nanos(jittered_nanos as u64)
}
fn should_drop_packet(&mut self) -> bool {
self.condition.loss_rate > 0.0 && self.rng.random::<f64>() < self.condition.loss_rate
}
pub fn step(&mut self, bytes_to_send: usize) -> usize {
let rtt = self.jittered_rtt();
let bytes_sent = bytes_to_send.min(self.controller.current_cwnd());
self.controller.on_send(bytes_sent);
self.advance_time(rtt);
if self.should_drop_packet() {
self.controller.on_loss();
return 0;
}
self.controller.on_ack(rtt, bytes_sent);
bytes_sent
}
pub fn run_rtts(&mut self, count: usize, bytes_per_rtt: usize) -> Vec<HarnessSnapshot> {
let mut snapshots = Vec::with_capacity(count);
for _ in 0..count {
self.step(bytes_per_rtt);
snapshots.push(self.snapshot());
}
snapshots
}
pub fn inject_timeout(&mut self) {
self.controller.on_timeout();
}
pub fn snapshot(&self) -> HarnessSnapshot {
HarnessSnapshot {
time_nanos: self.current_time_nanos(),
cwnd: self.controller.current_cwnd(),
state: self.controller.congestion_state.load(),
periodic_slowdowns: self
.controller
.periodic_slowdowns
.load(std::sync::atomic::Ordering::Relaxed),
flightsize: self.controller.flightsize(),
queuing_delay: self.controller.queuing_delay(),
base_delay: self.controller.base_delay(),
}
}
pub fn run_until_state(
&mut self,
target: CongestionState,
max_rtts: usize,
bytes_per_rtt: usize,
) -> Result<Vec<HarnessSnapshot>, Vec<HarnessSnapshot>> {
let mut snapshots = Vec::new();
for _ in 0..max_rtts {
self.step(bytes_per_rtt);
let snap = self.snapshot();
let state = snap.state;
snapshots.push(snap);
if state == target {
return Ok(snapshots);
}
}
Err(snapshots)
}
pub fn run_until<F>(
&mut self,
condition: F,
max_rtts: usize,
bytes_per_rtt: usize,
) -> Result<Vec<HarnessSnapshot>, Vec<HarnessSnapshot>>
where
F: Fn(&HarnessSnapshot) -> bool,
{
let mut snapshots = Vec::new();
for _ in 0..max_rtts {
self.step(bytes_per_rtt);
let snap = self.snapshot();
let done = condition(&snap);
snapshots.push(snap);
if done {
return Ok(snapshots);
}
}
Err(snapshots)
}
pub fn run_until_cwnd_exceeds(
&mut self,
threshold: usize,
max_rtts: usize,
bytes_per_rtt: usize,
) -> Result<Vec<HarnessSnapshot>, Vec<HarnessSnapshot>> {
self.run_until(|s| s.cwnd > threshold, max_rtts, bytes_per_rtt)
}
pub fn run_until_cwnd_below(
&mut self,
threshold: usize,
max_rtts: usize,
bytes_per_rtt: usize,
) -> Result<Vec<HarnessSnapshot>, Vec<HarnessSnapshot>> {
self.run_until(|s| s.cwnd < threshold, max_rtts, bytes_per_rtt)
}
pub fn run_until_slowdown(
&mut self,
max_rtts: usize,
bytes_per_rtt: usize,
) -> Result<Vec<HarnessSnapshot>, Vec<HarnessSnapshot>> {
let initial_slowdowns = self.snapshot().periodic_slowdowns;
self.run_until(
|s| s.periodic_slowdowns > initial_slowdowns,
max_rtts,
bytes_per_rtt,
)
}
pub fn set_condition(&mut self, condition: NetworkCondition) {
self.condition = condition;
}
pub fn current_gain(&self) -> f64 {
let base_delay = self.controller.base_delay();
if base_delay.is_zero() {
return 1.0 / MAX_GAIN_DIVISOR as f64;
}
let target_nanos = TARGET.as_nanos() as f64;
let base_nanos = base_delay.as_nanos() as f64;
let divisor = (2.0 * target_nanos / base_nanos).ceil() as u32;
let clamped = divisor.clamp(1, MAX_GAIN_DIVISOR);
1.0 / clamped as f64
}
pub fn expected_gain_for_rtt(rtt_ms: u64) -> f64 {
if rtt_ms == 0 {
return 1.0 / MAX_GAIN_DIVISOR as f64;
}
let target_ms = 60.0f64;
let divisor = (2.0 * target_ms / rtt_ms as f64).ceil() as u32;
let clamped = divisor.clamp(1, MAX_GAIN_DIVISOR);
1.0 / clamped as f64
}
}
#[test]
fn test_harness_slowdown_count_bounded_at_high_rtt() {
let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd: 2_848,
max_cwnd: 1_000_000,
ssthresh: 102_400,
enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let mut harness = LedbatTestHarness::new(
config,
NetworkCondition::INTERCONTINENTAL, 42, );
let rtts_to_simulate = 250;
let bytes_per_rtt = 100_000;
let snapshots = harness.run_rtts(rtts_to_simulate, bytes_per_rtt);
let final_slowdowns = snapshots.last().unwrap().periodic_slowdowns;
let max_expected = 20;
assert!(
final_slowdowns <= max_expected,
"Slowdown count {} exceeds maximum expected {} at 135ms RTT over 250 RTTs.\n\
This indicates the min_interval bug - slowdowns should occur at most \
every 18 RTTs (9 * 2 RTT freeze duration).",
final_slowdowns,
max_expected
);
println!(
"High-RTT test: {} slowdowns in {} RTTs (max expected: {})",
final_slowdowns, rtts_to_simulate, max_expected
);
}
#[test]
fn test_harness_cwnd_growth_across_rtts() {
for (name, rtt_ms) in [
("LAN (1ms)", 1u64),
("Datacenter (10ms)", 10),
("Continental (50ms)", 50),
("Intercontinental (135ms)", 135),
] {
let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd: 2_848,
max_cwnd: 1_000_000,
ssthresh: 500_000, enable_slow_start: true,
enable_periodic_slowdown: false, randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(rtt_ms, None, 0.0);
let mut harness = LedbatTestHarness::new(config.clone(), condition, 123);
let snapshots = harness.run_rtts(5, 500_000);
let initial_cwnd = config.initial_cwnd;
let final_cwnd = snapshots.last().unwrap().cwnd;
assert!(
final_cwnd > initial_cwnd,
"{}: cwnd should grow during slow start \
(initial: {} bytes, final: {} bytes)",
name,
initial_cwnd,
final_cwnd
);
println!(
"{}: cwnd grew from {}KB to {}KB in 5 RTTs",
name,
initial_cwnd / 1024,
final_cwnd / 1024
);
}
}
#[test]
fn test_harness_timeout_resets_state_correctly() {
let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd: 2_848,
max_cwnd: 1_000_000,
ssthresh: 102_400, enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let mut harness = LedbatTestHarness::new(config.clone(), NetworkCondition::CONTINENTAL, 456);
let result = harness.run_until_state(CongestionState::WaitingForSlowdown, 100, 100_000);
assert!(result.is_ok(), "Should reach WaitingForSlowdown state");
let pre_timeout_cwnd = harness.snapshot().cwnd;
assert!(
pre_timeout_cwnd > config.initial_cwnd,
"cwnd should have grown before timeout"
);
harness.inject_timeout();
let post_timeout = harness.snapshot();
assert_eq!(
post_timeout.state,
CongestionState::SlowStart,
"State should be SlowStart after timeout"
);
assert!(
post_timeout.cwnd <= config.ssthresh / 2,
"cwnd should be well below ssthresh after timeout: cwnd={} ssthresh={}",
post_timeout.cwnd,
config.ssthresh
);
assert!(
post_timeout.cwnd >= config.min_cwnd,
"cwnd should be at least min_cwnd after timeout"
);
let recovery = harness.run_rtts(10, 50_000);
let final_cwnd = recovery.last().unwrap().cwnd;
assert!(
final_cwnd > config.min_cwnd * 2,
"Should recover via slow start exponential growth: {} > {}",
final_cwnd,
config.min_cwnd * 2
);
}
#[test]
fn test_harness_determinism() {
let config = LedbatConfig {
randomize_ssthresh: false,
..Default::default()
};
let seed = 99999u64;
let condition = NetworkCondition::custom(50, None, 0.0);
let mut harness1 = LedbatTestHarness::new(config.clone(), condition, seed);
let snapshots1 = harness1.run_rtts(50, 100_000);
let mut harness2 = LedbatTestHarness::new(config.clone(), condition, seed);
let snapshots2 = harness2.run_rtts(50, 100_000);
assert_eq!(
snapshots1.len(),
snapshots2.len(),
"Snapshot counts should match"
);
for (i, (s1, s2)) in snapshots1.iter().zip(snapshots2.iter()).enumerate() {
assert_eq!(
s1.cwnd, s2.cwnd,
"RTT {}: cwnd mismatch ({} vs {})",
i, s1.cwnd, s2.cwnd
);
assert_eq!(
s1.state, s2.state,
"RTT {}: state mismatch ({:?} vs {:?})",
i, s1.state, s2.state
);
assert_eq!(
s1.periodic_slowdowns, s2.periodic_slowdowns,
"RTT {}: slowdown count mismatch ({} vs {})",
i, s1.periodic_slowdowns, s2.periodic_slowdowns
);
}
println!("Determinism verified over 50 RTTs with seed {}", seed);
}
#[test]
fn test_harness_different_seeds_differ() {
let config = LedbatConfig {
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::CONTINENTAL;
let mut harness1 = LedbatTestHarness::new(config.clone(), condition, 1);
let mut harness2 = LedbatTestHarness::new(config.clone(), condition, 2);
for _ in 0..10 {
harness1.step(50_000);
harness2.step(50_000);
}
let bd1 = harness1.snapshot().base_delay;
let bd2 = harness2.snapshot().base_delay;
println!("Seed 1 base_delay: {:?}, Seed 2 base_delay: {:?}", bd1, bd2);
}
#[test]
fn test_harness_state_transitions() {
let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd: 2_848,
max_cwnd: 1_000_000,
ssthresh: 40_000, enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(50, None, 0.0);
let mut harness = LedbatTestHarness::new(config, condition, 789);
let mut states_seen = vec![harness.snapshot().state];
for _ in 0..100 {
harness.step(100_000);
let state = harness.snapshot().state;
if states_seen.last() != Some(&state) {
states_seen.push(state);
}
}
assert_eq!(
states_seen[0],
CongestionState::SlowStart,
"Should start in SlowStart"
);
assert!(
states_seen.len() > 1,
"Should have transitioned out of SlowStart. States seen: {:?}",
states_seen
);
println!("State progression: {:?}", states_seen);
}
#[test]
fn test_harness_packet_loss_reduces_cwnd() {
let config = LedbatConfig {
initial_cwnd: 100_000,
min_cwnd: 2_848,
max_cwnd: 500_000,
enable_slow_start: false, enable_periodic_slowdown: false,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(50, None, 0.5);
let mut harness = LedbatTestHarness::new(config.clone(), condition, 12345);
let initial_cwnd = harness.snapshot().cwnd;
for _ in 0..20 {
harness.step(50_000);
}
let final_cwnd = harness.snapshot().cwnd;
assert!(
final_cwnd < initial_cwnd,
"With 50% loss, cwnd should decrease: initial {} -> final {}",
initial_cwnd,
final_cwnd
);
println!(
"50% loss: cwnd reduced from {}KB to {}KB",
initial_cwnd / 1024,
final_cwnd / 1024
);
}
#[test]
fn test_harness_state_invariants() {
let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd: 2_848,
max_cwnd: 1_000_000,
ssthresh: 50_000,
enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(50, None, 0.0);
let mut harness = LedbatTestHarness::new(config.clone(), condition, 111);
for rtt in 0..150 {
harness.step(100_000);
let snap = harness.snapshot();
assert!(
snap.cwnd >= config.min_cwnd,
"RTT {}: cwnd {} below min {}",
rtt,
snap.cwnd,
config.min_cwnd
);
assert!(
snap.cwnd <= config.max_cwnd,
"RTT {}: cwnd {} above max {}",
rtt,
snap.cwnd,
config.max_cwnd
);
assert!(
matches!(
snap.state,
CongestionState::SlowStart
| CongestionState::CongestionAvoidance
| CongestionState::WaitingForSlowdown
| CongestionState::InSlowdown
| CongestionState::Frozen
| CongestionState::RampingUp
),
"RTT {}: Invalid state {:?}",
rtt,
snap.state
);
}
println!("State invariants verified over 150 RTTs");
}
#[test]
fn test_harness_delay_convergence_at_various_rtts() {
for rtt_ms in [10u64, 50, 100, 150, 250] {
let config = LedbatConfig {
initial_cwnd: 50_000,
min_cwnd: 2_848,
max_cwnd: 500_000,
enable_slow_start: false, enable_periodic_slowdown: false,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(rtt_ms, None, 0.0);
let mut harness = LedbatTestHarness::new(config.clone(), condition, 42);
let snapshots = harness.run_rtts(100, 100_000);
let last_20: Vec<_> = snapshots.iter().rev().take(20).collect();
let cwnd_values: Vec<usize> = last_20.iter().map(|s| s.cwnd).collect();
let min_cwnd = *cwnd_values.iter().min().unwrap();
let max_cwnd = *cwnd_values.iter().max().unwrap();
let variance_ratio = if min_cwnd > 0 {
max_cwnd as f64 / min_cwnd as f64
} else {
1.0
};
assert!(
variance_ratio < 2.0,
"{}ms RTT: cwnd variance too high (min: {}, max: {}, ratio: {:.2}). \
This may indicate oscillation in the delay-based algorithm.",
rtt_ms,
min_cwnd,
max_cwnd,
variance_ratio
);
println!(
"{}ms RTT: cwnd stable at {}-{}KB (ratio: {:.2})",
rtt_ms,
min_cwnd / 1024,
max_cwnd / 1024,
variance_ratio
);
}
}
#[test]
fn test_harness_slowdown_cycle_with_jitter_and_loss() {
let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd: 2_848,
max_cwnd: 500_000,
ssthresh: 50_000,
enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(135, Some(0.1), 0.001);
let mut harness = LedbatTestHarness::new(config.clone(), condition, 999);
let mut states_encountered = std::collections::HashSet::new();
states_encountered.insert(harness.snapshot().state);
for _ in 0..200 {
harness.step(100_000);
states_encountered.insert(harness.snapshot().state);
}
let final_snap = harness.snapshot();
assert!(
states_encountered.contains(&CongestionState::WaitingForSlowdown)
|| states_encountered.contains(&CongestionState::InSlowdown)
|| states_encountered.contains(&CongestionState::Frozen)
|| states_encountered.contains(&CongestionState::RampingUp),
"Should have entered slowdown cycle. States seen: {:?}",
states_encountered
);
assert!(
final_snap.periodic_slowdowns >= 1,
"Should have at least 1 slowdown in 200 RTTs at 135ms"
);
assert!(
final_snap.cwnd >= config.min_cwnd,
"cwnd should not drop below minimum"
);
println!(
"Slowdown cycle test: {} slowdowns, final cwnd: {}KB, states: {:?}",
final_snap.periodic_slowdowns,
final_snap.cwnd / 1024,
states_encountered
);
}
#[test]
fn test_harness_gain_transitions() {
let test_cases: [(u64, u32); 6] = [
(4, 16), (8, 15), (15, 8), (30, 4), (60, 2), (120, 1), ];
for (rtt_ms, expected_divisor) in test_cases {
let expected_gain = 1.0 / expected_divisor as f64;
let actual_gain = LedbatTestHarness::expected_gain_for_rtt(rtt_ms);
assert!(
(actual_gain - expected_gain).abs() < 0.001,
"{}ms RTT: expected GAIN {:.4} (divisor {}), got {:.4}",
rtt_ms,
expected_gain,
expected_divisor,
actual_gain
);
let config = LedbatConfig {
initial_cwnd: 50_000,
min_cwnd: 2_848,
max_cwnd: 500_000,
enable_slow_start: false,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(rtt_ms, None, 0.0);
let mut harness = LedbatTestHarness::new(config, condition, 123);
harness.run_rtts(5, 50_000);
let measured_gain = harness.current_gain();
println!(
"{}ms RTT: expected GAIN = 1/{} = {:.4}, measured = {:.4}",
rtt_ms, expected_divisor, expected_gain, measured_gain
);
}
}
#[test]
fn test_harness_rtt_change_mid_transfer() {
let config = LedbatConfig {
initial_cwnd: 100_000,
min_cwnd: 2_848,
max_cwnd: 1_000_000,
enable_slow_start: false, enable_periodic_slowdown: false,
randomize_ssthresh: false,
..Default::default()
};
let low_rtt_condition = NetworkCondition::custom(50, None, 0.0);
let mut harness = LedbatTestHarness::new(config.clone(), low_rtt_condition, 777);
let low_rtt_snaps = harness.run_rtts(20, 100_000);
let cwnd_at_low_rtt = low_rtt_snaps.last().unwrap().cwnd;
let base_delay_at_low_rtt = harness.snapshot().base_delay;
println!(
"Before RTT change: cwnd = {}KB, base_delay = {:?}",
cwnd_at_low_rtt / 1024,
base_delay_at_low_rtt
);
harness.set_condition(NetworkCondition::custom(200, None, 0.0));
let high_rtt_snaps = harness.run_rtts(50, 100_000);
let cwnd_at_high_rtt = high_rtt_snaps.last().unwrap().cwnd;
let base_delay_after = harness.snapshot().base_delay;
println!(
"After RTT change: cwnd = {}KB, base_delay = {:?}",
cwnd_at_high_rtt / 1024,
base_delay_after
);
assert!(
base_delay_at_low_rtt <= Duration::from_millis(55),
"Initial base delay should be ~50ms, got {:?}",
base_delay_at_low_rtt
);
assert!(
cwnd_at_high_rtt >= config.min_cwnd,
"cwnd should stay above minimum after RTT change"
);
}
#[test]
fn test_harness_slow_start_exit_at_various_rtts() {
for (name, rtt_ms) in [
("Low RTT (10ms)", 10u64),
("Medium RTT (50ms)", 50),
("High RTT (150ms)", 150),
("Very High RTT (250ms)", 250),
] {
let config = LedbatConfig {
initial_cwnd: 20_000,
min_cwnd: 2_848,
max_cwnd: 500_000,
ssthresh: 100_000, enable_slow_start: true,
enable_periodic_slowdown: false, randomize_ssthresh: false,
delay_exit_threshold: 0.75, ..Default::default()
};
let condition = NetworkCondition::custom(rtt_ms, None, 0.0);
let mut harness = LedbatTestHarness::new(config.clone(), condition, 42);
assert_eq!(
harness.snapshot().state,
CongestionState::SlowStart,
"{}: Should start in SlowStart",
name
);
let result = harness.run_until_state(CongestionState::WaitingForSlowdown, 200, 100_000);
let snaps = result.as_ref().unwrap_or_else(|e| e);
let final_state = snaps.last().unwrap().state;
let final_cwnd = snaps.last().unwrap().cwnd;
assert_ne!(
final_state,
CongestionState::SlowStart,
"{}: Should have exited SlowStart within 200 RTTs",
name
);
println!(
"{}: exited slow start at {}KB cwnd after {} RTTs (state: {:?})",
name,
final_cwnd / 1024,
snaps.len(),
final_state
);
}
}
#[test]
fn test_harness_cwnd_growth_rate_scales_with_gain() {
let mut growth_rates: Vec<(u64, f64)> = Vec::new();
for rtt_ms in [15u64, 30, 60, 120] {
let expected_gain = LedbatTestHarness::expected_gain_for_rtt(rtt_ms);
let config = LedbatConfig {
initial_cwnd: 50_000,
min_cwnd: 2_848,
max_cwnd: 500_000,
enable_slow_start: false, enable_periodic_slowdown: false,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(rtt_ms, None, 0.0);
let mut harness = LedbatTestHarness::new(config.clone(), condition, 123);
let initial = harness.snapshot().cwnd;
harness.run_rtts(20, 100_000);
let final_cwnd = harness.snapshot().cwnd;
let growth = (final_cwnd as f64 - initial as f64) / initial as f64;
growth_rates.push((rtt_ms, growth));
println!(
"{}ms RTT (GAIN={:.3}): cwnd grew {:.1}% ({} -> {})",
rtt_ms,
expected_gain,
growth * 100.0,
initial,
final_cwnd
);
}
let growth_15ms = growth_rates
.iter()
.find(|(rtt, _)| *rtt == 15)
.map(|(_, g)| *g)
.unwrap_or(0.0);
let growth_120ms = growth_rates
.iter()
.find(|(rtt, _)| *rtt == 120)
.map(|(_, g)| *g)
.unwrap_or(0.0);
println!(
"Growth comparison: 15ms={:.2}%, 120ms={:.2}%",
growth_15ms * 100.0,
growth_120ms * 100.0
);
}
#[test]
fn test_harness_base_delay_stable_with_jitter() {
let config = LedbatConfig {
initial_cwnd: 50_000,
min_cwnd: 2_848,
max_cwnd: 500_000,
enable_slow_start: false,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(100, Some(0.3), 0.0);
let mut harness = LedbatTestHarness::new(config, condition, 54321);
harness.run_rtts(100, 50_000);
let base_delay = harness.snapshot().base_delay;
let base_delay_ms = base_delay.as_millis();
assert!(
(65..=90).contains(&base_delay_ms),
"Base delay with ±30% jitter should be near min range (70ms), got {}ms",
base_delay_ms
);
println!(
"With ±30% jitter on 100ms RTT: base_delay = {}ms (expected ~70ms)",
base_delay_ms
);
}
#[test]
fn test_harness_loss_and_slowdown_interaction() {
let config = LedbatConfig {
initial_cwnd: 50_000,
min_cwnd: 2_848,
max_cwnd: 300_000,
ssthresh: 60_000,
enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(150, None, 0.02);
let mut harness = LedbatTestHarness::new(config.clone(), condition, 11111);
let snapshots = harness.run_rtts(150, 100_000);
let final_snap = snapshots.last().unwrap();
println!(
"Loss + slowdown test: {} slowdowns, final cwnd: {}KB, final state: {:?}",
final_snap.periodic_slowdowns,
final_snap.cwnd / 1024,
final_snap.state
);
assert!(
final_snap.cwnd >= config.min_cwnd,
"cwnd should stay above minimum despite loss"
);
let losses = harness.controller().total_losses.load(Ordering::Relaxed);
println!("Total losses recorded: {}", losses);
}
#[test]
fn test_harness_convenience_run_until_cwnd_exceeds() {
let config = LedbatConfig {
initial_cwnd: 20_000,
min_cwnd: 2_848,
max_cwnd: 500_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(50, None, 0.0);
let mut harness = LedbatTestHarness::new(config, condition, 42);
let result = harness.run_until_cwnd_exceeds(100_000, 50, 100_000);
assert!(result.is_ok(), "Should reach 100KB cwnd within 50 RTTs");
let snaps = result.unwrap();
let final_cwnd = snaps.last().unwrap().cwnd;
assert!(
final_cwnd > 100_000,
"Final cwnd should exceed 100KB, got {}",
final_cwnd
);
println!(
"run_until_cwnd_exceeds: reached {}KB in {} RTTs",
final_cwnd / 1024,
snaps.len()
);
}
#[test]
fn test_harness_convenience_run_until_slowdown() {
let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd: 2_848,
max_cwnd: 500_000,
ssthresh: 50_000,
enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(50, None, 0.0);
let mut harness = LedbatTestHarness::new(config, condition, 42);
let result = harness.run_until_slowdown(100, 100_000);
assert!(result.is_ok(), "Should trigger a slowdown within 100 RTTs");
let snaps = result.unwrap();
let final_slowdowns = snaps.last().unwrap().periodic_slowdowns;
assert!(final_slowdowns >= 1, "Should have at least 1 slowdown");
println!(
"run_until_slowdown: first slowdown after {} RTTs",
snaps.len()
);
}
#[test]
fn test_harness_slowdown_skipped_when_cwnd_at_minimum() {
let min_cwnd = 2_848;
let config = LedbatConfig {
initial_cwnd: min_cwnd, min_cwnd,
max_cwnd: 500_000,
ssthresh: 50_000,
enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(135, None, 0.0); let mut harness = LedbatTestHarness::new(config.clone(), condition, 12345);
harness.inject_timeout();
let post_timeout = harness.snapshot();
assert_eq!(
post_timeout.cwnd, min_cwnd,
"cwnd should be at minimum after timeout"
);
let snapshots = harness.run_rtts(50, 100_000);
let mut futile_slowdowns = 0;
let mut prev_slowdowns = post_timeout.periodic_slowdowns;
for snap in &snapshots {
if snap.periodic_slowdowns > prev_slowdowns {
if snap.cwnd <= min_cwnd * SLOWDOWN_REDUCTION_FACTOR {
futile_slowdowns += 1;
}
}
prev_slowdowns = snap.periodic_slowdowns;
}
assert_eq!(
futile_slowdowns,
0,
"Slowdowns should be skipped when cwnd <= min_cwnd * {} ({}). \
Found {} futile slowdowns.",
SLOWDOWN_REDUCTION_FACTOR,
min_cwnd * SLOWDOWN_REDUCTION_FACTOR,
futile_slowdowns
);
let final_cwnd = snapshots.last().unwrap().cwnd;
println!(
"After 50 RTTs from minimum: cwnd grew from {}KB to {}KB",
min_cwnd / 1024,
final_cwnd / 1024
);
}
#[test]
fn test_harness_cwnd_can_grow_from_minimum() {
let min_cwnd = 2_848;
let config = LedbatConfig {
initial_cwnd: min_cwnd, min_cwnd,
max_cwnd: 500_000,
ssthresh: 100_000, enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(135, None, 0.0);
let mut harness = LedbatTestHarness::new(config, condition, 99999);
let snapshots = harness.run_rtts(20, 100_000);
let initial_cwnd = min_cwnd;
let final_cwnd = snapshots.last().unwrap().cwnd;
let slowdowns = snapshots.last().unwrap().periodic_slowdowns;
println!(
"Growth from minimum: {}B -> {}B ({:.1}x) in 20 RTTs ({} slowdowns)",
initial_cwnd,
final_cwnd,
final_cwnd as f64 / initial_cwnd as f64,
slowdowns
);
let expected_min_growth = min_cwnd * 10; assert!(
final_cwnd >= expected_min_growth,
"cwnd should grow at least 10x from {} to {} in slow start, but only reached {}. \
{} slowdowns occurred.",
min_cwnd,
expected_min_growth,
final_cwnd,
slowdowns
);
assert!(
slowdowns <= 1,
"Expected 0-1 slowdowns when growing from minimum, got {}",
slowdowns
);
}
#[test]
fn test_harness_instant_rampup_trap() {
let min_cwnd = 2_848;
let config = LedbatConfig {
initial_cwnd: min_cwnd,
min_cwnd,
max_cwnd: 500_000,
ssthresh: 100_000,
enable_slow_start: false, enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(135, None, 0.0);
let mut harness = LedbatTestHarness::new(config.clone(), condition, 77777);
let initial = harness.snapshot();
assert_eq!(initial.cwnd, min_cwnd);
assert_eq!(initial.state, CongestionState::CongestionAvoidance);
let snapshots = harness.run_rtts(100, 100_000);
let slowdowns = snapshots.last().unwrap().periodic_slowdowns;
let final_cwnd = snapshots.last().unwrap().cwnd;
let rtts_at_minimum = snapshots.iter().filter(|s| s.cwnd == min_cwnd).count();
let trap_detected = rtts_at_minimum > 50 && slowdowns > 3;
if trap_detected {
println!(
"TRAP DETECTED: {} slowdowns fired, spent {}/100 RTTs at minimum cwnd ({}KB)",
slowdowns,
rtts_at_minimum,
min_cwnd / 1024
);
}
assert!(
!trap_detected,
"Instant ramp-up trap detected: {} slowdowns with {}/100 RTTs at minimum. \
cwnd should be able to grow when slowdowns are futile.",
slowdowns, rtts_at_minimum
);
println!(
"No trap: final cwnd = {}KB after 100 RTTs ({} slowdowns, {} RTTs at minimum)",
final_cwnd / 1024,
slowdowns,
rtts_at_minimum
);
}
#[test]
fn test_harness_timeout_clears_scheduled_slowdown() {
let min_cwnd = 2_848;
let ssthresh = 80_000;
let config = LedbatConfig {
initial_cwnd: 100_000,
min_cwnd,
max_cwnd: 500_000,
ssthresh,
enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(100, None, 0.0);
let mut harness = LedbatTestHarness::new(config, condition, 11111);
let _slowdown_result = harness.run_until_slowdown(200, 100_000);
let pre_timeout = harness.snapshot();
println!(
"Before timeout: cwnd={}KB, slowdowns={}, state={:?}",
pre_timeout.cwnd / 1024,
pre_timeout.periodic_slowdowns,
pre_timeout.state
);
harness.inject_timeout();
let post_timeout = harness.snapshot();
assert!(
post_timeout.cwnd >= min_cwnd,
"cwnd should be at least min_cwnd: {}",
post_timeout.cwnd
);
assert!(
post_timeout.cwnd <= ssthresh / 2,
"cwnd should be well below ssthresh: cwnd={} ssthresh={}",
post_timeout.cwnd,
ssthresh
);
assert_eq!(post_timeout.state, CongestionState::SlowStart);
let recovery_snaps = harness.run_rtts(30, 100_000);
let slowdowns_during_recovery =
recovery_snaps.last().unwrap().periodic_slowdowns - pre_timeout.periodic_slowdowns;
let early_slowdown_while_small = recovery_snaps
.iter()
.take(18)
.any(|s| s.periodic_slowdowns > pre_timeout.periodic_slowdowns && s.cwnd < min_cwnd * 4);
assert!(
!early_slowdown_while_small,
"Slowdown fired during early recovery while cwnd was still small. \
Timeout should clear scheduled slowdowns to allow recovery."
);
let final_cwnd = recovery_snaps.last().unwrap().cwnd;
println!(
"Recovery after timeout: cwnd grew from {}KB to {}KB in 30 RTTs ({} new slowdowns)",
min_cwnd / 1024,
final_cwnd / 1024,
slowdowns_during_recovery
);
}
#[test]
fn test_harness_growth_rate_vs_slowdown_interval() {
let config = LedbatConfig {
initial_cwnd: 50_000,
min_cwnd: 2_848,
max_cwnd: 500_000,
ssthresh: 200_000,
enable_slow_start: false, enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(135, None, 0.0);
let mut harness = LedbatTestHarness::new(config.clone(), condition, 33333);
let snapshots = harness.run_rtts(100, 100_000);
let avg_cwnd: f64 =
snapshots.iter().map(|s| s.cwnd as f64).sum::<f64>() / snapshots.len() as f64;
let final_snap = snapshots.last().unwrap();
println!(
"135ms RTT over 100 RTTs: avg_cwnd = {}KB, final = {}KB, slowdowns = {}",
avg_cwnd as usize / 1024,
final_snap.cwnd / 1024,
final_snap.periodic_slowdowns
);
assert!(
avg_cwnd > (config.initial_cwnd as f64 * 0.8),
"Average cwnd ({}KB) should stay near initial ({}KB), \
but slowdowns are preventing sustained throughput",
avg_cwnd as usize / 1024,
config.initial_cwnd / 1024
);
}
#[test]
fn test_harness_production_scenario_250_rtts() {
let min_cwnd = 2_848;
let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd,
max_cwnd: 1_000_000,
ssthresh: 102_400,
enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let condition = NetworkCondition::custom(135, Some(0.1), 0.001); let mut harness = LedbatTestHarness::new(config.clone(), condition, 42);
let snapshots = harness.run_rtts(250, 100_000);
let final_snap = snapshots.last().unwrap();
let total_slowdowns = final_snap.periodic_slowdowns;
let total_bytes: usize = snapshots.iter().map(|s| s.cwnd).sum();
let rtts_at_minimum = snapshots.iter().filter(|s| s.cwnd <= min_cwnd * 2).count();
println!("Production scenario (250 RTTs at 135ms):");
println!(" Total slowdowns: {}", total_slowdowns);
println!(" Final cwnd: {}KB", final_snap.cwnd / 1024);
println!(
" Total throughput proxy: {}MB",
total_bytes / (1024 * 1024)
);
println!(" RTTs at/near minimum: {}/250", rtts_at_minimum);
assert!(
total_slowdowns <= 20,
"Too many slowdowns: {} (expected <= 20)",
total_slowdowns
);
assert!(
rtts_at_minimum < 50,
"Spent too much time at minimum cwnd: {}/250 RTTs. \
This indicates the minimum cwnd trap.",
rtts_at_minimum
);
let expected_min_bytes = 250 * 30_000; assert!(
total_bytes >= expected_min_bytes,
"Throughput too low: {}MB (expected >= {}MB). \
Futile slowdowns are destroying throughput.",
total_bytes / (1024 * 1024),
expected_min_bytes / (1024 * 1024)
);
}
mod proptest_math {
use super::*;
use proptest::prelude::*;
const RATE_TOLERANCE_PERCENT: f64 = 0.01; const RATIO_TOLERANCE: f64 = 0.01; const GAIN_TOLERANCE: f64 = 0.0001; const DURATION_TOLERANCE_MS: u64 = 1;
proptest! {
#[test]
fn slowdown_interval_is_at_least_18_rtts(base_delay_ms in 1u64..500) {
let base_delay = Duration::from_millis(base_delay_ms);
let config = LedbatConfig {
initial_cwnd: 160_000,
min_cwnd: 2848,
max_cwnd: 10_000_000,
ssthresh: 50_000,
enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let controller = LedbatController::new_with_config(config);
controller.congestion_state.enter_in_slowdown();
let start_nanos = controller.elapsed_nanos();
controller.slowdown_phase_start_nanos.store(start_nanos, Ordering::Release);
let completion_nanos = start_nanos + 1_000_000; controller.complete_slowdown(completion_nanos, base_delay);
let next_slowdown = controller.next_slowdown_time_nanos.load(Ordering::Acquire);
let next_interval_ns = next_slowdown.saturating_sub(completion_nanos);
let next_interval = Duration::from_nanos(next_interval_ns);
let min_expected = Duration::from_millis(
base_delay_ms * SLOWDOWN_FREEZE_RTTS as u64 * SLOWDOWN_INTERVAL_MULTIPLIER as u64
);
prop_assert!(
next_interval >= min_expected,
"Interval {:?} < 18 RTTs ({:?}) at {}ms base delay",
next_interval, min_expected, base_delay_ms
);
}
}
proptest! {
#[test]
fn dynamic_gain_in_valid_range(base_delay_ms in 0u64..1000) {
let base_delay = Duration::from_millis(base_delay_ms);
let controller = LedbatController::new(100_000, 2848, 10_000_000);
let gain = controller.calculate_dynamic_gain(base_delay);
let min_gain = 1.0 / MAX_GAIN_DIVISOR as f64;
let max_gain = 1.0;
prop_assert!(
gain >= min_gain && gain <= max_gain,
"GAIN {} out of valid range [{}, {}] at {}ms base delay",
gain, min_gain, max_gain, base_delay_ms
);
}
}
proptest! {
#[test]
fn rate_formula_is_correct(
cwnd_kb in 3u64..1000, rtt_ms in 0u64..1000 ) {
let cwnd = (cwnd_kb * 1024) as usize;
let rtt = Duration::from_millis(rtt_ms);
let min_cwnd = 2848;
let max_cwnd = 10_000_000;
let controller = LedbatController::new(cwnd, min_cwnd, max_cwnd);
controller.cwnd.store(cwnd, Ordering::Release);
let rate = controller.current_rate(rtt);
let effective_rtt = rtt.max(Duration::from_millis(1));
let expected_rate = (cwnd as f64 / effective_rtt.as_secs_f64()) as usize;
let tolerance = (expected_rate as f64 * RATE_TOLERANCE_PERCENT) as usize + 1;
prop_assert!(
(rate as i64 - expected_rate as i64).unsigned_abs() <= tolerance as u64,
"Rate {} != expected {} (±{}) at {}KB cwnd, {}ms RTT",
rate, expected_rate, tolerance, cwnd_kb, rtt_ms
);
}
}
proptest! {
#[test]
fn rate_proportional_to_cwnd(
cwnd_kb in 3u64..5_000, rtt_ms in 1u64..500
) {
let cwnd1 = (cwnd_kb * 1024) as usize;
let cwnd2 = cwnd1 * 2;
let rtt = Duration::from_millis(rtt_ms);
let min_cwnd = 2848;
let max_cwnd = 20_000_000;
let controller1 = LedbatController::new(cwnd1, min_cwnd, max_cwnd);
controller1.cwnd.store(cwnd1, Ordering::Release);
let rate1 = controller1.current_rate(rtt);
let controller2 = LedbatController::new(cwnd2, min_cwnd, max_cwnd);
controller2.cwnd.store(cwnd2, Ordering::Release);
let rate2 = controller2.current_rate(rtt);
let expected_ratio = 2.0;
let actual_ratio = rate2 as f64 / rate1 as f64;
prop_assert!(
(actual_ratio - expected_ratio).abs() < RATIO_TOLERANCE,
"Rate ratio {} != expected {} when cwnd doubled",
actual_ratio, expected_ratio
);
}
}
proptest! {
#[test]
fn rate_inversely_proportional_to_rtt(
cwnd_kb in 10u64..1000, rtt_ms in 2u64..250 ) {
let cwnd = (cwnd_kb * 1024) as usize;
let rtt1 = Duration::from_millis(rtt_ms);
let rtt2 = Duration::from_millis(rtt_ms * 2);
let min_cwnd = 2848;
let max_cwnd = 10_000_000;
let controller = LedbatController::new(cwnd, min_cwnd, max_cwnd);
controller.cwnd.store(cwnd, Ordering::Release);
let rate1 = controller.current_rate(rtt1);
let rate2 = controller.current_rate(rtt2);
let expected_ratio = 0.5;
let actual_ratio = rate2 as f64 / rate1 as f64;
prop_assert!(
(actual_ratio - expected_ratio).abs() < RATIO_TOLERANCE,
"Rate ratio {} != expected {} when RTT doubled from {}ms to {}ms",
actual_ratio, expected_ratio, rtt_ms, rtt_ms * 2
);
}
}
proptest! {
#[test]
fn cwnd_decrease_capped_at_half(
cwnd_kb in 10u64..1000,
off_target_ms in -1000i64..-1 ) {
let current_cwnd = (cwnd_kb * 1024) as f64;
let target_ms = TARGET.as_millis() as f64;
let off_target = off_target_ms as f64;
let gain = 1.0; let bytes_acked = current_cwnd;
let cwnd_change = gain * (off_target / target_ms) * bytes_acked * (MSS as f64)
/ current_cwnd;
let max_decrease = -(current_cwnd / 2.0);
let capped_change = cwnd_change.max(max_decrease);
prop_assert!(
capped_change >= max_decrease,
"Capped change {} < max decrease {} at {}KB cwnd",
capped_change, max_decrease, cwnd_kb
);
}
}
proptest! {
#[test]
fn queuing_delay_non_negative(
base_delay_ms in 1u64..500,
extra_delay_ms in 0u64..500
) {
let base_delay = Duration::from_millis(base_delay_ms);
let filtered_rtt = Duration::from_millis(base_delay_ms + extra_delay_ms);
let queuing_delay = filtered_rtt.saturating_sub(base_delay);
prop_assert!(
queuing_delay >= Duration::ZERO,
"Queuing delay {:?} is negative",
queuing_delay
);
let expected = Duration::from_millis(extra_delay_ms);
prop_assert_eq!(queuing_delay, expected);
}
}
proptest! {
#[test]
fn gain_divisor_formula_correct(base_delay_ms in 1u64..1000) {
let base_delay = Duration::from_millis(base_delay_ms);
let target_ms = TARGET.as_millis() as f64;
let base_ms = base_delay_ms as f64;
let raw_divisor = (2.0 * target_ms / base_ms).ceil() as u32;
let expected_divisor = raw_divisor.clamp(1, MAX_GAIN_DIVISOR);
let expected_gain = 1.0 / expected_divisor as f64;
let controller = LedbatController::new(100_000, 2848, 10_000_000);
let actual_gain = controller.calculate_dynamic_gain(base_delay);
prop_assert!(
(actual_gain - expected_gain).abs() < GAIN_TOLERANCE,
"GAIN {} != expected {} at {}ms base delay (divisor: raw={}, clamped={})",
actual_gain, expected_gain, base_delay_ms, raw_divisor, expected_divisor
);
}
}
proptest! {
#[test]
fn slowdown_interval_formula(
base_delay_ms in 1u64..500,
slowdown_duration_ms in 1u64..10_000
) {
let base_delay = Duration::from_millis(base_delay_ms);
let slowdown_duration_ns = slowdown_duration_ms * 1_000_000;
let next_interval = slowdown_duration_ns * SLOWDOWN_INTERVAL_MULTIPLIER as u64;
let min_slowdown_duration = base_delay.as_nanos() as u64 * SLOWDOWN_FREEZE_RTTS as u64;
let min_interval = min_slowdown_duration * SLOWDOWN_INTERVAL_MULTIPLIER as u64;
let actual_interval = next_interval.max(min_interval);
prop_assert!(
actual_interval >= next_interval,
"Interval {} < 9 * slowdown_duration {}",
actual_interval, next_interval
);
prop_assert!(
actual_interval >= min_interval,
"Interval {} < 18 RTTs {}",
actual_interval, min_interval
);
let expected = next_interval.max(min_interval);
prop_assert_eq!(actual_interval, expected);
}
}
proptest! {
#[test]
fn slow_start_exit_threshold(queuing_delay_ms in 0u64..100) {
let config = LedbatConfig::default();
let threshold = (TARGET.as_millis() as f64 * config.delay_exit_threshold) as u64;
let queuing_delay = Duration::from_millis(queuing_delay_ms);
let should_exit_on_delay = queuing_delay > Duration::from_millis(threshold);
prop_assert_eq!(threshold, 45);
if queuing_delay_ms > 45 {
prop_assert!(should_exit_on_delay, "Should exit slow start at {}ms delay", queuing_delay_ms);
} else {
prop_assert!(!should_exit_on_delay, "Should NOT exit slow start at {}ms delay", queuing_delay_ms);
}
}
}
proptest! {
#[test]
fn cwnd_bounds_enforced_after_timeout(
initial_cwnd_kb in 10u64..1000,
min_cwnd in 1000usize..5000,
max_cwnd in 1_000_000usize..10_000_000
) {
let initial_cwnd = (initial_cwnd_kb * 1024) as usize;
let initial_cwnd = initial_cwnd.clamp(min_cwnd, max_cwnd);
let config = LedbatConfig {
initial_cwnd,
min_cwnd,
max_cwnd,
ssthresh: max_cwnd / 2,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
..Default::default()
};
let controller = LedbatController::new_with_config(config);
controller.on_timeout();
let cwnd_after = controller.current_cwnd();
prop_assert!(
cwnd_after >= min_cwnd,
"cwnd {} < min_cwnd {} after timeout",
cwnd_after, min_cwnd
);
prop_assert!(
cwnd_after <= max_cwnd,
"cwnd {} > max_cwnd {} after timeout",
cwnd_after, max_cwnd
);
}
}
proptest! {
#[test]
fn cwnd_bounds_enforced_after_loss(
initial_cwnd_kb in 10u64..1000,
min_cwnd in 1000usize..5000,
max_cwnd in 1_000_000usize..10_000_000,
num_losses in 1u32..10
) {
let initial_cwnd = (initial_cwnd_kb * 1024) as usize;
let initial_cwnd = initial_cwnd.clamp(min_cwnd, max_cwnd);
let config = LedbatConfig {
initial_cwnd,
min_cwnd,
max_cwnd,
ssthresh: max_cwnd / 2,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
..Default::default()
};
let controller = LedbatController::new_with_config(config);
for _ in 0..num_losses {
controller.on_loss();
}
let cwnd_after = controller.current_cwnd();
prop_assert!(
cwnd_after >= min_cwnd,
"cwnd {} < min_cwnd {} after {} losses",
cwnd_after, min_cwnd, num_losses
);
prop_assert!(
cwnd_after <= max_cwnd,
"cwnd {} > max_cwnd {} after {} losses",
cwnd_after, max_cwnd, num_losses
);
}
}
proptest! {
#[test]
fn flightsize_never_negative(
sends in proptest::collection::vec(1000usize..10000, 1..20),
acks in proptest::collection::vec(1000usize..15000, 1..25)
) {
let controller = LedbatController::new(100_000, 2848, 10_000_000);
for &bytes in &sends {
controller.on_send(bytes);
}
for &bytes in &acks {
controller.on_ack_without_rtt(bytes);
let flightsize = controller.flightsize();
prop_assert!(
flightsize < usize::MAX / 2, "Flightsize {} appears to have wrapped negative",
flightsize
);
}
let final_flightsize = controller.flightsize();
prop_assert!(
final_flightsize < usize::MAX / 2,
"Final flightsize {} appears negative",
final_flightsize
);
}
}
proptest! {
#[test]
fn base_delay_only_decreases_on_smaller_samples(
initial_rtt_ms in 50u64..200,
sample_rtt_ms in 10u64..300
) {
let controller = LedbatController::new(100_000, 2848, 10_000_000);
let initial_rtt = Duration::from_millis(initial_rtt_ms);
controller.on_ack(initial_rtt, 1000);
let base_delay_before = controller.base_delay();
let sample_rtt = Duration::from_millis(sample_rtt_ms);
controller.on_ack(sample_rtt, 1000);
let base_delay_after = controller.base_delay();
if sample_rtt_ms < initial_rtt_ms {
prop_assert!(
base_delay_after <= base_delay_before,
"Base delay increased from {:?} to {:?} despite smaller sample {}ms < {}ms",
base_delay_before, base_delay_after, sample_rtt_ms, initial_rtt_ms
);
} else {
prop_assert!(
base_delay_after <= base_delay_before,
"Base delay increased from {:?} to {:?} on larger sample {}ms >= {}ms",
base_delay_before, base_delay_after, sample_rtt_ms, initial_rtt_ms
);
}
}
}
proptest! {
#[test]
fn base_delay_converges_to_minimum(
samples in proptest::collection::vec(10u64..500, 5..20)
) {
let controller = LedbatController::new(100_000, 2848, 10_000_000);
for &rtt_ms in &samples {
let rtt = Duration::from_millis(rtt_ms);
controller.on_ack(rtt, 1000);
}
let base_delay = controller.base_delay();
let expected_min = samples.iter().copied().min().unwrap();
prop_assert!(
base_delay.as_millis() as u64 <= expected_min + DURATION_TOLERANCE_MS,
"Base delay {:?} > minimum sample {}ms",
base_delay, expected_min
);
}
}
proptest! {
#[test]
fn ssthresh_respects_min_floor_after_timeout(
initial_cwnd in 50_000usize..500_000,
min_ssthresh_kb in 50usize..200, num_timeouts in 1usize..5
) {
let min_ssthresh = min_ssthresh_kb * 1024;
let config = LedbatConfig {
initial_cwnd,
min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: Some(min_ssthresh),
..Default::default()
};
let controller = LedbatController::new_with_config(config);
for _ in 0..num_timeouts {
controller.on_timeout();
}
let final_ssthresh = controller.ssthresh.load(Ordering::Acquire);
prop_assert!(
final_ssthresh >= min_ssthresh,
"ssthresh {} < min_ssthresh {} after {} timeouts",
final_ssthresh, min_ssthresh, num_timeouts
);
}
}
proptest! {
#[test]
fn ssthresh_uses_spec_floor_when_unconfigured(
initial_cwnd in 10_000usize..100_000,
min_cwnd in 2000usize..5000,
num_timeouts in 1usize..10
) {
let config = LedbatConfig {
initial_cwnd,
min_cwnd,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: None, ..Default::default()
};
let controller = LedbatController::new_with_config(config);
for _ in 0..num_timeouts {
controller.on_timeout();
}
let final_ssthresh = controller.ssthresh.load(Ordering::Acquire);
let spec_floor = min_cwnd * 2;
prop_assert!(
final_ssthresh >= spec_floor,
"ssthresh {} < spec floor (2*min_cwnd={}) after {} timeouts",
final_ssthresh, spec_floor, num_timeouts
);
let initial_ssthresh = controller.initial_ssthresh;
prop_assert!(
final_ssthresh <= initial_ssthresh,
"ssthresh {} should be <= initial_ssthresh {} after timeouts",
final_ssthresh, initial_ssthresh
);
}
}
proptest! {
#[test]
fn timeout_always_enters_slow_start(
initial_cwnd in 10_000usize..200_000,
rtt_ms in 10u64..300
) {
let config = LedbatConfig {
initial_cwnd,
min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: initial_cwnd / 2,
enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let controller = LedbatController::new_with_config(config);
let rtt = Duration::from_millis(rtt_ms);
controller.on_send(initial_cwnd);
for _ in 0..5 {
controller.on_ack(rtt, 1000);
}
controller.on_timeout();
prop_assert_eq!(
controller.congestion_state.load(),
CongestionState::SlowStart,
"Should be in SlowStart after timeout"
);
}
}
proptest! {
#[test]
fn cwnd_reset_on_timeout(
initial_cwnd in 10_000usize..1_000_000,
min_cwnd in 2000usize..5000,
rtt_ms in 10u64..200
) {
let config = LedbatConfig {
initial_cwnd,
min_cwnd,
max_cwnd: 10_000_000,
ssthresh: initial_cwnd * 2,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
..Default::default()
};
let controller = LedbatController::new_with_config(config);
let rtt = Duration::from_millis(rtt_ms);
controller.on_send(initial_cwnd * 2);
for _ in 0..10 {
controller.on_ack(rtt, 5000);
}
controller.on_timeout();
let cwnd_after = controller.current_cwnd();
prop_assert!(
cwnd_after <= min_cwnd + MSS,
"cwnd {} should be near min_cwnd {} after timeout",
cwnd_after, min_cwnd
);
}
}
proptest! {
#[test]
fn slowdown_cleared_on_timeout(
initial_cwnd in 50_000usize..200_000
) {
let config = LedbatConfig {
initial_cwnd,
min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: initial_cwnd / 4, enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let controller = LedbatController::new_with_config(config);
let rtt = Duration::from_millis(50);
controller.on_send(initial_cwnd * 2);
for _ in 0..20 {
controller.on_ack(rtt, 5000);
}
controller.on_timeout();
let next_slowdown = controller.next_slowdown_time_nanos.load(Ordering::Acquire);
prop_assert_eq!(
next_slowdown,
u64::MAX,
"Scheduled slowdown should be cleared after timeout"
);
}
}
proptest! {
#[test]
fn slow_start_reaches_useful_cwnd_with_min_ssthresh(
rtt_ms in 50u64..200, min_ssthresh_kb in 100usize..500 ) {
use crate::simulation::VirtualTime;
let min_ssthresh = min_ssthresh_kb * 1024;
let config = LedbatConfig {
initial_cwnd: 38_000, min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: true,
enable_periodic_slowdown: false, randomize_ssthresh: false,
min_ssthresh: Some(min_ssthresh),
..Default::default()
};
let time_source = VirtualTime::new();
let controller = LedbatController::new_with_time_source(config, time_source.clone());
let rtt = Duration::from_millis(rtt_ms);
time_source.advance(rtt);
controller.on_ack(rtt, 1000);
time_source.advance(rtt);
controller.on_ack(rtt, 1000);
controller.on_timeout();
let ssthresh_after = controller.ssthresh.load(Ordering::Acquire);
prop_assert!(
ssthresh_after >= min_ssthresh,
"ssthresh {} should be >= min_ssthresh {}",
ssthresh_after, min_ssthresh
);
controller.on_send(1_000_000); let mut prev_cwnd = controller.current_cwnd();
for i in 0..10 {
time_source.advance(rtt + Duration::from_millis(1));
controller.on_ack(rtt, 5000);
let current_cwnd = controller.current_cwnd();
if controller.congestion_state.load() == CongestionState::SlowStart {
prop_assert!(
current_cwnd >= prev_cwnd,
"cwnd should grow in slow start: {} -> {} (iter {})",
prev_cwnd, current_cwnd, i
);
}
prev_cwnd = current_cwnd;
}
let final_cwnd = controller.current_cwnd();
let min_cwnd = 2_848;
let expected_growth = 30_000;
prop_assert!(
final_cwnd >= min_cwnd + expected_growth,
"After 10 RTTs, cwnd {} should be at least {} (min_cwnd + 30KB growth)",
final_cwnd, min_cwnd + expected_growth
);
}
}
proptest! {
#[test]
fn skip_slowdown_when_cwnd_near_minimum(
min_cwnd in 2000usize..5000
) {
let threshold = min_cwnd * SLOWDOWN_REDUCTION_FACTOR;
let initial_cwnd = threshold;
let config = LedbatConfig {
initial_cwnd,
min_cwnd,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: false, enable_periodic_slowdown: true,
randomize_ssthresh: false,
..Default::default()
};
let controller = LedbatController::new_with_config(config);
let base_delay = Duration::from_millis(50);
let now_nanos = 1_000_000_000u64;
let started = controller.start_slowdown(now_nanos, base_delay);
prop_assert!(
!started,
"Slowdown should be skipped when cwnd ({}) <= min_cwnd * {} ({})",
initial_cwnd, SLOWDOWN_REDUCTION_FACTOR, threshold
);
prop_assert_eq!(
controller.congestion_state.load(),
CongestionState::CongestionAvoidance,
"State should remain CongestionAvoidance when slowdown is skipped"
);
}
}
}
#[test]
fn test_intercontinental_recovery_with_min_ssthresh() {
let min_ssthresh = 100 * 1024; let config = LedbatConfig {
initial_cwnd: 38_000, min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 1_000_000, enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
min_ssthresh: Some(min_ssthresh),
..Default::default()
};
let mut harness = LedbatTestHarness::new(
config,
NetworkCondition::INTERCONTINENTAL, 12345, );
println!("\n========== Intercontinental Recovery Test ==========");
println!("RTT: 135ms, min_ssthresh: {}KB", min_ssthresh / 1024);
let snapshots = harness.run_rtts(5, 100_000);
println!(
"After 5 RTTs: cwnd={}KB, state={:?}",
snapshots.last().unwrap().cwnd / 1024,
snapshots.last().unwrap().state
);
harness.inject_timeout();
let snap_after_timeout = harness.snapshot();
println!(
"After timeout: cwnd={}KB, state={:?}",
snap_after_timeout.cwnd / 1024,
snap_after_timeout.state
);
assert_eq!(
snap_after_timeout.state,
CongestionState::SlowStart,
"Should be in SlowStart after timeout"
);
let recovery_snapshots = harness.run_rtts(20, 100_000);
let final_cwnd = recovery_snapshots.last().unwrap().cwnd;
let rtt_ms = 135;
let throughput_mbps = (final_cwnd as f64 * 8.0) / (rtt_ms as f64 * 1000.0);
println!(
"After 20 RTTs recovery: cwnd={}KB, throughput=~{:.1} Mbps",
final_cwnd / 1024,
throughput_mbps
);
assert!(
final_cwnd >= 50_000,
"After 20 RTTs, cwnd {} should be at least 50KB for usable throughput",
final_cwnd
);
println!("✓ Intercontinental recovery test passed!");
}
#[test]
fn test_repeated_timeouts_stabilize_at_min_ssthresh() {
let min_ssthresh = 100 * 1024; let config = LedbatConfig {
initial_cwnd: 500_000, min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 1_000_000, enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: Some(min_ssthresh),
..Default::default()
};
let controller = LedbatController::new_with_config(config);
println!("\n========== Repeated Timeouts Test ==========");
println!("Initial ssthresh: 1MB, min_ssthresh floor: 100KB");
let mut ssthresh_values = vec![controller.ssthresh.load(Ordering::Acquire)];
for i in 0..10 {
controller.on_timeout();
let ssthresh = controller.ssthresh.load(Ordering::Acquire);
ssthresh_values.push(ssthresh);
println!("After timeout {}: ssthresh={}KB", i + 1, ssthresh / 1024);
}
let final_ssthresh = *ssthresh_values.last().unwrap();
assert!(
final_ssthresh >= min_ssthresh,
"ssthresh {} should stabilize at or above min_ssthresh {}",
final_ssthresh,
min_ssthresh
);
for (i, &ssthresh) in ssthresh_values.iter().enumerate() {
assert!(
ssthresh >= min_ssthresh,
"ssthresh {} at step {} was below min_ssthresh {}",
ssthresh,
i,
min_ssthresh
);
}
println!(
"✓ ssthresh stabilized at {}KB (floor: {}KB)",
final_ssthresh / 1024,
min_ssthresh / 1024
);
}
#[test]
fn test_timeout_cwnd_uses_adaptive_floor() {
let min_ssthresh = 100 * 1024; let config = LedbatConfig {
initial_cwnd: 200_000, min_cwnd: 2_848, max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: Some(min_ssthresh),
..Default::default()
};
let controller = LedbatController::new_with_config(config.clone());
println!("\n========== Timeout cwnd Adaptive Floor Test ==========");
println!(
"Initial: cwnd={}KB, ssthresh={}KB, min_ssthresh={}KB, min_cwnd={}KB",
controller.current_cwnd() / 1024,
controller.ssthresh.load(Ordering::Acquire) / 1024,
min_ssthresh / 1024,
config.min_cwnd / 1024
);
controller.on_timeout();
let cwnd_after = controller.current_cwnd();
let ssthresh_after = controller.ssthresh.load(Ordering::Acquire);
println!(
"After timeout: cwnd={}KB, ssthresh={}KB",
cwnd_after / 1024,
ssthresh_after / 1024
);
let expected_cwnd_floor = min_ssthresh / 4;
assert!(
cwnd_after >= expected_cwnd_floor,
"After timeout with min_ssthresh={}KB, cwnd should be at least {}KB (1/4 of floor), \
but got {}KB. This causes the 'LEDBAT death spiral' where recovery is too slow \
on high-latency paths.",
min_ssthresh / 1024,
expected_cwnd_floor / 1024,
cwnd_after / 1024
);
assert!(
ssthresh_after >= min_ssthresh,
"ssthresh {} should be at least min_ssthresh {}",
ssthresh_after,
min_ssthresh
);
assert!(
cwnd_after < ssthresh_after,
"cwnd {} should be less than ssthresh {} to allow slow start recovery",
cwnd_after,
ssthresh_after
);
println!(
"✓ cwnd={} KB is above adaptive floor ({} KB) and below ssthresh ({} KB)",
cwnd_after / 1024,
expected_cwnd_floor / 1024,
ssthresh_after / 1024
);
}
#[test]
fn test_repeated_timeouts_maintain_cwnd_floor() {
let min_ssthresh = 100 * 1024; let config = LedbatConfig {
initial_cwnd: 500_000, min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: Some(min_ssthresh),
..Default::default()
};
let controller = LedbatController::new_with_config(config.clone());
let expected_cwnd_floor = min_ssthresh / 4;
println!("\n========== Repeated Timeouts cwnd Floor Test ==========");
println!(
"Simulating high-loss path (like Argentina) with {} consecutive timeouts",
10
);
println!("Expected cwnd floor: {}KB", expected_cwnd_floor / 1024);
for i in 0..10 {
controller.on_timeout();
let cwnd = controller.current_cwnd();
let ssthresh = controller.ssthresh.load(Ordering::Acquire);
println!(
"After timeout {}: cwnd={}KB, ssthresh={}KB",
i + 1,
cwnd / 1024,
ssthresh / 1024
);
assert!(
cwnd >= expected_cwnd_floor,
"After timeout {}, cwnd {} should be at least {} (adaptive floor)",
i + 1,
cwnd,
expected_cwnd_floor
);
assert!(
ssthresh >= min_ssthresh,
"After timeout {}, ssthresh {} should be at least {}",
i + 1,
ssthresh,
min_ssthresh
);
}
println!(
"✓ cwnd maintained above {}KB floor through all timeouts",
expected_cwnd_floor / 1024
);
}
#[test]
fn test_slow_start_exit_cwnd_captured() {
let time_source = SharedMockTimeSource::new();
let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
..Default::default()
};
let controller = LedbatController::new_with_time_source(config, time_source.clone());
assert_eq!(
controller.slow_start_exit_cwnd.load(Ordering::Acquire),
0,
"slow_start_exit_cwnd should be 0 initially"
);
let rtt = Duration::from_millis(50);
controller.on_send(200_000);
for _ in 0..20 {
time_source.advance_time(rtt);
controller.on_ack(Duration::from_millis(50), 10_000);
}
let slow_start_exit_cwnd = controller.slow_start_exit_cwnd.load(Ordering::Acquire);
println!(
"slow_start_exit_cwnd: {}KB, current cwnd: {}KB",
slow_start_exit_cwnd / 1024,
controller.current_cwnd() / 1024
);
if slow_start_exit_cwnd > 0 {
assert!(
slow_start_exit_cwnd >= 38_000,
"slow_start_exit_cwnd {} should be >= initial_cwnd",
slow_start_exit_cwnd
);
}
}
#[test]
fn test_adaptive_floor_bdp_based() {
let time_source = SharedMockTimeSource::new();
let config = LedbatConfig {
initial_cwnd: 100_000,
min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 500_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: None, ..Default::default()
};
let controller = LedbatController::new_with_time_source(config, time_source.clone());
let exit_cwnd = 200_000;
let exit_base_delay = Duration::from_millis(50);
controller
.slow_start_exit_cwnd
.store(exit_cwnd, Ordering::Release);
controller
.slow_start_exit_base_delay_nanos
.store(exit_base_delay.as_nanos() as u64, Ordering::Release);
controller.on_ack(exit_base_delay, 1000);
time_source.advance_time(exit_base_delay);
controller.on_ack(exit_base_delay, 1000);
let floor = controller.calculate_adaptive_floor();
println!(
"BDP-based test: slow_start_exit={}KB, floor={}KB",
exit_cwnd / 1024,
floor / 1024
);
assert!(
floor >= exit_cwnd.min(500_000),
"BDP-based floor {} should be >= min(exit_cwnd, initial_ssthresh)",
floor
);
controller.cwnd.store(1_000_000, Ordering::Release);
controller.on_timeout();
let ssthresh_after = controller.ssthresh.load(Ordering::Acquire);
assert!(
ssthresh_after >= floor,
"ssthresh {} after timeout should be >= adaptive floor {}",
ssthresh_after,
floor
);
}
#[test]
fn test_adaptive_floor_rtt_scaling_with_explicit_min() {
let time_source = SharedMockTimeSource::new();
let min_ssthresh = 200 * 1024; let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: false, enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: Some(min_ssthresh), ..Default::default()
};
let controller = LedbatController::new_with_time_source(config, time_source.clone());
assert_eq!(controller.slow_start_exit_cwnd.load(Ordering::Acquire), 0);
let base_delay = Duration::from_millis(135); for _ in 0..5 {
controller.on_ack(base_delay, 1000);
time_source.advance_time(base_delay);
}
let floor = controller.calculate_adaptive_floor();
println!(
"RTT scaling test: base_delay={}ms, floor={}KB, min_ssthresh={}KB",
base_delay.as_millis(),
floor / 1024,
min_ssthresh / 1024
);
assert!(
floor >= min_ssthresh,
"Floor {} should be >= explicit min_ssthresh {}",
floor,
min_ssthresh
);
let spec_floor = 2_848 * 2;
assert!(
floor >= spec_floor,
"Floor {} should be >= spec floor {}",
floor,
spec_floor
);
}
#[test]
fn test_adaptive_floor_spec_fallback() {
let time_source = SharedMockTimeSource::new();
let min_cwnd = 2_848;
let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: false, enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: None, ..Default::default()
};
let controller = LedbatController::new_with_time_source(config, time_source.clone());
let spec_floor = min_cwnd * 2;
let base_delay = Duration::from_millis(135);
for _ in 0..5 {
controller.on_ack(base_delay, 1000);
time_source.advance_time(base_delay);
}
let floor = controller.calculate_adaptive_floor();
println!(
"Spec fallback test: base_delay={}ms, floor={}KB, spec_floor={}KB",
base_delay.as_millis(),
floor / 1024,
spec_floor / 1024
);
assert_eq!(
floor, spec_floor,
"Floor {} should equal spec floor {} without explicit min_ssthresh or BDP proxy",
floor, spec_floor
);
}
#[test]
fn test_adaptive_floor_path_change_detection() {
let time_source = SharedMockTimeSource::new();
let min_ssthresh = 150 * 1024; let config = LedbatConfig {
initial_cwnd: 100_000,
min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 500_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: Some(min_ssthresh), ..Default::default()
};
let controller = LedbatController::new_with_time_source(config, time_source.clone());
let exit_cwnd = 500_000; let exit_base_delay = Duration::from_millis(50);
controller
.slow_start_exit_cwnd
.store(exit_cwnd, Ordering::Release);
controller
.slow_start_exit_base_delay_nanos
.store(exit_base_delay.as_nanos() as u64, Ordering::Release);
controller.on_ack(exit_base_delay, 1000);
let floor_before = controller.calculate_adaptive_floor();
println!(
"Before path change: floor={}KB (should use BDP proxy ~{}KB capped at min_ssthresh)",
floor_before / 1024,
exit_cwnd / 1024
);
assert!(
floor_before >= min_ssthresh,
"Floor before path change {} should be >= min_ssthresh {}",
floor_before,
min_ssthresh
);
let new_base_delay = Duration::from_millis(100);
for _ in 0..5 {
controller.on_ack(new_base_delay, 1000);
time_source.advance_time(new_base_delay);
}
let floor_after = controller.calculate_adaptive_floor();
println!(
"After path change: exit_rtt=50ms, current_rtt=100ms, floor={}KB",
floor_after / 1024
);
assert!(
floor_after >= min_ssthresh,
"Floor after path change {} should still be >= min_ssthresh {}",
floor_after,
min_ssthresh
);
}
#[test]
fn test_adaptive_floor_spec_compliance() {
let time_source = SharedMockTimeSource::new();
let min_cwnd = 2_848;
let config = LedbatConfig {
initial_cwnd: 10_000,
min_cwnd,
max_cwnd: 10_000_000,
ssthresh: 50_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: None,
..Default::default()
};
let controller = LedbatController::new_with_time_source(config, time_source.clone());
let spec_floor = min_cwnd * 2;
controller
.slow_start_exit_cwnd
.store(1000, Ordering::Release);
controller.slow_start_exit_base_delay_nanos.store(
Duration::from_millis(10).as_nanos() as u64,
Ordering::Release,
);
controller.on_ack(Duration::from_millis(10), 1000);
time_source.advance_time(Duration::from_millis(10));
controller.on_ack(Duration::from_millis(10), 1000);
let floor = controller.calculate_adaptive_floor();
println!(
"Spec compliance test: slow_start_exit=1KB, floor={}KB, spec_floor={}KB",
floor / 1024,
spec_floor / 1024
);
assert!(
floor >= spec_floor,
"Adaptive floor {} must be >= spec floor {}",
floor,
spec_floor
);
}
#[test]
fn test_adaptive_floor_zero_base_delay_with_explicit_min() {
let time_source = SharedMockTimeSource::new();
let explicit_min = 100 * 1024; let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: Some(explicit_min),
..Default::default()
};
let controller = LedbatController::new_with_time_source(config, time_source.clone());
assert_eq!(controller.slow_start_exit_cwnd.load(Ordering::Acquire), 0);
let floor = controller.calculate_adaptive_floor();
let spec_floor = controller.min_cwnd * 2;
println!(
"Zero base_delay test: floor={}KB, explicit_min={}KB, spec_floor={}KB",
floor / 1024,
explicit_min / 1024,
spec_floor / 1024
);
assert!(
floor >= explicit_min,
"Floor {} should be >= explicit_min {}",
floor,
explicit_min
);
assert!(
floor >= spec_floor,
"Floor {} should be >= spec_floor {}",
floor,
spec_floor
);
}
#[test]
fn test_adaptive_floor_path_change_without_explicit_min() {
let time_source = SharedMockTimeSource::new();
let min_cwnd = 2_848;
let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: None, ..Default::default()
};
let controller = LedbatController::new_with_time_source(config, time_source.clone());
let spec_floor = min_cwnd * 2;
let initial_rtt = Duration::from_millis(50);
controller.on_ack(initial_rtt, 1000);
time_source.advance_time(initial_rtt);
controller.on_ack(initial_rtt, 1000);
controller
.slow_start_exit_cwnd
.store(500_000, Ordering::Release); controller
.slow_start_exit_base_delay_nanos
.store(initial_rtt.as_nanos() as u64, Ordering::Release);
let floor_before = controller.calculate_adaptive_floor();
println!(
"Before path change: floor={}KB, spec_floor={}KB",
floor_before / 1024,
spec_floor / 1024
);
assert!(
floor_before > spec_floor,
"BDP proxy should produce floor ({}) > spec_floor ({})",
floor_before,
spec_floor
);
let new_rtt = Duration::from_millis(80);
for _ in 0..50 {
controller.on_ack(new_rtt, 1000);
time_source.advance_time(new_rtt);
}
let current_base_delay = controller.base_delay();
println!(
"After adding new RTT samples: base_delay={}ms (target: 80ms)",
current_base_delay.as_millis()
);
let floor_after = controller.calculate_adaptive_floor();
println!(
"After path change: floor={}KB, spec_floor={}KB, base_delay={}ms",
floor_after / 1024,
spec_floor / 1024,
current_base_delay.as_millis()
);
if current_base_delay >= Duration::from_millis(75) {
assert_eq!(
floor_after, spec_floor,
"After path change without explicit min, floor ({}) should equal spec_floor ({})",
floor_after, spec_floor
);
} else {
assert!(
floor_after > spec_floor,
"Without path change detection, BDP proxy should still be used. floor={}, spec_floor={}",
floor_after,
spec_floor
);
println!("Note: base_delay window not fully flushed, testing BDP proxy behavior instead");
}
}
mod adaptive_floor_proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn adaptive_floor_bounded(
initial_cwnd in 10_000usize..500_000,
min_cwnd in 2000usize..5000,
slow_start_exit in 0usize..1_000_000,
base_delay_ms in 1u64..500
) {
let time_source = SharedMockTimeSource::new();
let config = LedbatConfig {
initial_cwnd,
min_cwnd,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: None,
..Default::default()
};
let controller = LedbatController::new_with_time_source(config, time_source.clone());
if slow_start_exit > 0 {
controller.slow_start_exit_cwnd.store(slow_start_exit, Ordering::Release);
controller.slow_start_exit_base_delay_nanos
.store(Duration::from_millis(base_delay_ms).as_nanos() as u64, Ordering::Release);
}
let rtt = Duration::from_millis(base_delay_ms);
for _ in 0..3 {
controller.on_ack(rtt, 1000);
time_source.advance_time(rtt);
}
let floor = controller.calculate_adaptive_floor();
let spec_floor = min_cwnd * 2;
let initial_ssthresh = controller.initial_ssthresh;
prop_assert!(
floor >= spec_floor,
"Floor {} < spec floor {}",
floor, spec_floor
);
prop_assert!(
floor <= initial_ssthresh,
"Floor {} > initial_ssthresh {}",
floor, initial_ssthresh
);
}
}
proptest! {
#[test]
fn timeout_with_adaptive_floor_recovers(
initial_cwnd in 50_000usize..200_000,
exit_cwnd in 100_000usize..500_000,
base_delay_ms in 50u64..200,
num_timeouts in 1usize..5
) {
let time_source = SharedMockTimeSource::new();
let config = LedbatConfig {
initial_cwnd,
min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: None, ..Default::default()
};
let controller = LedbatController::new_with_time_source(config, time_source.clone());
controller.slow_start_exit_cwnd.store(exit_cwnd, Ordering::Release);
controller.slow_start_exit_base_delay_nanos
.store(Duration::from_millis(base_delay_ms).as_nanos() as u64, Ordering::Release);
let rtt = Duration::from_millis(base_delay_ms);
for _ in 0..3 {
controller.on_ack(rtt, 1000);
time_source.advance_time(rtt);
}
let expected_floor = controller.calculate_adaptive_floor();
controller.cwnd.store(500_000, Ordering::Release);
for _ in 0..num_timeouts {
controller.on_timeout();
}
let final_ssthresh = controller.ssthresh.load(Ordering::Acquire);
prop_assert!(
final_ssthresh >= expected_floor,
"ssthresh {} < adaptive floor {} after {} timeouts",
final_ssthresh, expected_floor, num_timeouts
);
let spec_floor = 2_848 * 2;
prop_assert!(
final_ssthresh >= spec_floor,
"ssthresh {} should be >= spec floor {}",
final_ssthresh, spec_floor
);
}
}
}
#[test]
fn test_full_slow_start_timeout_recovery_cycle() {
let time_source = SharedMockTimeSource::new();
let min_ssthresh = 100 * 1024; let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: true,
enable_periodic_slowdown: false,
randomize_ssthresh: false,
min_ssthresh: Some(min_ssthresh), ..Default::default()
};
let controller = LedbatController::new_with_time_source(config, time_source.clone());
println!("\n========== Full Cycle Test ==========");
let base_rtt = Duration::from_millis(135); controller.on_send(500_000);
println!("Initial cwnd: {}KB", controller.current_cwnd() / 1024);
for i in 0..15 {
time_source.advance_time(base_rtt);
let queuing_delay_ms = 30 + (i * 2); let rtt = Duration::from_millis(135 + queuing_delay_ms);
controller.on_ack(rtt, 10_000);
}
let cwnd_after_slow_start = controller.current_cwnd();
let slow_start_exit_cwnd = controller.slow_start_exit_cwnd.load(Ordering::Acquire);
println!(
"After slow start: cwnd={}KB, slow_start_exit_cwnd={}KB",
cwnd_after_slow_start / 1024,
slow_start_exit_cwnd / 1024
);
controller.on_timeout();
let ssthresh_after_timeout = controller.ssthresh.load(Ordering::Acquire);
println!(
"After timeout: cwnd={}KB, ssthresh={}KB",
controller.current_cwnd() / 1024,
ssthresh_after_timeout / 1024
);
let expected_floor = controller.calculate_adaptive_floor();
println!("Adaptive floor: {}KB", expected_floor / 1024);
assert!(
ssthresh_after_timeout >= expected_floor,
"ssthresh {} should be >= adaptive floor {}",
ssthresh_after_timeout,
expected_floor
);
assert!(
expected_floor >= min_ssthresh,
"Adaptive floor {} should be >= min_ssthresh {}KB",
expected_floor / 1024,
min_ssthresh / 1024
);
println!("✓ Full cycle test passed");
}
#[test]
fn test_large_transfer_throughput_regression() {
let transfer_size: usize = 2_300_000; let rtt = Duration::from_millis(135); let min_ssthresh = 100 * 1024;
let config = LedbatConfig {
initial_cwnd: 38_000, min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 1_000_000,
enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
min_ssthresh: Some(min_ssthresh),
..Default::default()
};
let mut harness = LedbatTestHarness::new(
config,
NetworkCondition::INTERCONTINENTAL,
54321, );
println!("\n========== Large Transfer After Timeout Recovery Test ==========");
println!(
"Transfer: {} MB, RTT: {}ms, min_ssthresh: {}KB",
transfer_size / 1_000_000,
rtt.as_millis(),
min_ssthresh / 1024
);
println!("\nPhase 1: Simulating prior congestion (3 timeouts)...");
for i in 1..=3 {
harness.inject_timeout();
println!(
" Timeout {}: cwnd={}KB, ssthresh={}KB",
i,
harness.snapshot().cwnd / 1024,
harness.controller.ssthresh.load(Ordering::Acquire) / 1024
);
}
let snap_after_timeouts = harness.snapshot();
println!(
"After timeouts: cwnd={}KB, state={:?}",
snap_after_timeouts.cwnd / 1024,
snap_after_timeouts.state
);
println!("\nPhase 2: Starting 2.3MB transfer from degraded state...");
let mut bytes_sent: usize = 0;
let mut rtts_elapsed = 0;
let mut cwnd_samples: Vec<usize> = Vec::new();
let max_rtts = 150;
while bytes_sent < transfer_size && rtts_elapsed < max_rtts {
let snap = harness.snapshot();
let cwnd = snap.cwnd;
cwnd_samples.push(cwnd);
let bytes_this_rtt = cwnd.min(transfer_size - bytes_sent);
bytes_sent += bytes_this_rtt;
rtts_elapsed += 1;
harness.run_rtts(1, bytes_this_rtt);
if rtts_elapsed % 10 == 0 {
println!(
" RTT {}: cwnd={}KB, sent={}KB ({:.0}%)",
rtts_elapsed,
cwnd / 1024,
bytes_sent / 1024,
(bytes_sent as f64 / transfer_size as f64) * 100.0
);
}
}
let avg_cwnd: usize = cwnd_samples.iter().sum::<usize>() / cwnd_samples.len();
let min_cwnd_observed = *cwnd_samples.iter().min().unwrap();
let max_cwnd_observed = *cwnd_samples.iter().max().unwrap();
let simulated_time_ms = rtts_elapsed as u64 * rtt.as_millis() as u64;
let throughput_mbps = (bytes_sent as f64 * 8.0) / (simulated_time_ms as f64 * 1000.0);
println!("\nResults:");
println!(
" Bytes sent: {} / {} ({:.1}%)",
bytes_sent,
transfer_size,
(bytes_sent as f64 / transfer_size as f64) * 100.0
);
println!(" RTTs elapsed: {}", rtts_elapsed);
println!(
" Simulated time: {:.1}s",
simulated_time_ms as f64 / 1000.0
);
println!(" Average cwnd: {}KB", avg_cwnd / 1024);
println!(
" Min/Max cwnd: {}KB / {}KB",
min_cwnd_observed / 1024,
max_cwnd_observed / 1024
);
println!(" Throughput: {:.1} Mbps", throughput_mbps);
let final_stats = harness.controller.stats();
println!("\nLEDBAT Stats:");
println!(" ssthresh: {}KB", final_stats.ssthresh / 1024);
println!(
" min_ssthresh_floor: {}KB",
final_stats.min_ssthresh_floor / 1024
);
println!(" total_timeouts: {}", final_stats.total_timeouts);
println!(" total_losses: {}", final_stats.total_losses);
println!(" slow_start_exits: {}", final_stats.slow_start_exits);
assert!(
bytes_sent >= transfer_size,
"Transfer should complete: sent {} of {} bytes",
bytes_sent,
transfer_size
);
assert!(
rtts_elapsed <= 80,
"Transfer took {} RTTs ({:.1}s) - too slow! Expected <= 80 RTTs (~11s). \
Average cwnd was {}KB. This indicates a death spiral or recovery issue. \
ssthresh={}KB, min_floor={}KB",
rtts_elapsed,
simulated_time_ms as f64 / 1000.0,
avg_cwnd / 1024,
final_stats.ssthresh / 1024,
final_stats.min_ssthresh_floor / 1024
);
assert!(
avg_cwnd >= 30_000,
"Average cwnd was {}KB - too low! Expected >= 30KB. \
This indicates cwnd is not recovering properly after timeouts. \
ssthresh={}KB, min_floor={}KB",
avg_cwnd / 1024,
final_stats.ssthresh / 1024,
final_stats.min_ssthresh_floor / 1024
);
assert!(
final_stats.ssthresh >= min_ssthresh,
"Final ssthresh {}KB should be >= min_ssthresh floor {}KB - death spiral detected!",
final_stats.ssthresh / 1024,
min_ssthresh / 1024
);
assert_eq!(
final_stats.total_timeouts, 3,
"total_timeouts should have captured the 3 injected timeouts"
);
println!(
"\n✓ Large transfer completed in {} RTTs ({:.1}s) with avg cwnd {}KB - PASSED",
rtts_elapsed,
simulated_time_ms as f64 / 1000.0,
avg_cwnd / 1024
);
}
#[test]
fn test_cwnd_escapes_flightsize_trap_without_min_ssthresh() {
let ssthresh_value = 100 * 1024;
let config = LedbatConfig {
initial_cwnd: 38_000, min_cwnd: 2_848, max_cwnd: 10_000_000,
ssthresh: ssthresh_value,
enable_slow_start: true,
enable_periodic_slowdown: false, randomize_ssthresh: false,
min_ssthresh: None, ..Default::default()
};
let mut harness = LedbatTestHarness::new(
config,
NetworkCondition::INTERCONTINENTAL, 99999, );
println!("\n========== Trapped cwnd Escape Test (No min_ssthresh) ==========");
println!("ssthresh: {}KB, min_ssthresh: None", ssthresh_value / 1024);
println!("\nPhase 1: Running slow start until exit...");
let mut rtts = 0;
while harness.snapshot().state == CongestionState::SlowStart && rtts < 20 {
let cwnd = harness.snapshot().cwnd;
harness.step(cwnd);
rtts += 1;
}
let snap_after_ss = harness.snapshot();
println!(
"After slow start exit: cwnd={}KB, state={:?}, rtts={}",
snap_after_ss.cwnd / 1024,
snap_after_ss.state,
rtts
);
assert_ne!(
snap_after_ss.state,
CongestionState::SlowStart,
"Should have exited slow start"
);
println!("\nPhase 2: Injecting timeout to set up trapped scenario...");
harness.inject_timeout();
let snap_after_timeout = harness.snapshot();
let ssthresh_after_timeout = harness.controller.ssthresh.load(Ordering::Acquire);
println!(
"After timeout: cwnd={}KB, ssthresh={}KB, state={:?}",
snap_after_timeout.cwnd / 1024,
ssthresh_after_timeout / 1024,
snap_after_timeout.state
);
println!("\nPhase 3: Testing escape from flightsize trap...");
let small_send = 30 * 1024;
for i in 1..=10 {
let pre_cwnd = harness.snapshot().cwnd;
harness.step(small_send);
let post_cwnd = harness.snapshot().cwnd;
if i <= 3 || i == 10 {
println!(
" RTT {}: cwnd {} -> {}KB",
i,
pre_cwnd / 1024,
post_cwnd / 1024
);
}
}
let final_snap = harness.snapshot();
let final_ssthresh = harness.controller.ssthresh.load(Ordering::Acquire);
let strict_cap = small_send + 2 * MSS;
println!("\nResults:");
println!(" Final cwnd: {}KB", final_snap.cwnd / 1024);
println!(" Final ssthresh: {}KB", final_ssthresh / 1024);
println!(" Strict flightsize cap would be: {}KB", strict_cap / 1024);
println!(" State: {:?}", final_snap.state);
assert!(
final_snap.cwnd > strict_cap,
"cwnd ({:.1}KB) should exceed strict flightsize cap ({:.1}KB)! \
Without the fix, cwnd gets trapped at flightsize + 3KB. \
ssthresh={}KB should be reachable.",
final_snap.cwnd as f64 / 1024.0,
strict_cap as f64 / 1024.0,
final_ssthresh / 1024
);
let ssthresh_50_pct = final_ssthresh / 2;
assert!(
final_snap.cwnd >= ssthresh_50_pct,
"cwnd ({:.1}KB) should be at least 50% of ssthresh ({:.1}KB). \
Got {}% of ssthresh.",
final_snap.cwnd as f64 / 1024.0,
final_ssthresh as f64 / 1024.0,
(final_snap.cwnd * 100) / final_ssthresh
);
println!(
"\n✓ cwnd escaped flightsize trap: {}KB > strict cap {}KB - PASSED",
final_snap.cwnd / 1024,
strict_cap / 1024
);
}
#[test]
fn test_periodic_slowdown_applies_min_ssthresh_floor() {
let min_ssthresh = 200 * 1024;
let config = LedbatConfig {
initial_cwnd: 38_000,
min_cwnd: 2_848,
max_cwnd: 1_000_000,
ssthresh: 50_000,
enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
min_ssthresh: Some(min_ssthresh), ..Default::default()
};
let mut harness = LedbatTestHarness::new(
config,
NetworkCondition::INTERCONTINENTAL, 42,
);
println!("\n========== Periodic Slowdown min_ssthresh Floor Test ==========");
println!(
"Config: min_ssthresh={}KB, initial_ssthresh={}KB",
min_ssthresh / 1024,
50
);
println!("Strategy: Exit slow start at ~50KB (below 200KB floor), then trigger slowdown");
let result = harness.run_until_slowdown(100, 100_000);
assert!(
result.is_ok(),
"Should trigger a periodic slowdown within 100 RTTs"
);
let snapshots = result.unwrap();
let slowdowns_after = snapshots.last().unwrap().periodic_slowdowns;
assert!(
slowdowns_after >= 1,
"Expected at least 1 periodic slowdown, got {}",
slowdowns_after
);
let ssthresh_after = harness.controller.ssthresh.load(Ordering::Acquire);
let cwnd_after = harness.snapshot().cwnd;
println!(
"After {} slowdowns: ssthresh={}KB, cwnd={}KB, floor={}KB",
slowdowns_after,
ssthresh_after / 1024,
cwnd_after / 1024,
min_ssthresh / 1024
);
assert!(
ssthresh_after >= min_ssthresh,
"ssthresh ({} bytes = {}KB) fell below min_ssthresh floor ({} bytes = {}KB)!\n\
This indicates the periodic slowdown handler is not applying the \
min_ssthresh floor. The timeout handler correctly applies the floor \
but the periodic slowdown handler at line ~1382 does not.",
ssthresh_after,
ssthresh_after / 1024,
min_ssthresh,
min_ssthresh / 1024
);
println!(
"✓ ssthresh ({:.0}KB) >= min_ssthresh floor ({:.0}KB) - PASSED",
ssthresh_after as f64 / 1024.0,
min_ssthresh as f64 / 1024.0
);
}
#[test]
fn test_large_transfer_high_latency_completes_in_reasonable_time() {
let transfer_size = 2_500_000; let rtt_ms = 135; let min_ssthresh = 100 * 1024;
let max_rtts = 150;
let min_avg_throughput_per_rtt = transfer_size / max_rtts;
let config = LedbatConfig {
initial_cwnd: 2_848, min_cwnd: 2_848,
max_cwnd: 10_000_000,
ssthresh: 1_000_000, enable_slow_start: true,
enable_periodic_slowdown: true,
randomize_ssthresh: false,
min_ssthresh: Some(min_ssthresh),
..Default::default()
};
let condition = NetworkCondition::custom(rtt_ms, Some(0.1), 0.0);
let mut harness = LedbatTestHarness::new(config, condition, 42);
println!("\n========== Large Transfer High-Latency Test ==========");
println!(
"Transfer: {:.1}MB, RTT: {}ms, min_ssthresh: {}KB",
transfer_size as f64 / (1024.0 * 1024.0),
rtt_ms,
min_ssthresh / 1024
);
println!(
"Success criteria: Complete in <{} RTTs (<{:.1}s)",
max_rtts,
max_rtts as f64 * rtt_ms as f64 / 1000.0
);
let mut total_bytes_transferred: usize = 0;
let mut rtts_elapsed = 0;
let mut min_ssthresh_observed = usize::MAX;
let mut max_slowdowns_observed = 0;
let mut snapshots_below_floor = 0;
while total_bytes_transferred < transfer_size && rtts_elapsed < max_rtts {
harness.step(100_000); let snap = harness.snapshot();
total_bytes_transferred += snap.cwnd;
rtts_elapsed += 1;
let ssthresh = harness.controller.ssthresh.load(Ordering::Acquire);
min_ssthresh_observed = min_ssthresh_observed.min(ssthresh);
max_slowdowns_observed = max_slowdowns_observed.max(snap.periodic_slowdowns);
if ssthresh < min_ssthresh {
snapshots_below_floor += 1;
}
if rtts_elapsed % 25 == 0 || total_bytes_transferred >= transfer_size {
println!(
" RTT {}: transferred {:.1}MB/{:.1}MB, cwnd={}KB, ssthresh={}KB, slowdowns={}",
rtts_elapsed,
total_bytes_transferred as f64 / (1024.0 * 1024.0),
transfer_size as f64 / (1024.0 * 1024.0),
snap.cwnd / 1024,
ssthresh / 1024,
snap.periodic_slowdowns
);
}
}
let transfer_complete = total_bytes_transferred >= transfer_size;
let transfer_time_s = rtts_elapsed as f64 * rtt_ms as f64 / 1000.0;
let avg_throughput_per_rtt = total_bytes_transferred / rtts_elapsed.max(1);
let throughput_mbps = (total_bytes_transferred as f64 * 8.0) / (transfer_time_s * 1_000_000.0);
println!("\n--- Results ---");
println!(
"Transfer complete: {} ({:.1}MB in {} RTTs = {:.1}s)",
if transfer_complete { "YES" } else { "NO" },
total_bytes_transferred as f64 / (1024.0 * 1024.0),
rtts_elapsed,
transfer_time_s
);
println!("Throughput: {:.1} Mbps", throughput_mbps);
println!(
"Avg cwnd/RTT: {}KB (min required: {}KB)",
avg_throughput_per_rtt / 1024,
min_avg_throughput_per_rtt / 1024
);
println!(
"Min ssthresh observed: {}KB (floor: {}KB)",
min_ssthresh_observed / 1024,
min_ssthresh / 1024
);
println!("Total slowdowns: {}", max_slowdowns_observed);
println!("RTTs with ssthresh below floor: {}", snapshots_below_floor);
assert!(
transfer_complete,
"Transfer did not complete in {} RTTs ({:.1}s)! \
Only transferred {:.1}MB of {:.1}MB. \
This indicates a throughput problem on high-latency paths. \
Check: ssthresh floor enforcement, cwnd growth, slowdown frequency.",
max_rtts,
transfer_time_s,
total_bytes_transferred as f64 / (1024.0 * 1024.0),
transfer_size as f64 / (1024.0 * 1024.0)
);
assert!(
avg_throughput_per_rtt >= min_avg_throughput_per_rtt,
"Average throughput {}KB/RTT below minimum {}KB/RTT required for acceptable transfer speed",
avg_throughput_per_rtt / 1024,
min_avg_throughput_per_rtt / 1024
);
assert_eq!(
snapshots_below_floor, 0,
"ssthresh fell below min_ssthresh floor {} times! \
This indicates the floor is not being enforced in all code paths.",
snapshots_below_floor
);
if rtts_elapsed > max_rtts * 3 / 4 {
println!(
"WARNING: Transfer used {}% of RTT budget - throughput is marginal",
rtts_elapsed * 100 / max_rtts
);
}
println!(
"\n✓ Large transfer test PASSED: {:.1}MB in {:.1}s ({:.1} Mbps)",
transfer_size as f64 / (1024.0 * 1024.0),
transfer_time_s,
throughput_mbps
);
}