use std::{collections::HashMap, time::Duration};
use super::messages::{LinkMessage, TcMessage};
#[derive(Debug, Clone, Default)]
pub struct LinkStats {
pub name: Option<String>,
pub rx_bytes: u64,
pub tx_bytes: u64,
pub rx_packets: u64,
pub tx_packets: u64,
pub rx_errors: u64,
pub tx_errors: u64,
pub rx_dropped: u64,
pub tx_dropped: u64,
pub multicast: u64,
pub collisions: u64,
}
impl LinkStats {
pub fn from_link_message(msg: &LinkMessage) -> Self {
if let Some(ref stats) = msg.stats {
Self {
name: msg.name.clone(),
rx_bytes: stats.rx_bytes,
tx_bytes: stats.tx_bytes,
rx_packets: stats.rx_packets,
tx_packets: stats.tx_packets,
rx_errors: stats.rx_errors,
tx_errors: stats.tx_errors,
rx_dropped: stats.rx_dropped,
tx_dropped: stats.tx_dropped,
multicast: stats.multicast,
collisions: stats.collisions,
}
} else {
Self {
name: msg.name.clone(),
..Default::default()
}
}
}
pub fn total_bytes(&self) -> u64 {
self.rx_bytes + self.tx_bytes
}
pub fn total_packets(&self) -> u64 {
self.rx_packets + self.tx_packets
}
pub fn total_errors(&self) -> u64 {
self.rx_errors + self.tx_errors
}
pub fn total_dropped(&self) -> u64 {
self.rx_dropped + self.tx_dropped
}
}
#[derive(Debug, Clone, Default)]
pub struct LinkRates {
pub name: Option<String>,
pub rx_bytes_per_sec: f64,
pub tx_bytes_per_sec: f64,
pub rx_packets_per_sec: f64,
pub tx_packets_per_sec: f64,
pub rx_errors_per_sec: f64,
pub tx_errors_per_sec: f64,
pub rx_dropped_per_sec: f64,
pub tx_dropped_per_sec: f64,
}
impl LinkRates {
pub fn total_bytes_per_sec(&self) -> f64 {
self.rx_bytes_per_sec + self.tx_bytes_per_sec
}
pub fn total_packets_per_sec(&self) -> f64 {
self.rx_packets_per_sec + self.tx_packets_per_sec
}
pub fn rx_bps(&self) -> f64 {
self.rx_bytes_per_sec * 8.0
}
pub fn tx_bps(&self) -> f64 {
self.tx_bytes_per_sec * 8.0
}
pub fn total_bps(&self) -> f64 {
self.total_bytes_per_sec() * 8.0
}
}
#[derive(Debug, Clone, Default)]
pub struct TcStats {
pub kind: Option<String>,
pub bytes: u64,
pub packets: u64,
pub drops: u32,
pub overlimits: u32,
pub requeues: u32,
pub qlen: u32,
pub backlog: u32,
}
impl TcStats {
pub fn from_tc_message(msg: &TcMessage) -> Self {
Self {
kind: msg.kind.clone(),
bytes: msg.bytes(),
packets: msg.packets(),
drops: msg.drops(),
overlimits: msg.overlimits(),
requeues: msg.requeues(),
qlen: msg.qlen(),
backlog: msg.backlog(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TcRates {
pub kind: Option<String>,
pub bytes_per_sec: f64,
pub packets_per_sec: f64,
pub drops_per_sec: f64,
pub overlimits_per_sec: f64,
pub requeues_per_sec: f64,
}
impl TcRates {
pub fn bps(&self) -> f64 {
self.bytes_per_sec * 8.0
}
}
#[derive(Debug, Clone, Default)]
pub struct StatsSnapshot {
pub links: HashMap<u32, LinkStats>,
pub qdiscs: HashMap<(u32, u32), TcStats>,
pub classes: HashMap<(u32, u32), TcStats>,
}
impl StatsSnapshot {
pub fn new() -> Self {
Self::default()
}
pub fn from_links(links: &[LinkMessage]) -> Self {
let mut snapshot = Self::new();
for link in links {
let stats = LinkStats::from_link_message(link);
snapshot.links.insert(link.ifindex(), stats);
}
snapshot
}
pub fn from_tc(qdiscs: &[TcMessage], classes: &[TcMessage]) -> Self {
let mut snapshot = Self::new();
for qdisc in qdiscs {
let stats = TcStats::from_tc_message(qdisc);
snapshot
.qdiscs
.insert((qdisc.ifindex(), qdisc.handle_raw()), stats);
}
for class in classes {
let stats = TcStats::from_tc_message(class);
snapshot
.classes
.insert((class.ifindex(), class.handle_raw()), stats);
}
snapshot
}
pub fn add_links(&mut self, links: &[LinkMessage]) {
for link in links {
let stats = LinkStats::from_link_message(link);
self.links.insert(link.ifindex(), stats);
}
}
pub fn add_qdiscs(&mut self, qdiscs: &[TcMessage]) {
for qdisc in qdiscs {
let stats = TcStats::from_tc_message(qdisc);
self.qdiscs
.insert((qdisc.ifindex(), qdisc.handle_raw()), stats);
}
}
pub fn add_classes(&mut self, classes: &[TcMessage]) {
for class in classes {
let stats = TcStats::from_tc_message(class);
self.classes
.insert((class.ifindex(), class.handle_raw()), stats);
}
}
pub fn rates(&self, previous: &StatsSnapshot, duration: Duration) -> RatesSnapshot {
let secs = duration.as_secs_f64();
if secs <= 0.0 {
return RatesSnapshot::default();
}
let mut rates = RatesSnapshot::new();
for (ifindex, current) in &self.links {
if let Some(prev) = previous.links.get(ifindex) {
rates.links.insert(
*ifindex,
LinkRates {
name: current.name.clone(),
rx_bytes_per_sec: delta_u64(current.rx_bytes, prev.rx_bytes) / secs,
tx_bytes_per_sec: delta_u64(current.tx_bytes, prev.tx_bytes) / secs,
rx_packets_per_sec: delta_u64(current.rx_packets, prev.rx_packets) / secs,
tx_packets_per_sec: delta_u64(current.tx_packets, prev.tx_packets) / secs,
rx_errors_per_sec: delta_u64(current.rx_errors, prev.rx_errors) / secs,
tx_errors_per_sec: delta_u64(current.tx_errors, prev.tx_errors) / secs,
rx_dropped_per_sec: delta_u64(current.rx_dropped, prev.rx_dropped) / secs,
tx_dropped_per_sec: delta_u64(current.tx_dropped, prev.tx_dropped) / secs,
},
);
}
}
for (key, current) in &self.qdiscs {
if let Some(prev) = previous.qdiscs.get(key) {
rates.qdiscs.insert(
*key,
TcRates {
kind: current.kind.clone(),
bytes_per_sec: delta_u64(current.bytes, prev.bytes) / secs,
packets_per_sec: delta_u64(current.packets, prev.packets) / secs,
drops_per_sec: delta_u32(current.drops, prev.drops) / secs,
overlimits_per_sec: delta_u32(current.overlimits, prev.overlimits) / secs,
requeues_per_sec: delta_u32(current.requeues, prev.requeues) / secs,
},
);
}
}
for (key, current) in &self.classes {
if let Some(prev) = previous.classes.get(key) {
rates.classes.insert(
*key,
TcRates {
kind: current.kind.clone(),
bytes_per_sec: delta_u64(current.bytes, prev.bytes) / secs,
packets_per_sec: delta_u64(current.packets, prev.packets) / secs,
drops_per_sec: delta_u32(current.drops, prev.drops) / secs,
overlimits_per_sec: delta_u32(current.overlimits, prev.overlimits) / secs,
requeues_per_sec: delta_u32(current.requeues, prev.requeues) / secs,
},
);
}
}
rates
}
}
#[derive(Debug, Clone, Default)]
pub struct RatesSnapshot {
pub links: HashMap<u32, LinkRates>,
pub qdiscs: HashMap<(u32, u32), TcRates>,
pub classes: HashMap<(u32, u32), TcRates>,
}
impl RatesSnapshot {
pub fn new() -> Self {
Self::default()
}
pub fn total_rx_bytes_per_sec(&self) -> f64 {
self.links.values().map(|r| r.rx_bytes_per_sec).sum()
}
pub fn total_tx_bytes_per_sec(&self) -> f64 {
self.links.values().map(|r| r.tx_bytes_per_sec).sum()
}
pub fn total_bytes_per_sec(&self) -> f64 {
self.total_rx_bytes_per_sec() + self.total_tx_bytes_per_sec()
}
}
#[inline]
fn delta_u64(current: u64, previous: u64) -> f64 {
if current >= previous {
(current - previous) as f64
} else {
(u64::MAX - previous + current + 1) as f64
}
}
#[inline]
fn delta_u32(current: u32, previous: u32) -> f64 {
if current >= previous {
(current - previous) as f64
} else {
(u32::MAX - previous + current + 1) as f64
}
}
#[derive(Debug, Clone)]
pub struct StatsTracker {
previous: Option<StatsSnapshot>,
previous_time: Option<std::time::Instant>,
last_rates: Option<RatesSnapshot>,
}
impl Default for StatsTracker {
fn default() -> Self {
Self::new()
}
}
impl StatsTracker {
pub fn new() -> Self {
Self {
previous: None,
previous_time: None,
last_rates: None,
}
}
pub fn update(&mut self, snapshot: StatsSnapshot) -> Option<RatesSnapshot> {
let now = std::time::Instant::now();
let rates = if let (Some(prev), Some(prev_time)) = (&self.previous, self.previous_time) {
let duration = now.duration_since(prev_time);
Some(snapshot.rates(prev, duration))
} else {
None
};
self.previous = Some(snapshot);
self.previous_time = Some(now);
self.last_rates = rates.clone();
rates
}
pub fn reset(&mut self) {
self.previous = None;
self.previous_time = None;
self.last_rates = None;
}
pub fn previous(&self) -> Option<&StatsSnapshot> {
self.previous.as_ref()
}
pub fn last_rates(&self) -> Option<&RatesSnapshot> {
self.last_rates.as_ref()
}
pub fn get_link_rate(&self, ifindex: u32) -> Option<&LinkRates> {
self.last_rates.as_ref()?.links.get(&ifindex)
}
pub fn get_all_link_rates(&self) -> Option<&HashMap<u32, LinkRates>> {
self.last_rates.as_ref().map(|r| &r.links)
}
pub fn get_qdisc_rate(&self, ifindex: u32, handle: u32) -> Option<&TcRates> {
self.last_rates.as_ref()?.qdiscs.get(&(ifindex, handle))
}
pub fn get_class_rate(&self, ifindex: u32, handle: u32) -> Option<&TcRates> {
self.last_rates.as_ref()?.classes.get(&(ifindex, handle))
}
pub fn has_rates(&self) -> bool {
self.last_rates.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_delta_u64() {
assert_eq!(delta_u64(100, 50), 50.0);
assert_eq!(delta_u64(50, 50), 0.0);
assert_eq!(delta_u64(10, u64::MAX - 10), 21.0);
}
#[test]
fn test_delta_u32() {
assert_eq!(delta_u32(100, 50), 50.0);
assert_eq!(delta_u32(50, 50), 0.0);
assert_eq!(delta_u32(10, u32::MAX - 10), 21.0);
}
#[test]
fn test_link_stats_totals() {
let stats = LinkStats {
rx_bytes: 1000,
tx_bytes: 2000,
rx_packets: 10,
tx_packets: 20,
rx_errors: 1,
tx_errors: 2,
rx_dropped: 3,
tx_dropped: 4,
..Default::default()
};
assert_eq!(stats.total_bytes(), 3000);
assert_eq!(stats.total_packets(), 30);
assert_eq!(stats.total_errors(), 3);
assert_eq!(stats.total_dropped(), 7);
}
#[test]
fn test_link_rates_bps() {
let rates = LinkRates {
rx_bytes_per_sec: 1000.0,
tx_bytes_per_sec: 2000.0,
..Default::default()
};
assert_eq!(rates.rx_bps(), 8000.0);
assert_eq!(rates.tx_bps(), 16000.0);
assert_eq!(rates.total_bps(), 24000.0);
}
#[test]
fn test_stats_snapshot_rates() {
let mut prev = StatsSnapshot::new();
prev.links.insert(
1,
LinkStats {
name: Some("eth0".to_string()),
rx_bytes: 1000,
tx_bytes: 2000,
..Default::default()
},
);
let mut curr = StatsSnapshot::new();
curr.links.insert(
1,
LinkStats {
name: Some("eth0".to_string()),
rx_bytes: 2000,
tx_bytes: 4000,
..Default::default()
},
);
let rates = curr.rates(&prev, Duration::from_secs(1));
let link_rates = rates.links.get(&1).unwrap();
assert_eq!(link_rates.rx_bytes_per_sec, 1000.0);
assert_eq!(link_rates.tx_bytes_per_sec, 2000.0);
}
#[test]
fn test_stats_tracker_caching() {
let mut tracker = StatsTracker::new();
let mut snapshot1 = StatsSnapshot::new();
snapshot1.links.insert(
1,
LinkStats {
name: Some("eth0".to_string()),
rx_bytes: 1000,
tx_bytes: 2000,
..Default::default()
},
);
assert!(tracker.update(snapshot1).is_none());
assert!(!tracker.has_rates());
assert!(tracker.last_rates().is_none());
assert!(tracker.get_link_rate(1).is_none());
let mut snapshot2 = StatsSnapshot::new();
snapshot2.links.insert(
1,
LinkStats {
name: Some("eth0".to_string()),
rx_bytes: 2000,
tx_bytes: 4000,
..Default::default()
},
);
let rates = tracker.update(snapshot2);
assert!(rates.is_some());
assert!(tracker.has_rates());
let cached = tracker.last_rates().unwrap();
assert_eq!(cached.links.len(), 1);
let link_rate = tracker.get_link_rate(1);
assert!(link_rate.is_some());
let all_rates = tracker.get_all_link_rates().unwrap();
assert_eq!(all_rates.len(), 1);
assert!(all_rates.contains_key(&1));
assert!(tracker.get_link_rate(999).is_none());
}
#[test]
fn test_stats_tracker_reset_clears_cache() {
let mut tracker = StatsTracker::new();
let mut snapshot1 = StatsSnapshot::new();
snapshot1.links.insert(1, LinkStats::default());
tracker.update(snapshot1);
let mut snapshot2 = StatsSnapshot::new();
snapshot2.links.insert(1, LinkStats::default());
tracker.update(snapshot2);
assert!(tracker.has_rates());
tracker.reset();
assert!(!tracker.has_rates());
assert!(tracker.last_rates().is_none());
assert!(tracker.previous().is_none());
}
}