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 requests: u64,
26 request_ms: u64,
27 io_ticks_ms: u64,
29}
30
31#[derive(Debug, Default)]
33pub struct DiskStatsMeter {
34 previous: HashMap<String, Counters>,
35 last_sample: Option<Instant>,
36}
37
38impl DiskStatsMeter {
39 pub fn new() -> Self {
41 Self::default()
42 }
43
44 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
92fn is_virtual(name: &str) -> bool {
94 name.starts_with("loop") || name.starts_with("ram")
95}
96
97pub(crate) fn parse(text: &str) -> HashMap<String, Counters> {
99 text.lines()
100 .filter_map(|line| {
101 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 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 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 let third = meter.sample(&roots, t0 + Duration::from_millis(1000));
187 assert_eq!(third[0].latency_ms, None);
188 }
189}