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