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 {
70
     #[default]
70
     #[default]
71
     Cpu,
71
     Cpu,
72
     Memory,
72
     Memory,
73
+    DiskRead,
74
+    DiskWrite,
75
+    DiskTotal,
76
+    NetConnections,
73
     Pid,
77
     Pid,
74
     Name,
78
     Name,
75
 }
79
 }
@@ -164,6 +168,16 @@ pub struct ProcessInfo {
164
     pub rss: u64,
168
     pub rss: u64,
165
     /// Virtual memory size in bytes.
169
     /// Virtual memory size in bytes.
166
     pub vsize: u64,
170
     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,
167
     /// Process state.
181
     /// Process state.
168
     pub state: String,
182
     pub state: String,
169
     /// User owning the process.
183
     /// User owning the process.
gartop/src/collector/process.rsmodified
@@ -2,14 +2,29 @@
2
 
2
 
3
 use crate::error::{Error, Result};
3
 use crate::error::{Error, Result};
4
 use gartop_ipc::{ProcessInfo, SortField};
4
 use gartop_ipc::{ProcessInfo, SortField};
5
-use procfs::process::{all_processes, Process};
5
+use procfs::process::{all_processes, FDTarget, Process};
6
 use procfs::{Current, CurrentSI};
6
 use procfs::{Current, CurrentSI};
7
 use std::collections::HashMap;
7
 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
+}
8
 
23
 
9
-/// Process collector with previous CPU times for percentage calculation.
24
+/// Process collector with previous stats for rate calculations.
10
 pub struct ProcessCollector {
25
 pub struct ProcessCollector {
11
-    /// Previous CPU times per PID for delta calculation.
26
+    /// Previous stats per PID for delta calculation.
12
-    prev_times: HashMap<i32, (u64, u64)>, // (utime + stime, total_system_time)
27
+    prev_stats: HashMap<i32, PrevProcessStats>,
13
     /// Total memory for percentage calculation.
28
     /// Total memory for percentage calculation.
14
     total_memory: u64,
29
     total_memory: u64,
15
 }
30
 }
@@ -20,7 +35,7 @@ impl ProcessCollector {
20
         let meminfo = procfs::Meminfo::current()?;
35
         let meminfo = procfs::Meminfo::current()?;
21
 
36
 
22
         Ok(Self {
37
         Ok(Self {
23
-            prev_times: HashMap::new(),
38
+            prev_stats: HashMap::new(),
24
             total_memory: meminfo.mem_total,
39
             total_memory: meminfo.mem_total,
25
         })
40
         })
26
     }
41
     }
@@ -31,6 +46,7 @@ impl ProcessCollector {
31
         sort_by: SortField,
46
         sort_by: SortField,
32
         limit: Option<usize>,
47
         limit: Option<usize>,
33
     ) -> Result<Vec<ProcessInfo>> {
48
     ) -> Result<Vec<ProcessInfo>> {
49
+        let now = Instant::now();
34
         let kernel_stats = procfs::KernelStats::current()?;
50
         let kernel_stats = procfs::KernelStats::current()?;
35
         let total = &kernel_stats.total;
51
         let total = &kernel_stats.total;
36
         let system_total = total.user
52
         let system_total = total.user
@@ -54,14 +70,14 @@ impl ProcessCollector {
54
             let pid = proc.pid();
70
             let pid = proc.pid();
55
             current_pids.push(pid);
71
             current_pids.push(pid);
56
 
72
 
57
-            match self.process_info(&proc, system_total) {
73
+            match self.process_info(&proc, system_total, now) {
58
                 Ok(info) => processes.push(info),
74
                 Ok(info) => processes.push(info),
59
                 Err(_) => continue, // Skip processes we can't read
75
                 Err(_) => continue, // Skip processes we can't read
60
             }
76
             }
61
         }
77
         }
62
 
78
 
63
         // Clean up old entries
79
         // Clean up old entries
64
-        self.prev_times.retain(|pid, _| current_pids.contains(pid));
80
+        self.prev_stats.retain(|pid, _| current_pids.contains(pid));
65
 
81
 
66
         // Sort
82
         // Sort
