#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_possible_wrap)]
use std::collections::VecDeque;
const DEFAULT_WINDOW_SIZE: usize = 1000;
const MAX_GAP_BEFORE_RESET: u64 = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LossEvent {
pub start_seq: u64,
pub count: u64,
pub detection_time: u64,
}
impl LossEvent {
pub fn new(start_seq: u64, count: u64, detection_time: u64) -> Self {
Self {
start_seq,
count,
detection_time,
}
}
pub fn end_seq(&self) -> u64 {
self.start_seq + self.count.saturating_sub(1)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LossStats {
pub total_received: u64,
pub total_lost: u64,
pub total_expected: u64,
pub loss_rate: f64,
pub window_loss_rate: f64,
pub loss_event_count: usize,
pub avg_burst_length: f64,
pub max_burst_length: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LossSeverity {
None,
Minor,
Moderate,
Severe,
Critical,
}
impl LossSeverity {
pub fn from_rate(rate: f64) -> Self {
if rate <= 0.0 {
Self::None
} else if rate < 0.001 {
Self::Minor
} else if rate < 0.01 {
Self::Moderate
} else if rate < 0.05 {
Self::Severe
} else {
Self::Critical
}
}
pub fn description(self) -> &'static str {
match self {
Self::None => "no loss",
Self::Minor => "minor (<0.1%, FEC recoverable)",
Self::Moderate => "moderate (0.1%-1%, possible artifacts)",
Self::Severe => "severe (1%-5%, noticeable degradation)",
Self::Critical => "critical (>5%, severely degraded)",
}
}
}
#[derive(Debug)]
pub struct PacketLossTracker {
next_expected_seq: Option<u64>,
total_received: u64,
total_lost: u64,
window: VecDeque<bool>,
window_size: usize,
loss_events: Vec<LossEvent>,
time_counter: u64,
}
impl PacketLossTracker {
pub fn new(window_size: usize) -> Self {
let ws = if window_size == 0 {
DEFAULT_WINDOW_SIZE
} else {
window_size
};
Self {
next_expected_seq: None,
total_received: 0,
total_lost: 0,
window: VecDeque::with_capacity(ws),
window_size: ws,
loss_events: Vec::new(),
time_counter: 0,
}
}
pub fn with_defaults() -> Self {
Self::new(DEFAULT_WINDOW_SIZE)
}
pub fn record_packet(&mut self, seq: u64) {
self.time_counter += 1;
match self.next_expected_seq {
None => {
self.next_expected_seq = Some(seq + 1);
self.total_received += 1;
self.push_window(true);
}
Some(expected) => {
if seq == expected {
self.next_expected_seq = Some(seq + 1);
self.total_received += 1;
self.push_window(true);
} else if seq > expected {
let gap = seq - expected;
if gap > MAX_GAP_BEFORE_RESET {
self.next_expected_seq = Some(seq + 1);
self.total_received += 1;
self.push_window(true);
} else {
self.total_lost += gap;
self.loss_events
.push(LossEvent::new(expected, gap, self.time_counter));
for _ in 0..gap {
self.push_window(false);
}
self.next_expected_seq = Some(seq + 1);
self.total_received += 1;
self.push_window(true);
}
} else {
self.total_received += 1;
self.push_window(true);
}
}
}
}
fn push_window(&mut self, received: bool) {
self.window.push_back(received);
if self.window.len() > self.window_size {
self.window.pop_front();
}
}
pub fn window_loss_rate(&self) -> f64 {
if self.window.is_empty() {
return 0.0;
}
let lost = self.window.iter().filter(|&&r| !r).count();
lost as f64 / self.window.len() as f64
}
pub fn overall_loss_rate(&self) -> f64 {
let total = self.total_received + self.total_lost;
if total == 0 {
return 0.0;
}
self.total_lost as f64 / total as f64
}
pub fn severity(&self) -> LossSeverity {
LossSeverity::from_rate(self.window_loss_rate())
}
pub fn compute_stats(&self) -> LossStats {
let total_expected = self.total_received + self.total_lost;
let avg_burst = if self.loss_events.is_empty() {
0.0
} else {
let total_burst: u64 = self.loss_events.iter().map(|e| e.count).sum();
total_burst as f64 / self.loss_events.len() as f64
};
let max_burst = self.loss_events.iter().map(|e| e.count).max().unwrap_or(0);
LossStats {
total_received: self.total_received,
total_lost: self.total_lost,
total_expected,
loss_rate: self.overall_loss_rate(),
window_loss_rate: self.window_loss_rate(),
loss_event_count: self.loss_events.len(),
avg_burst_length: avg_burst,
max_burst_length: max_burst,
}
}
pub fn loss_events(&self) -> &[LossEvent] {
&self.loss_events
}
pub fn total_received(&self) -> u64 {
self.total_received
}
pub fn total_lost(&self) -> u64 {
self.total_lost
}
pub fn reset(&mut self) {
self.next_expected_seq = None;
self.total_received = 0;
self.total_lost = 0;
self.window.clear();
self.loss_events.clear();
self.time_counter = 0;
}
}
impl Default for PacketLossTracker {
fn default() -> Self {
Self::with_defaults()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_loss_event_creation() {
let e = LossEvent::new(10, 3, 100);
assert_eq!(e.start_seq, 10);
assert_eq!(e.count, 3);
assert_eq!(e.end_seq(), 12);
}
#[test]
fn test_empty_tracker() {
let t = PacketLossTracker::with_defaults();
assert_eq!(t.total_received(), 0);
assert_eq!(t.total_lost(), 0);
assert!((t.overall_loss_rate()).abs() < f64::EPSILON);
}
#[test]
fn test_sequential_packets_no_loss() {
let mut t = PacketLossTracker::with_defaults();
for i in 0..100 {
t.record_packet(i);
}
assert_eq!(t.total_received(), 100);
assert_eq!(t.total_lost(), 0);
assert!((t.overall_loss_rate()).abs() < f64::EPSILON);
assert_eq!(t.severity(), LossSeverity::None);
}
#[test]
fn test_single_loss() {
let mut t = PacketLossTracker::with_defaults();
t.record_packet(0);
t.record_packet(1);
t.record_packet(3);
assert_eq!(t.total_lost(), 1);
assert_eq!(t.loss_events().len(), 1);
assert_eq!(t.loss_events()[0].start_seq, 2);
assert_eq!(t.loss_events()[0].count, 1);
}
#[test]
fn test_burst_loss() {
let mut t = PacketLossTracker::with_defaults();
t.record_packet(0);
t.record_packet(5);
assert_eq!(t.total_lost(), 4);
assert_eq!(t.loss_events().len(), 1);
assert_eq!(t.loss_events()[0].count, 4);
}
#[test]
fn test_loss_rate_calculation() {
let mut t = PacketLossTracker::new(100);
for i in 0..10 {
t.record_packet(i);
}
t.record_packet(11);
let rate = t.overall_loss_rate();
assert!((rate - 1.0 / 12.0).abs() < 0.001);
}
#[test]
fn test_window_loss_rate() {
let mut t = PacketLossTracker::new(10);
for i in 0..5 {
t.record_packet(i);
}
t.record_packet(6);
let wlr = t.window_loss_rate();
assert!(wlr > 0.0);
assert!(wlr < 0.5);
}
#[test]
fn test_severity_none() {
let mut t = PacketLossTracker::with_defaults();
for i in 0..100 {
t.record_packet(i);
}
assert_eq!(t.severity(), LossSeverity::None);
}
#[test]
fn test_severity_classification() {
assert_eq!(LossSeverity::from_rate(0.0), LossSeverity::None);
assert_eq!(LossSeverity::from_rate(0.0005), LossSeverity::Minor);
assert_eq!(LossSeverity::from_rate(0.005), LossSeverity::Moderate);
assert_eq!(LossSeverity::from_rate(0.03), LossSeverity::Severe);
assert_eq!(LossSeverity::from_rate(0.1), LossSeverity::Critical);
}
#[test]
fn test_severity_description() {
assert!(!LossSeverity::None.description().is_empty());
assert!(!LossSeverity::Critical.description().is_empty());
}
#[test]
fn test_stats_computation() {
let mut t = PacketLossTracker::new(100);
for i in 0..10 {
t.record_packet(i);
}
t.record_packet(13);
t.record_packet(15);
let stats = t.compute_stats();
assert_eq!(stats.total_received, 12);
assert_eq!(stats.total_lost, 4);
assert_eq!(stats.loss_event_count, 2);
assert_eq!(stats.max_burst_length, 3);
}
#[test]
fn test_sequence_reset() {
let mut t = PacketLossTracker::with_defaults();
t.record_packet(0);
t.record_packet(1);
t.record_packet(100_000);
assert_eq!(t.total_lost(), 0);
assert_eq!(t.total_received(), 3);
}
#[test]
fn test_reset() {
let mut t = PacketLossTracker::with_defaults();
t.record_packet(0);
t.record_packet(2); t.reset();
assert_eq!(t.total_received(), 0);
assert_eq!(t.total_lost(), 0);
assert!(t.loss_events().is_empty());
}
}