use crate::{HypomnesisError, Result};
pub fn process_rss() -> Result<u64> {
#[cfg(target_os = "windows")]
{
windows_rss()
}
#[cfg(target_os = "linux")]
{
linux_rss()
}
#[cfg(target_os = "macos")]
{
macos_rss()
}
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
{
Err(HypomnesisError::Ram(
"RAM measurement not supported on this platform".into(),
))
}
}
#[cfg(target_os = "windows")]
mod win_ffi {
#[repr(C)]
pub(super) struct ProcessMemoryCounters {
pub cb: u32,
pub page_fault_count: u32,
pub peak_working_set_size: usize,
pub working_set_size: usize,
pub quota_peak_paged_pool_usage: usize,
pub quota_paged_pool_usage: usize,
pub quota_peak_non_paged_pool_usage: usize,
pub quota_non_paged_pool_usage: usize,
pub pagefile_usage: usize,
pub peak_pagefile_usage: usize,
}
#[allow(unsafe_code)]
unsafe extern "system" {
pub(super) safe fn GetCurrentProcess() -> isize;
pub(super) unsafe fn K32GetProcessMemoryInfo(
process: isize,
ppsmem_counters: *mut ProcessMemoryCounters,
cb: u32,
) -> i32;
pub(super) safe fn GetLastError() -> u32;
}
}
#[cfg(target_os = "windows")]
#[allow(unsafe_code)]
fn windows_rss() -> Result<u64> {
let mut counters = win_ffi::ProcessMemoryCounters {
cb: 0,
page_fault_count: 0,
peak_working_set_size: 0,
working_set_size: 0,
quota_peak_paged_pool_usage: 0,
quota_paged_pool_usage: 0,
quota_peak_non_paged_pool_usage: 0,
quota_non_paged_pool_usage: 0,
pagefile_usage: 0,
peak_pagefile_usage: 0,
};
#[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
let cb = std::mem::size_of::<win_ffi::ProcessMemoryCounters>() as u32;
counters.cb = cb;
let handle = win_ffi::GetCurrentProcess();
let ok = unsafe { win_ffi::K32GetProcessMemoryInfo(handle, &raw mut counters, cb) };
if ok != 0 {
#[allow(clippy::as_conversions)]
let rss = counters.working_set_size as u64;
Ok(rss)
} else {
let code = win_ffi::GetLastError();
Err(HypomnesisError::Ram(format!(
"K32GetProcessMemoryInfo failed (GetLastError = {code})"
)))
}
}
#[cfg(target_os = "linux")]
fn linux_rss() -> Result<u64> {
let status = std::fs::read_to_string("/proc/self/status")
.map_err(|e| HypomnesisError::Ram(format!("failed to read /proc/self/status: {e}")))?;
parse_vmrss(&status)
}
#[cfg(target_os = "linux")]
fn parse_vmrss(status: &str) -> Result<u64> {
for line in status.lines() {
if let Some(rest) = line.strip_prefix("VmRSS:") {
let kb_str = rest.trim().trim_end_matches(" kB").trim();
let kb: u64 = kb_str.parse().map_err(|e| {
HypomnesisError::Ram(format!("failed to parse VmRSS value '{kb_str}': {e}"))
})?;
return Ok(kb * 1024);
}
}
Err(HypomnesisError::Ram(
"VmRSS not found in /proc/self/status".into(),
))
}
#[cfg(target_os = "macos")]
mod darwin_ffi {
#[repr(C)]
pub(super) struct TaskVmInfo {
pub virtual_size: u64,
pub region_count: i32,
pub page_size: i32,
pub resident_size: u64,
pub resident_size_peak: u64,
pub device: u64,
pub device_peak: u64,
pub internal: u64,
pub internal_peak: u64,
pub external: u64,
pub external_peak: u64,
pub reusable: u64,
pub reusable_peak: u64,
pub purgeable_volatile_pmap: u64,
pub purgeable_volatile_resident: u64,
pub purgeable_volatile_virtual: u64,
pub compressed: u64,
pub compressed_peak: u64,
pub compressed_lifetime: u64,
pub phys_footprint: u64,
pub min_address: u64,
pub max_address: u64,
pub ledger_phys_footprint_peak: i64,
pub ledger_purgeable_nonvolatile: i64,
pub ledger_purgeable_novolatile_compressed: i64,
pub ledger_purgeable_volatile: i64,
pub ledger_purgeable_volatile_compressed: i64,
pub ledger_tag_network_nonvolatile: i64,
pub ledger_tag_network_nonvolatile_compressed: i64,
pub ledger_tag_network_volatile: i64,
pub ledger_tag_network_volatile_compressed: i64,
pub ledger_tag_media_footprint: i64,
pub ledger_tag_media_footprint_compressed: i64,
pub ledger_tag_media_nofootprint: i64,
pub ledger_tag_media_nofootprint_compressed: i64,
pub ledger_tag_graphics_footprint: i64,
pub ledger_tag_graphics_footprint_compressed: i64,
pub ledger_tag_neural_footprint: i64,
pub ledger_tag_neural_footprint_compressed: i64,
}
pub(super) const TASK_VM_INFO_PURGEABLE: u32 = 22;
#[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
pub(super) const TASK_VM_INFO_PURGEABLE_COUNT: u32 =
(size_of::<TaskVmInfo>() / size_of::<u32>()) as u32;
pub(super) const KERN_SUCCESS: i32 = 0;
#[allow(unsafe_code)]
unsafe extern "C" {
pub(super) safe fn mach_task_self() -> u32;
#[allow(non_snake_case)]
pub(super) unsafe fn task_info(
target_task: u32,
flavor: u32,
task_info_out: *mut u32,
task_info_outCnt: *mut u32,
) -> i32;
}
}
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
fn macos_rss() -> Result<u64> {
let mut info: darwin_ffi::TaskVmInfo = unsafe { core::mem::zeroed() };
let mut count: u32 = darwin_ffi::TASK_VM_INFO_PURGEABLE_COUNT;
let task = darwin_ffi::mach_task_self();
#[allow(clippy::as_conversions, clippy::ptr_as_ptr)]
let kr = unsafe {
darwin_ffi::task_info(
task,
darwin_ffi::TASK_VM_INFO_PURGEABLE,
(&raw mut info).cast::<u32>(),
&raw mut count,
)
};
if kr == darwin_ffi::KERN_SUCCESS {
Ok(info.phys_footprint)
} else {
Err(HypomnesisError::Ram(format!(
"task_info(TASK_VM_INFO_PURGEABLE) failed (kern_return = {kr})"
)))
}
}
#[cfg(all(test, target_os = "linux"))]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::missing_docs_in_private_items
)]
mod tests {
use super::*;
#[test]
fn parse_vmrss_basic() {
let status = "Name:\tcat\nVmPeak:\t 50000 kB\nVmRSS:\t 12345 kB\nThreads:\t1\n";
let rss = parse_vmrss(status).unwrap();
assert_eq!(rss, 12_345 * 1024);
}
#[test]
fn parse_vmrss_zero() {
let status = "VmRSS:\t 0 kB\n";
let rss = parse_vmrss(status).unwrap();
assert_eq!(rss, 0);
}
#[test]
fn parse_vmrss_no_kb_suffix() {
let status = "VmRSS:\t 42\n";
let rss = parse_vmrss(status).unwrap();
assert_eq!(rss, 42 * 1024);
}
#[test]
fn parse_vmrss_missing() {
let status = "Name:\tcat\nVmPeak:\t 50000 kB\nThreads:\t1\n";
let err = parse_vmrss(status).unwrap_err();
assert!(err.to_string().contains("VmRSS not found"));
}
#[test]
fn parse_vmrss_unparseable() {
let status = "Name:\tcat\nVmRSS:\tnot_a_number kB\n";
let err = parse_vmrss(status).unwrap_err();
assert!(err.to_string().contains("failed to parse"));
}
#[test]
fn parse_vmrss_real_proc_status() {
let status = "Name:\tbash\n\
Umask:\t0022\n\
State:\tS (sleeping)\n\
Tgid:\t12345\n\
VmPeak:\t 12000 kB\n\
VmSize:\t 11000 kB\n\
VmLck:\t 0 kB\n\
VmRSS:\t 4096 kB\n\
VmData:\t 500 kB\n\
Threads:\t1\n";
let rss = parse_vmrss(status).unwrap();
assert_eq!(rss, 4096 * 1024);
}
}