Skip to main content

codetether_agent/telemetry/
memory.rs

1//! Process memory telemetry.
2//!
3//! Reads `/proc/self/status` on Linux to surface VmRSS / VmPeak / Threads.
4//! All fields are best-effort; a missing or unreadable field is reported as
5//! `None` rather than failing the caller.
6
7use serde::{Deserialize, Serialize};
8
9/// Snapshot of a process's memory usage, in kilobytes.
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct MemorySnapshot {
12    /// Resident set size, in KiB.
13    pub rss_kb: Option<u64>,
14    /// Peak resident set size, in KiB.
15    pub peak_rss_kb: Option<u64>,
16    /// Virtual memory size, in KiB.
17    pub vsize_kb: Option<u64>,
18    /// Peak virtual memory size, in KiB.
19    pub peak_vsize_kb: Option<u64>,
20    /// Current thread count for the process.
21    pub threads: Option<u64>,
22}
23
24impl MemorySnapshot {
25    /// Capture a snapshot of the current process's memory usage.
26    ///
27    /// Returns a default-filled snapshot (all `None`) on non-Linux targets
28    /// or when `/proc/self/status` is unreadable.
29    pub fn capture() -> Self {
30        #[cfg(target_os = "linux")]
31        {
32            match std::fs::read_to_string("/proc/self/status") {
33                Ok(text) => Self::parse(&text),
34                Err(_) => Self::default(),
35            }
36        }
37        #[cfg(not(target_os = "linux"))]
38        {
39            Self::default()
40        }
41    }
42
43    /// Parse `/proc/self/status`-style content.
44    ///
45    /// Only `VmRSS`, `VmHWM`, `VmSize`, `VmPeak`, and `Threads` are extracted.
46    ///
47    /// # Examples
48    ///
49    /// ```rust
50    /// use codetether_agent::telemetry::memory::MemorySnapshot;
51    ///
52    /// let text = "VmRSS:\t 1234 kB\nVmHWM:\t 2345 kB\nThreads:\t   7\n";
53    /// let snap = MemorySnapshot::parse(text);
54    /// assert_eq!(snap.rss_kb, Some(1234));
55    /// assert_eq!(snap.peak_rss_kb, Some(2345));
56    /// assert_eq!(snap.threads, Some(7));
57    /// ```
58    pub fn parse(text: &str) -> Self {
59        let mut s = Self::default();
60        for line in text.lines() {
61            let (key, rest) = match line.split_once(':') {
62                Some((k, v)) => (k.trim(), v.trim()),
63                None => continue,
64            };
65            // Values like "1234 kB" or bare "7".
66            let num = rest
67                .split_whitespace()
68                .next()
69                .and_then(|n| n.parse::<u64>().ok());
70            match key {
71                "VmRSS" => s.rss_kb = num,
72                "VmHWM" => s.peak_rss_kb = num,
73                "VmSize" => s.vsize_kb = num,
74                "VmPeak" => s.peak_vsize_kb = num,
75                "Threads" => s.threads = num,
76                _ => {}
77            }
78        }
79        s
80    }
81
82    /// RSS in MiB, rounded down. Returns `None` if RSS is unavailable.
83    pub fn rss_mib(&self) -> Option<u64> {
84        self.rss_kb.map(|kb| kb / 1024)
85    }
86}