use crate::{HypomnesisError, Result};
pub fn process_rss() -> Result<u64> {
#[cfg(target_os = "windows")]
{
windows_rss()
}
#[cfg(target_os = "linux")]
{
linux_rss()
}
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
{
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(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);
}
}