commonware-runtime 2026.4.0

Execute asynchronous tasks with a configurable scheduler.
Documentation
//! Process metrics collection.

use crate::telemetry::metrics::status::GaugeExt;
use prometheus_client::{metrics::gauge::Gauge, registry::Registry};
use std::{future::Future, time::Duration};
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};

/// The interval at which to update process metrics.
const TICK_INTERVAL: Duration = Duration::from_secs(10);

/// Process metrics collector.
pub struct Metrics {
    /// Resident set size in bytes.
    pub rss: Gauge,
    /// Virtual memory size in bytes.
    pub virtual_memory: Gauge,

    /// Process ID.
    pid: sysinfo::Pid,
    /// System information handle.
    system: System,
}

impl Metrics {
    /// Initialize process metrics and register them with the given registry.
    pub fn init(registry: &mut Registry) -> Self {
        let metrics = Self {
            pid: sysinfo::Pid::from_u32(std::process::id()),
            rss: Gauge::default(),
            virtual_memory: Gauge::default(),

            system: System::new(),
        };

        // Register all metrics
        registry.register(
            "process_rss",
            "Resident set size of the current process",
            metrics.rss.clone(),
        );
        registry.register(
            "process_virtual_memory",
            "Virtual memory size of the current process",
            metrics.virtual_memory.clone(),
        );

        metrics
    }

    /// Update all process metrics.
    fn update(&mut self) {
        // Refresh process information
        self.system.refresh_processes_specifics(
            ProcessesToUpdate::Some(&[self.pid]),
            false,
            ProcessRefreshKind::nothing().with_memory(),
        );

        // If the process exists, update the metrics
        if let Some(process) = self.system.process(self.pid) {
            let _ = self.rss.try_set(process.memory());
            let _ = self.virtual_memory.try_set(process.virtual_memory());
        }
    }

    /// Update process metrics periodically.
    ///
    /// This function takes a sleep function as a parameter to allow different runtimes
    /// to provide their own implementation.
    pub async fn collect<F, Fut>(mut self, sleep_fn: F)
    where
        F: Fn(Duration) -> Fut,
        Fut: Future<Output = ()>,
    {
        loop {
            self.update();
            sleep_fn(TICK_INTERVAL).await;
        }
    }
}

#[cfg(test)]
#[cfg(not(target_os = "windows"))]
mod tests {
    use super::*;

    #[test]
    fn test_process_metrics_init() {
        let mut registry = Registry::default();
        let mut metrics = Metrics::init(&mut registry);

        // Update metrics
        metrics.update();
        let rss = metrics.rss.get();
        assert!(rss > 1024 * 1024); // 1MB
        let virt = metrics.virtual_memory.get();
        assert!(
            virt >= rss,
            "Expected virtual memory ({virt}) to be >= RSS ({rss})"
        );

        // Update metrics
        metrics.update();
        let new_rss = metrics.rss.get();
        assert!(new_rss > 1024 * 1024); // 1MB
        let new_virt = metrics.virtual_memory.get();
        assert!(
            new_virt >= new_rss,
            "Expected virtual memory ({new_virt}) to be >= RSS ({new_rss})"
        );

        // Because tests may be run in parallel, we can't assert anything about the value of the metrics.
    }
}