67
         match sort_by {
83
         match sort_by {
@@ -75,6 +91,26 @@ impl ProcessCollector {
75
                     .partial_cmp(&a.memory_percent)
91
                     .partial_cmp(&a.memory_percent)
76
                     .unwrap_or(std::cmp::Ordering::Equal)
92
                     .unwrap_or(std::cmp::Ordering::Equal)
77
             }),
93
             }),
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
+            }),
78
             SortField::Pid => processes.sort_by_key(|p| p.pid),
114
             SortField::Pid => processes.sort_by_key(|p| p.pid),
79
             SortField::Name => processes.sort_by(|a, b| a.name.cmp(&b.name)),
115
             SortField::Name => processes.sort_by(|a, b| a.name.cmp(&b.name)),
80
         }
116
         }
@@ -88,7 +124,7 @@ impl ProcessCollector {
88
     }
124
     }
89
 
125
 
90
     /// Get info for a single process.
126
     /// 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> {
92
         let stat = proc.stat()?;
128
         let stat = proc.stat()?;
93
         let status = proc.status()?;
129
         let status = proc.status()?;
94
 
130
 
@@ -99,21 +135,49 @@ impl ProcessCollector {
99
             .map(|v| v.join(" "))
135
             .map(|v| v.join(" "))
100
             .unwrap_or_else(|_| name.clone());
136
             .unwrap_or_else(|_| name.clone());
101
 
137
 
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
103
         let proc_time = stat.utime + stat.stime;
145
         let proc_time = stat.utime + stat.stime;
104
-        let cpu_percent =
146
+        let (cpu_percent, io_read_rate, io_write_rate) =
105
-            if let Some((prev_proc_time, prev_sys_time)) = self.prev_times.get(&pid) {
147
+            if let Some(prev) = self.prev_stats.get(&pid) {
106
-                let proc_delta = proc_time.saturating_sub(*prev_proc_time);
148
+                let elapsed = now.duration_since(prev.timestamp).as_secs_f64();
107
-                let sys_delta = system_total.saturating_sub(*prev_sys_time);
149
+
108
-                if sys_delta > 0 {
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 {
109
                     (proc_delta as f64 / sys_delta as f64) * 100.0
154
                     (proc_delta as f64 / sys_delta as f64) * 100.0
110
                 } else {
155
                 } else {
111
                     0.0
156
                     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)
113
                 } else {
164
                 } else {
114
-                0.0
165
+                    (0.0, 0.0)
115
                 };
166
                 };
116
-        self.prev_times.insert(pid, (proc_time, system_total));
167
+
168
+                (cpu_pct, read_rate, write_rate)
169
+            } else {
170
+                (0.0, 0.0, 0.0)
171
+            };
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
+        });
117
 
181
 
118
         // Memory
182
         // Memory
119
         let rss = stat.rss as u64 * 4096; // Pages to bytes
183
         let rss = stat.rss as u64 * 4096; // Pages to bytes
