Skip to main content

concinnity_engine/app/
sysmem.rs

1//! Host-memory queries used to compute the process memory budget (see
2//! `app::budget`) and to report live usage. Two values, each best-effort: the
3//! machine's total physical RAM (the budget is a fraction of it) and this
4//! process's resident set size (what it is actually using now).
5//!
6//! Deliberately a small hand-rolled platform shim rather than a dependency like
7//! `sysinfo`: the engine needs exactly these two numbers, and both are a single
8//! syscall per platform. Every query returns `None` when the platform call is
9//! unavailable or fails, and the callers degrade gracefully (the budget falls
10//! back to the hard ceiling; a usage readout shows "unknown").
11
12// Total physical RAM installed on the machine, in bytes. `None` if the platform
13// query is unsupported or fails.
14pub(crate) fn total_physical_bytes() -> Option<u64> {
15    imp::total_physical_bytes()
16}
17
18/// Resident set size of the current process, in bytes: the physical memory it
19/// currently occupies. `None` if the platform query is unsupported or fails.
20pub fn process_resident_bytes() -> Option<u64> {
21    imp::process_resident_bytes()
22}
23
24#[cfg(target_os = "macos")]
25mod imp {
26    // hw.memsize is total physical RAM; task_info(MACH_TASK_BASIC_INFO) carries
27    // the process resident size.
28    pub(super) fn total_physical_bytes() -> Option<u64> {
29        let mut value: u64 = 0;
30        let mut len = std::mem::size_of::<u64>();
31        let name = c"hw.memsize";
32        // SAFETY: `name` is a valid NUL-terminated C string; `value`/`len`
33        // describe a correctly sized u64 output buffer for this sysctl.
34        let rc = unsafe {
35            libc::sysctlbyname(
36                name.as_ptr(),
37                &mut value as *mut u64 as *mut libc::c_void,
38                &mut len,
39                std::ptr::null_mut(),
40                0,
41            )
42        };
43        (rc == 0 && value > 0).then_some(value)
44    }
45
46    // `libc` deprecates its mach bindings in favor of the `mach2` crate;
47    // `mach_task_self_` is a stable fundamental symbol, so we keep the direct
48    // libc use rather than pull in another dependency for one static.
49    #[expect(
50        deprecated,
51        reason = "mach_task_self_ is a stable fundamental symbol, kept over pulling in mach2 for one static"
52    )]
53    pub(super) fn process_resident_bytes() -> Option<u64> {
54        // SAFETY: `mach_task_basic_info` is a plain C struct of integer
55        // fields, so all-zero is a valid inhabitant.
56        let mut info: libc::mach_task_basic_info = unsafe { std::mem::zeroed() };
57        let mut count = (std::mem::size_of::<libc::mach_task_basic_info>()
58            / std::mem::size_of::<libc::natural_t>())
59            as libc::mach_msg_type_number_t;
60        // SAFETY: `info`/`count` are a correctly sized MACH_TASK_BASIC_INFO
61        // output buffer; `mach_task_self_` is the current task port (the raw
62        // static behind the deprecated `mach_task_self()` wrapper).
63        let rc = unsafe {
64            libc::task_info(
65                libc::mach_task_self_,
66                libc::MACH_TASK_BASIC_INFO,
67                &mut info as *mut _ as libc::task_info_t,
68                &mut count,
69            )
70        };
71        (rc == libc::KERN_SUCCESS).then_some(info.resident_size)
72    }
73}
74
75#[cfg(target_os = "linux")]
76mod imp {
77    // MemTotal from /proc/meminfo (kB); resident pages (field 2) from
78    // /proc/self/statm times the page size.
79    pub(super) fn total_physical_bytes() -> Option<u64> {
80        let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
81        for line in meminfo.lines() {
82            if let Some(rest) = line.strip_prefix("MemTotal:") {
83                let kb: u64 = rest.split_whitespace().next()?.parse().ok()?;
84                return Some(kb * 1024);
85            }
86        }
87        None
88    }
89
90    pub(super) fn process_resident_bytes() -> Option<u64> {
91        let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
92        let resident_pages: u64 = statm.split_whitespace().nth(1)?.parse().ok()?;
93        // SAFETY: sysconf with a valid name has no memory effects.
94        let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
95        (page_size > 0).then(|| resident_pages * page_size as u64)
96    }
97}
98
99#[cfg(target_os = "windows")]
100mod imp {
101    use windows::Win32::System::ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};
102    use windows::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX};
103    use windows::Win32::System::Threading::GetCurrentProcess;
104
105    pub(super) fn total_physical_bytes() -> Option<u64> {
106        let mut status = MEMORYSTATUSEX {
107            dwLength: std::mem::size_of::<MEMORYSTATUSEX>() as u32,
108            ..Default::default()
109        };
110        // SAFETY: `status.dwLength` is set to the struct size as the API requires.
111        unsafe { GlobalMemoryStatusEx(&mut status) }.ok()?;
112        (status.ullTotalPhys > 0).then_some(status.ullTotalPhys)
113    }
114
115    pub(super) fn process_resident_bytes() -> Option<u64> {
116        let mut counters = PROCESS_MEMORY_COUNTERS::default();
117        // SAFETY: the counters buffer is sized to its own type, as the API requires.
118        let ok = unsafe {
119            GetProcessMemoryInfo(
120                GetCurrentProcess(),
121                &mut counters,
122                std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32,
123            )
124        };
125        ok.ok().map(|()| counters.WorkingSetSize as u64)
126    }
127}
128
129#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
130mod imp {
131    pub(super) fn total_physical_bytes() -> Option<u64> {
132        None
133    }
134    pub(super) fn process_resident_bytes() -> Option<u64> {
135        None
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    // On a supported platform the machine has some RAM and this process holds
144    // some resident memory; both queries return a plausible positive value.
145    // (macOS / Linux / Windows are all supported; other targets return None and
146    // skip the assertion.)
147    #[test]
148    fn queries_return_plausible_values() {
149        if cfg!(any(
150            target_os = "macos",
151            target_os = "linux",
152            target_os = "windows"
153        )) {
154            let total = total_physical_bytes().expect("total RAM query works on this platform");
155            assert!(
156                total >= 256 * 1024 * 1024,
157                "implausibly small total RAM: {total}"
158            );
159
160            let rss = process_resident_bytes().expect("RSS query works on this platform");
161            assert!(rss > 0, "process resident size should be positive");
162            assert!(rss <= total, "RSS {rss} exceeds total RAM {total}");
163        }
164    }
165}