use std::time::Instant;
#[derive(Debug, Clone, Copy)]
pub struct BandwidthRate {
pub rx_bytes_per_sec: f64,
pub tx_bytes_per_sec: f64,
}
#[derive(Debug)]
pub struct BandwidthTracker {
prev_sample: Option<(u64, u64, Instant)>, pub current_rate: Option<BandwidthRate>,
}
impl Default for BandwidthTracker {
fn default() -> Self {
Self::new()
}
}
impl BandwidthTracker {
pub fn new() -> Self {
Self {
prev_sample: None,
current_rate: None,
}
}
pub fn sample(&mut self) {
let (rx, tx) = match read_counters() {
Some(v) => v,
None => return,
};
let now = Instant::now();
if let Some((prev_rx, prev_tx, prev_time)) = self.prev_sample {
let elapsed = now.duration_since(prev_time).as_secs_f64();
if elapsed > 0.0 {
let rx_delta = rx.saturating_sub(prev_rx) as f64;
let tx_delta = tx.saturating_sub(prev_tx) as f64;
self.current_rate = Some(BandwidthRate {
rx_bytes_per_sec: rx_delta / elapsed,
tx_bytes_per_sec: tx_delta / elapsed,
});
}
}
self.prev_sample = Some((rx, tx, now));
}
}
pub fn format_rate(bytes_per_sec: f64) -> String {
if bytes_per_sec < 1024.0 {
format!("{:.0} B/s", bytes_per_sec)
} else if bytes_per_sec < 1024.0 * 1024.0 {
format!("{:.1} KB/s", bytes_per_sec / 1024.0)
} else if bytes_per_sec < 1024.0 * 1024.0 * 1024.0 {
format!("{:.1} MB/s", bytes_per_sec / (1024.0 * 1024.0))
} else {
format!("{:.1} GB/s", bytes_per_sec / (1024.0 * 1024.0 * 1024.0))
}
}
fn read_counters() -> Option<(u64, u64)> {
#[cfg(target_os = "linux")]
{
read_counters_linux()
}
#[cfg(target_os = "macos")]
{
read_counters_macos()
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
None
}
}
#[cfg(target_os = "linux")]
fn read_counters_linux() -> Option<(u64, u64)> {
let content = std::fs::read_to_string("/proc/net/dev").ok()?;
parse_proc_net_dev(&content)
}
#[cfg(target_os = "macos")]
fn read_counters_macos() -> Option<(u64, u64)> {
let output = std::process::Command::new("netstat")
.args(["-ib"])
.output()
.ok()?;
let content = String::from_utf8(output.stdout).ok()?;
parse_netstat_ib(&content)
}
#[allow(dead_code)]
fn parse_proc_net_dev(content: &str) -> Option<(u64, u64)> {
let mut total_rx = 0u64;
let mut total_tx = 0u64;
let mut found = false;
for line in content.lines().skip(2) {
let line = line.trim();
if line.is_empty() {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 10 {
continue;
}
if parts[0].starts_with("lo:") || parts[0] == "lo:" {
continue;
}
if let (Ok(rx), Ok(tx)) = (parts[1].parse::<u64>(), parts[9].parse::<u64>()) {
total_rx += rx;
total_tx += tx;
found = true;
}
}
if found {
Some((total_rx, total_tx))
} else {
None
}
}
#[allow(dead_code)]
fn parse_netstat_ib(content: &str) -> Option<(u64, u64)> {
let mut total_rx = 0u64;
let mut total_tx = 0u64;
let mut found = false;
for line in content.lines().skip(1) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 11 {
continue;
}
if parts[0].starts_with("lo") {
continue;
}
if !parts[2].starts_with("<Link#") {
continue;
}
if let (Ok(rx), Ok(tx)) = (parts[6].parse::<u64>(), parts[9].parse::<u64>()) {
total_rx += rx;
total_tx += tx;
found = true;
}
}
if found {
Some((total_rx, total_tx))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_rate_bytes() {
assert_eq!(format_rate(0.0), "0 B/s");
assert_eq!(format_rate(500.0), "500 B/s");
assert_eq!(format_rate(1023.0), "1023 B/s");
}
#[test]
fn format_rate_kilobytes() {
assert_eq!(format_rate(1024.0), "1.0 KB/s");
assert_eq!(format_rate(1536.0), "1.5 KB/s");
assert_eq!(format_rate(500_000.0), "488.3 KB/s");
}
#[test]
fn format_rate_megabytes() {
assert_eq!(format_rate(1_048_576.0), "1.0 MB/s");
assert_eq!(format_rate(10_000_000.0), "9.5 MB/s");
}
#[test]
fn format_rate_gigabytes() {
assert_eq!(format_rate(1_073_741_824.0), "1.0 GB/s");
}
#[test]
fn tracker_first_sample_no_rate() {
let mut t = BandwidthTracker::new();
assert!(t.current_rate.is_none());
t.sample();
}
#[test]
fn parse_proc_net_dev_valid() {
let content = "\
Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
lo: 100 10 0 0 0 0 0 0 100 10 0 0 0 0 0 0
eth0: 5000 50 0 0 0 0 0 0 3000 30 0 0 0 0 0 0
eth1: 2000 20 0 0 0 0 0 0 1000 10 0 0 0 0 0 0
";
let (rx, tx) = parse_proc_net_dev(content).unwrap();
assert_eq!(rx, 7000); assert_eq!(tx, 4000);
}
#[test]
fn parse_proc_net_dev_empty() {
let content = "\
Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
";
assert!(parse_proc_net_dev(content).is_none());
}
#[test]
fn parse_netstat_ib_valid() {
let content = "\
Name Mtu Network Address Ipkts Ierrs Ibytes Opkts Oerrs Obytes Coll
lo0 16384 <Link#1> 1000 0 500000 1000 0 500000 0
en0 1500 <Link#6> aa:bb:cc:dd:ee:ff 5000 0 3000000 4000 0 2000000 0
en0 1500 192.168.1 192.168.1.100 5000 0 3000000 4000 0 2000000 0
";
let (rx, tx) = parse_netstat_ib(content).unwrap();
assert_eq!(rx, 3_000_000); assert_eq!(tx, 2_000_000);
}
#[test]
fn parse_netstat_ib_empty() {
let content = "Name Mtu Network Address\n";
assert!(parse_netstat_ib(content).is_none());
}
}