mz_ore/memory.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Physical memory introspection.
17
18// Only this probe uses the cgroup helpers, so the module lives here rather
19// than at the crate root. The file carries more surface than the probe
20// needs, hence the dead-code allowance.
21#[cfg(target_os = "linux")]
22#[path = "cgroup.rs"]
23#[allow(dead_code)]
24mod cgroup;
25
26/// Returns the physical memory available to this process in bytes: the
27/// host's RAM, clamped by the cgroup memory limit when one is set. Both
28/// cgroup v1 and v2 are honored, resolved through `/proc/self/mountinfo`
29/// and `/proc/self/cgroup` rather than an assumed mount path, so containers
30/// (including host-namespace cgroup mounts) do not derive budgets from host
31/// RAM. `None` if detection fails.
32///
33/// Deliberately distinct from any *announced* memory limit: on nodes whose
34/// disk is provisioned as swap, the announced limit includes swap so the
35/// memory limiter can bound total heap. Budgets that bound *resident* bytes
36/// must instead derive from memory that can be resident, which is what this
37/// reports.
38pub fn physical_memory_bytes() -> Option<usize> {
39 let host = host_memory_bytes()?;
40 match cgroup_memory_max() {
41 Some(limit) if limit < host => Some(limit),
42 _ => Some(host),
43 }
44}
45
46#[cfg(target_os = "linux")]
47fn host_memory_bytes() -> Option<usize> {
48 let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
49 let line = meminfo.lines().find(|l| l.starts_with("MemTotal:"))?;
50 let kib: usize = line.split_whitespace().nth(1)?.parse().ok()?;
51 Some(kib * 1024)
52}
53
54#[cfg(target_os = "macos")]
55fn host_memory_bytes() -> Option<usize> {
56 let mut size: u64 = 0;
57 let mut len = std::mem::size_of::<u64>();
58 // SAFETY: `sysctlbyname` reads into an out-buffer of the size we report;
59 // `hw.memsize` is a `u64` and `len` matches.
60 let ret = unsafe {
61 libc::sysctlbyname(
62 c"hw.memsize".as_ptr(),
63 std::ptr::from_mut(&mut size).cast::<libc::c_void>(),
64 &mut len,
65 std::ptr::null_mut(),
66 0,
67 )
68 };
69 if ret == 0 {
70 usize::try_from(size).ok()
71 } else {
72 None
73 }
74}
75
76#[cfg(not(any(target_os = "linux", target_os = "macos")))]
77fn host_memory_bytes() -> Option<usize> {
78 None
79}
80
81/// The RAM limit of the cgroup governing this process, if any.
82#[cfg(target_os = "linux")]
83fn cgroup_memory_max() -> Option<usize> {
84 cgroup::detect_memory_limit()?.max
85}
86
87#[cfg(not(target_os = "linux"))]
88fn cgroup_memory_max() -> Option<usize> {
89 None
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[mz_ore::test]
97 fn detects_some_memory() {
98 let bytes = physical_memory_bytes().expect("detection works on test platforms");
99 // Sanity: more than 64 MiB, less than 1 PiB.
100 assert!(bytes > 64 << 20);
101 assert!(bytes < 1 << 50);
102 }
103}