use crate::util::DetRng;
use std::time::Duration;
#[derive(Clone, Debug)]
pub struct NetworkConfig {
pub seed: u64,
pub default_conditions: NetworkConditions,
pub capture_trace: bool,
pub max_queue_depth: usize,
pub tick_resolution: Duration,
pub enable_bandwidth: bool,
pub default_bandwidth: u64,
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
seed: 0x4E45_5457,
default_conditions: NetworkConditions::ideal(),
capture_trace: false,
max_queue_depth: 10_000,
tick_resolution: Duration::from_micros(100),
enable_bandwidth: false,
default_bandwidth: 1_000_000_000,
}
}
}
#[derive(Clone, Debug)]
pub struct NetworkConditions {
pub latency: LatencyModel,
pub packet_loss: f64,
pub packet_corrupt: f64,
pub packet_duplicate: f64,
pub packet_reorder: f64,
pub max_in_flight: usize,
pub bandwidth: Option<u64>,
pub jitter: Option<JitterModel>,
}
impl NetworkConditions {
#[must_use]
pub fn ideal() -> Self {
Self {
latency: LatencyModel::Fixed(Duration::ZERO),
packet_loss: 0.0,
packet_corrupt: 0.0,
packet_duplicate: 0.0,
packet_reorder: 0.0,
max_in_flight: usize::MAX,
bandwidth: None,
jitter: None,
}
}
#[must_use]
pub fn local() -> Self {
Self {
latency: LatencyModel::Fixed(Duration::from_millis(1)),
..Self::ideal()
}
}
#[must_use]
pub fn lan() -> Self {
Self {
latency: LatencyModel::Uniform {
min: Duration::from_millis(1),
max: Duration::from_millis(5),
},
packet_loss: 0.0001,
bandwidth: Some(1_000_000_000),
..Self::ideal()
}
}
#[must_use]
pub fn wan() -> Self {
Self {
latency: LatencyModel::Normal {
mean: Duration::from_millis(50),
std_dev: Duration::from_millis(20),
},
packet_loss: 0.001,
packet_reorder: 0.001,
bandwidth: Some(100_000_000),
jitter: Some(JitterModel::Uniform {
max: Duration::from_millis(10),
}),
..Self::ideal()
}
}
#[must_use]
pub fn lossy() -> Self {
Self {
packet_loss: 0.1,
..Self::lan()
}
}
#[must_use]
pub fn satellite() -> Self {
Self {
latency: LatencyModel::Normal {
mean: Duration::from_millis(600),
std_dev: Duration::from_millis(50),
},
packet_loss: 0.01,
bandwidth: Some(10_000_000),
..Self::ideal()
}
}
#[must_use]
pub fn congested() -> Self {
Self {
latency: LatencyModel::Normal {
mean: Duration::from_millis(100),
std_dev: Duration::from_millis(50),
},
packet_loss: 0.05,
packet_reorder: 0.02,
bandwidth: Some(1_000_000),
max_in_flight: 100,
jitter: Some(JitterModel::Bursty {
normal_jitter: Duration::from_millis(5),
burst_jitter: Duration::from_millis(100),
burst_probability: 0.1,
}),
..Self::ideal()
}
}
}
#[derive(Clone, Debug)]
pub enum LatencyModel {
Fixed(Duration),
Uniform {
min: Duration,
max: Duration,
},
Normal {
mean: Duration,
std_dev: Duration,
},
LogNormal {
mu: f64,
sigma: f64,
},
Bimodal {
low: Duration,
high: Duration,
high_probability: f64,
},
}
impl LatencyModel {
#[must_use]
pub fn sample(&self, rng: &mut DetRng) -> Duration {
match self {
Self::Fixed(d) => *d,
Self::Uniform { min, max } => {
if min >= max {
return *min;
}
let range = max.as_nanos().saturating_sub(min.as_nanos());
let rand_u128 = (u128::from(rng.next_u64()) << 64) | u128::from(rng.next_u64());
let offset = rand_u128 % (range + 1);
duration_from_total_nanos_saturating(min.as_nanos().saturating_add(offset))
}
Self::Normal { mean, std_dev } => {
let z = sample_standard_normal(rng);
let sample = std_dev.as_secs_f64().mul_add(z, mean.as_secs_f64());
duration_from_secs_f64(sample)
}
Self::LogNormal { mu, sigma } => {
let z = sample_standard_normal(rng);
let sample = sigma.mul_add(z, *mu).exp();
duration_from_secs_f64(sample)
}
Self::Bimodal {
low,
high,
high_probability,
} => {
let p = next_unit_f64(rng);
if p < high_probability.clamp(0.0, 1.0) {
*high
} else {
*low
}
}
}
}
}
#[derive(Clone, Debug)]
pub enum JitterModel {
Uniform {
max: Duration,
},
Bursty {
normal_jitter: Duration,
burst_jitter: Duration,
burst_probability: f64,
},
}
impl JitterModel {
#[must_use]
pub fn sample(&self, rng: &mut DetRng) -> Duration {
match self {
Self::Uniform { max } => {
if max.is_zero() {
return Duration::ZERO;
}
let nanos = max.as_nanos();
let rand_u128 = (u128::from(rng.next_u64()) << 64) | u128::from(rng.next_u64());
let offset = rand_u128 % (nanos + 1);
duration_from_total_nanos_saturating(offset)
}
Self::Bursty {
normal_jitter,
burst_jitter,
burst_probability,
} => {
let p = next_unit_f64(rng);
let range = if p < burst_probability.clamp(0.0, 1.0) {
*burst_jitter
} else {
*normal_jitter
};
if range.is_zero() {
Duration::ZERO
} else {
let nanos = range.as_nanos();
let rand_u128 = (u128::from(rng.next_u64()) << 64) | u128::from(rng.next_u64());
let offset = rand_u128 % (nanos + 1);
duration_from_total_nanos_saturating(offset)
}
}
}
}
}
#[allow(clippy::cast_precision_loss)]
fn next_unit_f64(rng: &mut DetRng) -> f64 {
let raw = rng.next_u64() >> 11;
let mut v = raw as f64 / (1u64 << 53) as f64;
if v <= 0.0 {
v = f64::MIN_POSITIVE;
}
v
}
fn sample_standard_normal(rng: &mut DetRng) -> f64 {
let u1 = next_unit_f64(rng);
let u2 = next_unit_f64(rng);
let r = (-2.0 * u1.ln()).sqrt();
let theta = 2.0 * std::f64::consts::PI * u2;
r * theta.cos()
}
#[allow(clippy::cast_precision_loss)]
fn duration_from_secs_f64(secs: f64) -> Duration {
if !secs.is_finite() || secs <= 0.0 {
return Duration::ZERO;
}
Duration::try_from_secs_f64(secs).unwrap_or_else(|_| max_duration())
}
const MAX_DURATION_NANOS: u128 = (u64::MAX as u128) * 1_000_000_000 + 999_999_999;
fn max_duration() -> Duration {
Duration::new(u64::MAX, 999_999_999)
}
fn duration_from_total_nanos_saturating(total_nanos: u128) -> Duration {
if total_nanos >= MAX_DURATION_NANOS {
return max_duration();
}
let secs = (total_nanos / 1_000_000_000) as u64;
let nanos = (total_nanos % 1_000_000_000) as u32;
Duration::new(secs, nanos)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn latency_models_are_deterministic() {
let mut rng1 = DetRng::new(42);
let mut rng2 = DetRng::new(42);
let model = LatencyModel::Uniform {
min: Duration::from_millis(1),
max: Duration::from_millis(5),
};
for _ in 0..100 {
assert_eq!(model.sample(&mut rng1), model.sample(&mut rng2));
}
}
#[test]
fn latency_models_constant_cases() {
let mut rng = DetRng::new(7);
let fixed = LatencyModel::Fixed(Duration::from_millis(5));
assert_eq!(fixed.sample(&mut rng), Duration::from_millis(5));
let uniform = LatencyModel::Uniform {
min: Duration::from_millis(3),
max: Duration::from_millis(3),
};
assert_eq!(uniform.sample(&mut rng), Duration::from_millis(3));
let normal = LatencyModel::Normal {
mean: Duration::from_millis(12),
std_dev: Duration::ZERO,
};
assert_eq!(normal.sample(&mut rng), Duration::from_millis(12));
let log_normal = LatencyModel::LogNormal {
mu: 0.0,
sigma: 0.0,
};
assert_eq!(log_normal.sample(&mut rng), duration_from_secs_f64(1.0));
let bimodal_low = LatencyModel::Bimodal {
low: Duration::from_millis(4),
high: Duration::from_millis(9),
high_probability: 0.0,
};
assert_eq!(bimodal_low.sample(&mut rng), Duration::from_millis(4));
let bimodal_high = LatencyModel::Bimodal {
low: Duration::from_millis(4),
high: Duration::from_millis(9),
high_probability: 1.0,
};
assert_eq!(bimodal_high.sample(&mut rng), Duration::from_millis(9));
}
#[test]
fn network_config_default() {
let config = NetworkConfig::default();
assert_eq!(config.seed, 0x4E45_5457);
assert!(!config.capture_trace);
assert_eq!(config.max_queue_depth, 10_000);
assert_eq!(config.tick_resolution, Duration::from_micros(100));
assert!(!config.enable_bandwidth);
assert_eq!(config.default_bandwidth, 1_000_000_000);
}
#[test]
fn network_config_debug_clone() {
let config = NetworkConfig::default();
let dbg = format!("{config:?}");
assert!(dbg.contains("NetworkConfig"), "{dbg}");
let cloned = config;
assert_eq!(cloned.seed, 0x4E45_5457);
}
#[test]
fn network_conditions_ideal() {
let c = NetworkConditions::ideal();
assert!(
(c.packet_loss).abs() < f64::EPSILON,
"expected 0.0, got {}",
c.packet_loss
);
assert!(
(c.packet_corrupt).abs() < f64::EPSILON,
"expected 0.0, got {}",
c.packet_corrupt
);
assert!(
(c.packet_duplicate).abs() < f64::EPSILON,
"expected 0.0, got {}",
c.packet_duplicate
);
assert!(
(c.packet_reorder).abs() < f64::EPSILON,
"expected 0.0, got {}",
c.packet_reorder
);
assert_eq!(c.max_in_flight, usize::MAX);
assert!(c.bandwidth.is_none());
assert!(c.jitter.is_none());
assert!(matches!(c.latency, LatencyModel::Fixed(d) if d == Duration::ZERO));
}
#[test]
fn network_conditions_local() {
let c = NetworkConditions::local();
assert!(matches!(c.latency, LatencyModel::Fixed(d) if d == Duration::from_millis(1)));
assert!(
(c.packet_loss).abs() < f64::EPSILON,
"expected 0.0, got {}",
c.packet_loss
);
}
#[test]
fn network_conditions_lan() {
let c = NetworkConditions::lan();
assert!(matches!(c.latency, LatencyModel::Uniform { .. }));
assert!(c.packet_loss > 0.0);
assert_eq!(c.bandwidth, Some(1_000_000_000));
}
#[test]
fn network_conditions_wan() {
let c = NetworkConditions::wan();
assert!(matches!(c.latency, LatencyModel::Normal { .. }));
assert!(c.packet_loss > 0.0);
assert!(c.packet_reorder > 0.0);
assert_eq!(c.bandwidth, Some(100_000_000));
assert!(c.jitter.is_some());
}
#[test]
fn network_conditions_lossy() {
let c = NetworkConditions::lossy();
assert!((c.packet_loss - 0.1).abs() < f64::EPSILON);
}
#[test]
fn network_conditions_satellite() {
let c = NetworkConditions::satellite();
assert!(
matches!(c.latency, LatencyModel::Normal { mean, .. } if mean > Duration::from_millis(500))
);
assert!(c.packet_loss > 0.0);
assert_eq!(c.bandwidth, Some(10_000_000));
}
#[test]
fn network_conditions_congested() {
let c = NetworkConditions::congested();
assert!(c.packet_loss > 0.01);
assert!(c.packet_reorder > 0.0);
assert_eq!(c.max_in_flight, 100);
assert!(matches!(c.jitter, Some(JitterModel::Bursty { .. })));
}
#[test]
fn network_conditions_debug_clone() {
let c = NetworkConditions::wan();
let dbg = format!("{c:?}");
assert!(dbg.contains("NetworkConditions"), "{dbg}");
let cloned = c.clone();
assert!((cloned.packet_loss - c.packet_loss).abs() < f64::EPSILON);
}
#[test]
fn latency_model_debug_clone() {
let fixed = LatencyModel::Fixed(Duration::from_millis(5));
let dbg = format!("{fixed:?}");
assert!(dbg.contains("Fixed"), "{dbg}");
let cloned = fixed.clone();
let mut rng1 = DetRng::new(1);
let mut rng2 = DetRng::new(1);
assert_eq!(cloned.sample(&mut rng1), fixed.sample(&mut rng2));
}
#[test]
fn latency_model_log_normal_produces_positive() {
let model = LatencyModel::LogNormal {
mu: -2.0,
sigma: 0.5,
};
let mut rng = DetRng::new(123);
for _ in 0..50 {
let sample = model.sample(&mut rng);
assert!(sample >= Duration::ZERO);
}
}
#[test]
fn latency_model_uniform_min_exceeds_max() {
let model = LatencyModel::Uniform {
min: Duration::from_millis(10),
max: Duration::from_millis(5),
};
let mut rng = DetRng::new(7);
assert_eq!(model.sample(&mut rng), Duration::from_millis(10));
}
#[test]
fn jitter_model_debug_clone() {
let uniform = JitterModel::Uniform {
max: Duration::from_millis(5),
};
let cloned = uniform.clone();
let dbg = format!("{uniform:?}");
assert!(dbg.contains("Uniform"), "{dbg}");
assert!(format!("{cloned:?}").contains("Uniform"));
}
#[test]
fn jitter_model_uniform_zero_max() {
let model = JitterModel::Uniform {
max: Duration::ZERO,
};
let mut rng = DetRng::new(42);
assert_eq!(model.sample(&mut rng), Duration::ZERO);
}
#[test]
fn jitter_model_bursty_zero_ranges() {
let model = JitterModel::Bursty {
normal_jitter: Duration::ZERO,
burst_jitter: Duration::ZERO,
burst_probability: 0.5,
};
let mut rng = DetRng::new(99);
for _ in 0..20 {
assert_eq!(model.sample(&mut rng), Duration::ZERO);
}
}
#[test]
fn duration_from_secs_f64_negative() {
assert_eq!(duration_from_secs_f64(-1.0), Duration::ZERO);
}
#[test]
fn duration_from_secs_f64_nan() {
assert_eq!(duration_from_secs_f64(f64::NAN), Duration::ZERO);
}
#[test]
fn duration_from_secs_f64_infinity() {
assert_eq!(duration_from_secs_f64(f64::INFINITY), Duration::ZERO);
}
#[test]
fn duration_from_secs_f64_valid() {
let d = duration_from_secs_f64(0.001);
assert_eq!(d, Duration::from_millis(1));
}
#[test]
fn duration_from_secs_f64_large_value_preserved() {
let secs = 1_000_000_000_000.0;
let d = duration_from_secs_f64(secs);
assert_eq!(d.as_secs(), 1_000_000_000_000);
}
#[test]
fn latency_model_uniform_large_range_does_not_truncate() {
let min = Duration::from_secs(20_000_000_000);
let max = min + Duration::from_secs(1);
let model = LatencyModel::Uniform { min, max };
let mut rng = DetRng::new(1234);
for _ in 0..64 {
let sample = model.sample(&mut rng);
assert!(sample >= min, "sample below min: {sample:?} < {min:?}");
assert!(sample <= max, "sample above max: {sample:?} > {max:?}");
}
}
#[test]
fn jitter_model_uniform_large_max_does_not_truncate() {
let max = Duration::from_secs(20_000_000_000);
let model = JitterModel::Uniform { max };
let mut rng = DetRng::new(2026);
for _ in 0..256 {
let sample = model.sample(&mut rng);
assert!(sample <= max, "sample above max: {sample:?} > {max:?}");
}
}
#[test]
fn jitter_models_respect_bounds() {
let mut rng = DetRng::new(99);
let uniform = JitterModel::Uniform {
max: Duration::from_millis(6),
};
for _ in 0..100 {
assert!(uniform.sample(&mut rng) <= Duration::from_millis(6));
}
let bursty_normal = JitterModel::Bursty {
normal_jitter: Duration::from_millis(2),
burst_jitter: Duration::from_millis(10),
burst_probability: 0.0,
};
for _ in 0..100 {
assert!(bursty_normal.sample(&mut rng) <= Duration::from_millis(2));
}
let bursty_burst = JitterModel::Bursty {
normal_jitter: Duration::from_millis(2),
burst_jitter: Duration::from_millis(10),
burst_probability: 1.0,
};
for _ in 0..100 {
assert!(bursty_burst.sample(&mut rng) <= Duration::from_millis(10));
}
}
}