@@ -2,14 +2,29 @@ |
| 2 | 2 | |
| 3 | 3 | use crate::error::{Error, Result}; |
| 4 | 4 | use gartop_ipc::{ProcessInfo, SortField}; |
| 5 | | -use procfs::process::{all_processes, Process}; |
| 5 | +use procfs::process::{all_processes, FDTarget, Process}; |
| 6 | 6 | use procfs::{Current, CurrentSI}; |
| 7 | 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 | 25 | 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>, |
| 13 | 28 | /// Total memory for percentage calculation. |
| 14 | 29 | total_memory: u64, |
| 15 | 30 | } |
@@ -20,7 +35,7 @@ impl ProcessCollector { |
| 20 | 35 | let meminfo = procfs::Meminfo::current()?; |
| 21 | 36 | |
| 22 | 37 | Ok(Self { |
| 23 | | - prev_times: HashMap::new(), |
| 38 | + prev_stats: HashMap::new(), |
| 24 | 39 | total_memory: meminfo.mem_total, |
| 25 | 40 | }) |
| 26 | 41 | } |
@@ -31,6 +46,7 @@ impl ProcessCollector { |
| 31 | 46 | sort_by: SortField, |
| 32 | 47 | limit: Option<usize>, |
| 33 | 48 | ) -> Result<Vec<ProcessInfo>> { |
| 49 | + let now = Instant::now(); |
| 34 | 50 | let kernel_stats = procfs::KernelStats::current()?; |
| 35 | 51 | let total = &kernel_stats.total; |
| 36 | 52 | let system_total = total.user |
@@ -54,14 +70,14 @@ impl ProcessCollector { |
| 54 | 70 | let pid = proc.pid(); |
| 55 | 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 | 74 | Ok(info) => processes.push(info), |
| 59 | 75 | Err(_) => continue, // Skip processes we can't read |
| 60 | 76 | } |
| 61 | 77 | } |
| 62 | 78 | |
| 63 | 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 | 82 | // Sort |
| 67 | 83 | match sort_by { |
@@ -75,6 +91,26 @@ impl ProcessCollector { |
| 75 | 91 | .partial_cmp(&a.memory_percent) |
| 76 | 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 | 114 | SortField::Pid => processes.sort_by_key(|p| p.pid), |
| 79 | 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 | 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 | 128 | let stat = proc.stat()?; |
| 93 | 129 | let status = proc.status()?; |
| 94 | 130 | |
@@ -99,21 +135,49 @@ impl ProcessCollector { |
| 99 | 135 | .map(|v| v.join(" ")) |
| 100 | 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 | 145 | 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 { |
| 109 | 154 | (proc_delta as f64 / sys_delta as f64) * 100.0 |
| 110 | 155 | } else { |
| 111 | 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) |
| 164 | + } else { |
| 165 | + (0.0, 0.0) |
| 166 | + }; |
| 167 | + |
| 168 | + (cpu_pct, read_rate, write_rate) |
| 113 | 169 | } else { |
| 114 | | - 0.0 |
| 170 | + (0.0, 0.0, 0.0) |
| 115 | 171 | }; |
| 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 | + }); |
| 117 | 181 | |
| 118 | 182 | // Memory |
| 119 | 183 | let rss = stat.rss as u64 * 4096; // Pages to bytes |
@@ -142,6 +206,16 @@ impl ProcessCollector { |
| 142 | 206 | .map(|u| u.name().to_string_lossy().to_string()) |
| 143 | 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 | 219 | Ok(ProcessInfo { |
| 146 | 220 | pid, |
| 147 | 221 | name, |
@@ -150,6 +224,11 @@ impl ProcessCollector { |
| 150 | 224 | memory_percent, |
| 151 | 225 | rss, |
| 152 | 226 | vsize, |
| 227 | + io_read_bytes, |
| 228 | + io_write_bytes, |
| 229 | + io_read_rate, |
| 230 | + io_write_rate, |
| 231 | + net_connections, |
| 153 | 232 | state, |
| 154 | 233 | user, |
| 155 | 234 | }) |