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