Skip to main content

nucleus/resources/
stats.rs

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