use parking_lot::RwLock;
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Default)]
pub struct NetworkStats {
pub packets_sent: u64,
pub packets_received: u64,
pub bytes_sent: u64,
pub bytes_received: u64,
pub packets_lost: u64,
pub packets_recovered: u64,
pub packets_out_of_order: u64,
pub packets_duplicate: u64,
pub current_bitrate: u64,
pub avg_rtt_us: u64,
pub jitter_us: u64,
pub loss_rate: f64,
}
impl NetworkStats {
#[must_use]
pub const fn new() -> Self {
Self {
packets_sent: 0,
packets_received: 0,
bytes_sent: 0,
bytes_received: 0,
packets_lost: 0,
packets_recovered: 0,
packets_out_of_order: 0,
packets_duplicate: 0,
current_bitrate: 0,
avg_rtt_us: 0,
jitter_us: 0,
loss_rate: 0.0,
}
}
pub fn update_loss_rate(&mut self) {
let total_expected = self.packets_received + self.packets_lost;
if total_expected > 0 {
self.loss_rate = self.packets_lost as f64 / total_expected as f64;
}
}
#[must_use]
pub fn is_healthy(&self) -> bool {
self.loss_rate < 0.01 && self.jitter_us < 10000 }
#[must_use]
pub fn health_score(&self) -> f64 {
let loss_score = (1.0 - self.loss_rate).max(0.0);
let jitter_score = (1.0 - (self.jitter_us as f64 / 50000.0)).max(0.0);
(loss_score + jitter_score) / 2.0
}
}
pub struct StatsTracker {
stats: Arc<RwLock<NetworkStats>>,
last_update: Instant,
bytes_since_update: u64,
update_interval: Duration,
}
impl StatsTracker {
#[must_use]
pub fn new() -> Self {
Self {
stats: Arc::new(RwLock::new(NetworkStats::new())),
last_update: Instant::now(),
bytes_since_update: 0,
update_interval: Duration::from_secs(1),
}
}
pub fn record_sent(&mut self, bytes: usize) {
let mut stats = self.stats.write();
stats.packets_sent += 1;
stats.bytes_sent += bytes as u64;
drop(stats);
self.bytes_since_update += bytes as u64;
self.update_bitrate_stats();
}
pub fn record_received(&mut self, bytes: usize) {
let mut stats = self.stats.write();
stats.packets_received += 1;
stats.bytes_received += bytes as u64;
drop(stats);
self.bytes_since_update += bytes as u64;
self.update_bitrate_stats();
}
pub fn record_lost(&self) {
let mut stats = self.stats.write();
stats.packets_lost += 1;
stats.update_loss_rate();
}
pub fn record_recovered(&self) {
let mut stats = self.stats.write();
stats.packets_recovered += 1;
}
pub fn record_out_of_order(&self) {
let mut stats = self.stats.write();
stats.packets_out_of_order += 1;
}
pub fn record_duplicate(&self) {
let mut stats = self.stats.write();
stats.packets_duplicate += 1;
}
pub fn update_jitter(&self, jitter_us: u64) {
let mut stats = self.stats.write();
const ALPHA: f64 = 0.125;
stats.jitter_us =
((1.0 - ALPHA) * stats.jitter_us as f64 + ALPHA * jitter_us as f64) as u64;
}
pub fn update_rtt(&self, rtt_us: u64) {
let mut stats = self.stats.write();
const ALPHA: f64 = 0.125;
stats.avg_rtt_us = ((1.0 - ALPHA) * stats.avg_rtt_us as f64 + ALPHA * rtt_us as f64) as u64;
}
fn update_bitrate_stats(&mut self) {
let elapsed = self.last_update.elapsed();
if elapsed >= self.update_interval {
let bits = self.bytes_since_update * 8;
let bitrate = (bits as f64 / elapsed.as_secs_f64()) as u64;
const ALPHA: f64 = 0.2;
let mut stats = self.stats.write();
stats.current_bitrate =
((1.0 - ALPHA) * stats.current_bitrate as f64 + ALPHA * bitrate as f64) as u64;
self.last_update = Instant::now();
self.bytes_since_update = 0;
}
}
#[allow(dead_code)]
fn update_bitrate(&mut self, stats: &mut NetworkStats) {
let elapsed = self.last_update.elapsed();
if elapsed >= self.update_interval {
let bits = self.bytes_since_update * 8;
let bitrate = (bits as f64 / elapsed.as_secs_f64()) as u64;
const ALPHA: f64 = 0.2;
stats.current_bitrate =
((1.0 - ALPHA) * stats.current_bitrate as f64 + ALPHA * bitrate as f64) as u64;
self.last_update = Instant::now();
self.bytes_since_update = 0;
}
}
#[must_use]
pub fn get_stats(&self) -> NetworkStats {
self.stats.read().clone()
}
#[must_use]
pub fn stats(&self) -> Arc<RwLock<NetworkStats>> {
Arc::clone(&self.stats)
}
pub fn reset(&mut self) {
let mut stats = self.stats.write();
*stats = NetworkStats::new();
self.last_update = Instant::now();
self.bytes_since_update = 0;
}
}
impl Default for StatsTracker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_network_stats_creation() {
let stats = NetworkStats::new();
assert_eq!(stats.packets_sent, 0);
assert_eq!(stats.packets_received, 0);
}
#[test]
fn test_loss_rate_calculation() {
let mut stats = NetworkStats::new();
stats.packets_received = 95;
stats.packets_lost = 5;
stats.update_loss_rate();
assert!((stats.loss_rate - 0.05).abs() < f64::EPSILON);
}
#[test]
fn test_health_check() {
let mut stats = NetworkStats::new();
stats.packets_received = 1000;
stats.packets_lost = 5;
stats.jitter_us = 5000;
stats.update_loss_rate();
assert!(stats.is_healthy());
}
#[test]
fn test_unhealthy_stream() {
let mut stats = NetworkStats::new();
stats.packets_received = 100;
stats.packets_lost = 10;
stats.update_loss_rate();
assert!(!stats.is_healthy());
}
#[test]
fn test_health_score() {
let stats = NetworkStats::new();
let score = stats.health_score();
assert!((score - 1.0).abs() < 0.01); }
#[test]
fn test_stats_tracker() {
let mut tracker = StatsTracker::new();
tracker.record_sent(1000);
tracker.record_received(1000);
let stats = tracker.get_stats();
assert_eq!(stats.packets_sent, 1);
assert_eq!(stats.packets_received, 1);
assert_eq!(stats.bytes_sent, 1000);
assert_eq!(stats.bytes_received, 1000);
}
#[test]
fn test_stats_tracker_lost_packets() {
let tracker = StatsTracker::new();
tracker.record_lost();
tracker.record_recovered();
let stats = tracker.get_stats();
assert_eq!(stats.packets_lost, 1);
assert_eq!(stats.packets_recovered, 1);
}
#[test]
fn test_jitter_update() {
let tracker = StatsTracker::new();
tracker.update_jitter(1000);
tracker.update_jitter(2000);
let stats = tracker.get_stats();
assert!(stats.jitter_us > 0);
}
#[test]
fn test_rtt_update() {
let tracker = StatsTracker::new();
tracker.update_rtt(5000);
tracker.update_rtt(6000);
let stats = tracker.get_stats();
assert!(stats.avg_rtt_us > 0);
}
#[test]
fn test_stats_reset() {
let mut tracker = StatsTracker::new();
tracker.record_sent(1000);
tracker.reset();
let stats = tracker.get_stats();
assert_eq!(stats.packets_sent, 0);
assert_eq!(stats.bytes_sent, 0);
}
}