#[cfg(any(target_os = "linux", test))]
#[path = "sysres_cgroup.rs"]
mod cgroup;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
use std::time::Instant;
const SAMPLE_CACHE_INTERVAL_MICROS: u64 = 50_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemorySource {
CgroupV2,
CgroupV1,
ProcSelf,
MachTaskInfo,
WindowsProcess,
}
impl MemorySource {
pub fn as_str(self) -> &'static str {
match self {
MemorySource::CgroupV2 => "cgroup_v2",
MemorySource::CgroupV1 => "cgroup_v1",
MemorySource::ProcSelf => "proc_self",
MemorySource::MachTaskInfo => "mach_task_info",
MemorySource::WindowsProcess => "windows_process",
}
}
fn to_code(self) -> u8 {
match self {
MemorySource::CgroupV2 => 1,
MemorySource::CgroupV1 => 2,
MemorySource::ProcSelf => 3,
MemorySource::MachTaskInfo => 4,
MemorySource::WindowsProcess => 5,
}
}
fn from_code(code: u8) -> Option<Self> {
match code {
1 => Some(MemorySource::CgroupV2),
2 => Some(MemorySource::CgroupV1),
3 => Some(MemorySource::ProcSelf),
4 => Some(MemorySource::MachTaskInfo),
5 => Some(MemorySource::WindowsProcess),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MemoryReading {
pub used_bytes: u64,
pub limit_bytes: Option<u64>,
pub limit_is_enforced: bool,
pub source: MemorySource,
}
impl MemoryReading {
pub fn utilisation(&self) -> Option<f64> {
let limit = self.limit_bytes.filter(|limit| *limit > 0)?;
Some(self.used_bytes as f64 / limit as f64)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RssSample {
pub current_bytes: Option<u64>,
pub peak_bytes: Option<u64>,
}
pub fn sample() -> RssSample {
RssSample {
current_bytes: current_rss(),
peak_bytes: peak_rss(),
}
}
static CACHE_USED: AtomicU64 = AtomicU64::new(0);
static CACHE_LIMIT: AtomicU64 = AtomicU64::new(u64::MAX);
static CACHE_SOURCE: AtomicU8 = AtomicU8::new(0);
static CACHE_AT_MICROS: AtomicU64 = AtomicU64::new(0);
const ENFORCED_LIMIT_BIT: u8 = 0b1000_0000;
fn process_start() -> Instant {
static START: OnceLock<Instant> = OnceLock::new();
*START.get_or_init(Instant::now)
}
pub fn memory_reading() -> Option<MemoryReading> {
let now = process_start().elapsed().as_micros().max(1) as u64;
let last = CACHE_AT_MICROS.load(Ordering::Relaxed);
if last != 0 && now.saturating_sub(last) < SAMPLE_CACHE_INTERVAL_MICROS {
return load_cached();
}
let fresh = memory_reading_uncached();
store_cached(fresh, now);
fresh
}
pub fn memory_reading_uncached() -> Option<MemoryReading> {
read_memory()
}
pub fn detected_memory_limit() -> Option<(u64, bool)> {
let reading = memory_reading()?;
Some((reading.limit_bytes?, reading.limit_is_enforced))
}
fn encode_reading(reading: Option<MemoryReading>) -> (u64, u64, u8) {
match reading {
Some(reading) => {
let enforced = if reading.limit_is_enforced {
ENFORCED_LIMIT_BIT
} else {
0
};
(
reading.used_bytes,
reading.limit_bytes.unwrap_or(u64::MAX),
reading.source.to_code() | enforced,
)
}
None => (0, u64::MAX, 0),
}
}
fn decode_reading(used: u64, limit: u64, source: u8) -> Option<MemoryReading> {
Some(MemoryReading {
used_bytes: used,
limit_bytes: (limit != u64::MAX).then_some(limit),
limit_is_enforced: source & ENFORCED_LIMIT_BIT != 0,
source: MemorySource::from_code(source & !ENFORCED_LIMIT_BIT)?,
})
}
fn load_cached() -> Option<MemoryReading> {
decode_reading(
CACHE_USED.load(Ordering::Relaxed),
CACHE_LIMIT.load(Ordering::Relaxed),
CACHE_SOURCE.load(Ordering::Relaxed),
)
}
fn store_cached(reading: Option<MemoryReading>, at_micros: u64) {
let (used, limit, source) = encode_reading(reading);
CACHE_USED.store(used, Ordering::Relaxed);
CACHE_LIMIT.store(limit, Ordering::Relaxed);
CACHE_SOURCE.store(source, Ordering::Relaxed);
CACHE_AT_MICROS.store(at_micros, Ordering::Relaxed);
}
#[cfg(target_os = "linux")]
fn read_memory() -> Option<MemoryReading> {
let charged = cgroup::read_v2()
.map(|reading| (reading, MemorySource::CgroupV2))
.or_else(|| cgroup::read_v1().map(|reading| (reading, MemorySource::CgroupV1)));
if let Some((reading, source)) = charged {
let (limit_bytes, limit_is_enforced) = effective_limit(reading.limit_bytes, cgroup::read_mem_total());
return Some(MemoryReading {
used_bytes: reading.used_bytes,
limit_bytes,
limit_is_enforced,
source,
});
}
Some(MemoryReading {
used_bytes: cgroup::read_statm_rss()?,
limit_bytes: cgroup::read_mem_total(),
limit_is_enforced: false,
source: MemorySource::ProcSelf,
})
}
#[cfg(any(target_os = "linux", test))]
fn effective_limit(cgroup_limit: Option<u64>, mem_total: Option<u64>) -> (Option<u64>, bool) {
match (cgroup_limit, mem_total) {
(Some(limit), Some(total)) if limit < total => (Some(limit), true),
(Some(limit), None) => (Some(limit), true),
(_, total) => (total, false),
}
}
#[cfg(target_os = "macos")]
fn read_memory() -> Option<MemoryReading> {
Some(MemoryReading {
used_bytes: mac_phys_footprint()?,
limit_bytes: mac_hw_memsize(),
limit_is_enforced: false,
source: MemorySource::MachTaskInfo,
})
}
#[cfg(windows)]
fn read_memory() -> Option<MemoryReading> {
Some(MemoryReading {
used_bytes: windows_working_set()?,
limit_bytes: windows_total_physical(),
limit_is_enforced: false,
source: MemorySource::WindowsProcess,
})
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
fn read_memory() -> Option<MemoryReading> {
None
}
#[cfg(target_os = "macos")]
fn mac_hw_memsize() -> Option<u64> {
let mut value: u64 = 0;
let mut len = std::mem::size_of::<u64>();
let name = c"hw.memsize";
let rc = unsafe {
libc::sysctlbyname(
name.as_ptr(),
(&raw mut value).cast::<libc::c_void>(),
&mut len,
std::ptr::null_mut(),
0,
)
};
(rc == 0 && value > 0).then_some(value)
}
#[cfg(target_os = "macos")]
fn mac_phys_footprint() -> Option<u64> {
use std::mem;
use mach2::kern_return::KERN_SUCCESS;
use mach2::message::mach_msg_type_number_t;
use mach2::task::task_info;
use mach2::task_info::{TASK_VM_INFO, task_info_t, task_vm_info};
use mach2::traps::mach_task_self;
unsafe {
let mut info = task_vm_info::default();
let mut count = (mem::size_of::<task_vm_info>() / mem::size_of::<u32>()) as mach_msg_type_number_t;
let kr = task_info(
mach_task_self(),
TASK_VM_INFO,
(&mut info as *mut task_vm_info).cast::<i32>() as task_info_t,
&mut count,
);
if kr == KERN_SUCCESS {
let footprint = info.phys_footprint;
Some(footprint)
} else {
None
}
}
}
pub fn phys_footprint() -> Option<u64> {
memory_reading().map(|reading| reading.used_bytes)
}
#[cfg(windows)]
#[repr(C)]
#[derive(Default)]
struct MemoryStatusEx {
length: u32,
memory_load: u32,
total_phys: u64,
avail_phys: u64,
total_page_file: u64,
avail_page_file: u64,
total_virtual: u64,
avail_virtual: u64,
avail_extended_virtual: u64,
}
#[cfg(windows)]
#[repr(C)]
#[derive(Default)]
struct ProcessMemoryCounters {
cb: u32,
page_fault_count: u32,
peak_working_set_size: usize,
working_set_size: usize,
quota_peak_paged_pool_usage: usize,
quota_paged_pool_usage: usize,
quota_peak_non_paged_pool_usage: usize,
quota_non_paged_pool_usage: usize,
pagefile_usage: usize,
peak_pagefile_usage: usize,
}
#[cfg(windows)]
#[link(name = "kernel32")]
unsafe extern "system" {
fn GlobalMemoryStatusEx(buffer: *mut MemoryStatusEx) -> i32;
fn GetCurrentProcess() -> *mut core::ffi::c_void;
fn K32GetProcessMemoryInfo(process: *mut core::ffi::c_void, counters: *mut ProcessMemoryCounters, cb: u32) -> i32;
}
#[cfg(windows)]
fn windows_working_set() -> Option<u64> {
let mut counters = ProcessMemoryCounters {
cb: std::mem::size_of::<ProcessMemoryCounters>() as u32,
..ProcessMemoryCounters::default()
};
let ok = unsafe { K32GetProcessMemoryInfo(GetCurrentProcess(), &mut counters, counters.cb) };
(ok != 0).then_some(counters.working_set_size as u64)
}
#[cfg(windows)]
fn windows_total_physical() -> Option<u64> {
let mut status = MemoryStatusEx {
length: std::mem::size_of::<MemoryStatusEx>() as u32,
..MemoryStatusEx::default()
};
let ok = unsafe { GlobalMemoryStatusEx(&mut status) };
(ok != 0 && status.total_phys > 0).then_some(status.total_phys)
}
#[cfg(target_os = "macos")]
fn current_rss() -> Option<u64> {
use std::mem;
use mach2::kern_return::KERN_SUCCESS;
use mach2::message::mach_msg_type_number_t;
use mach2::task::task_info;
use mach2::task_info::{MACH_TASK_BASIC_INFO, mach_task_basic_info, task_info_t};
use mach2::traps::mach_task_self;
unsafe {
let mut info = mem::zeroed::<mach_task_basic_info>();
let mut count = (mem::size_of::<mach_task_basic_info>() / mem::size_of::<u32>()) as mach_msg_type_number_t;
let kr = task_info(
mach_task_self(),
MACH_TASK_BASIC_INFO,
(&mut info as *mut mach_task_basic_info).cast::<i32>() as task_info_t,
&mut count,
);
if kr == KERN_SUCCESS {
Some(info.resident_size)
} else {
None
}
}
}
#[cfg(target_os = "linux")]
fn current_rss() -> Option<u64> {
cgroup::read_statm_rss()
}
#[cfg(windows)]
fn current_rss() -> Option<u64> {
windows_working_set()
}
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
fn current_rss() -> Option<u64> {
None
}
#[cfg(unix)]
fn peak_rss() -> Option<u64> {
use std::mem;
unsafe {
let mut usage: libc::rusage = mem::zeroed();
if libc::getrusage(libc::RUSAGE_SELF, &mut usage) != 0 {
return None;
}
let maxrss = usage.ru_maxrss.max(0) as u64;
#[cfg(target_os = "macos")]
let bytes = maxrss;
#[cfg(not(target_os = "macos"))]
let bytes = maxrss.saturating_mul(1024);
Some(bytes)
}
}
#[cfg(windows)]
fn peak_rss() -> Option<u64> {
let mut counters = ProcessMemoryCounters {
cb: std::mem::size_of::<ProcessMemoryCounters>() as u32,
..ProcessMemoryCounters::default()
};
let ok = unsafe { K32GetProcessMemoryInfo(GetCurrentProcess(), &mut counters, counters.cb) };
(ok != 0).then_some(counters.peak_working_set_size as u64)
}
#[cfg(not(any(unix, windows)))]
fn peak_rss() -> Option<u64> {
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_codes_round_trip_and_reserve_zero() {
for source in [
MemorySource::CgroupV2,
MemorySource::CgroupV1,
MemorySource::ProcSelf,
MemorySource::MachTaskInfo,
MemorySource::WindowsProcess,
] {
assert_ne!(source.to_code(), 0, "0 is the no-reading marker");
assert_eq!(MemorySource::from_code(source.to_code()), Some(source));
}
assert_eq!(MemorySource::from_code(0), None);
assert_eq!(MemorySource::from_code(200), None);
}
#[test]
fn a_cgroup_limit_below_total_ram_is_the_enforced_one() {
assert_eq!(effective_limit(Some(2 << 30), Some(64 << 30)), (Some(2 << 30), true));
}
#[test]
fn a_cgroup_with_no_limit_still_gets_total_ram_as_an_advisory_ceiling() {
assert_eq!(effective_limit(None, Some(64 << 30)), (Some(64 << 30), false));
}
#[test]
fn a_cgroup_limit_above_total_ram_is_not_the_binding_constraint() {
assert_eq!(
effective_limit(Some(100 << 30), Some(16 << 30)),
(Some(16 << 30), false)
);
assert_eq!(effective_limit(Some(16 << 30), Some(16 << 30)), (Some(16 << 30), false));
}
#[test]
fn a_cgroup_limit_stands_alone_when_meminfo_is_unreadable() {
assert_eq!(effective_limit(Some(2 << 30), None), (Some(2 << 30), true));
assert_eq!(effective_limit(None, None), (None, false));
}
#[test]
fn utilisation_is_none_without_a_usable_limit() {
let mut reading = MemoryReading {
used_bytes: 512,
limit_bytes: Some(1024),
limit_is_enforced: true,
source: MemorySource::CgroupV2,
};
assert_eq!(reading.utilisation(), Some(0.5));
reading.limit_bytes = None;
assert_eq!(reading.utilisation(), None);
reading.limit_bytes = Some(0);
assert_eq!(reading.utilisation(), None);
}
#[test]
fn cache_encoding_round_trips_including_the_absent_limit() {
for reading in [
Some(MemoryReading {
used_bytes: 4096,
limit_bytes: None,
limit_is_enforced: false,
source: MemorySource::ProcSelf,
}),
Some(MemoryReading {
used_bytes: 8192,
limit_bytes: Some(1 << 30),
limit_is_enforced: true,
source: MemorySource::CgroupV2,
}),
Some(MemoryReading {
used_bytes: 8192,
limit_bytes: Some(1 << 30),
limit_is_enforced: false,
source: MemorySource::CgroupV2,
}),
None,
] {
let (used, limit, source) = encode_reading(reading);
assert_eq!(decode_reading(used, limit, source), reading);
}
}
#[test]
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
fn memory_reading_answers_on_a_supported_platform() {
let reading = memory_reading_uncached().expect("a supported platform must report memory");
assert!(
reading.used_bytes > 0,
"usage must be positive, got {}",
reading.used_bytes
);
let limit = reading.limit_bytes.expect("a supported platform must report a limit");
assert!(limit > 0, "limit must be positive, got {limit}");
}
#[test]
#[cfg(unix)]
fn sample_reports_nonzero_rss_on_unix() {
let s = sample();
let current = s.current_bytes.expect("current RSS should be readable on unix");
assert!(current > 0, "current RSS must be positive, got {current}");
let peak = s.peak_bytes.expect("peak RSS should be readable on unix");
assert!(peak > 0, "peak RSS must be positive, got {peak}");
#[cfg(target_os = "macos")]
assert!(
peak >= current,
"peak RSS ({peak}) should be >= current RSS ({current})"
);
}
#[test]
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
fn phys_footprint_reports_positive_bytes() {
let footprint = phys_footprint().expect("phys_footprint should be readable here");
assert!(footprint > 0, "phys_footprint must be positive, got {footprint}");
}
}