Skip to main content

openentropy_core/sources/
vmstat.rs

1//! VmstatSource — Runs macOS `vm_stat`, parses counter output, takes multiple
2//! snapshots, and extracts entropy from the deltas of changing counters.
3
4use std::collections::HashMap;
5use std::thread;
6use std::time::Duration;
7
8use crate::source::{EntropySource, SourceCategory, SourceInfo};
9
10use super::helpers::{extract_delta_bytes_i64, run_command};
11
12/// Delay between consecutive vm_stat snapshots.
13const SNAPSHOT_DELAY: Duration = Duration::from_millis(50);
14
15/// Number of snapshot rounds to collect.
16const NUM_ROUNDS: usize = 4;
17
18pub struct VmstatSource {
19    info: SourceInfo,
20}
21
22impl VmstatSource {
23    pub fn new() -> Self {
24        Self {
25            info: SourceInfo {
26                name: "vmstat_deltas",
27                description: "Samples macOS vm_stat counters and extracts entropy from memory management deltas",
28                physics: "Samples macOS vm_stat counters (page faults, pageins, pageouts, \
29                    compressions, decompressions, swap activity). These track physical memory \
30                    management \u{2014} each counter changes when hardware page table walks, TLB \
31                    misses, or memory pressure triggers compressor/swap.",
32                category: SourceCategory::System,
33                platform_requirements: &["macos"],
34                entropy_rate_estimate: 1000.0,
35                composite: false,
36            },
37        }
38    }
39}
40
41impl Default for VmstatSource {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47/// Locate the `vm_stat` binary. Checks the standard macOS path first, then PATH.
48fn vm_stat_path() -> Option<String> {
49    let standard = "/usr/bin/vm_stat";
50    if std::path::Path::new(standard).exists() {
51        return Some(standard.to_string());
52    }
53
54    // Fall back to searching PATH via `which`
55    let path = run_command("which", &["vm_stat"])?;
56    let path = path.trim().to_string();
57    if !path.is_empty() {
58        return Some(path);
59    }
60
61    None
62}
63
64/// Run `vm_stat` and parse output into a map of counter names to values.
65///
66/// vm_stat output looks like:
67/// ```text
68/// Mach Virtual Memory Statistics: (page size of 16384 bytes)
69/// Pages free:                               12345.
70/// Pages active:                             67890.
71/// ```
72///
73/// We strip the trailing period and parse the integer.
74fn snapshot_vmstat(path: &str) -> Option<HashMap<String, i64>> {
75    let stdout = run_command(path, &[])?;
76    let mut map = HashMap::new();
77
78    for line in stdout.lines() {
79        // Skip the header line
80        if line.starts_with("Mach") || line.is_empty() {
81            continue;
82        }
83
84        // Lines look like: "Pages active:                             67890."
85        if let Some(colon_idx) = line.rfind(':') {
86            let key = line[..colon_idx].trim().to_string();
87            let val_str = line[colon_idx + 1..].trim().trim_end_matches('.');
88
89            if let Ok(v) = val_str.parse::<i64>() {
90                map.insert(key, v);
91            }
92        }
93    }
94
95    Some(map)
96}
97
98impl EntropySource for VmstatSource {
99    fn info(&self) -> &SourceInfo {
100        &self.info
101    }
102
103    fn is_available(&self) -> bool {
104        vm_stat_path().is_some()
105    }
106
107    fn collect(&self, n_samples: usize) -> Vec<u8> {
108        let path = match vm_stat_path() {
109            Some(p) => p,
110            None => return Vec::new(),
111        };
112
113        // Take NUM_ROUNDS snapshots with delays between them
114        let mut snapshots: Vec<HashMap<String, i64>> = Vec::with_capacity(NUM_ROUNDS);
115
116        for i in 0..NUM_ROUNDS {
117            if i > 0 {
118                thread::sleep(SNAPSHOT_DELAY);
119            }
120            match snapshot_vmstat(&path) {
121                Some(snap) => snapshots.push(snap),
122                None => return Vec::new(),
123            }
124        }
125
126        // Compute deltas between consecutive rounds
127        let mut all_deltas: Vec<i64> = Vec::new();
128
129        for pair in snapshots.windows(2) {
130            let prev = &pair[0];
131            let curr = &pair[1];
132
133            for (key, curr_val) in curr {
134                if let Some(prev_val) = prev.get(key) {
135                    let delta = curr_val.wrapping_sub(*prev_val);
136                    if delta != 0 {
137                        all_deltas.push(delta);
138                    }
139                }
140            }
141        }
142
143        extract_delta_bytes_i64(&all_deltas, n_samples)
144    }
145}