Skip to main content

nucleus/resources/
stats.rs

1use crate::error::{NucleusError, Result};
2use std::fs;
3use std::path::Path;
4
5/// Resource usage statistics for a container
6#[derive(Debug, Clone)]
7pub struct ResourceStats {
8    /// Memory usage in bytes
9    pub memory_usage: u64,
10
11    /// Memory limit in bytes (0 = unlimited)
12    pub memory_limit: u64,
13
14    /// Swap usage in bytes
15    pub memory_swap_usage: u64,
16
17    /// CPU usage in nanoseconds
18    pub cpu_usage_ns: u64,
19
20    /// Number of PIDs in the cgroup
21    pub pid_count: u64,
22
23    /// Memory usage percentage (0-100)
24    pub memory_percent: f64,
25}
26
27impl ResourceStats {
28    /// Read resource stats from a cgroup path
29    pub fn from_cgroup(cgroup_path: &str) -> Result<Self> {
30        let cgroup_path = Path::new(cgroup_path);
31
32        // Read memory stats
33        let memory_usage = Self::read_memory_current(cgroup_path)?;
34        let memory_limit = Self::read_memory_max(cgroup_path)?;
35
36        // Calculate memory percentage
37        let memory_percent = if memory_limit > 0 {
38            (memory_usage as f64 / memory_limit as f64) * 100.0
39        } else {
40            0.0
41        };
42
43        // Read swap usage
44        let memory_swap_usage = Self::read_memory_swap(cgroup_path).unwrap_or(0);
45
46        // Read CPU stats
47        let cpu_usage_ns = Self::read_cpu_usage(cgroup_path)?;
48
49        // Read PID stats
50        let pid_count = Self::read_pid_current(cgroup_path)?;
51
52        Ok(Self {
53            memory_usage,
54            memory_limit,
55            memory_swap_usage,
56            cpu_usage_ns,
57            pid_count,
58            memory_percent,
59        })
60    }
61
62    /// Read memory.current (current memory usage)
63    fn read_memory_current(cgroup_path: &Path) -> Result<u64> {
64        let path = cgroup_path.join("memory.current");
65        Self::read_u64_file(&path)
66    }
67
68    /// Read memory.max (memory limit)
69    fn read_memory_max(cgroup_path: &Path) -> Result<u64> {
70        let path = cgroup_path.join("memory.max");
71        let content = fs::read_to_string(&path).map_err(|e| {
72            NucleusError::ResourceError(format!("Failed to read {:?}: {}", path, e))
73        })?;
74
75        // memory.max can be "max" for unlimited
76        if content.trim() == "max" {
77            Ok(0)
78        } else {
79            content.trim().parse().map_err(|e| {
80                NucleusError::ResourceError(format!("Failed to parse memory.max: {}", e))
81            })
82        }
83    }
84
85    /// Read memory.swap.current (swap usage)
86    fn read_memory_swap(cgroup_path: &Path) -> Result<u64> {
87        let path = cgroup_path.join("memory.swap.current");
88        Self::read_u64_file(&path)
89    }
90
91    /// Read cpu.stat (CPU usage)
92    fn read_cpu_usage(cgroup_path: &Path) -> Result<u64> {
93        let path = cgroup_path.join("cpu.stat");
94        let content = fs::read_to_string(&path).map_err(|e| {
95            NucleusError::ResourceError(format!("Failed to read {:?}: {}", path, e))
96        })?;
97
98        // Parse cpu.stat format:
99        // usage_usec 12345
100        // user_usec 6789
101        // system_usec 5556
102        for line in content.lines() {
103            if let Some(value_str) = line.strip_prefix("usage_usec ") {
104                let usec: u64 = value_str.parse().map_err(|e| {
105                    NucleusError::ResourceError(format!("Failed to parse CPU usage: {}", e))
106                })?;
107                // Convert microseconds to nanoseconds
108                return Ok(usec * 1000);
109            }
110        }
111
112        Err(NucleusError::ResourceError(format!(
113            "cpu.stat at {:?} does not contain 'usage_usec' key",
114            path
115        )))
116    }
117
118    /// Read pids.current (current number of PIDs)
119    fn read_pid_current(cgroup_path: &Path) -> Result<u64> {
120        let path = cgroup_path.join("pids.current");
121        Self::read_u64_file(&path)
122    }
123
124    /// Read a file containing a single u64 value
125    fn read_u64_file(path: &Path) -> Result<u64> {
126        let content = fs::read_to_string(path).map_err(|e| {
127            NucleusError::ResourceError(format!("Failed to read {:?}: {}", path, e))
128        })?;
129
130        content
131            .trim()
132            .parse()
133            .map_err(|e| NucleusError::ResourceError(format!("Failed to parse {:?}: {}", path, e)))
134    }
135
136    /// Format memory size in human-readable format
137    pub fn format_memory(bytes: u64) -> String {
138        const KB: u64 = 1024;
139        const MB: u64 = KB * 1024;
140        const GB: u64 = MB * 1024;
141
142        if bytes >= GB {
143            format!("{:.2}G", bytes as f64 / GB as f64)
144        } else if bytes >= MB {
145            format!("{:.2}M", bytes as f64 / MB as f64)
146        } else if bytes >= KB {
147            format!("{:.2}K", bytes as f64 / KB as f64)
148        } else {
149            format!("{}B", bytes)
150        }
151    }
152
153    /// Format CPU usage in seconds
154    pub fn format_cpu_time(ns: u64) -> String {
155        let seconds = ns as f64 / 1_000_000_000.0;
156        format!("{:.2}s", seconds)
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_format_memory() {
166        assert_eq!(ResourceStats::format_memory(512), "512B");
167        assert_eq!(ResourceStats::format_memory(1024), "1.00K");
168        assert_eq!(ResourceStats::format_memory(1024 * 1024), "1.00M");
169        assert_eq!(ResourceStats::format_memory(1024 * 1024 * 1024), "1.00G");
170        assert_eq!(ResourceStats::format_memory(512 * 1024 * 1024), "512.00M");
171    }
172
173    #[test]
174    fn test_format_cpu_time() {
175        assert_eq!(ResourceStats::format_cpu_time(0), "0.00s");
176        assert_eq!(ResourceStats::format_cpu_time(1_000_000_000), "1.00s");
177        assert_eq!(ResourceStats::format_cpu_time(5_500_000_000), "5.50s");
178    }
179}