use crate::transport::TRANSPORT_METRICS;
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)]
pub(crate) struct ResourceUtilization {
pub memory_rss_bytes: Option<u64>,
pub memory_limit_bytes: Option<u64>,
pub cpu_time_seconds: Option<f64>,
pub cumulative_bytes_sent: u64,
pub cumulative_bytes_received: u64,
}
impl ResourceUtilization {
pub(crate) fn to_telemetry_value(&self) -> serde_json::Value {
serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
}
}
pub(crate) fn sample() -> ResourceUtilization {
ResourceUtilization {
memory_rss_bytes: rss_bytes(),
memory_limit_bytes: crate::wasm_runtime::read_total_ram_bytes().map(|v| v as u64),
cpu_time_seconds: cpu_time_seconds(),
cumulative_bytes_sent: TRANSPORT_METRICS.cumulative_bytes_sent(),
cumulative_bytes_received: TRANSPORT_METRICS.cumulative_bytes_received(),
}
}
pub(crate) fn rss_bytes() -> Option<u64> {
#[cfg(target_os = "linux")]
{
let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
let rss_pages: u64 = statm.split_whitespace().nth(1)?.parse().ok()?;
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if page_size > 0 {
Some(rss_pages * page_size as u64)
} else {
None
}
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
pub(crate) fn cpu_time_seconds() -> Option<f64> {
#[cfg(target_os = "linux")]
{
let stat = std::fs::read_to_string("/proc/self/stat").ok()?;
let ticks = parse_proc_stat_cpu_ticks(&stat)?;
let clk_tck = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
if clk_tck > 0 {
Some(ticks as f64 / clk_tck as f64)
} else {
None
}
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
#[cfg(target_os = "linux")]
fn parse_proc_stat_cpu_ticks(stat: &str) -> Option<u64> {
let after_comm = stat.rsplit_once(')').map(|(_, rest)| rest.trim_start())?;
let mut fields = after_comm.split_whitespace();
let utime: u64 = fields.nth(11)?.parse().ok()?;
let stime: u64 = fields.next()?.parse().ok()?;
Some(utime + stime)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sample_populates_resource_axes() {
let sample = sample();
let _ = sample.cumulative_bytes_sent;
let _ = sample.cumulative_bytes_received;
#[cfg(target_os = "linux")]
{
assert!(
sample.memory_rss_bytes.is_some_and(|rss| rss > 0),
"RSS must be sampled and non-zero on Linux, got {:?}",
sample.memory_rss_bytes
);
assert!(
sample.memory_limit_bytes.is_some_and(|limit| limit > 0),
"memory limit (min RAM/cgroup) must be sampled on Linux, got {:?}",
sample.memory_limit_bytes
);
assert!(
sample.cpu_time_seconds.is_some_and(|cpu| cpu >= 0.0),
"CPU time must be sampled on Linux, got {:?}",
sample.cpu_time_seconds
);
}
}
#[test]
fn telemetry_value_contains_all_axes() {
let value = sample().to_telemetry_value();
let obj = value
.as_object()
.expect("sample serializes to a JSON object");
for key in [
"memory_rss_bytes",
"memory_limit_bytes",
"cpu_time_seconds",
"cumulative_bytes_sent",
"cumulative_bytes_received",
] {
assert!(obj.contains_key(key), "telemetry value missing key `{key}`");
}
#[cfg(target_os = "linux")]
{
assert!(
obj["memory_rss_bytes"].is_number(),
"memory_rss_bytes should be a number on Linux, got {}",
obj["memory_rss_bytes"]
);
assert!(
obj["memory_limit_bytes"].is_number(),
"memory_limit_bytes should be a number on Linux, got {}",
obj["memory_limit_bytes"]
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn parse_proc_stat_cpu_ticks_handles_parens_in_comm() {
let line = "1234 (freenet) S 1 1234 1234 0 -1 4194304 100 0 0 0 \
100 55 0 0 20 0 8 0 999 12345 678 rest ignored";
assert_eq!(parse_proc_stat_cpu_ticks(line), Some(155));
}
#[cfg(target_os = "linux")]
#[test]
fn parse_proc_stat_cpu_ticks_handles_spaces_and_parens_in_comm() {
let line = "42 (weird )name) R 1 42 42 0 -1 0 7 3 0 0 \
12 8 0 0 20 0 1 0 100 200 300 tail";
assert_eq!(parse_proc_stat_cpu_ticks(line), Some(20));
}
#[cfg(target_os = "linux")]
#[test]
fn parse_proc_stat_cpu_ticks_rejects_garbage() {
assert_eq!(parse_proc_stat_cpu_ticks("not a stat line"), None);
assert_eq!(parse_proc_stat_cpu_ticks(""), None);
}
}