Skip to main content

sklears_core/
system_info.rs

1//! Real system memory statistics.
2//!
3//! Reads actual memory information from the OS — never fabricates values.
4//! On unsupported platforms returns `None` or `Err` rather than inventing numbers.
5
6use crate::error::{Result, SklearsError};
7
8/// System-wide memory statistics in bytes.
9#[derive(Debug, Clone)]
10pub struct SystemMemory {
11    /// Total physical RAM in bytes.
12    pub total: u64,
13    /// Available (free + reclaimable) RAM in bytes.
14    pub available: u64,
15    /// Used RAM in bytes (`total - available`).
16    pub used: u64,
17}
18
19/// Read real system-wide memory statistics.
20///
21/// # Platform support
22/// - **Linux**: parses `/proc/meminfo` (MemTotal / MemAvailable lines).
23/// - **Other Unix**: uses `libc::sysconf(_SC_PHYS_PAGES)` and `_SC_AVPHYS_PAGES`.
24/// - **Windows**: uses `winapi::um::sysinfoapi::GlobalMemoryStatusEx`.
25///
26/// Returns `Err` if the platform APIs are unavailable or parsing fails.
27pub fn system_memory() -> Result<SystemMemory> {
28    system_memory_impl()
29}
30
31/// Read this process's Resident Set Size in bytes.
32///
33/// Returns `None` on unsupported platforms (honest unknown, not zero).
34pub fn process_rss_bytes() -> Option<u64> {
35    process_rss_impl()
36}
37
38// ── Linux implementation ──────────────────────────────────────────────────────
39
40#[cfg(target_os = "linux")]
41fn system_memory_impl() -> Result<SystemMemory> {
42    let content = std::fs::read_to_string("/proc/meminfo")
43        .map_err(|e| SklearsError::InvalidOperation(format!("cannot read /proc/meminfo: {}", e)))?;
44
45    let mut mem_total: Option<u64> = None;
46    let mut mem_available: Option<u64> = None;
47
48    for line in content.lines() {
49        if let Some(rest) = line.strip_prefix("MemTotal:") {
50            mem_total = Some(parse_kb_line(rest)?);
51        } else if let Some(rest) = line.strip_prefix("MemAvailable:") {
52            mem_available = Some(parse_kb_line(rest)?);
53        }
54        if mem_total.is_some() && mem_available.is_some() {
55            break;
56        }
57    }
58
59    let total = mem_total.ok_or_else(|| {
60        SklearsError::InvalidOperation("MemTotal not found in /proc/meminfo".to_string())
61    })?;
62    let available = mem_available.ok_or_else(|| {
63        SklearsError::InvalidOperation("MemAvailable not found in /proc/meminfo".to_string())
64    })?;
65    let used = total.saturating_sub(available);
66
67    Ok(SystemMemory {
68        total,
69        available,
70        used,
71    })
72}
73
74#[cfg(target_os = "linux")]
75fn parse_kb_line(rest: &str) -> Result<u64> {
76    // Format: "   <number> kB"
77    let trimmed = rest.trim();
78    let kb_str = trimmed
79        .split_whitespace()
80        .next()
81        .ok_or_else(|| SklearsError::InvalidOperation("empty /proc/meminfo value".to_string()))?;
82    let kb: u64 = kb_str.parse().map_err(|_| {
83        SklearsError::InvalidOperation(format!("cannot parse /proc/meminfo value: {}", kb_str))
84    })?;
85    Ok(kb * 1024)
86}
87
88#[cfg(target_os = "linux")]
89fn process_rss_impl() -> Option<u64> {
90    // /proc/self/statm: fields space-separated, field index 1 = resident pages
91    let content = std::fs::read_to_string("/proc/self/statm").ok()?;
92    let resident_pages: u64 = content.split_whitespace().nth(1)?.parse().ok()?;
93    let page_size = page_size_bytes()?;
94    Some(resident_pages * page_size)
95}
96
97// ── Non-Linux Unix implementation ─────────────────────────────────────────────
98
99#[cfg(all(
100    target_family = "unix",
101    not(target_os = "linux"),
102    not(target_os = "macos")
103))]
104fn system_memory_impl() -> Result<SystemMemory> {
105    let total = unix_sysconf_bytes(libc::_SC_PHYS_PAGES).ok_or_else(|| {
106        SklearsError::InvalidOperation("sysconf(_SC_PHYS_PAGES) returned unavailable".to_string())
107    })?;
108    let available = unix_sysconf_bytes(libc::_SC_AVPHYS_PAGES).ok_or_else(|| {
109        SklearsError::InvalidOperation("sysconf(_SC_AVPHYS_PAGES) returned unavailable".to_string())
110    })?;
111    let used = total.saturating_sub(available);
112    Ok(SystemMemory {
113        total,
114        available,
115        used,
116    })
117}
118
119#[cfg(all(
120    target_family = "unix",
121    not(target_os = "linux"),
122    not(target_os = "macos")
123))]
124fn unix_sysconf_bytes(name: libc::c_int) -> Option<u64> {
125    // SAFETY: sysconf is safe to call with standard constants
126    let pages = unsafe { libc::sysconf(name) };
127    if pages < 0 {
128        return None;
129    }
130    let page_size = page_size_bytes()?;
131    Some(pages as u64 * page_size)
132}
133
134#[cfg(all(
135    target_family = "unix",
136    not(target_os = "linux"),
137    not(target_os = "macos")
138))]
139fn process_rss_impl() -> Option<u64> {
140    // rusage.ru_maxrss — on BSDs (non-macOS, non-Linux) this is kilobytes.
141    let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
142    // SAFETY: &mut usage is valid, RUSAGE_SELF is a valid constant
143    let ret = unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) };
144    if ret != 0 {
145        return None;
146    }
147    let rss = usage.ru_maxrss as u64 * 1024; // kB on other BSDs
148    Some(rss)
149}
150
151// ── macOS implementation ──────────────────────────────────────────────────────
152
153// Declare mach_host_self directly to avoid the libc deprecation warning;
154// the libc crate marks it deprecated in favour of the `mach2` crate, but
155// adding a new dependency for a single trap call is unnecessary.
156#[cfg(target_os = "macos")]
157extern "C" {
158    fn mach_host_self() -> libc::mach_port_t;
159}
160
161#[cfg(target_os = "macos")]
162fn system_memory_impl() -> Result<SystemMemory> {
163    let total = macos_total_memory().ok_or_else(|| {
164        SklearsError::InvalidOperation("sysctl hw.memsize failed on macOS".to_string())
165    })?;
166    let available = macos_available_memory().ok_or_else(|| {
167        SklearsError::InvalidOperation("host_statistics64 failed on macOS".to_string())
168    })?;
169    let used = total.saturating_sub(available);
170    Ok(SystemMemory {
171        total,
172        available,
173        used,
174    })
175}
176
177#[cfg(target_os = "macos")]
178fn macos_total_memory() -> Option<u64> {
179    let mut value: u64 = 0;
180    let mut len = std::mem::size_of::<u64>();
181    // SAFETY: sysctlbyname with a well-known constant name; output pointer is valid
182    let ret = unsafe {
183        libc::sysctlbyname(
184            c"hw.memsize".as_ptr(),
185            &mut value as *mut u64 as *mut libc::c_void,
186            &mut len,
187            std::ptr::null_mut(),
188            0,
189        )
190    };
191    if ret == 0 {
192        Some(value)
193    } else {
194        None
195    }
196}
197
198#[cfg(target_os = "macos")]
199fn macos_available_memory() -> Option<u64> {
200    let page_size = page_size_bytes()?;
201    let mut vm_stats: libc::vm_statistics64 = unsafe { std::mem::zeroed() };
202    let mut count: libc::mach_msg_type_number_t = (std::mem::size_of::<libc::vm_statistics64>()
203        / std::mem::size_of::<libc::integer_t>())
204        as libc::mach_msg_type_number_t;
205    // SAFETY: mach_host_self() is always valid; pointers are properly sized
206    let ret = unsafe {
207        libc::host_statistics64(
208            mach_host_self(),
209            libc::HOST_VM_INFO64 as libc::host_flavor_t,
210            &mut vm_stats as *mut _ as *mut libc::integer_t,
211            &mut count,
212        )
213    };
214    if ret != libc::KERN_SUCCESS {
215        return None;
216    }
217    let free_pages = vm_stats.free_count as u64 + vm_stats.inactive_count as u64;
218    Some(free_pages * page_size)
219}
220
221#[cfg(target_os = "macos")]
222fn process_rss_impl() -> Option<u64> {
223    let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
224    // SAFETY: getrusage is safe with RUSAGE_SELF and a valid pointer
225    let ret = unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) };
226    if ret != 0 {
227        return None;
228    }
229    // On macOS, ru_maxrss is in bytes (unlike Linux where it's kilobytes)
230    Some(usage.ru_maxrss as u64)
231}
232
233// ── Windows implementation ───────────────────────────────────────────────────
234
235#[cfg(target_os = "windows")]
236fn system_memory_impl() -> Result<SystemMemory> {
237    use winapi::um::processthreadsapi::GetCurrentProcess;
238    use winapi::um::psapi::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};
239    use winapi::um::sysinfoapi::{GlobalMemoryStatusEx, MEMORYSTATUSEX};
240
241    let mut mem_status: MEMORYSTATUSEX = unsafe { std::mem::zeroed() };
242    mem_status.dwLength = std::mem::size_of::<MEMORYSTATUSEX>() as u32;
243
244    // SAFETY: mem_status is correctly initialised above
245    let ok = unsafe { GlobalMemoryStatusEx(&mut mem_status) };
246    if ok == 0 {
247        return Err(SklearsError::InvalidOperation(
248            "GlobalMemoryStatusEx failed".to_string(),
249        ));
250    }
251
252    let total = mem_status.ullTotalPhys;
253    let available = mem_status.ullAvailPhys;
254    let used = total.saturating_sub(available);
255
256    Ok(SystemMemory {
257        total,
258        available,
259        used,
260    })
261}
262
263#[cfg(target_os = "windows")]
264fn process_rss_impl() -> Option<u64> {
265    use winapi::um::processthreadsapi::GetCurrentProcess;
266    use winapi::um::psapi::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};
267
268    let mut pmc: PROCESS_MEMORY_COUNTERS = unsafe { std::mem::zeroed() };
269    let size = std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;
270    // SAFETY: pmc is zero-initialised, size is correct
271    let ok = unsafe { GetProcessMemoryInfo(GetCurrentProcess(), &mut pmc, size) };
272    if ok == 0 {
273        return None;
274    }
275    Some(pmc.WorkingSetSize as u64)
276}
277
278// ── Unsupported platforms ────────────────────────────────────────────────────
279
280#[cfg(not(any(target_family = "unix", target_os = "windows")))]
281fn system_memory_impl() -> Result<SystemMemory> {
282    Err(SklearsError::NotImplemented(
283        "system_memory() is not implemented on this platform".to_string(),
284    ))
285}
286
287#[cfg(not(any(target_family = "unix", target_os = "windows")))]
288fn process_rss_impl() -> Option<u64> {
289    None
290}
291
292// ── Shared helpers ──────────────────────────────────────────────────────────
293
294/// Returns the OS page size in bytes, or `None` if unavailable.
295#[cfg(target_family = "unix")]
296fn page_size_bytes() -> Option<u64> {
297    // SAFETY: _SC_PAGESIZE is a valid sysconf constant
298    let ps = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
299    if ps <= 0 {
300        None
301    } else {
302        Some(ps as u64)
303    }
304}