openentropy_core/sources/
vmstat.rs1use std::collections::HashMap;
5use std::thread;
6use std::time::Duration;
7
8use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
9
10use super::helpers::{extract_delta_bytes_i64, run_command};
11
12const SNAPSHOT_DELAY: Duration = Duration::from_millis(50);
14
15const NUM_ROUNDS: usize = 4;
17
18pub struct VmstatSource;
24
25static VMSTAT_INFO: SourceInfo = 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: Platform::MacOS,
34 requirements: &[],
35 entropy_rate_estimate: 1000.0,
36 composite: false,
37};
38
39impl VmstatSource {
40 pub fn new() -> Self {
41 Self
42 }
43}
44
45impl Default for VmstatSource {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51fn vm_stat_path() -> Option<String> {
53 let standard = "/usr/bin/vm_stat";
54 if std::path::Path::new(standard).exists() {
55 return Some(standard.to_string());
56 }
57
58 let path = run_command("which", &["vm_stat"])?;
60 let path = path.trim().to_string();
61 if !path.is_empty() {
62 return Some(path);
63 }
64
65 None
66}
67
68fn snapshot_vmstat(path: &str) -> Option<HashMap<String, i64>> {
79 let stdout = run_command(path, &[])?;
80 let mut map = HashMap::new();
81
82 for line in stdout.lines() {
83 if line.starts_with("Mach") || line.is_empty() {
85 continue;
86 }
87
88 if let Some(colon_idx) = line.rfind(':') {
90 let key = line[..colon_idx].trim().to_string();
91 let val_str = line[colon_idx + 1..].trim().trim_end_matches('.');
92
93 if let Ok(v) = val_str.parse::<i64>() {
94 map.insert(key, v);
95 }
96 }
97 }
98
99 Some(map)
100}
101
102impl EntropySource for VmstatSource {
103 fn info(&self) -> &SourceInfo {
104 &VMSTAT_INFO
105 }
106
107 fn is_available(&self) -> bool {
108 vm_stat_path().is_some()
109 }
110
111 fn collect(&self, n_samples: usize) -> Vec<u8> {
112 let path = match vm_stat_path() {
113 Some(p) => p,
114 None => return Vec::new(),
115 };
116
117 let mut snapshots: Vec<HashMap<String, i64>> = Vec::with_capacity(NUM_ROUNDS);
119
120 for i in 0..NUM_ROUNDS {
121 if i > 0 {
122 thread::sleep(SNAPSHOT_DELAY);
123 }
124 match snapshot_vmstat(&path) {
125 Some(snap) => snapshots.push(snap),
126 None => return Vec::new(),
127 }
128 }
129
130 let mut all_deltas: Vec<i64> = Vec::new();
132
133 for pair in snapshots.windows(2) {
134 let prev = &pair[0];
135 let curr = &pair[1];
136
137 for (key, curr_val) in curr {
138 if let Some(prev_val) = prev.get(key) {
139 let delta = curr_val.wrapping_sub(*prev_val);
140 if delta != 0 {
141 all_deltas.push(delta);
142 }
143 }
144 }
145 }
146
147 extract_delta_bytes_i64(&all_deltas, n_samples)
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn vmstat_info() {
157 let src = VmstatSource::new();
158 assert_eq!(src.name(), "vmstat_deltas");
159 assert_eq!(src.info().category, SourceCategory::System);
160 assert!(!src.info().composite);
161 }
162
163 #[test]
164 #[cfg(target_os = "macos")]
165 #[ignore] fn vmstat_collects_bytes() {
167 let src = VmstatSource::new();
168 if src.is_available() {
169 let data = src.collect(64);
170 assert!(!data.is_empty());
171 assert!(data.len() <= 64);
172 }
173 }
174}