Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

System wide disk I/O statistics #683

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions Cargo.toml
Expand Up @@ -31,6 +31,9 @@ libc = "^0.2.112"
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
core-foundation-sys = "0.8"

[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
procfs = "0.12"

[target.'cfg(all(target_os = "linux", not(target_os = "android")))'.dev-dependencies]
tempfile = "3.2"

Expand Down
10 changes: 9 additions & 1 deletion src/apple/disk.rs
@@ -1,7 +1,7 @@
// Take a look at the license at the top of the repository in the LICENSE file.

use crate::utils::to_cpath;
use crate::{DiskExt, DiskType};
use crate::{DiskExt, DiskType, DiskUsageExt};

#[cfg(target_os = "macos")]
pub(crate) use crate::sys::inner::disk::*;
Expand Down Expand Up @@ -51,6 +51,14 @@ impl DiskExt for Disk {
self.is_removable
}

fn usage(&self) -> DiskUsageExt {
todo!()
}

fn refresh_usage(&mut self) -> bool {
todo!()
}

fn refresh(&mut self) -> bool {
unsafe {
let mut stat: statfs = mem::zeroed();
Expand Down
54 changes: 54 additions & 0 deletions src/common.rs
Expand Up @@ -688,6 +688,60 @@ pub struct DiskUsage {
pub read_bytes: u64,
}

/// Type containing read and written bytes plus read and written number of operations.
///
/// It is returned by [`DiskExt::usage`][crate::DiskExt::usage].
///
/// ```no_run
/// use sysinfo::{DiskExt, System, SystemExt};
///
/// let mut s = System::new_all();
/// s.refresh_disks_list();
/// s.refresh_disks_usage();
/// for disk in s.disks() {
/// let disk_usage = disk.usage();
/// println!("[{:?}] read bytes : new/total => {}/{} B",
/// disk.name(),
/// disk_usage.read_bytes,
/// disk_usage.total_read_bytes,
/// );
/// println!("[{:?}] written bytes: new/total => {}/{} B",
/// disk.name(),
/// disk_usage.written_bytes,
/// disk_usage.total_written_bytes,
/// );
/// println!("[{:?}] read ops : new/total => {}/{}",
/// disk.name(),
/// disk_usage.read_ops,
/// disk_usage.total_read_ops,
/// );
/// println!("[{:?}] written ops: new/total => {}/{}",
/// disk.name(),
/// disk_usage.written_ops,
/// disk_usage.total_written_ops,
/// );
/// }
/// ```
#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)]
pub struct DiskUsageExt {
/// Total number of written bytes.
pub total_written_bytes: u64,
/// Number of written bytes since the last refresh.
pub written_bytes: u64,
/// Total number of read bytes.
pub total_read_bytes: u64,
/// Number of read bytes since the last refresh.
pub read_bytes: u64,
/// Total number of written ops.
pub total_written_ops: u64,
/// Number of written ops since the last refresh.
pub written_ops: u64,
/// Total number of read ops.
pub total_read_ops: u64,
/// Number of read ops since the last refresh.
pub read_ops: u64,
}

/// Enum describing the different status of a process.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ProcessStatus {
Expand Down
10 changes: 9 additions & 1 deletion src/freebsd/disk.rs
@@ -1,6 +1,6 @@
// Take a look at the license at the top of the repository in the LICENSE file.

use crate::{DiskExt, DiskType};
use crate::{DiskExt, DiskType, DiskUsageExt};

use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -47,6 +47,14 @@ impl DiskExt for Disk {
self.is_removable
}

fn usage(&self) -> DiskUsageExt {
todo!()
}

fn refresh_usage(&mut self) -> bool {
todo!()
}

fn refresh(&mut self) -> bool {
unsafe {
let mut vfs: libc::statvfs = std::mem::zeroed();
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Expand Up @@ -59,7 +59,7 @@ cfg_if::cfg_if! {
}

pub use common::{
get_current_pid, DiskType, DiskUsage, Gid, LoadAvg, NetworksIter, Pid, PidExt,
get_current_pid, DiskType, DiskUsage, DiskUsageExt, Gid, LoadAvg, NetworksIter, Pid, PidExt,
ProcessRefreshKind, ProcessStatus, RefreshKind, Signal, Uid, User,
};
pub use sys::{Component, Disk, NetworkData, Networks, Process, Processor, System};
Expand Down
97 changes: 96 additions & 1 deletion src/linux/disk.rs
@@ -1,16 +1,19 @@
// Take a look at the license at the top of the repository in the LICENSE file.

use crate::sys::utils::get_all_data;
use crate::{utils, DiskExt, DiskType};
use crate::{utils, DiskExt, DiskType, DiskUsageExt};

use libc::statvfs;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::io::{BufRead, BufReader};
use std::mem;
use std::num::Wrapping;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};

const SECTOR_SIZE: u64 = 512;

macro_rules! cast {
($x:expr) => {
u64::from($x)
Expand All @@ -21,12 +24,43 @@ macro_rules! cast {
#[derive(PartialEq)]
pub struct Disk {
type_: DiskType,

device_name: OsString,

#[doc(hidden)]
pub(crate) actual_device_name: String,

file_system: Vec<u8>,
mount_point: PathBuf,
total_space: u64,
available_space: u64,
is_removable: bool,

old_read_bytes: u64,
old_written_bytes: u64,
read_bytes: u64,
written_bytes: u64,

old_read_ops: u64,
old_written_ops: u64,
read_ops: u64,
written_ops: u64,
}

impl Disk {
#[inline]
pub(crate) fn update_disk_stats(&mut self, stat: &procfs::DiskStat) {
self.old_read_bytes = self.read_bytes;
self.old_written_bytes = self.written_bytes;
self.old_read_ops = self.read_ops;
self.old_written_ops = self.written_ops;

self.read_ops = stat.reads + stat.merged;
self.written_ops = stat.writes + stat.writes_merged;

self.read_bytes = stat.sectors_read * SECTOR_SIZE;
self.written_bytes = stat.sectors_written * SECTOR_SIZE;
}
}

impl DiskExt for Disk {
Expand Down Expand Up @@ -58,6 +92,47 @@ impl DiskExt for Disk {
self.is_removable
}

fn usage(&self) -> DiskUsageExt {
DiskUsageExt {
written_bytes: self.written_bytes - self.old_written_bytes,
total_written_bytes: self.written_bytes,
read_bytes: self.read_bytes - self.old_read_bytes,
total_read_bytes: self.read_bytes,
written_ops: self.written_ops - self.old_written_ops,
total_written_ops: self.written_ops,
read_ops: self.read_ops - self.old_read_ops,
total_read_ops: self.read_ops,
}
}

fn refresh_usage(&mut self) -> bool {
#[inline]
fn refresh_usage(disk: &mut Disk) -> Result<(), std::io::Error> {
for line in BufReader::new(fs::File::open("/proc/diskstats")?).lines() {
let line = line?;

let stat = match procfs::DiskStat::from_line(&line) {
Ok(stat) => stat,
Err(procfs::ProcError::Io(err, _)) => return Err(err),
Err(err) => return Err(std::io::Error::new(std::io::ErrorKind::Other, err)),
};

if stat.name != disk.actual_device_name {
continue;
}

disk.update_disk_stats(&stat);

return Ok(());
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Device not found",
))
}
refresh_usage(self).is_ok()
}

fn refresh(&mut self) -> bool {
unsafe {
let mut stat: statvfs = mem::zeroed();
Expand All @@ -73,6 +148,15 @@ impl DiskExt for Disk {
}
}

// FIXME: I think this may be completely incorrect, in many different ways.
fn find_device_name(device_name: &OsStr) -> String {
device_name
.to_string_lossy()
.strip_prefix("/dev/")
.map(|s| s.to_string())
.unwrap_or_else(|| device_name.to_string_lossy().into_owned())
}

fn new_disk(
device_name: &OsStr,
mount_point: &Path,
Expand Down Expand Up @@ -102,12 +186,23 @@ fn new_disk(
.any(|e| e.as_os_str() == device_name);
Some(Disk {
type_,
actual_device_name: find_device_name(device_name),
device_name: device_name.to_owned(),
file_system: file_system.to_owned(),
mount_point,
total_space: cast!(total),
available_space: cast!(available),
is_removable,

old_read_bytes: 0,
old_written_bytes: 0,
read_bytes: 0,
written_bytes: 0,

old_read_ops: 0,
old_written_ops: 0,
read_ops: 0,
written_ops: 0,
})
}

Expand Down
35 changes: 35 additions & 0 deletions src/linux/system.rs
Expand Up @@ -529,6 +529,41 @@ impl SystemExt for System {
&mut self.disks
}

// We reimplement this for efficiency
// Refreshing each disk individually is an O(n ^ 2) operation, but this implementation is O(n)
fn refresh_disks_usage(&mut self) {
if self.disks.is_empty() {
return;
}

#[inline]
fn refresh_usage<S: SystemExt>(disk: &mut S) -> Result<(), std::io::Error> {
for line in BufReader::new(std::fs::File::open("/proc/diskstats")?).lines() {
let line = line?;

let stat = match procfs::DiskStat::from_line(&line) {
Ok(stat) => stat,
Err(procfs::ProcError::Io(err, _)) => return Err(err),
Err(err) => return Err(std::io::Error::new(std::io::ErrorKind::Other, err)),
};

let disk = match disk
.disks_mut()
.iter_mut()
.find(|disk| disk.actual_device_name == stat.name)
{
Some(disk) => disk,
None => continue,
};

disk.update_disk_stats(&stat);
}

Ok(())
}
let _ = refresh_usage(self);
}

fn uptime(&self) -> u64 {
let content = get_all_data("/proc/uptime", 50).unwrap_or_default();
content
Expand Down
2 changes: 1 addition & 1 deletion src/sysinfo.h
Expand Up @@ -18,7 +18,7 @@ void sysinfo_refresh_processes(CSystem system);
void sysinfo_refresh_process(CSystem system, pid_t pid);
#endif
void sysinfo_refresh_disks(CSystem system);
void sysinfo_refresh_disk_list(CSystem system);
void sysinfo_refresh_disks_list(CSystem system);
size_t sysinfo_get_total_memory(CSystem system);
size_t sysinfo_get_free_memory(CSystem system);
size_t sysinfo_get_used_memory(CSystem system);
Expand Down