1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
//! Disk I/O sensor.
use super::data::HISTORY_SIZE;
use super::Sensor;
use std::collections::VecDeque;
use std::fs;
use std::time::Instant;
/// Disk I/O sensor that reads from /proc/diskstats.
pub struct DiskSensor {
name: String,
device: String,
last_read_sectors: u64,
last_write_sectors: u64,
last_time: Option<Instant>,
last_read_rate: f64,
last_write_rate: f64,
/// History of combined I/O rates (bytes/sec)
history: VecDeque<f64>,
/// History of read rates (bytes/sec)
read_history: VecDeque<f64>,
/// History of write rates (bytes/sec)
write_history: VecDeque<f64>,
}
impl DiskSensor {
/// Creates a new disk sensor for a specific device (e.g., "sda", "nvme0n1").
pub fn new(device: &str) -> Self {
Self {
name: format!("disk_{}", device),
device: device.to_string(),
last_read_sectors: 0,
last_write_sectors: 0,
last_time: None,
last_read_rate: 0.0,
last_write_rate: 0.0,
history: VecDeque::with_capacity(HISTORY_SIZE),
read_history: VecDeque::with_capacity(HISTORY_SIZE),
write_history: VecDeque::with_capacity(HISTORY_SIZE),
}
}
/// Creates a disk sensor that auto-detects the primary disk.
pub fn auto() -> Self {
// Try to find the primary disk
let device = Self::detect_primary_disk().unwrap_or_else(|| "sda".to_string());
Self::new(&device)
}
/// Detects the primary disk device.
fn detect_primary_disk() -> Option<String> {
// Try common disk device names in order of preference
let candidates = ["nvme0n1", "sda", "vda", "xvda", "mmcblk0"];
for candidate in candidates {
let path = format!("/sys/block/{}", candidate);
if std::path::Path::new(&path).exists() {
return Some(candidate.to_string());
}
}
None
}
/// Reads disk stats from /proc/diskstats.
///
/// Format: https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
/// Fields: major minor name reads_completed reads_merged sectors_read time_reading
/// writes_completed writes_merged sectors_written time_writing
/// ios_in_progress time_doing_io weighted_time_doing_io
fn read_stats(&self) -> Option<(u64, u64)> {
let content = fs::read_to_string("/proc/diskstats").ok()?;
for line in content.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 10 && parts[2] == self.device {
// sectors_read is field 5 (0-indexed)
// sectors_written is field 9 (0-indexed)
let read_sectors: u64 = parts[5].parse().ok()?;
let write_sectors: u64 = parts[9].parse().ok()?;
return Some((read_sectors, write_sectors));
}
}
None
}
/// Returns the current read rate in bytes/second.
pub fn read_rate(&self) -> f64 {
self.last_read_rate
}
/// Returns the current write rate in bytes/second.
pub fn write_rate(&self) -> f64 {
self.last_write_rate
}
/// Returns the I/O history (combined read+write rates).
pub fn history(&self) -> &VecDeque<f64> {
&self.history
}
/// Returns the read rate history (bytes/sec).
pub fn read_history(&self) -> &VecDeque<f64> {
&self.read_history
}
/// Returns the write rate history (bytes/sec).
pub fn write_history(&self) -> &VecDeque<f64> {
&self.write_history
}
}
impl Sensor for DiskSensor {
fn name(&self) -> &str {
&self.name
}
fn sample(&mut self) -> f64 {
if let Some((read_sectors, write_sectors)) = self.read_stats() {
if let Some(last_time) = self.last_time {
let elapsed = last_time.elapsed().as_secs_f64();
if elapsed > 0.0 {
let read_delta = read_sectors.saturating_sub(self.last_read_sectors);
let write_delta = write_sectors.saturating_sub(self.last_write_sectors);
// Sectors are typically 512 bytes
const SECTOR_SIZE: f64 = 512.0;
self.last_read_rate = (read_delta as f64 * SECTOR_SIZE) / elapsed;
self.last_write_rate = (write_delta as f64 * SECTOR_SIZE) / elapsed;
// Record combined rate in history
let combined = self.last_read_rate + self.last_write_rate;
if self.history.len() >= HISTORY_SIZE {
self.history.pop_front();
}
self.history.push_back(combined);
// Record separate read/write histories
if self.read_history.len() >= HISTORY_SIZE {
self.read_history.pop_front();
}
self.read_history.push_back(self.last_read_rate);
if self.write_history.len() >= HISTORY_SIZE {
self.write_history.pop_front();
}
self.write_history.push_back(self.last_write_rate);
}
}
self.last_read_sectors = read_sectors;
self.last_write_sectors = write_sectors;
self.last_time = Some(Instant::now());
}
// Return combined rate in KB/s
(self.last_read_rate + self.last_write_rate) / 1024.0
}
fn min(&self) -> f64 {
0.0
}
fn max(&self) -> f64 {
1000000.0 // 1 GB/s max
}
fn unit(&self) -> &str {
"KB/s"
}
}