#![allow(dead_code)]
use std::collections::VecDeque;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct QosSample {
pub timestamp_ms: u64,
pub jitter_us: u64,
pub loss_fraction: f64,
pub rtt_ms: Option<u64>,
}
impl QosSample {
#[allow(clippy::cast_precision_loss)]
pub fn new(timestamp_ms: u64, jitter_us: u64, loss_fraction: f64, rtt_ms: Option<u64>) -> Self {
Self {
timestamp_ms,
jitter_us,
loss_fraction: loss_fraction.clamp(0.0, 1.0),
rtt_ms,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HealthRating {
Excellent,
Good,
Fair,
Poor,
Critical,
}
impl fmt::Display for HealthRating {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::Excellent => "Excellent",
Self::Good => "Good",
Self::Fair => "Fair",
Self::Poor => "Poor",
Self::Critical => "Critical",
};
f.write_str(label)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct QosSummary {
pub sample_count: usize,
pub mean_jitter_us: f64,
pub max_jitter_us: u64,
pub mean_loss: f64,
pub mean_rtt_ms: Option<f64>,
pub health_score: f64,
pub rating: HealthRating,
}
#[derive(Debug)]
pub struct QosMonitor {
samples: VecDeque<QosSample>,
capacity: usize,
}
impl QosMonitor {
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "capacity must be > 0");
Self {
samples: VecDeque::with_capacity(capacity),
capacity,
}
}
pub fn push(&mut self, sample: QosSample) {
if self.samples.len() == self.capacity {
self.samples.pop_front();
}
self.samples.push_back(sample);
}
pub fn len(&self) -> usize {
self.samples.len()
}
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
pub fn reset(&mut self) {
self.samples.clear();
}
#[allow(clippy::cast_precision_loss)]
pub fn summary(&self) -> QosSummary {
if self.samples.is_empty() {
return QosSummary {
sample_count: 0,
mean_jitter_us: 0.0,
max_jitter_us: 0,
mean_loss: 0.0,
mean_rtt_ms: None,
health_score: 100.0,
rating: HealthRating::Excellent,
};
}
let n = self.samples.len() as f64;
let mean_jitter_us: f64 = self.samples.iter().map(|s| s.jitter_us as f64).sum::<f64>() / n;
let max_jitter_us = self.samples.iter().map(|s| s.jitter_us).max().unwrap_or(0);
let mean_loss: f64 = self.samples.iter().map(|s| s.loss_fraction).sum::<f64>() / n;
let rtt_samples: Vec<u64> = self.samples.iter().filter_map(|s| s.rtt_ms).collect();
let mean_rtt_ms = if rtt_samples.is_empty() {
None
} else {
Some(rtt_samples.iter().map(|&r| r as f64).sum::<f64>() / rtt_samples.len() as f64)
};
let health_score = Self::compute_health(mean_jitter_us, mean_loss, mean_rtt_ms);
let rating = Self::rating_from_score(health_score);
QosSummary {
sample_count: self.samples.len(),
mean_jitter_us,
max_jitter_us,
mean_loss,
mean_rtt_ms,
health_score,
rating,
}
}
fn compute_health(mean_jitter_us: f64, mean_loss: f64, mean_rtt_ms: Option<f64>) -> f64 {
let jitter_penalty = (mean_jitter_us / 1000.0 * 5.0).min(40.0);
let loss_penalty = (mean_loss * 100.0 * 20.0).min(50.0);
let rtt_penalty = mean_rtt_ms
.map(|rtt| (rtt / 100.0 * 5.0).min(30.0))
.unwrap_or(0.0);
(100.0 - jitter_penalty - loss_penalty - rtt_penalty).max(0.0)
}
fn rating_from_score(score: f64) -> HealthRating {
if score >= 90.0 {
HealthRating::Excellent
} else if score >= 70.0 {
HealthRating::Good
} else if score >= 50.0 {
HealthRating::Fair
} else if score >= 20.0 {
HealthRating::Poor
} else {
HealthRating::Critical
}
}
pub fn latest(&self) -> Option<&QosSample> {
self.samples.back()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_sample(ts: u64, jitter: u64, loss: f64, rtt: Option<u64>) -> QosSample {
QosSample::new(ts, jitter, loss, rtt)
}
#[test]
fn test_monitor_starts_empty() {
let m = QosMonitor::new(10);
assert!(m.is_empty());
assert_eq!(m.len(), 0);
}
#[test]
fn test_push_increases_len() {
let mut m = QosMonitor::new(10);
m.push(make_sample(0, 100, 0.0, None));
assert_eq!(m.len(), 1);
}
#[test]
fn test_eviction() {
let mut m = QosMonitor::new(3);
for i in 0..5 {
m.push(make_sample(i, 100, 0.0, None));
}
assert_eq!(m.len(), 3);
}
#[test]
fn test_summary_empty() {
let m = QosMonitor::new(5);
let s = m.summary();
assert_eq!(s.sample_count, 0);
assert_eq!(s.rating, HealthRating::Excellent);
}
#[test]
fn test_perfect_stream() {
let mut m = QosMonitor::new(5);
for i in 0..5 {
m.push(make_sample(i * 100, 50, 0.0, Some(10)));
}
let s = m.summary();
assert!(s.health_score >= 90.0);
assert_eq!(s.rating, HealthRating::Excellent);
}
#[test]
fn test_high_loss_poor() {
let mut m = QosMonitor::new(4);
for i in 0..4 {
m.push(make_sample(i * 100, 100, 0.05, Some(50)));
}
let s = m.summary();
assert!(s.health_score < 50.0, "score={}", s.health_score);
}
#[test]
fn test_mean_jitter() {
let mut m = QosMonitor::new(2);
m.push(make_sample(0, 200, 0.0, None));
m.push(make_sample(100, 400, 0.0, None));
let s = m.summary();
assert!((s.mean_jitter_us - 300.0).abs() < f64::EPSILON);
}
#[test]
fn test_max_jitter() {
let mut m = QosMonitor::new(3);
m.push(make_sample(0, 100, 0.0, None));
m.push(make_sample(100, 500, 0.0, None));
m.push(make_sample(200, 200, 0.0, None));
let s = m.summary();
assert_eq!(s.max_jitter_us, 500);
}
#[test]
fn test_mean_rtt_partial() {
let mut m = QosMonitor::new(3);
m.push(make_sample(0, 0, 0.0, Some(100)));
m.push(make_sample(100, 0, 0.0, None));
m.push(make_sample(200, 0, 0.0, Some(200)));
let s = m.summary();
assert!((s.mean_rtt_ms.expect("should succeed in test") - 150.0).abs() < f64::EPSILON);
}
#[test]
fn test_loss_clamped() {
let s = QosSample::new(0, 0, 2.0, None);
assert!((s.loss_fraction - 1.0).abs() < f64::EPSILON);
let s2 = QosSample::new(0, 0, -0.5, None);
assert!(s2.loss_fraction.abs() < f64::EPSILON);
}
#[test]
fn test_reset() {
let mut m = QosMonitor::new(5);
m.push(make_sample(0, 100, 0.0, None));
m.reset();
assert!(m.is_empty());
}
#[test]
fn test_latest() {
let mut m = QosMonitor::new(5);
m.push(make_sample(0, 100, 0.0, None));
m.push(make_sample(100, 200, 0.01, Some(50)));
let latest = m.latest().expect("should succeed in test");
assert_eq!(latest.timestamp_ms, 100);
}
#[test]
fn test_health_rating_display() {
assert_eq!(format!("{}", HealthRating::Critical), "Critical");
assert_eq!(format!("{}", HealthRating::Good), "Good");
}
#[test]
fn test_rating_boundaries() {
assert_eq!(
QosMonitor::rating_from_score(100.0),
HealthRating::Excellent
);
assert_eq!(QosMonitor::rating_from_score(90.0), HealthRating::Excellent);
assert_eq!(QosMonitor::rating_from_score(89.9), HealthRating::Good);
assert_eq!(QosMonitor::rating_from_score(70.0), HealthRating::Good);
assert_eq!(QosMonitor::rating_from_score(50.0), HealthRating::Fair);
assert_eq!(QosMonitor::rating_from_score(20.0), HealthRating::Poor);
assert_eq!(QosMonitor::rating_from_score(0.0), HealthRating::Critical);
}
}