gardesk/gartop / 3b4c806

Browse files

add data collectors (cpu, memory, process) and history ring buffer

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
3b4c80697017735f22ec47b3c98fbf8207d2dfad
Parents
b9657a9
Tree
f77bcac

5 changed files

StatusFile+-
A gartop/src/collector/cpu.rs 128 0
A gartop/src/collector/history.rs 105 0
A gartop/src/collector/memory.rs 63 0
M gartop/src/collector/mod.rs 9 1
A gartop/src/collector/process.rs 174 0
gartop/src/collector/cpu.rsadded
@@ -0,0 +1,128 @@
1
+//! CPU statistics collection from /proc/stat
2
+
3
+use crate::error::Result;
4
+use gartop_ipc::CpuStats;
5
+use procfs::CurrentSI;
6
+use std::time::{SystemTime, UNIX_EPOCH};
7
+
8
+/// CPU times for calculating deltas.
9
+#[derive(Debug, Clone, Default)]
10
+struct CpuTimes {
11
+    user: u64,
12
+    nice: u64,
13
+    system: u64,
14
+    idle: u64,
15
+    iowait: u64,
16
+    irq: u64,
17
+    softirq: u64,
18
+    steal: u64,
19
+}
20
+
21
+impl CpuTimes {
22
+    fn total(&self) -> u64 {
23
+        self.user
24
+            + self.nice
25
+            + self.system
26
+            + self.idle
27
+            + self.iowait
28
+            + self.irq
29
+            + self.softirq
30
+            + self.steal
31
+    }
32
+
33
+    fn active(&self) -> u64 {
34
+        self.user + self.nice + self.system + self.irq + self.softirq + self.steal
35
+    }
36
+}
37
+
38
+/// CPU collector with state for delta calculations.
39
+pub struct CpuCollector {
40
+    prev_total: CpuTimes,
41
+    prev_per_core: Vec<CpuTimes>,
42
+    core_count: usize,
43
+}
44
+
45
+impl CpuCollector {
46
+    /// Create a new CPU collector.
47
+    pub fn new() -> Result<Self> {
48
+        let kernel_stats = procfs::KernelStats::current()?;
49
+        let core_count = kernel_stats.cpu_time.len().max(1);
50
+
51
+        Ok(Self {
52
+            prev_total: CpuTimes::default(),
53
+            prev_per_core: vec![CpuTimes::default(); core_count],
54
+            core_count,
55
+        })
56
+    }
57
+
58
+    /// Collect current CPU statistics.
59
+    pub fn collect(&mut self) -> Result<CpuStats> {
60
+        let kernel_stats = procfs::KernelStats::current()?;
61
+
62
+        // Parse total CPU times
63
+        let total = &kernel_stats.total;
64
+        let curr_total = CpuTimes {
65
+            user: total.user,
66
+            nice: total.nice,
67
+            system: total.system,
68
+            idle: total.idle,
69
+            iowait: total.iowait.unwrap_or(0),
70
+            irq: total.irq.unwrap_or(0),
71
+            softirq: total.softirq.unwrap_or(0),
72
+            steal: total.steal.unwrap_or(0),
73
+        };
74
+
75
+        // Calculate overall usage
76
+        let total_delta = curr_total.total().saturating_sub(self.prev_total.total());
77
+        let active_delta = curr_total.active().saturating_sub(self.prev_total.active());
78
+        let usage_percent = if total_delta > 0 {
79
+            (active_delta as f64 / total_delta as f64) * 100.0
80
+        } else {
81
+            0.0
82
+        };
83
+
84
+        // Calculate per-core usage
85
+        let mut per_core = Vec::with_capacity(self.core_count);
86
+        for (i, cpu) in kernel_stats.cpu_time.iter().enumerate() {
87
+            if i >= self.core_count {
88
+                break;
89
+            }
90
+            let curr = CpuTimes {
91
+                user: cpu.user,
92
+                nice: cpu.nice,
93
+                system: cpu.system,
94
+                idle: cpu.idle,
95
+                iowait: cpu.iowait.unwrap_or(0),
96
+                irq: cpu.irq.unwrap_or(0),
97
+                softirq: cpu.softirq.unwrap_or(0),
98
+                steal: cpu.steal.unwrap_or(0),
99
+            };
100
+
101
+            let prev: &CpuTimes = &self.prev_per_core[i];
102
+            let total_d = curr.total().saturating_sub(prev.total());
103
+            let active_d = curr.active().saturating_sub(prev.active());
104
+            let core_usage = if total_d > 0 {
105
+                (active_d as f64 / total_d as f64) * 100.0
106
+            } else {
107
+                0.0
108
+            };
109
+            per_core.push(core_usage);
110
+
111
+            self.prev_per_core[i] = curr;
112
+        }
113
+
114
+        self.prev_total = curr_total;
115
+
116
+        let timestamp = SystemTime::now()
117
+            .duration_since(UNIX_EPOCH)
118
+            .map(|d| d.as_millis() as u64)
119
+            .unwrap_or(0);
120
+
121
+        Ok(CpuStats {
122
+            usage_percent,
123
+            per_core,
124
+            core_count: self.core_count,
125
+            timestamp,
126
+        })
127
+    }
128
+}
gartop/src/collector/history.rsadded
@@ -0,0 +1,105 @@
1
+//! Generic ring buffer for time-series history
2
+
3
+use std::collections::VecDeque;
4
+
5
+/// Ring buffer for maintaining a fixed-size history of samples.
6
+#[derive(Debug, Clone)]
7
+pub struct History<T> {
8
+    buffer: VecDeque<T>,
9
+    capacity: usize,
10
+}
11
+
12
+impl<T: Clone> History<T> {
13
+    /// Create a new history buffer with the given capacity.
14
+    pub fn new(capacity: usize) -> Self {
15
+        Self {
16
+            buffer: VecDeque::with_capacity(capacity),
17
+            capacity,
18
+        }
19
+    }
20
+
21
+    /// Push a new sample, evicting the oldest if at capacity.
22
+    pub fn push(&mut self, value: T) {
23
+        if self.buffer.len() >= self.capacity {
24
+            self.buffer.pop_front();
25
+        }
26
+        self.buffer.push_back(value);
27
+    }
28
+
29
+    /// Get the most recent sample.
30
+    pub fn latest(&self) -> Option<&T> {
31
+        self.buffer.back()
32
+    }
33
+
34
+    /// Get all samples as owned values (oldest to newest).
35
+    pub fn to_vec(&self) -> Vec<T> {
36
+        self.buffer.iter().cloned().collect()
37
+    }
38
+
39
+    /// Get the last N samples as owned values (oldest to newest).
40
+    pub fn last_n(&self, n: usize) -> Vec<T> {
41
+        let skip = self.buffer.len().saturating_sub(n);
42
+        self.buffer.iter().skip(skip).cloned().collect()
43
+    }
44
+
45
+    /// Current number of samples.
46
+    pub fn len(&self) -> usize {
47
+        self.buffer.len()
48
+    }
49
+
50
+    /// Check if empty.
51
+    pub fn is_empty(&self) -> bool {
52
+        self.buffer.is_empty()
53
+    }
54
+
55
+    /// Clear all samples.
56
+    pub fn clear(&mut self) {
57
+        self.buffer.clear();
58
+    }
59
+}
60
+
61
+impl<T: Clone> Default for History<T> {
62
+    fn default() -> Self {
63
+        Self::new(300) // Default: 5 minutes at 1 sample/second
64
+    }
65
+}
66
+
67
+#[cfg(test)]
68
+mod tests {
69
+    use super::*;
70
+
71
+    #[test]
72
+    fn test_push_and_latest() {
73
+        let mut h: History<i32> = History::new(3);
74
+        assert!(h.latest().is_none());
75
+
76
+        h.push(1);
77
+        assert_eq!(h.latest(), Some(&1));
78
+
79
+        h.push(2);
80
+        assert_eq!(h.latest(), Some(&2));
81
+    }
82
+
83
+    #[test]
84
+    fn test_capacity() {
85
+        let mut h: History<i32> = History::new(3);
86
+        h.push(1);
87
+        h.push(2);
88
+        h.push(3);
89
+        assert_eq!(h.len(), 3);
90
+
91
+        h.push(4);
92
+        assert_eq!(h.len(), 3);
93
+        assert_eq!(h.to_vec(), vec![2, 3, 4]);
94
+    }
95
+
96
+    #[test]
97
+    fn test_last_n() {
98
+        let mut h: History<i32> = History::new(5);
99
+        for i in 1..=5 {
100
+            h.push(i);
101
+        }
102
+        assert_eq!(h.last_n(3), vec![3, 4, 5]);
103
+        assert_eq!(h.last_n(10), vec![1, 2, 3, 4, 5]);
104
+    }
105
+}
gartop/src/collector/memory.rsadded
@@ -0,0 +1,63 @@
1
+//! Memory statistics collection from /proc/meminfo
2
+
3
+use crate::error::Result;
4
+use gartop_ipc::MemoryStats;
5
+use procfs::Current;
6
+use std::time::{SystemTime, UNIX_EPOCH};
7
+
8
+/// Memory collector.
9
+pub struct MemoryCollector;
10
+
11
+impl MemoryCollector {
12
+    /// Create a new memory collector.
13
+    pub fn new() -> Self {
14
+        Self
15
+    }
16
+
17
+    /// Collect current memory statistics.
18
+    pub fn collect(&self) -> Result<MemoryStats> {
19
+        let meminfo = procfs::Meminfo::current()?;
20
+
21
+        let total = meminfo.mem_total;
22
+        let free = meminfo.mem_free;
23
+        let available = meminfo.mem_available.unwrap_or(free);
24
+        let buffers = meminfo.buffers;
25
+        let cached = meminfo.cached;
26
+        let slab_reclaimable = meminfo.s_reclaimable.unwrap_or(0);
27
+
28
+        // Calculate used memory (excluding buffers/cache)
29
+        let used = total.saturating_sub(free + buffers + cached + slab_reclaimable);
30
+
31
+        let swap_total = meminfo.swap_total;
32
+        let swap_free = meminfo.swap_free;
33
+        let swap_used = swap_total.saturating_sub(swap_free);
34
+
35
+        let usage_percent = if total > 0 {
36
+            (used as f64 / total as f64) * 100.0
37
+        } else {
38
+            0.0
39
+        };
40
+
41
+        let timestamp = SystemTime::now()
42
+            .duration_since(UNIX_EPOCH)
43
+            .map(|d| d.as_millis() as u64)
44
+            .unwrap_or(0);
45
+
46
+        Ok(MemoryStats {
47
+            total,
48
+            used,
49
+            free,
50
+            available,
51
+            swap_total,
52
+            swap_used,
53
+            usage_percent,
54
+            timestamp,
55
+        })
56
+    }
57
+}
58
+
59
+impl Default for MemoryCollector {
60
+    fn default() -> Self {
61
+        Self::new()
62
+    }
63
+}
gartop/src/collector/mod.rsmodified
@@ -2,4 +2,12 @@
22
 //!
