#[derive(Debug, Clone, Default)]
pub struct ProcessMetrics {
pub rss_bytes: u64,
pub cpu_percent: f32,
}
pub struct MetricsCollector {
last_cpu_time_us: u64,
last_wall_time: std::time::Instant,
}
impl Default for MetricsCollector {
fn default() -> Self {
Self::new()
}
}
impl MetricsCollector {
pub fn new() -> Self {
Self {
last_cpu_time_us: current_cpu_time_us(),
last_wall_time: std::time::Instant::now(),
}
}
pub fn sample(&mut self) -> ProcessMetrics {
let rss_bytes = current_rss_bytes();
let now_cpu = current_cpu_time_us();
let now_wall = std::time::Instant::now();
let cpu_delta_us = now_cpu.saturating_sub(self.last_cpu_time_us);
let wall_delta_us = now_wall.duration_since(self.last_wall_time).as_micros() as u64;
let cpu_percent = if wall_delta_us > 0 {
(cpu_delta_us as f64 / wall_delta_us as f64 * 100.0) as f32
} else {
0.0
};
self.last_cpu_time_us = now_cpu;
self.last_wall_time = now_wall;
ProcessMetrics {
rss_bytes,
cpu_percent,
}
}
}
#[cfg(target_os = "macos")]
unsafe extern "C" {
static mach_task_self_: libc::mach_port_t;
}
#[cfg(target_os = "macos")]
fn current_rss_bytes() -> u64 {
use std::mem;
#[repr(C)]
struct TaskVmInfo {
virtual_size: u64,
region_count: i32,
page_size: i32,
resident_size: u64,
resident_size_peak: u64,
device: u64,
device_peak: u64,
internal: u64,
internal_peak: u64,
external: u64,
external_peak: u64,
reusable: u64,
reusable_peak: u64,
purgeable_volatile_pmap: u64,
purgeable_volatile_resident: u64,
purgeable_volatile_virtual: u64,
compressed: u64,
compressed_peak: u64,
phys_footprint: u64,
}
const TASK_VM_INFO: u32 = 22;
unsafe {
let mut info: TaskVmInfo = mem::zeroed();
let mut count = (mem::size_of::<TaskVmInfo>() / mem::size_of::<libc::natural_t>())
as libc::mach_msg_type_number_t;
let kr = libc::task_info(
mach_task_self_,
TASK_VM_INFO,
&mut info as *mut _ as libc::task_info_t,
&mut count,
);
if kr == libc::KERN_SUCCESS {
if info.phys_footprint > 0 {
info.phys_footprint
} else {
info.resident_size
}
} else {
0
}
}
}
#[cfg(target_os = "linux")]
fn page_size() -> u64 {
unsafe { libc::sysconf(libc::_SC_PAGESIZE) as u64 }
}
#[cfg(target_os = "linux")]
fn current_rss_bytes() -> u64 {
let ps = page_size();
std::fs::read_to_string("/proc/self/statm")
.ok()
.and_then(|s| {
let mut parts = s.split_whitespace();
parts.next(); parts
.next()
.and_then(|rss| rss.parse::<u64>().ok())
.map(|pages| pages * ps)
})
.unwrap_or(0)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
fn current_rss_bytes() -> u64 {
0
}
pub fn child_rss_bytes(pid: u32) -> u64 {
child_rss_bytes_impl(pid)
}
#[cfg(target_os = "macos")]
fn child_rss_bytes_impl(pid: u32) -> u64 {
use std::mem;
#[repr(C)]
struct ProcTaskInfo {
pti_virtual_size: u64,
pti_resident_size: u64,
_padding: [u8; 120],
}
const PROC_PIDTASKINFO: i32 = 4;
unsafe extern "C" {
fn proc_pidinfo(
pid: i32,
flavor: i32,
arg: u64,
buffer: *mut std::ffi::c_void,
buffersize: i32,
) -> i32;
}
unsafe {
let mut info: ProcTaskInfo = mem::zeroed();
let size = mem::size_of::<ProcTaskInfo>() as i32;
let ret = proc_pidinfo(
pid as i32,
PROC_PIDTASKINFO,
0,
&mut info as *mut _ as *mut std::ffi::c_void,
size,
);
if ret > 0 { info.pti_resident_size } else { 0 }
}
}
#[cfg(target_os = "linux")]
fn child_rss_bytes_impl(pid: u32) -> u64 {
let ps = page_size();
std::fs::read_to_string(format!("/proc/{pid}/statm"))
.ok()
.and_then(|s| {
let mut parts = s.split_whitespace();
parts.next(); parts
.next()
.and_then(|rss| rss.parse::<u64>().ok())
.map(|pages| pages * ps)
})
.unwrap_or(0)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
fn child_rss_bytes_impl(_pid: u32) -> u64 {
0
}
#[cfg(unix)]
fn current_cpu_time_us() -> u64 {
unsafe {
let mut usage: libc::rusage = std::mem::zeroed();
if libc::getrusage(libc::RUSAGE_SELF, &mut usage) == 0 {
let user_us = usage.ru_utime.tv_sec as u64 * 1_000_000 + usage.ru_utime.tv_usec as u64;
let sys_us = usage.ru_stime.tv_sec as u64 * 1_000_000 + usage.ru_stime.tv_usec as u64;
user_us + sys_us
} else {
0
}
}
}
#[cfg(not(unix))]
fn current_cpu_time_us() -> u64 {
0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_collector_produces_values() {
let mut collector = MetricsCollector::new();
let data: Vec<u8> = std::hint::black_box(vec![1u8; 1024 * 1024]); std::hint::black_box(&data);
let metrics = collector.sample();
#[cfg(any(target_os = "macos", target_os = "linux"))]
assert!(metrics.rss_bytes > 0, "RSS should be non-zero");
assert!(metrics.cpu_percent >= 0.0);
}
#[test]
fn test_format_rss() {
let m = ProcessMetrics {
rss_bytes: 52_428_800, cpu_percent: 12.5,
};
assert_eq!(m.rss_bytes / 1_048_576, 50);
}
}