rigtop_core/linux/
diskstats.rs1use std::collections::HashMap;
8use std::fs;
9use std::time::Instant;
10
11use super::{SysRoots, sorted_entries};
12use crate::snapshot::DiskIo;
13
14const SECTOR_BYTES: f64 = 512.0;
17
18#[derive(Debug, Clone, Copy, Default, PartialEq)]
20pub(crate) struct Counters {
21 sectors_read: u64,
22 sectors_written: u64,
23 io_ticks_ms: u64,
25}
26
27#[derive(Debug, Default)]
29pub struct DiskStatsMeter {
30 previous: HashMap<String, Counters>,
31 last_sample: Option<Instant>,
32}
33
34impl DiskStatsMeter {
35 pub fn new() -> Self {
37 Self::default()
38 }
39
40 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
83fn is_virtual(name: &str) -> bool {
85 name.starts_with("loop") || name.starts_with("ram")
86}
87
88pub(crate) fn parse(text: &str) -> HashMap<String, Counters> {
90 text.lines()
91 .filter_map(|line| {
92 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 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 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}