Skip to main content

rigtop_core/linux/
diskstats.rs

1//! Per-device disk I/O from `/proc/diskstats`.
2//!
3//! Only whole block devices listed in `/sys/block` are reported (partitions
4//! are skipped, as are loop and RAM disks). For btrfs RAID this shows the load
5//! on each member device, which per-mount statistics cannot.
6
7use std::collections::HashMap;
8use std::fs;
9use std::time::Instant;
10
11use super::{SysRoots, sorted_entries};
12use crate::snapshot::DiskIo;
13
14/// `/proc/diskstats` always counts in 512-byte sectors, whatever the device's
15/// logical block size.
16const SECTOR_BYTES: f64 = 512.0;
17
18/// Raw cumulative counters of one device.
19#[derive(Debug, Clone, Copy, Default, PartialEq)]
20pub(crate) struct Counters {
21    sectors_read: u64,
22    sectors_written: u64,
23    /// Milliseconds spent with I/O in flight.
24    io_ticks_ms: u64,
25}
26
27/// Turns successive `/proc/diskstats` readings into rates.
28#[derive(Debug, Default)]
29pub struct DiskStatsMeter {
30    previous: HashMap<String, Counters>,
31    last_sample: Option<Instant>,
32}
33
34impl DiskStatsMeter {
35    /// Creates a meter with no history.
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Reads the current counters and returns rates since the previous call.
41    /// Rates are zero on the first call.
42    pub fn sample(&mut self, roots: &SysRoots, now: Instant) -> Vec<DiskIo> {
43        let whole_disks: Vec<String> = sorted_entries(&roots.sys.join("block"))
44            .into_iter()
45            .filter(|name| !is_virtual(name))
46            .collect();
47        let text = fs::read_to_string(roots.proc.join("diskstats")).unwrap_or_default();
48        let current = parse(&text);
49        let elapsed = self
50            .last_sample
51            .map(|prev| now.saturating_duration_since(prev).as_secs_f64())
52            .filter(|secs| *secs > 0.0);
53
54        let disks = whole_disks
55            .iter()
56            .filter_map(|name| {
57                let cur = current.get(name)?;
58                let mut io = DiskIo {
59                    name: name.clone(),
60                    ..Default::default()
61                };
62                if let (Some(prev), Some(secs)) = (self.previous.get(name), elapsed) {
63                    io.read_bytes_per_sec =
64                        cur.sectors_read.saturating_sub(prev.sectors_read) as f64 * SECTOR_BYTES
65                            / secs;
66                    io.write_bytes_per_sec =
67                        cur.sectors_written.saturating_sub(prev.sectors_written) as f64
68                            * SECTOR_BYTES
69                            / secs;
70                    let busy_ms = cur.io_ticks_ms.saturating_sub(prev.io_ticks_ms) as f64;
71                    io.busy_percent = Some((busy_ms / (secs * 1000.0) * 100.0).min(100.0) as f32);
72                }
73                Some(io)
74            })
75            .collect();
76
77        self.previous = current;
78        self.last_sample = Some(now);
79        disks
80    }
81}
82
83/// Loop devices and RAM disks only add noise to the disk panel.
84fn is_virtual(name: &str) -> bool {
85    name.starts_with("loop") || name.starts_with("ram")
86}
87
88/// Parses `/proc/diskstats` into counters keyed by device name.
89pub(crate) fn parse(text: &str) -> HashMap<String, Counters> {
90    text.lines()
91        .filter_map(|line| {
92            // Fields: major minor name reads reads_merged sectors_read ms_reading
93            //         writes writes_merged sectors_written ms_writing
94            //         in_flight io_ticks ...
95            let fields: Vec<&str> = line.split_whitespace().collect();
96            if fields.len() < 13 {
97                return None;
98            }
99            let num = |i: usize| fields[i].parse::<u64>().ok();
100            Some((
101                fields[2].to_owned(),
102                Counters {
103                    sectors_read: num(5)?,
104                    sectors_written: num(9)?,
105                    io_ticks_ms: num(12)?,
106                },
107            ))
108        })
109        .collect()
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::linux::testutil::write;
116    use std::time::Duration;
117
118    const SAMPLE: &str = "\
119   7       0 loop0 14 0 34 0 0 0 0 0 0 0 0 0 0 0 0 0 0
120 259       0 nvme0n1 1000 0 2000 10 500 0 4000 20 0 100 30 0 0 0 0 0 0
121 259       1 nvme0n1p1 900 0 1800 9 400 0 3000 15 0 90 24 0 0 0 0 0 0
122";
123
124    #[test]
125    fn parse_reads_sector_and_tick_fields() {
126        let counters = parse(SAMPLE);
127        assert_eq!(
128            counters["nvme0n1"],
129            Counters {
130                sectors_read: 2000,
131                sectors_written: 4000,
132                io_ticks_ms: 100
133            }
134        );
135        assert_eq!(counters.len(), 3);
136    }
137
138    #[test]
139    fn meter_reports_whole_disks_with_rates() {
140        let tmp = tempfile::tempdir().unwrap();
141        let root = tmp.path();
142        // /sys/block lists whole disks only; partitions are absent there.
143        write(root, "sys/block/nvme0n1/size", "0\n");
144        write(root, "sys/block/loop0/size", "0\n");
145        write(root, "proc/diskstats", SAMPLE);
146        let roots = SysRoots {
147            sys: root.join("sys"),
148            proc: root.join("proc"),
149        };
150
151        let mut meter = DiskStatsMeter::new();
152        let t0 = Instant::now();
153        let first = meter.sample(&roots, t0);
154        assert_eq!(first.len(), 1);
155        assert_eq!(first[0].name, "nvme0n1");
156        assert_eq!(first[0].read_bytes_per_sec, 0.0);
157
158        // +2048 sectors read (1 MiB), +1024 written, +250 ms busy, over 0.5 s.
159        write(
160            root,
161            "proc/diskstats",
162            " 259       0 nvme0n1 1100 0 4048 10 600 0 5024 20 0 350 30 0 0 0 0 0 0\n",
163        );
164        let second = meter.sample(&roots, t0 + Duration::from_millis(500));
165        assert!((second[0].read_bytes_per_sec - 2.0 * 1024.0 * 1024.0).abs() < 1e-6);
166        assert!((second[0].write_bytes_per_sec - 1024.0 * 1024.0).abs() < 1e-6);
167        assert!((second[0].busy_percent.unwrap() - 50.0).abs() < 1e-3);
168    }
169}