use bevy_reflect::Reflect;
use core::time::Duration;
use lightyear_core::time::Instant;
use lightyear_utils::ready_buffer::ReadyBuffer;
use rand::RngExt;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LinkConditionerState {
#[default]
Good,
Bad,
}
#[derive(Clone, Debug, Default, PartialEq, Reflect)]
pub struct LinkConditionerConfig {
pub incoming_latency: Duration,
pub incoming_jitter: Duration,
pub good_loss: f32,
pub bad_loss: f32,
pub good_to_bad: f32,
pub bad_to_good: f32,
}
#[derive(Debug, Clone)]
pub struct LinkConditioner<P: Eq> {
config: LinkConditionerConfig,
state: LinkConditionerState,
pub time_queue: ReadyBuffer<Instant, P>,
}
impl<P: Eq> LinkConditioner<P> {
pub fn new(config: LinkConditionerConfig) -> Self {
LinkConditioner {
config,
state: LinkConditionerState::default(),
time_queue: ReadyBuffer::new(),
}
}
pub(crate) fn condition_packet(&mut self, packet: P, instant: Instant) {
let mut rng = rand::rng();
let config = &self.config;
let (packet_loss_probability, state_transition_probability) = match self.state {
LinkConditionerState::Good => (config.good_loss, config.good_to_bad),
LinkConditionerState::Bad => (config.bad_loss, config.bad_to_good),
};
if rng.random_range(0.0..1.0) < state_transition_probability {
self.state = match self.state {
LinkConditionerState::Good => LinkConditionerState::Bad,
LinkConditionerState::Bad => LinkConditionerState::Good,
};
}
if rng.random_range(0.0..1.0) < packet_loss_probability {
return;
}
let mut latency: i32 = self.config.incoming_latency.as_millis() as i32;
let mut packet_timestamp = instant;
if self.config.incoming_jitter > Duration::default() {
let jitter: i32 = self.config.incoming_jitter.as_millis() as i32;
latency += rng.random_range(-jitter..jitter);
}
if latency > 0 {
packet_timestamp += Duration::from_millis(latency as u64);
}
self.time_queue.push(packet_timestamp, packet);
}
pub(crate) fn pop_packet(&mut self, instant: Instant) -> Option<P> {
self.time_queue.pop_item(&instant).map(|(_, packet)| packet)
}
}
impl LinkConditionerConfig {
#[must_use]
pub fn with_incoming_latency(mut self, incoming_latency: Duration) -> Self {
self.incoming_latency = incoming_latency;
self
}
#[must_use]
pub fn with_incoming_jitter(mut self, incoming_jitter: Duration) -> Self {
self.incoming_jitter = incoming_jitter;
self
}
#[must_use]
pub fn with_fixed_loss(mut self, loss_probability: f32) -> Self {
debug_assert!(
(0.0..=1.0).contains(&loss_probability),
"Loss probability must be in 0.0..=1.0, got {loss_probability}"
);
self.good_loss = loss_probability;
self.bad_loss = loss_probability;
self.good_to_bad = 0.0;
self.bad_to_good = 0.0;
self
}
#[must_use]
pub fn with_simple_gilbert_loss(self, mean_loss: f32, mean_bad_len: f32) -> Self {
self.with_gilbert_elliott_loss(mean_loss, mean_bad_len, 1.0, 0.0)
}
#[must_use]
pub fn with_gilbert_loss(self, mean_loss: f32, mean_bad_len: f32, bad_loss: f32) -> Self {
self.with_gilbert_elliott_loss(mean_loss, mean_bad_len, bad_loss, 0.0)
}
#[must_use]
pub fn with_gilbert_elliott_loss(
mut self,
mean_loss: f32,
mean_bad_len: f32,
bad_loss: f32,
min_loss: f32,
) -> Self {
debug_assert!(
(0.0..=1.0).contains(&min_loss) && (0.0..=1.0).contains(&bad_loss),
"Loss probabilities must be in 0.0..=1.0, got min_loss {min_loss}, bad_loss {bad_loss}"
);
debug_assert!(
min_loss <= mean_loss && mean_loss < bad_loss,
"Mean loss must be in range [min_loss, bad_loss) = [{min_loss}, {bad_loss}), got {mean_loss}"
);
debug_assert!(
mean_bad_len >= 1.0,
"Mean bad-state visit length must be >= 1.0 (a visit is at least one packet), got {mean_bad_len}"
);
let bad_to_good = 1.0 / mean_bad_len;
let stationary_bad = (mean_loss - min_loss) / (bad_loss - min_loss);
let good_to_bad = stationary_bad * bad_to_good / (1.0 - stationary_bad);
self.good_loss = min_loss;
self.bad_loss = bad_loss;
self.good_to_bad = good_to_bad;
self.bad_to_good = bad_to_good;
self
}
pub fn mean_loss(&self) -> f32 {
let transition = self.good_to_bad + self.bad_to_good;
if transition <= 0.0 {
return self.good_loss;
}
let stationary_bad = self.good_to_bad / transition;
(1.0 - stationary_bad) * self.good_loss + stationary_bad * self.bad_loss
}
pub fn half(self) -> Self {
LinkConditionerConfig {
incoming_latency: self.incoming_latency / 2,
incoming_jitter: self.incoming_jitter / 2,
good_loss: self.good_loss / 2.0,
bad_loss: self.bad_loss / 2.0,
..self
}
}
pub fn good_condition() -> Self {
Self::default()
.with_incoming_latency(Duration::from_millis(40))
.with_incoming_jitter(Duration::from_millis(6))
.with_fixed_loss(0.002)
}
pub fn average_condition() -> Self {
Self::default()
.with_incoming_latency(Duration::from_millis(100))
.with_incoming_jitter(Duration::from_millis(15))
.with_fixed_loss(0.02)
}
pub fn poor_condition() -> Self {
Self::default()
.with_incoming_latency(Duration::from_millis(200))
.with_incoming_jitter(Duration::from_millis(30))
.with_fixed_loss(0.10)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn fixed_loss_probability_is_state_independent() {
let config = LinkConditionerConfig::default().with_fixed_loss(0.3);
assert_relative_eq!(config.mean_loss(), 0.3, epsilon = 1e-4);
assert_eq!(config.good_loss, config.bad_loss);
}
#[test]
fn default_is_lossless() {
assert_eq!(LinkConditionerConfig::default().mean_loss(), 0.0);
}
#[test]
fn simple_gilbert_hits_target_mean_loss() {
for &(mean_loss, mean_bad_len) in
&[(0.002_f32, 3.0_f32), (0.02, 4.0), (0.10, 6.0), (0.5, 10.0)]
{
let config =
LinkConditionerConfig::default().with_simple_gilbert_loss(mean_loss, mean_bad_len);
assert_relative_eq!(config.mean_loss(), mean_loss, epsilon = 1e-4);
assert_relative_eq!(1.0 / config.bad_to_good, mean_bad_len, epsilon = 1e-4);
}
}
#[test]
fn gilbert_hits_target_mean_loss() {
for &(mean_loss, mean_bad_len, bad_loss) in &[
(0.002_f32, 3.0_f32, 0.5_f32),
(0.02, 4.0, 0.8),
(0.10, 6.0, 0.3),
] {
let config = LinkConditionerConfig::default().with_gilbert_loss(
mean_loss,
mean_bad_len,
bad_loss,
);
assert_relative_eq!(config.mean_loss(), mean_loss, epsilon = 1e-4);
assert_relative_eq!(1.0 / config.bad_to_good, mean_bad_len, epsilon = 1e-4);
assert_eq!(config.good_loss, 0.0);
assert_eq!(config.bad_loss, bad_loss);
}
}
#[test]
fn gilbert_elliott_hits_target_mean_loss() {
for &(mean_loss, mean_bad_len, bad_loss, min_loss) in &[
(0.01_f32, 3.0_f32, 0.5_f32, 0.001_f32),
(0.05, 8.0, 0.8, 0.01),
(0.10, 6.0, 0.3, 0.02),
] {
let config = LinkConditionerConfig::default().with_gilbert_elliott_loss(
mean_loss,
mean_bad_len,
bad_loss,
min_loss,
);
assert_relative_eq!(config.mean_loss(), mean_loss, epsilon = 1e-4);
assert_relative_eq!(1.0 / config.bad_to_good, mean_bad_len, epsilon = 1e-4);
assert_eq!(config.good_loss, min_loss);
assert_eq!(config.bad_loss, bad_loss);
}
}
#[test]
fn constructor_ladder_reduces_downward() {
let (mean_loss, mean_bad_len) = (0.02_f32, 4.0_f32);
let base = LinkConditionerConfig::default;
let simple = base().with_simple_gilbert_loss(mean_loss, mean_bad_len);
assert_eq!(
simple,
base().with_gilbert_loss(mean_loss, mean_bad_len, 1.0)
);
assert_eq!(
base().with_gilbert_loss(mean_loss, mean_bad_len, 0.5),
base().with_gilbert_elliott_loss(mean_loss, mean_bad_len, 0.5, 0.0)
);
}
}