use std::fmt::Debug;
use std::path::Path;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct TelemetrySample {
pub process_memory_gb: Option<f64>,
pub total_memory_gb: Option<f64>,
pub available_memory_gb: Option<f64>,
pub logical_cpus: Option<usize>,
pub load_average_per_cpu: Option<f64>,
pub process_count: Option<usize>,
pub gpu_devices: Option<usize>,
pub gpu_utilization: Option<f64>,
pub total_disk_gb: Option<f64>,
pub available_disk_gb: Option<f64>,
pub network_bandwidth_mbps: Option<f64>,
pub temperature_celsius: Option<f64>,
pub power_watts: Option<f64>,
}
impl TelemetrySample {
pub fn unknown() -> Self {
Self::default()
}
pub fn is_fully_unknown(&self) -> bool {
*self == Self::default()
}
}
pub trait TelemetrySource: Send + Sync + Debug {
fn name(&self) -> &str;
fn sample(&self) -> TelemetrySample;
}
#[derive(Debug, Clone, Default)]
pub struct StdTelemetry;
impl StdTelemetry {
pub fn new() -> Self {
Self
}
}
impl TelemetrySource for StdTelemetry {
fn name(&self) -> &str {
"std"
}
fn sample(&self) -> TelemetrySample {
let logical_cpus = std::thread::available_parallelism()
.ok()
.map(|count| count.get());
let process_memory_gb = read_to_string_if_present("/proc/self/status")
.and_then(|status| parse_vm_rss_kib(&status))
.map(|kib| kib as f64 / (1024.0 * 1024.0));
let (total_memory_gb, available_memory_gb) = match read_to_string_if_present(
"/proc/meminfo",
)
.and_then(|info| parse_meminfo(&info))
{
Some((total_kib, available_kib)) => (
Some(total_kib as f64 / (1024.0 * 1024.0)),
available_kib.map(|kib| kib as f64 / (1024.0 * 1024.0)),
),
None => (None, None),
};
let load_average_per_cpu = read_to_string_if_present("/proc/loadavg")
.and_then(|loadavg| parse_loadavg(&loadavg))
.and_then(|load| logical_cpus.map(|cpus| load / cpus.max(1) as f64));
let process_count = count_proc_processes();
TelemetrySample {
process_memory_gb,
total_memory_gb,
available_memory_gb,
logical_cpus,
load_average_per_cpu,
process_count,
gpu_devices: None,
gpu_utilization: None,
total_disk_gb: None,
available_disk_gb: None,
network_bandwidth_mbps: None,
temperature_celsius: None,
power_watts: None,
}
}
}
fn read_to_string_if_present(path: &str) -> Option<String> {
let path = Path::new(path);
if !path.exists() {
return None;
}
std::fs::read_to_string(path).ok()
}
pub fn parse_vm_rss_kib(status: &str) -> Option<u64> {
for line in status.lines() {
let Some(rest) = line.strip_prefix("VmRSS:") else {
continue;
};
let mut fields = rest.split_whitespace();
let value: u64 = fields.next()?.parse().ok()?;
return match fields.next() {
Some(unit) if unit.eq_ignore_ascii_case("kb") => Some(value),
None => Some(value),
Some(_) => None,
};
}
None
}
pub fn parse_meminfo(meminfo: &str) -> Option<(u64, Option<u64>)> {
let mut total = None;
let mut available = None;
for line in meminfo.lines() {
let mut parts = line.split(':');
let key = parts.next()?.trim();
let Some(rest) = parts.next() else {
continue;
};
let Some(value) = rest.split_whitespace().next() else {
continue;
};
let Ok(parsed) = value.parse::<u64>() else {
continue;
};
match key {
"MemTotal" => total = Some(parsed),
"MemAvailable" => available = Some(parsed),
_ => {}
}
}
total.map(|total| (total, available))
}
pub fn parse_loadavg(loadavg: &str) -> Option<f64> {
loadavg
.split_whitespace()
.next()
.and_then(|field| field.parse::<f64>().ok())
.filter(|value| value.is_finite() && *value >= 0.0)
}
fn count_proc_processes() -> Option<usize> {
let entries = std::fs::read_dir("/proc").ok()?;
let count = entries
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.file_name()
.to_str()
.map(|name| !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()))
.unwrap_or(false)
})
.count();
Some(count)
}
#[derive(Debug, Clone)]
pub struct FixedTelemetry {
name: String,
sample: TelemetrySample,
}
impl FixedTelemetry {
pub fn new(name: impl Into<String>, sample: TelemetrySample) -> Self {
Self {
name: name.into(),
sample,
}
}
}
impl TelemetrySource for FixedTelemetry {
fn name(&self) -> &str {
&self.name
}
fn sample(&self) -> TelemetrySample {
self.sample.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
const STATUS_FIXTURE: &str = "Name:\tcargo\nUmask:\t0022\nState:\tR (running)\n\
VmPeak:\t 2097152 kB\nVmSize:\t 1048576 kB\n\
VmRSS:\t 524288 kB\nThreads:\t8\n";
const MEMINFO_FIXTURE: &str = "MemTotal: 32899072 kB\nMemFree: 1234567 kB\n\
MemAvailable: 20971520 kB\nBuffers: 123456 kB\n";
#[test]
fn vm_rss_is_parsed_from_the_status_fixture() {
assert_eq!(parse_vm_rss_kib(STATUS_FIXTURE), Some(524_288));
let gib: f64 = 524_288.0 / (1024.0 * 1024.0);
assert!((gib - 0.5).abs() < 1e-12);
}
#[test]
fn vm_rss_reports_unknown_rather_than_guessing() {
assert_eq!(parse_vm_rss_kib("Name:\tcargo\nThreads:\t8\n"), None);
assert_eq!(parse_vm_rss_kib(""), None);
assert_eq!(parse_vm_rss_kib("VmRSS:\t 512 pages\n"), None);
assert_eq!(parse_vm_rss_kib("VmRSS:\t 4096\n"), Some(4096));
assert_eq!(parse_vm_rss_kib("VmRSS:\tnot-a-number kB\n"), None);
}
#[test]
fn meminfo_is_parsed_from_the_fixture() {
let (total, available) = parse_meminfo(MEMINFO_FIXTURE).expect("MemTotal present");
assert_eq!(total, 32_899_072);
assert_eq!(available, Some(20_971_520));
}
#[test]
fn meminfo_tolerates_a_kernel_without_memavailable() {
let (total, available) =
parse_meminfo("MemTotal: 1048576 kB\nMemFree: 524288 kB\n").expect("MemTotal");
assert_eq!(total, 1_048_576);
assert_eq!(available, None);
assert_eq!(parse_meminfo("MemFree: 1 kB\n"), None);
assert_eq!(parse_meminfo(""), None);
}
#[test]
fn loadavg_is_parsed_and_validated() {
assert_eq!(parse_loadavg("0.52 0.58 0.59 2/1234 56789\n"), Some(0.52));
assert_eq!(parse_loadavg(""), None);
assert_eq!(parse_loadavg("garbage 0.1 0.2\n"), None);
assert_eq!(parse_loadavg("-1.0 0.1 0.2\n"), None);
}
#[test]
fn the_default_source_reports_only_measurable_values() {
let sample = StdTelemetry::new().sample();
assert!(
sample.logical_cpus.is_some_and(|cpus| cpus >= 1),
"available_parallelism must be reported"
);
assert_eq!(sample.gpu_devices, None, "the invented 4 GPUs must be gone");
assert_eq!(
sample.gpu_utilization, None,
"the invented 80% must be gone"
);
assert_eq!(sample.total_disk_gb, None, "the invented 1 TB must be gone");
assert_eq!(sample.available_disk_gb, None);
assert_eq!(
sample.network_bandwidth_mbps, None,
"the invented 1 GB/s must be gone"
);
assert_eq!(
sample.temperature_celsius, None,
"the invented 65 C must be gone"
);
assert_eq!(sample.power_watts, None, "the invented 250 W must be gone");
if let Some(total) = sample.total_memory_gb {
assert!(total > 0.0 && total.is_finite(), "got {total}");
assert_ne!(total, 32.0, "32.0 GB was the fabricated constant");
}
if let Some(process) = sample.process_memory_gb {
assert!(process > 0.0 && process.is_finite(), "got {process}");
assert_ne!(process, 16.0, "16.0 GB was the fabricated constant");
}
if let Some(load) = sample.load_average_per_cpu {
assert!(load >= 0.0 && load.is_finite());
}
}
#[test]
fn a_fixed_source_reports_exactly_what_it_was_given() {
let sample = TelemetrySample {
process_memory_gb: Some(3.5),
total_memory_gb: Some(64.0),
logical_cpus: Some(16),
temperature_celsius: Some(71.0),
..TelemetrySample::unknown()
};
let source = FixedTelemetry::new("injected", sample.clone());
assert_eq!(source.name(), "injected");
assert_eq!(source.sample(), sample);
assert!(!sample.is_fully_unknown());
assert!(TelemetrySample::unknown().is_fully_unknown());
}
}