@@ -142,6 +206,16 @@ impl ProcessCollector {
142
             .map(|u| u.name().to_string_lossy().to_string())
206
             .map(|u| u.name().to_string_lossy().to_string())
143
             .unwrap_or_else(|| status.ruid.to_string());
207
             .unwrap_or_else(|| status.ruid.to_string());
144
 
208
 
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
+
145
         Ok(ProcessInfo {
219
         Ok(ProcessInfo {
146
             pid,
220
             pid,
147
             name,
221
             name,
@@ -150,6 +224,11 @@ impl ProcessCollector {
150
             memory_percent,
224
             memory_percent,
151
             rss,
225
             rss,
152
             vsize,
226
             vsize,
227
+            io_read_bytes,
228
+            io_write_bytes,
229
+            io_read_rate,
230
+            io_write_rate,
231
+            net_connections,
153
             state,
232
             state,
154
             user,
233
             user,
155
         })
234
         })
gartop/src/gui/app.rsmodified
@@ -245,7 +245,8 @@ impl App {
245
         let sort_field = match self.tab_bar.active() {
245
         let sort_field = match self.tab_bar.active() {
246
             Tab::Cpu => SortField::Cpu,
246
             Tab::Cpu => SortField::Cpu,
247
             Tab::Memory => SortField::Memory,
247
             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,
249
         };
250
         };
250
         if let Some(resp) = self.send_command(&Command::GetProcesses {
251
         if let Some(resp) = self.send_command(&Command::GetProcesses {
251
             sort_by: Some(sort_field),
252
             sort_by: Some(sort_field),
@@ -634,7 +635,8 @@ impl App {
634
                 let sort_field = match tab {
635
                 let sort_field = match tab {
635
                     Tab::Cpu => SortField::Cpu,
636
                     Tab::Cpu => SortField::Cpu,
636
                     Tab::Memory => SortField::Memory,
637
                     Tab::Memory => SortField::Memory,
637
-                    Tab::Network | Tab::Disk => SortField::Cpu,
638
+                    Tab::Network => SortField::NetConnections,
639
+                    Tab::Disk => SortField::DiskTotal,
638
                 };
640
                 };
639
                 self.process_list.set_sort(sort_field);
641
                 self.process_list.set_sort(sort_field);
640
                 // Force refresh to get re-sorted processes
642
                 // Force refresh to get re-sorted processes
@@ -740,7 +742,8 @@ impl App {
740
                             let sort_field = match new_tab {
742
                             let sort_field = match new_tab {
741
                                 Tab::Cpu => SortField::Cpu,
743
                                 Tab::Cpu => SortField::Cpu,
742
                                 Tab::Memory => SortField::Memory,
744
                                 Tab::Memory => SortField::Memory,
743
-                                Tab::Network | Tab::Disk => SortField::Cpu,
745
+                                Tab::Network => SortField::NetConnections,
746
+                                Tab::Disk => SortField::DiskTotal,
744
                             };
747
                             };
745
                             self.process_list.set_sort(sort_field);
748
                             self.process_list.set_sort(sort_field);
746
                             self.last_refresh = Instant::now() - std::time::Duration::from_secs(10);
749
                             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};
5
 use gartop_ipc::{ProcessInfo, SortField};
5
 use gartop_ipc::{ProcessInfo, SortField};
6
 use super::theme::Theme;
6
 use super::theme::Theme;
7
 
7
 
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
+
8
 /// Row height for process list.
27
 /// Row height for process list.
9
 const ROW_HEIGHT: u32 = 22;
28
 const ROW_HEIGHT: u32 = 22;
10
 
29
 
@@ -151,12 +170,24 @@ impl ProcessList {
151
             color: match self.sort_field {
170
             color: match self.sort_field {
152
                 SortField::Cpu => theme.cpu_color,
171
                 SortField::Cpu => theme.cpu_color,
153
                 SortField::Memory => theme.memory_color,
172
                 SortField::Memory => theme.memory_color,
173
+                SortField::DiskRead | SortField::DiskWrite | SortField::DiskTotal => theme.disk_color,
174
+                SortField::NetConnections => theme.network_color,
154
                 _ => theme.text_secondary,
175
                 _ => theme.text_secondary,
155
             },
176
             },
156
             ..header_style.clone()
177
             ..header_style.clone()
157
         };
178
         };
158
 
179
 
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 {
160
             renderer.text("CPU%", col_cpu, header_y, &sort_style)?;
191
             renderer.text("CPU%", col_cpu, header_y, &sort_style)?;
161
             renderer.text("Mem%", col_mem, header_y, &header_style)?;
192
             renderer.text("Mem%", col_mem, header_y, &header_style)?;
162
         } else {
193
         } else {
@@ -209,6 +240,40 @@ impl ProcessList {
209
             };
240
             };
210
             renderer.text(&name, col_name, text_y, &text_style)?;
241
             renderer.text(&name, col_name, text_y, &text_style)?;
211
 
242
 
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)?;
276
+            } else {
212
                 // CPU %
277
                 // CPU %
213
                 let cpu_style = if process.cpu_percent > 50.0 {
278
                 let cpu_style = if process.cpu_percent > 50.0 {
214
                     TextStyle { color: theme.cpu_color, ..text_style.clone() }
279
                     TextStyle { color: theme.cpu_color, ..text_style.clone() }
@@ -224,6 +289,7 @@ impl ProcessList {
224
                     dim_style.clone()
289
                     dim_style.clone()
225
                 };
290
                 };
226
                 renderer.text(&format!("{:.1}", process.memory_percent), col_mem, text_y, &mem_style)?;
291
                 renderer.text(&format!("{:.1}", process.memory_percent), col_mem, text_y, &mem_style)?;
292
+            }
227
 
293
 
228
             // User
294
             // User
229
             let user = if process.user.len() > 10 {
295
             let user = if process.user.len() > 10 {