gardesk/gartop / 6de6ba9

Browse files

Add per-process disk I/O and network socket stats

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
6de6ba955f475200aa76dc7089dd3267c0e23660
Parents
32386d7
Tree
9450344

4 changed files

StatusFile+-
M gartop-ipc/src/lib.rs 14 0
M gartop/src/collector/process.rs 96 17
M gartop/src/gui/app.rs 6 3
M gartop/src/gui/process_list.rs 81 15
gartop-ipc/src/lib.rsmodified
@@ -70,6 +70,10 @@ pub enum SortField {
7070
     #[default]
7171
     Cpu,
7272
     Memory,
73
+    DiskRead,
74
+    DiskWrite,
75
+    DiskTotal,
76
+    NetConnections,
7377
     Pid,
7478
     Name,
7579
 }
@@ -164,6 +168,16 @@ pub struct ProcessInfo {
164168
     pub rss: u64,
165169
     /// Virtual memory size in bytes.
166170
     pub vsize: u64,
171
+    /// Disk read bytes (cumulative).
172
+    pub io_read_bytes: u64,
173
+    /// Disk write bytes (cumulative).
174
+    pub io_write_bytes: u64,
175
+    /// Disk read rate in bytes/sec.
176
+    pub io_read_rate: f64,
177
+    /// Disk write rate in bytes/sec.
178
+    pub io_write_rate: f64,
179
+    /// Number of open network connections (TCP + UDP).
180
+    pub net_connections: u32,
167181
     /// Process state.
168182
     pub state: String,
169183
     /// User owning the process.
gartop/src/collector/process.rsmodified
@@ -2,14 +2,29 @@
22
 
33
 use crate::error::{Error, Result};
44
 use gartop_ipc::{ProcessInfo, SortField};
5
-use procfs::process::{all_processes, Process};
5
+use procfs::process::{all_processes, FDTarget, Process};
66
 use procfs::{Current, CurrentSI};
77
 use std::collections::HashMap;
8
+use std::time::Instant;
9
+
10
+/// Previous process stats for delta calculations.
11
+struct PrevProcessStats {
12
+    /// Previous CPU time (utime + stime).
13
+    cpu_time: u64,
14
+    /// Previous system total CPU time.
15
+    system_time: u64,
16
+    /// Previous I/O read bytes.
17
+    io_read_bytes: u64,
18
+    /// Previous I/O write bytes.
19
+    io_write_bytes: u64,
20
+    /// Timestamp of previous sample.
21
+    timestamp: Instant,
22
+}
823
 
9
-/// Process collector with previous CPU times for percentage calculation.
24
+/// Process collector with previous stats for rate calculations.
1025
 pub struct ProcessCollector {
11
-    /// Previous CPU times per PID for delta calculation.
12
-    prev_times: HashMap<i32, (u64, u64)>, // (utime + stime, total_system_time)
26
+    /// Previous stats per PID for delta calculation.
27
+    prev_stats: HashMap<i32, PrevProcessStats>,
1328
     /// Total memory for percentage calculation.
1429
     total_memory: u64,
1530
 }
@@ -20,7 +35,7 @@ impl ProcessCollector {
2035
         let meminfo = procfs::Meminfo::current()?;
2136
 
2237
         Ok(Self {
23
-            prev_times: HashMap::new(),
38
+            prev_stats: HashMap::new(),
2439
             total_memory: meminfo.mem_total,
2540
         })
2641
     }
@@ -31,6 +46,7 @@ impl ProcessCollector {
3146
         sort_by: SortField,
3247
         limit: Option<usize>,
3348
     ) -> Result<Vec<ProcessInfo>> {
49
+        let now = Instant::now();
3450
         let kernel_stats = procfs::KernelStats::current()?;
3551
         let total = &kernel_stats.total;
3652
         let system_total = total.user
@@ -54,14 +70,14 @@ impl ProcessCollector {
5470
             let pid = proc.pid();
5571
             current_pids.push(pid);
5672
 
57
-            match self.process_info(&proc, system_total) {
73
+            match self.process_info(&proc, system_total, now) {
5874
                 Ok(info) => processes.push(info),
5975
                 Err(_) => continue, // Skip processes we can't read
6076
             }
6177
         }
6278
 
6379
         // Clean up old entries
64
-        self.prev_times.retain(|pid, _| current_pids.contains(pid));
80
+        self.prev_stats.retain(|pid, _| current_pids.contains(pid));
6581
 
6682
         // Sort
6783
         match sort_by {
@@ -75,6 +91,26 @@ impl ProcessCollector {
7591
                     .partial_cmp(&a.memory_percent)
7692
                     .unwrap_or(std::cmp::Ordering::Equal)
7793
             }),
94
+            SortField::DiskRead => processes.sort_by(|a, b| {
95
+                b.io_read_rate
96
+                    .partial_cmp(&a.io_read_rate)
97
+                    .unwrap_or(std::cmp::Ordering::Equal)
98
+            }),
99
+            SortField::DiskWrite => processes.sort_by(|a, b| {
100
+                b.io_write_rate
101
+                    .partial_cmp(&a.io_write_rate)
102
+                    .unwrap_or(std::cmp::Ordering::Equal)
103
+            }),
104
+            SortField::DiskTotal => processes.sort_by(|a, b| {
105
+                let a_total = a.io_read_rate + a.io_write_rate;
106
+                let b_total = b.io_read_rate + b.io_write_rate;
107
+                b_total
108
+                    .partial_cmp(&a_total)
109
+                    .unwrap_or(std::cmp::Ordering::Equal)
110
+            }),
111
+            SortField::NetConnections => processes.sort_by(|a, b| {
112
+                b.net_connections.cmp(&a.net_connections)
113
+            }),
78114
             SortField::Pid => processes.sort_by_key(|p| p.pid),
79115
             SortField::Name => processes.sort_by(|a, b| a.name.cmp(&b.name)),
80116
         }
@@ -88,7 +124,7 @@ impl ProcessCollector {
88124
     }
89125
 
90126
     /// Get info for a single process.
91
-    fn process_info(&mut self, proc: &Process, system_total: u64) -> Result<ProcessInfo> {
127
+    fn process_info(&mut self, proc: &Process, system_total: u64, now: Instant) -> Result<ProcessInfo> {
92128
         let stat = proc.stat()?;
93129
         let status = proc.status()?;
94130
 
@@ -99,21 +135,49 @@ impl ProcessCollector {
99135
             .map(|v| v.join(" "))
100136
             .unwrap_or_else(|_| name.clone());
101137
 
102
-        // Calculate CPU percentage
138
+        // Get I/O stats (may fail for some processes due to permissions)
139
+        let (io_read_bytes, io_write_bytes) = proc
140
+            .io()
141
+            .map(|io| (io.read_bytes, io.write_bytes))
142
+            .unwrap_or((0, 0));
143
+
144
+        // Calculate CPU percentage and I/O rates from previous stats
103145
         let proc_time = stat.utime + stat.stime;
104
-        let cpu_percent =
105
-            if let Some((prev_proc_time, prev_sys_time)) = self.prev_times.get(&pid) {
106
-                let proc_delta = proc_time.saturating_sub(*prev_proc_time);
107
-                let sys_delta = system_total.saturating_sub(*prev_sys_time);
108
-                if sys_delta > 0 {
146
+        let (cpu_percent, io_read_rate, io_write_rate) =
147
+            if let Some(prev) = self.prev_stats.get(&pid) {
148
+                let elapsed = now.duration_since(prev.timestamp).as_secs_f64();
149
+
150
+                // CPU percentage
151
+                let proc_delta = proc_time.saturating_sub(prev.cpu_time);
152
+                let sys_delta = system_total.saturating_sub(prev.system_time);
153
+                let cpu_pct = if sys_delta > 0 {
109154
                     (proc_delta as f64 / sys_delta as f64) * 100.0
110155
                 } else {
111156
                     0.0
112
-                }
157
+                };
158
+
159
+                // I/O rates
160
+                let (read_rate, write_rate) = if elapsed > 0.0 {
161
+                    let read_delta = io_read_bytes.saturating_sub(prev.io_read_bytes) as f64;
162
+                    let write_delta = io_write_bytes.saturating_sub(prev.io_write_bytes) as f64;
163
+                    (read_delta / elapsed, write_delta / elapsed)
164
+                } else {
165
+                    (0.0, 0.0)
166
+                };
167
+
168
+                (cpu_pct, read_rate, write_rate)
113169
             } else {
114
-                0.0
170
+                (0.0, 0.0, 0.0)
115171
             };
116
-        self.prev_times.insert(pid, (proc_time, system_total));
172
+
173
+        // Store current stats for next calculation
174
+        self.prev_stats.insert(pid, PrevProcessStats {
175
+            cpu_time: proc_time,
176
+            system_time: system_total,
177
+            io_read_bytes,
178
+            io_write_bytes,
179
+            timestamp: now,
180
+        });
117181
 
118182
         // Memory
119183
         let rss = stat.rss as u64 * 4096; // Pages to bytes
@@ -142,6 +206,16 @@ impl ProcessCollector {
142206
             .map(|u| u.name().to_string_lossy().to_string())
143207
             .unwrap_or_else(|| status.ruid.to_string());
144208
 
209
+        // Count network connections (sockets)
210
+        let net_connections = proc
211
+            .fd()
212
+            .map(|fds| {
213
+                fds.filter_map(|fd| fd.ok())
214
+                    .filter(|fd| matches!(fd.target, FDTarget::Socket(_)))
215
+                    .count() as u32
216
+            })
217
+            .unwrap_or(0);
218
+
145219
         Ok(ProcessInfo {
146220
             pid,
147221
             name,
@@ -150,6 +224,11 @@ impl ProcessCollector {
150224
             memory_percent,
151225
             rss,
152226
             vsize,
227
+            io_read_bytes,
228
+            io_write_bytes,
229
+            io_read_rate,
230
+            io_write_rate,
231
+            net_connections,
153232
             state,
154233
             user,
155234
         })
gartop/src/gui/app.rsmodified
@@ -245,7 +245,8 @@ impl App {
245245
         let sort_field = match self.tab_bar.active() {
246246
             Tab::Cpu => SortField::Cpu,
247247
             Tab::Memory => SortField::Memory,
248
-            Tab::Network | Tab::Disk => SortField::Cpu, // Default for I/O tabs
248
+            Tab::Network => SortField::NetConnections,
249
+            Tab::Disk => SortField::DiskTotal,
249250
         };
250251
         if let Some(resp) = self.send_command(&Command::GetProcesses {
251252
             sort_by: Some(sort_field),
@@ -634,7 +635,8 @@ impl App {
634635
                 let sort_field = match tab {
635636
                     Tab::Cpu => SortField::Cpu,
636637
                     Tab::Memory => SortField::Memory,
637
-                    Tab::Network | Tab::Disk => SortField::Cpu,
638
+                    Tab::Network => SortField::NetConnections,
639
+                    Tab::Disk => SortField::DiskTotal,
638640
                 };
639641
                 self.process_list.set_sort(sort_field);
640642
                 // Force refresh to get re-sorted processes
@@ -740,7 +742,8 @@ impl App {
740742
                             let sort_field = match new_tab {
741743
                                 Tab::Cpu => SortField::Cpu,
742744
                                 Tab::Memory => SortField::Memory,
743
-                                Tab::Network | Tab::Disk => SortField::Cpu,
745
+                                Tab::Network => SortField::NetConnections,
746
+                                Tab::Disk => SortField::DiskTotal,
744747
                             };
745748
                             self.process_list.set_sort(sort_field);
746749
                             self.last_refresh = Instant::now() - std::time::Duration::from_secs(10);
gartop/src/gui/process_list.rsmodified
@@ -5,6 +5,25 @@ use gartk_render::{Renderer, TextStyle};
55
 use gartop_ipc::{ProcessInfo, SortField};
66
 use super::theme::Theme;
77
 
8
+/// Format rate (bytes/second) to human-readable compact string.
9
+fn format_rate(bytes_per_sec: f64) -> String {
10
+    const KIB: f64 = 1024.0;
11
+    const MIB: f64 = KIB * 1024.0;
12
+    const GIB: f64 = MIB * 1024.0;
13
+
14
+    if bytes_per_sec >= GIB {
15
+        format!("{:.1}G", bytes_per_sec / GIB)
16
+    } else if bytes_per_sec >= MIB {
17
+        format!("{:.1}M", bytes_per_sec / MIB)
18
+    } else if bytes_per_sec >= KIB {
19
+        format!("{:.0}K", bytes_per_sec / KIB)
20
+    } else if bytes_per_sec > 0.0 {
21
+        format!("{:.0}B", bytes_per_sec)
22
+    } else {
23
+        "0".to_string()
24
+    }
25
+}
26
+
827
 /// Row height for process list.
928
 const ROW_HEIGHT: u32 = 22;
1029
 
@@ -151,12 +170,24 @@ impl ProcessList {
151170
             color: match self.sort_field {
152171
                 SortField::Cpu => theme.cpu_color,
153172
                 SortField::Memory => theme.memory_color,
173
+                SortField::DiskRead | SortField::DiskWrite | SortField::DiskTotal => theme.disk_color,
174
+                SortField::NetConnections => theme.network_color,
154175
                 _ => theme.text_secondary,
155176
             },
156177
             ..header_style.clone()
157178
         };
158179
 
159
-        if self.sort_field == SortField::Cpu {
180
+        // Show different columns based on sort field
181
+        let is_disk_sort = matches!(self.sort_field, SortField::DiskRead | SortField::DiskWrite | SortField::DiskTotal);
182
+        let is_net_sort = matches!(self.sort_field, SortField::NetConnections);
183
+
184
+        if is_disk_sort {
185
+            renderer.text("Read/s", col_cpu, header_y, &sort_style)?;
186
+            renderer.text("Write/s", col_mem, header_y, &sort_style)?;
187
+        } else if is_net_sort {
188
+            renderer.text("Sockets", col_cpu, header_y, &sort_style)?;
189
+            renderer.text("Mem%", col_mem, header_y, &header_style)?;
190
+        } else if self.sort_field == SortField::Cpu {
160191
             renderer.text("CPU%", col_cpu, header_y, &sort_style)?;
161192
             renderer.text("Mem%", col_mem, header_y, &header_style)?;
162193
         } else {
@@ -209,21 +240,56 @@ impl ProcessList {
209240
             };
210241
             renderer.text(&name, col_name, text_y, &text_style)?;
211242
 
212
-            // CPU %
213
-            let cpu_style = if process.cpu_percent > 50.0 {
214
-                TextStyle { color: theme.cpu_color, ..text_style.clone() }
243
+            // Show CPU/Memory or I/O or Network depending on sort field
244
+            if is_disk_sort {
245
+                // Read rate
246
+                let read_style = if process.io_read_rate > 1_000_000.0 {
247
+                    TextStyle { color: theme.disk_color, ..text_style.clone() }
248
+                } else {
249
+                    dim_style.clone()
250
+                };
251
+                renderer.text(&format_rate(process.io_read_rate), col_cpu, text_y, &read_style)?;
252
+
253
+                // Write rate
254
+                let write_style = if process.io_write_rate > 1_000_000.0 {
255
+                    TextStyle { color: theme.disk_color, ..text_style.clone() }
256
+                } else {
257
+                    dim_style.clone()
258
+                };
259
+                renderer.text(&format_rate(process.io_write_rate), col_mem, text_y, &write_style)?;
260
+            } else if is_net_sort {
261
+                // Socket count
262
+                let net_style = if process.net_connections > 10 {
263
+                    TextStyle { color: theme.network_color, ..text_style.clone() }
264
+                } else {
265
+                    dim_style.clone()
266
+                };
267
+                renderer.text(&process.net_connections.to_string(), col_cpu, text_y, &net_style)?;
268
+
269
+                // Memory %
270
+                let mem_style = if process.memory_percent > 10.0 {
271
+                    TextStyle { color: theme.memory_color, ..text_style.clone() }
272
+                } else {
273
+                    dim_style.clone()
274
+                };
275
+                renderer.text(&format!("{:.1}", process.memory_percent), col_mem, text_y, &mem_style)?;
215276
             } else {
216
-                dim_style.clone()
217
-            };
218
-            renderer.text(&format!("{:.1}", process.cpu_percent), col_cpu, text_y, &cpu_style)?;
219
-
220
-            // Memory %
221
-            let mem_style = if process.memory_percent > 10.0 {
222
-                TextStyle { color: theme.memory_color, ..text_style.clone() }
223
-            } else {
224
-                dim_style.clone()
225
-            };
226
-            renderer.text(&format!("{:.1}", process.memory_percent), col_mem, text_y, &mem_style)?;
277
+                // CPU %
278
+                let cpu_style = if process.cpu_percent > 50.0 {
279
+                    TextStyle { color: theme.cpu_color, ..text_style.clone() }
280
+                } else {
281
+                    dim_style.clone()
282
+                };
283
+                renderer.text(&format!("{:.1}", process.cpu_percent), col_cpu, text_y, &cpu_style)?;
284
+
285
+                // Memory %
286
+                let mem_style = if process.memory_percent > 10.0 {
287
+                    TextStyle { color: theme.memory_color, ..text_style.clone() }
288
+                } else {
289
+                    dim_style.clone()
290
+                };
291
+                renderer.text(&format!("{:.1}", process.memory_percent), col_mem, text_y, &mem_style)?;
292
+            }
227293
 
228294
             // User
229295
             let user = if process.user.len() > 10 {