#![allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FlowMetric {
Bitrate,
PacketLoss,
JitterMs,
RttMs,
}
impl FlowMetric {
#[must_use]
pub fn unit(&self) -> &'static str {
match self {
FlowMetric::Bitrate => "bps",
FlowMetric::PacketLoss => "%",
FlowMetric::JitterMs => "ms",
FlowMetric::RttMs => "ms",
}
}
}
#[derive(Debug, Clone)]
pub struct FlowStats {
total_bytes: u64,
elapsed_secs: f64,
packets_sent: u64,
packets_lost: u64,
jitter_sum_ms: f64,
jitter_samples: u64,
}
impl FlowStats {
#[must_use]
pub fn new() -> Self {
Self {
total_bytes: 0,
elapsed_secs: 0.0,
packets_sent: 0,
packets_lost: 0,
jitter_sum_ms: 0.0,
jitter_samples: 0,
}
}
pub fn update_bitrate(&mut self, bytes: u64, secs: f64) {
self.total_bytes += bytes;
self.elapsed_secs += secs;
}
pub fn record_packets(&mut self, sent: u64, lost: u64) {
self.packets_sent += sent;
self.packets_lost += lost;
}
pub fn add_jitter_sample(&mut self, jitter_ms: f64) {
self.jitter_sum_ms += jitter_ms;
self.jitter_samples += 1;
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn bitrate_bps(&self) -> f64 {
if self.elapsed_secs <= 0.0 {
return 0.0;
}
(self.total_bytes as f64 * 8.0) / self.elapsed_secs
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn packet_loss_rate(&self) -> f64 {
if self.packets_sent == 0 {
return 0.0;
}
self.packets_lost as f64 / self.packets_sent as f64
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn jitter_ms(&self) -> f64 {
if self.jitter_samples == 0 {
return 0.0;
}
self.jitter_sum_ms / self.jitter_samples as f64
}
#[must_use]
pub fn has_excessive_loss(&self, threshold: f64) -> bool {
self.packet_loss_rate() > threshold
}
}
impl Default for FlowStats {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct FlowStatsReport {
entries: Vec<FlowStats>,
}
impl FlowStatsReport {
#[must_use]
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn add(&mut self, stats: FlowStats) {
self.entries.push(stats);
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn worst_metric(&self) -> Option<FlowMetric> {
if self.entries.is_empty() {
return None;
}
let max_loss = self
.entries
.iter()
.map(FlowStats::packet_loss_rate)
.fold(0.0_f64, f64::max);
let max_jitter = self
.entries
.iter()
.map(FlowStats::jitter_ms)
.fold(0.0_f64, f64::max);
if max_loss >= 0.01 {
Some(FlowMetric::PacketLoss)
} else if max_jitter >= 1.0 {
Some(FlowMetric::JitterMs)
} else {
Some(FlowMetric::Bitrate)
}
}
#[must_use]
pub fn total_bitrate_bps(&self) -> f64 {
self.entries.iter().map(FlowStats::bitrate_bps).sum()
}
}
impl Default for FlowStatsReport {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_flow_metric_unit_bitrate() {
assert_eq!(FlowMetric::Bitrate.unit(), "bps");
}
#[test]
fn test_flow_metric_unit_packet_loss() {
assert_eq!(FlowMetric::PacketLoss.unit(), "%");
}
#[test]
fn test_flow_metric_unit_jitter() {
assert_eq!(FlowMetric::JitterMs.unit(), "ms");
}
#[test]
fn test_flow_metric_unit_rtt() {
assert_eq!(FlowMetric::RttMs.unit(), "ms");
}
#[test]
fn test_flow_stats_default_zero() {
let s = FlowStats::new();
assert_eq!(s.bitrate_bps(), 0.0);
assert_eq!(s.packet_loss_rate(), 0.0);
assert_eq!(s.jitter_ms(), 0.0);
}
#[test]
fn test_update_bitrate() {
let mut s = FlowStats::new();
s.update_bitrate(1_000_000, 1.0); assert!((s.bitrate_bps() - 8_000_000.0).abs() < 1.0);
}
#[test]
fn test_packet_loss_rate_zero_when_no_loss() {
let mut s = FlowStats::new();
s.record_packets(100, 0);
assert_eq!(s.packet_loss_rate(), 0.0);
}
#[test]
fn test_packet_loss_rate_calculation() {
let mut s = FlowStats::new();
s.record_packets(100, 5);
assert!((s.packet_loss_rate() - 0.05).abs() < 1e-9);
}
#[test]
fn test_jitter_ms_mean() {
let mut s = FlowStats::new();
s.add_jitter_sample(2.0);
s.add_jitter_sample(4.0);
assert!((s.jitter_ms() - 3.0).abs() < 1e-9);
}
#[test]
fn test_has_excessive_loss_true() {
let mut s = FlowStats::new();
s.record_packets(100, 10);
assert!(s.has_excessive_loss(0.05));
}
#[test]
fn test_has_excessive_loss_false() {
let mut s = FlowStats::new();
s.record_packets(100, 1);
assert!(!s.has_excessive_loss(0.05));
}
#[test]
fn test_report_empty() {
let r = FlowStatsReport::new();
assert!(r.is_empty());
assert_eq!(r.worst_metric(), None);
}
#[test]
fn test_report_worst_metric_packet_loss() {
let mut r = FlowStatsReport::new();
let mut s = FlowStats::new();
s.record_packets(100, 5); r.add(s);
assert_eq!(r.worst_metric(), Some(FlowMetric::PacketLoss));
}
#[test]
fn test_report_worst_metric_jitter() {
let mut r = FlowStatsReport::new();
let mut s = FlowStats::new();
s.add_jitter_sample(10.0);
r.add(s);
assert_eq!(r.worst_metric(), Some(FlowMetric::JitterMs));
}
#[test]
fn test_report_total_bitrate() {
let mut r = FlowStatsReport::new();
let mut s1 = FlowStats::new();
s1.update_bitrate(125_000, 1.0); let mut s2 = FlowStats::new();
s2.update_bitrate(125_000, 1.0); r.add(s1);
r.add(s2);
assert!((r.total_bitrate_bps() - 2_000_000.0).abs() < 1.0);
}
#[test]
fn test_flow_stats_default_trait() {
let s: FlowStats = Default::default();
assert_eq!(s.bitrate_bps(), 0.0);
}
}