33
 //! Collects CPU, memory, and process data from procfs.
44
 
5
-// TODO: Sprint 2 - implement collectors
5
+mod cpu;
6
+mod history;
7
+mod memory;
8
+mod process;
9
+
10
+pub use cpu::CpuCollector;
11
+pub use history::History;
12
+pub use memory::MemoryCollector;
13
+pub use process::ProcessCollector;
gartop/src/collector/process.rsadded
@@ -0,0 +1,174 @@
1
+//! Process information collection from /proc/[pid]/
2
+
3
+use crate::error::{Error, Result};
4
+use gartop_ipc::{ProcessInfo, SortField};
5
+use procfs::process::{all_processes, Process};
6
+use procfs::{Current, CurrentSI};
7
+use std::collections::HashMap;
8
+
9
+/// Process collector with previous CPU times for percentage calculation.
10
+pub struct ProcessCollector {
11
+    /// Previous CPU times per PID for delta calculation.
12
+    prev_times: HashMap<i32, (u64, u64)>, // (utime + stime, total_system_time)
13
+    /// Total memory for percentage calculation.
14
+    total_memory: u64,
15
+}
16
+
17
+impl ProcessCollector {
18
+    /// Create a new process collector.
19
+    pub fn new() -> Result<Self> {
20
+        let meminfo = procfs::Meminfo::current()?;
21
+
22
+        Ok(Self {
23
+            prev_times: HashMap::new(),
24
+            total_memory: meminfo.mem_total,
25
+        })
26
+    }
27
+
28
+    /// Collect all running processes.
29
+    pub fn collect(
30
+        &mut self,
31
+        sort_by: SortField,
32
+        limit: Option<usize>,
33
+    ) -> Result<Vec<ProcessInfo>> {
34
+        let kernel_stats = procfs::KernelStats::current()?;
35
+        let total = &kernel_stats.total;
36
+        let system_total = total.user
37
+            + total.nice
38
+            + total.system
39
+            + total.idle
40
+            + total.iowait.unwrap_or(0)
41
+            + total.irq.unwrap_or(0)
42
+            + total.softirq.unwrap_or(0)
43
+            + total.steal.unwrap_or(0);
44
+
45
+        let mut processes = Vec::new();
46
+        let mut current_pids = Vec::new();
47
+
48
+        for proc_result in all_processes()? {
49
+            let proc = match proc_result {
50
+                Ok(p) => p,
51
+                Err(_) => continue,
52
+            };
53
+
54
+            let pid = proc.pid();
55
+            current_pids.push(pid);
56
+
57
+            match self.process_info(&proc, system_total) {
58
+                Ok(info) => processes.push(info),
59
+                Err(_) => continue, // Skip processes we can't read
60
+            }
61
+        }
62
+
63
+        // Clean up old entries
64
+        self.prev_times.retain(|pid, _| current_pids.contains(pid));
65
+
66
+        // Sort
67
+        match sort_by {
68
+            SortField::Cpu => processes.sort_by(|a, b| {
69
+                b.cpu_percent
70
+                    .partial_cmp(&a.cpu_percent)
71
+                    .unwrap_or(std::cmp::Ordering::Equal)
72
+            }),
73
+            SortField::Memory => processes.sort_by(|a, b| {
74
+                b.memory_percent
75
+                    .partial_cmp(&a.memory_percent)
76
+                    .unwrap_or(std::cmp::Ordering::Equal)
77
+            }),
78
+            SortField::Pid => processes.sort_by_key(|p| p.pid),
79
+            SortField::Name => processes.sort_by(|a, b| a.name.cmp(&b.name)),
80
+        }
81
+
82
+        // Limit
83
+        if let Some(n) = limit {
84
+            processes.truncate(n);
85
+        }
86
+
87
+        Ok(processes)
88
+    }
89
+
90
+    /// Get info for a single process.
91
+    fn process_info(&mut self, proc: &Process, system_total: u64) -> Result<ProcessInfo> {
92
+        let stat = proc.stat()?;
93
+        let status = proc.status()?;
94
+
95
+        let pid = proc.pid();
96
+        let name = stat.comm.clone();
97
+        let cmdline = proc
98
+            .cmdline()
99
+            .map(|v| v.join(" "))
100
+            .unwrap_or_else(|_| name.clone());
101
+
102
+        // Calculate CPU percentage
103
+        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 {
109
+                    (proc_delta as f64 / sys_delta as f64) * 100.0
110
+                } else {
111
+                    0.0
112
+                }
113
+            } else {
114
+                0.0
115
+            };
116
+        self.prev_times.insert(pid, (proc_time, system_total));
117
+
118
+        // Memory
119
+        let rss = stat.rss as u64 * 4096; // Pages to bytes
120
+        let vsize = stat.vsize;
121
+        let memory_percent = if self.total_memory > 0 {
122
+            (rss as f64 / self.total_memory as f64) * 100.0
123
+        } else {
124
+            0.0
125
+        };
126
+
127
+        // State
128
+        let state = match stat.state {
129
+            'R' => "running",
130
+            'S' => "sleeping",
131
+            'D' => "disk sleep",
132
+            'Z' => "zombie",
133
+            'T' => "stopped",
134
+            't' => "tracing stop",
135
+            'X' | 'x' => "dead",
136
+            _ => "unknown",
137
+        }
138
+        .to_string();
139
+
140
+        // User
141
+        let user = users::get_user_by_uid(status.ruid)
142
+            .map(|u| u.name().to_string_lossy().to_string())
143
+            .unwrap_or_else(|| status.ruid.to_string());
144
+
145
+        Ok(ProcessInfo {
146
+            pid,
147
+            name,
148
+            cmdline,
149
+            cpu_percent,
150
+            memory_percent,
151
+            rss,
152
+            vsize,
153
+            state,
154
+            user,
155
+        })
156
+    }
157
+
158
+    /// Kill a process by PID.
159
+    pub fn kill(&self, pid: i32, signal: i32) -> Result<()> {
160
+        use nix::sys::signal::{kill, Signal};
161
+        use nix::unistd::Pid;
162
+
163
+        let sig = Signal::try_from(signal)
164
+            .map_err(|_| Error::Ipc(format!("Invalid signal: {}", signal)))?;
165
+
166
+        kill(Pid::from_raw(pid), sig).map_err(|e| match e {
167
+            nix::errno::Errno::ESRCH => Error::ProcessNotFound(pid),
168
+            nix::errno::Errno::EPERM => {
169
+                Error::PermissionDenied(format!("Cannot kill PID {}", pid))
170
+            }
171
+            _ => Error::Io(std::io::Error::from_raw_os_error(e as i32)),
172
+        })
173
+    }
174
+}