iotree 0.1.2

A command-line tool to monitor disk I/O statistics in a tree view

use std::collections::HashMap;

const SECTOR_SIZE : u64 = 512;
const MICROSECONDS_IN_MILLISECOND : u64 = 1000;

pub struct DiskStatsReport {
    devices: HashMap<String, DeviceDiskStats>,
}

pub struct IoStatsReport {
    pub devices: HashMap<String, DeviceIoStats>,
}
    

impl DiskStatsReport {
    pub fn from_output(output: &str) -> DiskStatsReport {
        let mut devices = HashMap::new();
        output.lines().skip(1).for_each(|line| {
            let stats = DeviceDiskStats::from_line(line);
            devices.insert(format!("{}:{}", stats.major, stats.minor), stats);
        });
        
        DiskStatsReport { devices }
    }

    pub fn delta(&self, other: &DiskStatsReport, delta_secs: f64) -> IoStatsReport {
        let mut devices = HashMap::new();
        for (device, stats) in self.devices.iter() {
            if let Some(other_stats) = other.devices.get(device) {
                if let Some(io_stats) = stats.delta(other_stats, delta_secs) {
                    devices.insert(device.clone(), io_stats);
                }
            }
        }
        IoStatsReport { devices }
    }
}



pub struct DeviceDiskStats {
    major: i32,
    minor: i32,
    name: String,
    read_ios: u64,
    read_merges: u64,
    read_sectors: u64,
    read_ticks: u64,
    write_ios: u64,
    write_merges: u64,
    write_sectors: u64,
    write_ticks: u64,
    inflight : u64,
    io_ticks: u64,
    weighted_io_ticks: u64,
    discard_ios: u64,
    discard_merges: u64,
    discard_sectors: u64,
    discard_ticks: u64,
}

#[derive(Debug)]
pub struct DeviceIoStats {
    pub read_ios: f64,
    pub read_bw : f64,
    pub read_io_size : f64,
    pub read_latency : f64,    
    pub write_ios: f64,
    pub write_bw : f64,
    pub write_io_size : f64,
    pub write_latency : f64,
    pub discard_ios: f64,
    pub discard_bw : f64,
    pub discard_io_size : f64,
    pub discard_latency : f64,
    pub queue_depth : f64,
}

impl DeviceDiskStats {
    pub fn from_line(line: &str) -> DeviceDiskStats {
        let mut parts = line.split_whitespace();
        let major = parts.next().unwrap().parse::<i32>().unwrap();
        let minor = parts.next().unwrap().parse::<i32>().unwrap();
        let name = parts.next().unwrap().to_string();
        let read_ios = parts.next().unwrap().parse::<u64>().unwrap();
        let read_merges = parts.next().unwrap().parse::<u64>().unwrap();
        let read_sectors = parts.next().unwrap().parse::<u64>().unwrap();
        let read_ticks = parts.next().unwrap().parse::<u64>().unwrap();
        let write_ios = parts.next().unwrap().parse::<u64>().unwrap();
        let write_merges = parts.next().unwrap().parse::<u64>().unwrap();
        let write_sectors = parts.next().unwrap().parse::<u64>().unwrap();
        let write_ticks = parts.next().unwrap().parse::<u64>().unwrap();
        let inflight = parts.next().unwrap().parse::<u64>().unwrap();
        let io_ticks = parts.next().unwrap().parse::<u64>().unwrap();
        let weighted_io_ticks = parts.next().unwrap().parse::<u64>().unwrap();

        // Parse discard fields if they exist (newer kernel versions)
        let mut discard_ios = 0;
        let mut discard_merges = 0;
        let mut discard_sectors = 0;
        let mut discard_ticks = 0;
        
        // Check if we have discard fields
        if let Some(val) = parts.next() {
            discard_ios = val.parse::<u64>().unwrap_or(0);
            discard_merges = parts.next().unwrap_or("0").parse::<u64>().unwrap_or(0);
            discard_sectors = parts.next().unwrap_or("0").parse::<u64>().unwrap_or(0);
            discard_ticks = parts.next().unwrap_or("0").parse::<u64>().unwrap_or(0);
        }
        DeviceDiskStats {
            major,
            minor,
            name,
            read_ios,
            read_merges,
            read_sectors,
            read_ticks,
            write_ios,
            write_merges,
            write_sectors,
            write_ticks,
            inflight,
            io_ticks,
            weighted_io_ticks,
            discard_ios,
            discard_merges,
            discard_sectors,
            discard_ticks,
        }
    }

    pub fn delta(&self, other: &DeviceDiskStats, delta_secs: f64) -> Option<DeviceIoStats> {
        if self.major != other.major || self.minor != other.minor {
            return None;
        }

        let mut stats = DeviceIoStats {
            read_ios: ((self.read_ios - other.read_ios) as f64) / delta_secs,
            write_ios: ((self.write_ios - other.write_ios) as f64) / delta_secs,
            discard_ios: ((self.discard_ios - other.discard_ios) as f64) / delta_secs,
            read_latency: 0.0,
            read_io_size: 0.0,
            read_bw: 0.0,
            write_latency: 0.0,
            write_io_size: 0.0,
            write_bw: 0.0,
            discard_latency: 0.0,
            discard_io_size: 0.0,
            discard_bw: 0.0,
            queue_depth: 0.0,
        };

        if stats.read_ios > 0.0 {
            let read_time = self.read_ticks - other.read_ticks;            
            stats.read_latency = (read_time as f64 / stats.read_ios) * MICROSECONDS_IN_MILLISECOND as f64 / delta_secs;
            stats.read_bw = (((self.read_sectors - other.read_sectors) * SECTOR_SIZE) as f64) / delta_secs;
            stats.read_io_size = stats.read_bw / stats.read_ios;
        }

        if stats.write_ios > 0.0 {
            let write_time = self.write_ticks - other.write_ticks;
            stats.write_latency = (write_time as f64 / stats.write_ios) * MICROSECONDS_IN_MILLISECOND as f64 / delta_secs;
            stats.write_bw = (((self.write_sectors - other.write_sectors) * SECTOR_SIZE) as f64) / delta_secs;
            stats.write_io_size = stats.write_bw / stats.write_ios;
        }
        
        if stats.discard_ios > 0.0 {
            let discard_time = self.discard_ticks - other.discard_ticks;
            stats.discard_latency = (discard_time as f64 / stats.discard_ios) * MICROSECONDS_IN_MILLISECOND as f64 / delta_secs;
            stats.discard_bw = (((self.discard_sectors - other.discard_sectors) * SECTOR_SIZE) as f64) / delta_secs;
            stats.discard_io_size = stats.discard_bw / stats.discard_ios;
        }

        stats.queue_depth = (self.inflight + other.inflight) as f64 / 2.0;

        Some(stats)
    }
}