#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IoCounters {
pub device: String,
pub read: u64,
pub write: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct IoRate {
pub device: String,
pub read: f64,
pub write: f64,
}
const DISKSTATS_SECTOR_BYTES: u64 = 512;
const DISKSTATS_SECTORS_READ: usize = 5;
const DISKSTATS_SECTORS_WRITTEN: usize = 9;
pub fn parse_diskstats<F>(content: &str, keep: F) -> Vec<IoCounters>
where
F: Fn(&str) -> bool,
{
let mut out = Vec::new();
for line in content.lines() {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() <= DISKSTATS_SECTORS_WRITTEN {
continue;
}
let name = fields[2];
if !keep(name) {
continue;
}
let (Ok(read_sectors), Ok(written_sectors)) = (
fields[DISKSTATS_SECTORS_READ].parse::<u64>(),
fields[DISKSTATS_SECTORS_WRITTEN].parse::<u64>(),
) else {
continue;
};
out.push(IoCounters {
device: name.to_string(),
read: read_sectors.saturating_mul(DISKSTATS_SECTOR_BYTES),
write: written_sectors.saturating_mul(DISKSTATS_SECTOR_BYTES),
});
}
out
}
pub fn compute_rates(
before: &[IoCounters],
after: &[IoCounters],
elapsed_secs: f64,
) -> Vec<IoRate> {
if !elapsed_secs.is_finite() || elapsed_secs <= 0.0 {
return Vec::new();
}
after
.iter()
.filter_map(|now| {
let prev = before.iter().find(|p| p.device == now.device)?;
Some(IoRate {
device: now.device.clone(),
read: now.read.saturating_sub(prev.read) as f64 / elapsed_secs,
write: now.write.saturating_sub(prev.write) as f64 / elapsed_secs,
})
})
.collect()
}
pub fn format_rate(bytes_per_sec: f64) -> String {
let clamped = if bytes_per_sec.is_finite() && bytes_per_sec > 0.0 {
bytes_per_sec.round() as u64
} else {
0
};
format!("{}/s", crate::network::format_bytes(clamped))
}
pub fn format_io_line(rate: &IoRate, read_label: &str, write_label: &str) -> String {
format!(
"{} {}: {} {}: {}",
rate.device,
read_label,
format_rate(rate.read),
write_label,
format_rate(rate.write)
)
}
pub fn select_net_rates(rates: Vec<IoRate>, active: Option<&str>) -> Vec<IoRate> {
if let Some(active) = active {
let selected: Vec<IoRate> = rates
.iter()
.filter(|r| r.device == active)
.cloned()
.collect();
if !selected.is_empty() {
return selected;
}
}
rates
.into_iter()
.filter(|r| r.read > 0.0 || r.write > 0.0)
.collect()
}
pub fn sample_disk_io() -> Vec<IoCounters> {
#[cfg(target_os = "linux")]
{
let Ok(content) = std::fs::read_to_string("/proc/diskstats") else {
return Vec::new();
};
parse_diskstats(&content, is_physical_disk)
}
#[cfg(target_os = "windows")]
{
win_ffi::sample_physical_drives()
}
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
{
Vec::new()
}
}
#[cfg(target_os = "linux")]
fn is_physical_disk(name: &str) -> bool {
if crate::disk::is_virtual_block_name(name) {
return false;
}
let dev = std::path::Path::new("/sys/class/block").join(name);
!dev.join("partition").exists() && dev.join("queue").exists()
}
pub fn sample_net_io() -> Vec<IoCounters> {
#[cfg(target_os = "linux")]
{
let Ok(entries) = std::fs::read_dir("/sys/class/net") else {
return Vec::new();
};
let mut out = Vec::new();
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name == "lo" || name.starts_with("lo:") {
continue;
}
let stats = entry.path().join("statistics");
let read = read_counter(&stats.join("rx_bytes"));
let write = read_counter(&stats.join("tx_bytes"));
if let (Some(read), Some(write)) = (read, write) {
out.push(IoCounters {
device: name,
read,
write,
});
}
}
out.sort_by(|a, b| a.device.cmp(&b.device));
out
}
#[cfg(target_os = "windows")]
{
win_ffi::sample_interfaces()
}
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
{
Vec::new()
}
}
#[cfg(target_os = "linux")]
fn read_counter(path: &std::path::Path) -> Option<u64> {
std::fs::read_to_string(path)
.ok()?
.trim()
.parse::<u64>()
.ok()
}
#[cfg(any(target_os = "windows", test))]
fn physical_drive_name(index: u32) -> String {
format!("PhysicalDrive{index}")
}
#[cfg(target_os = "windows")]
mod win_ffi {
use super::{physical_drive_name, IoCounters};
use std::ffi::{c_void, OsStr};
use std::mem::size_of;
use std::os::windows::ffi::OsStrExt;
use std::ptr;
#[allow(clippy::upper_case_acronyms)]
type HANDLE = *mut c_void;
const INVALID_HANDLE_VALUE: HANDLE = -1isize as HANDLE;
const FILE_SHARE_READ: u32 = 0x0000_0001;
const FILE_SHARE_WRITE: u32 = 0x0000_0002;
const OPEN_EXISTING: u32 = 3;
const IOCTL_DISK_PERFORMANCE: u32 = 0x0007_0020;
#[repr(C)]
#[derive(Default)]
struct DiskPerformance {
bytes_read: i64,
bytes_written: i64,
read_time: i64,
write_time: i64,
idle_time: i64,
read_count: u32,
write_count: u32,
queue_depth: u32,
split_count: u32,
query_time: i64,
storage_device_number: u32,
storage_manager_name: [u16; 8],
}
extern "system" {
fn CreateFileW(
lp_file_name: *const u16,
dw_desired_access: u32,
dw_share_mode: u32,
lp_security_attributes: *mut c_void,
dw_creation_disposition: u32,
dw_flags_and_attributes: u32,
h_template_file: HANDLE,
) -> HANDLE;
fn DeviceIoControl(
h_device: HANDLE,
dw_io_control_code: u32,
lp_in_buffer: *const c_void,
n_in_buffer_size: u32,
lp_out_buffer: *mut c_void,
n_out_buffer_size: u32,
lp_bytes_returned: *mut u32,
lp_overlapped: *mut c_void,
) -> i32;
fn CloseHandle(h_object: HANDLE) -> i32;
}
pub fn sample_physical_drives() -> Vec<IoCounters> {
(0..crate::disk::MAX_PHYSICAL_DRIVES)
.filter_map(query_drive_counters)
.collect()
}
fn query_drive_counters(index: u32) -> Option<IoCounters> {
let path = format!(r"\\.\PhysicalDrive{index}");
let path_w: Vec<u16> = OsStr::new(&path).encode_wide().chain(Some(0)).collect();
let handle = unsafe {
CreateFileW(
path_w.as_ptr(),
0,
FILE_SHARE_READ | FILE_SHARE_WRITE,
ptr::null_mut(),
OPEN_EXISTING,
0,
ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE || handle.is_null() {
return None;
}
let mut perf = DiskPerformance::default();
let mut returned: u32 = 0;
let ok = unsafe {
DeviceIoControl(
handle,
IOCTL_DISK_PERFORMANCE,
ptr::null(),
0,
&mut perf as *mut _ as *mut c_void,
size_of::<DiskPerformance>() as u32,
&mut returned,
ptr::null_mut(),
)
};
unsafe {
CloseHandle(handle);
}
if ok == 0 || (returned as usize) < size_of::<DiskPerformance>() {
return None;
}
Some(IoCounters {
device: physical_drive_name(index),
read: perf.bytes_read.max(0) as u64,
write: perf.bytes_written.max(0) as u64,
})
}
pub fn sample_interfaces() -> Vec<IoCounters> {
crate::win_iftable::interfaces()
.into_iter()
.map(|row| IoCounters {
device: row.name,
read: row.in_octets,
write: row.out_octets,
})
.collect()
}
#[cfg(test)]
mod layout {
use std::mem::{offset_of, size_of};
#[test]
fn ffi_struct_layout() {
assert_eq!(size_of::<super::DiskPerformance>(), 88);
assert_eq!(offset_of!(super::DiskPerformance, bytes_read), 0);
assert_eq!(offset_of!(super::DiskPerformance, bytes_written), 8);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const DISKSTATS: &str = "\
259 0 nvme0n1 881658 6545 23259904 398678 110634 278 3327562 320368 0 53695 721779 6070 0 2002840 1798 2314 934
259 1 nvme0n1p1 338 1067 10262 173 2 0 2 0 0 24 173 0 0 0 0 0 0
259 2 nvme0n1p2 289 12 7954 45 22 17 288 6 0 42 51 0 0 0 0 0 0
259 3 nvme0n1p3 880938 5466 23238992 398446 110607 261 3327272 320361 0 65541 720606 6070 0 2002840 1798 0 0
251 0 zram0 46534 0 381008 231 252558 0 2769832 3263 0 5270 3494 0 0 0 0 0 0
";
#[test]
fn test_parse_diskstats_reads_the_sector_columns() {
let parsed = parse_diskstats(DISKSTATS, |n| n == "nvme0n1");
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].read, 23_259_904 * 512);
assert_eq!(parsed[0].write, 3_327_562 * 512);
}
#[test]
fn test_parse_diskstats_honors_the_injected_filter() {
let all = parse_diskstats(DISKSTATS, |_| true);
assert_eq!(all.len(), 5);
let whole = parse_diskstats(DISKSTATS, |n| !n.starts_with("zram") && !n.contains('p'));
assert_eq!(
whole.iter().map(|c| c.device.as_str()).collect::<Vec<_>>(),
vec!["nvme0n1"]
);
}
#[test]
fn test_parse_diskstats_skips_malformed_lines() {
let content = "259 0 nvme0n1 1 2\n259 0 sda 1 2 x 4 5 6 notanumber 8 9 10\n";
assert!(parse_diskstats(content, |_| true).is_empty());
}
#[test]
fn test_compute_rates_divides_the_delta_by_the_window() {
let before = vec![IoCounters {
device: "nvme0n1".into(),
read: 1_000,
write: 2_000,
}];
let after = vec![IoCounters {
device: "nvme0n1".into(),
read: 3_000,
write: 2_000,
}];
let rates = compute_rates(&before, &after, 0.5);
assert_eq!(rates.len(), 1);
assert_eq!(rates[0].read, 4_000.0);
assert_eq!(rates[0].write, 0.0);
}
#[test]
fn test_compute_rates_drops_devices_missing_from_either_sample() {
let before = vec![IoCounters {
device: "eth0".into(),
read: 10,
write: 10,
}];
let after = vec![
IoCounters {
device: "eth0".into(),
read: 20,
write: 10,
},
IoCounters {
device: "wt0".into(),
read: 9_999_999,
write: 9_999_999,
},
];
let rates = compute_rates(&before, &after, 1.0);
assert_eq!(rates.len(), 1);
assert_eq!(rates[0].device, "eth0");
}
#[test]
fn test_compute_rates_clamps_a_counter_reset_to_zero() {
let before = vec![
IoCounters {
device: "wlan0".into(),
read: 5_000_000,
write: 5_000_000,
},
IoCounters {
device: "eth0".into(),
read: 1_000,
write: 1_000,
},
];
let after = vec![
IoCounters {
device: "wlan0".into(),
read: 1_024,
write: 0,
},
IoCounters {
device: "eth0".into(),
read: 3_000,
write: 1_000,
},
];
let rates = compute_rates(&before, &after, 1.0);
assert_eq!(rates[0].device, "wlan0");
assert_eq!(rates[0].read, 0.0);
assert_eq!(rates[0].write, 0.0);
assert_eq!(rates[1].device, "eth0");
assert_eq!(rates[1].read, 2_000.0);
}
#[test]
fn test_compute_rates_refuses_a_zero_or_negative_window() {
let sample = vec![IoCounters {
device: "nvme0n1".into(),
read: 1,
write: 1,
}];
assert!(compute_rates(&sample, &sample, 0.0).is_empty());
assert!(compute_rates(&sample, &sample, -1.0).is_empty());
assert!(compute_rates(&sample, &sample, f64::NAN).is_empty());
}
#[test]
fn test_format_rate_matches_the_net_field_units() {
assert_eq!(format_rate(0.0), "0 B/s");
assert_eq!(format_rate(512.0), "512 B/s");
assert_eq!(format_rate(1024.0), "1.0 KB/s");
assert_eq!(format_rate(1024.0 * 1024.0 * 1.5), "1.5 MB/s");
assert_eq!(format_rate(f64::NAN), "0 B/s");
assert_eq!(format_rate(-1.0), "0 B/s");
}
#[test]
fn test_format_io_line() {
let rate = IoRate {
device: "nvme0n1".into(),
read: 0.0,
write: 1024.0 * 308.0,
};
assert_eq!(
format_io_line(&rate, "R", "W"),
"nvme0n1 R: 0 B/s W: 308.0 KB/s"
);
}
#[test]
fn test_select_net_rates_prefers_the_active_interface() {
let rates = vec![
IoRate {
device: "wlp0s20f3".into(),
read: 100.0,
write: 50.0,
},
IoRate {
device: "wt0".into(),
read: 10.0,
write: 10.0,
},
];
let selected = select_net_rates(rates, Some("wlp0s20f3"));
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].device, "wlp0s20f3");
}
#[test]
fn test_select_net_rates_keeps_an_idle_active_interface() {
let rates = vec![IoRate {
device: "eth0".into(),
read: 0.0,
write: 0.0,
}];
let selected = select_net_rates(rates, Some("eth0"));
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].device, "eth0");
}
#[test]
fn test_physical_drive_name_matches_the_device_path() {
assert_eq!(physical_drive_name(0), "PhysicalDrive0");
assert_eq!(physical_drive_name(31), "PhysicalDrive31");
}
#[test]
fn test_select_net_rates_falls_back_to_busy_interfaces() {
let rates = vec![
IoRate {
device: "eth0".into(),
read: 0.0,
write: 0.0,
},
IoRate {
device: "wt0".into(),
read: 1.0,
write: 0.0,
},
];
let selected = select_net_rates(rates.clone(), None);
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].device, "wt0");
let selected = select_net_rates(rates, Some("ppp0"));
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].device, "wt0");
}
}