use dragonfly_client_core::Result;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};
use tokio::sync::Mutex;
use tracing::debug;
#[derive(Debug, Clone, Default)]
pub struct DiskStats {
pub total: u64,
pub free: u64,
pub usage: u64,
pub used_percent: f64,
}
#[derive(Debug, Clone, Default)]
pub struct ProcessDiskStats {
pub write_bandwidth: u64,
pub read_bandwidth: u64,
}
#[derive(Debug, Clone, Default)]
pub struct CgroupDiskStats {
pub write_bandwidth: u64,
pub read_bandwidth: u64,
}
#[derive(Debug, Clone, Default)]
pub struct Disk {
mutex: Arc<Mutex<()>>,
}
impl Disk {
const DEFAULT_DISK_REFRESH_INTERVAL: Duration = Duration::from_secs(1);
pub fn new() -> Self {
Self {
mutex: Arc::new(Mutex::new(())),
}
}
pub fn get_stats(&self, path: &Path) -> Result<DiskStats> {
let stats = fs2::statvfs(path)?;
let total_space = stats.total_space();
let available_space = stats.available_space();
let usage_space = total_space - available_space;
let used_percent = (usage_space as f64 / (total_space) as f64) * 100.0;
debug!(
"disk total space: {} bytes, available space: {} bytes, usage space: {} bytes, used percent: {}%",
total_space, available_space, usage_space, used_percent
);
Ok(DiskStats {
total: total_space,
free: available_space,
usage: usage_space,
used_percent: used_percent.clamp(0.0, 100.0),
})
}
pub async fn get_process_stats(&self, pid: u32) -> ProcessDiskStats {
let _guard = self.mutex.lock().await;
let mut sys = System::new();
sys.refresh_processes_specifics(
ProcessesToUpdate::Some(&[Pid::from_u32(pid)]),
false,
ProcessRefreshKind::nothing().with_disk_usage(),
);
tokio::time::sleep(Self::DEFAULT_DISK_REFRESH_INTERVAL).await;
sys.refresh_processes_specifics(
ProcessesToUpdate::Some(&[Pid::from_u32(pid)]),
false,
ProcessRefreshKind::nothing().with_disk_usage(),
);
let disk_usage = sys.process(Pid::from_u32(pid)).unwrap().disk_usage();
let write_bandwidth =
disk_usage.written_bytes / Self::DEFAULT_DISK_REFRESH_INTERVAL.as_secs();
let read_bandwidth = disk_usage.read_bytes / Self::DEFAULT_DISK_REFRESH_INTERVAL.as_secs();
debug!(
"process {} disk write bandwidth: {} bytes/s, read bandwidth: {} bytes/s",
pid, write_bandwidth, read_bandwidth
);
ProcessDiskStats {
write_bandwidth,
read_bandwidth,
}
}
#[allow(unused_variables)]
pub async fn get_cgroup_stats(&self, pid: u32) -> Option<CgroupDiskStats> {
#[cfg(target_os = "linux")]
{
let _guard = self.mutex.lock().await;
let (baseline_read_bytes, baseline_written_bytes) = Self::get_cgroup_io_counters(pid)?;
tokio::time::sleep(Self::DEFAULT_DISK_REFRESH_INTERVAL).await;
let (read_bytes, written_bytes) = Self::get_cgroup_io_counters(pid)?;
let write_bandwidth = (written_bytes.saturating_sub(baseline_written_bytes) as f64
/ Self::DEFAULT_DISK_REFRESH_INTERVAL.as_secs_f64())
.round() as u64;
let read_bandwidth = (read_bytes.saturating_sub(baseline_read_bytes) as f64
/ Self::DEFAULT_DISK_REFRESH_INTERVAL.as_secs_f64())
.round() as u64;
debug!(
"process {} cgroup disk write bandwidth: {} bytes/s, read bandwidth: {} bytes/s",
pid, write_bandwidth, read_bandwidth
);
Some(CgroupDiskStats {
write_bandwidth,
read_bandwidth,
})
}
#[cfg(not(target_os = "linux"))]
None
}
#[cfg(target_os = "linux")]
fn get_cgroup_io_counters(pid: u32) -> Option<(u64, u64)> {
use cgroups_rs::fs::hierarchies;
if hierarchies::auto().v2() {
Self::get_cgroup_v2_io_counters(pid)
} else {
Self::get_cgroup_v1_io_counters(pid)
}
}
#[cfg(target_os = "linux")]
fn get_cgroup_v2_io_counters(pid: u32) -> Option<(u64, u64)> {
use crate::cgroups::get_cgroup_v2_path_by_pid;
use tracing::error;
let path = match get_cgroup_v2_path_by_pid(pid) {
Ok(path) => path.join("io.stat"),
Err(err) => {
error!("failed to get cgroup v2 path for pid {}: {}", pid, err);
return None;
}
};
match std::fs::read_to_string(&path) {
Ok(content) => Some(Self::parse_cgroup_v2_io_stat(&content)),
Err(err) => {
error!("failed to read {}: {}", path.display(), err);
None
}
}
}
#[cfg(target_os = "linux")]
fn parse_cgroup_v2_io_stat(content: &str) -> (u64, u64) {
let (mut read_bytes, mut written_bytes) = (0u64, 0u64);
for part in content.split_whitespace() {
if let Some(value) = part.strip_prefix("rbytes=") {
read_bytes = read_bytes.saturating_add(value.parse().unwrap_or(0));
} else if let Some(value) = part.strip_prefix("wbytes=") {
written_bytes = written_bytes.saturating_add(value.parse().unwrap_or(0));
}
}
(read_bytes, written_bytes)
}
#[cfg(target_os = "linux")]
fn get_cgroup_v1_io_counters(pid: u32) -> Option<(u64, u64)> {
use crate::cgroups::get_cgroup_by_pid;
use cgroups_rs::fs::blkio::BlkIoController;
use tracing::error;
let cgroup = match get_cgroup_by_pid(pid) {
Ok(cgroup) => cgroup,
Err(err) => {
error!("failed to get cgroup for pid {}: {}", pid, err);
return None;
}
};
let Some(blkio_controller) = cgroup.controller_of::<BlkIoController>() else {
error!("no blkio controller found for pid {}", pid);
return None;
};
let blkio = blkio_controller.blkio();
let io_service_bytes = if !blkio.throttle.io_service_bytes.is_empty() {
blkio.throttle.io_service_bytes
} else {
blkio.io_service_bytes
};
Some(Self::parse_cgroup_v1_io_stat(&io_service_bytes))
}
#[cfg(target_os = "linux")]
fn parse_cgroup_v1_io_stat(
io_service_bytes: &[cgroups_rs::fs::blkio::IoService],
) -> (u64, u64) {
io_service_bytes
.iter()
.fold((0u64, 0u64), |(read_bytes, written_bytes), io_service| {
(
read_bytes.saturating_add(io_service.read),
written_bytes.saturating_add(io_service.write),
)
})
}
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;
#[test]
fn test_parse_cgroup_v2_io_stat() {
let content = "8:0 rbytes=90430464 wbytes=299008000 rios=8950 wios=1252 dbytes=50331648 dios=3021\n253:0 rbytes=1459200 wbytes=314773504 rios=192 wios=353 dbytes=0 dios=0";
assert_eq!(
Disk::parse_cgroup_v2_io_stat(content),
(90430464 + 1459200, 299008000 + 314773504)
);
assert_eq!(Disk::parse_cgroup_v2_io_stat(""), (0, 0));
}
#[test]
fn test_parse_cgroup_v1_io_stat() {
use cgroups_rs::fs::blkio::IoService;
let io_service_bytes = vec![
IoService {
major: 8,
minor: 0,
read: 90430464,
write: 299008000,
..Default::default()
},
IoService {
major: 253,
minor: 0,
read: 1459200,
write: 314773504,
..Default::default()
},
];
assert_eq!(
Disk::parse_cgroup_v1_io_stat(&io_service_bytes),
(90430464 + 1459200, 299008000 + 314773504)
);
assert_eq!(Disk::parse_cgroup_v1_io_stat(&[]), (0, 0));
}
}