use std::time::Duration;
use hwhkit_core::ShutdownToken;
use sysinfo::{Pid, ProcessRefreshKind, RefreshKind, System};
const SAMPLE_INTERVAL: Duration = Duration::from_secs(5);
pub fn spawn(shutdown: ShutdownToken) {
tokio::spawn(async move {
run(shutdown).await;
});
}
async fn run(shutdown: ShutdownToken) {
let pid = match get_self_pid() {
Some(p) => p,
None => {
tracing::warn!("process_metrics: unable to determine current pid; sampler disabled");
return;
}
};
let refresh = RefreshKind::new().with_processes(ProcessRefreshKind::everything());
let mut sys = System::new_with_specifics(refresh);
let mut state = SamplerState::default();
loop {
sample_once(&mut sys, refresh, pid, &mut state);
tokio::select! {
_ = tokio::time::sleep(SAMPLE_INTERVAL) => {}
_ = shutdown.cancelled() => {
tracing::debug!("process_metrics sampler stopping");
sample_once(&mut sys, refresh, pid, &mut state);
break;
}
}
}
}
#[derive(Default)]
struct SamplerState {
cpu_total_ms: u64,
cpu_residual_ms: f64,
}
fn sample_once(sys: &mut System, refresh: RefreshKind, pid: Pid, state: &mut SamplerState) {
sys.refresh_specifics(refresh);
let Some(proc) = sys.process(pid) else { return };
let resident = proc.memory();
let virt = proc.virtual_memory();
metrics::gauge!("process_resident_memory_bytes").set(resident as f64);
metrics::gauge!("process_virtual_memory_bytes").set(virt as f64);
#[allow(clippy::cast_possible_truncation)]
let cpu_pct = proc.cpu_usage() as f64;
let core_count = sys.cpus().len().max(1) as f64;
let elapsed_ms = SAMPLE_INTERVAL.as_millis() as f64;
let core_seconds_window = (cpu_pct / 100.0) * elapsed_ms / 1000.0 / core_count;
let added_ms_f64 = core_seconds_window * 1000.0;
state.cpu_residual_ms += added_ms_f64;
if state.cpu_residual_ms >= 1.0 {
let bumps = state.cpu_residual_ms.floor() as u64;
state.cpu_total_ms = state.cpu_total_ms.saturating_add(bumps);
state.cpu_residual_ms -= bumps as f64;
metrics::counter!("process_cpu_seconds_total").absolute(state.cpu_total_ms / 1000);
}
#[cfg(target_os = "linux")]
if let Some(fd_count) = read_open_fds_linux() {
metrics::gauge!("process_open_fds").set(fd_count as f64);
}
#[cfg(target_os = "macos")]
if let Some(fd_count) = read_open_fds_macos() {
metrics::gauge!("process_open_fds").set(fd_count as f64);
}
#[cfg(target_os = "linux")]
if let Some(n) = read_thread_count_linux() {
metrics::gauge!("process_threads").set(n as f64);
}
#[cfg(not(target_os = "linux"))]
{
let n = std::thread::available_parallelism()
.map(|p| p.get())
.unwrap_or(1);
metrics::gauge!("process_threads").set(n as f64);
}
}
fn get_self_pid() -> Option<Pid> {
sysinfo::get_current_pid().ok()
}
#[cfg(target_os = "linux")]
fn read_open_fds_linux() -> Option<usize> {
std::fs::read_dir("/proc/self/fd").ok().map(|it| it.count())
}
#[cfg(target_os = "macos")]
fn read_open_fds_macos() -> Option<usize> {
use std::os::raw::c_int;
extern "C" {
fn proc_pidinfo(
pid: c_int,
flavor: c_int,
arg: u64,
buffer: *mut std::ffi::c_void,
buffersize: c_int,
) -> c_int;
}
const PROC_PIDLISTFDS: c_int = 1;
let pid = unsafe { libc::getpid() };
let needed = unsafe { proc_pidinfo(pid, PROC_PIDLISTFDS, 0, std::ptr::null_mut(), 0) };
if needed <= 0 {
return None;
}
let entry_size = std::mem::size_of::<libc::proc_fdinfo>();
if entry_size == 0 {
return None;
}
Some(needed as usize / entry_size)
}
#[cfg(target_os = "linux")]
fn read_thread_count_linux() -> Option<usize> {
std::fs::read_dir("/proc/self/task")
.ok()
.map(|it| it.count())
}