metrale_bench/hardware/
collect.rs1use std::time::{SystemTime, UNIX_EPOCH};
23
24use super::parse;
25use super::state::{HardwareState, MachineIdentity, ThermalZone};
26
27fn run(tool: &str, args: &[&str]) -> Option<String> {
30 let out = std::process::Command::new(tool)
31 .args(args)
32 .stdin(std::process::Stdio::null())
33 .output()
34 .ok()?;
35 out.status
36 .success()
37 .then(|| String::from_utf8_lossy(&out.stdout).to_string())
38 .filter(|s| !s.trim().is_empty())
39}
40
41fn read(path: &str) -> Option<String> {
42 std::fs::read_to_string(path).ok()
43}
44
45fn now_secs() -> u64 {
46 SystemTime::now()
47 .duration_since(UNIX_EPOCH)
48 .map(|d| d.as_secs())
49 .unwrap_or(0)
50}
51
52fn thermal_zones() -> Option<Vec<ThermalZone>> {
58 let mut entries: Vec<_> = std::fs::read_dir("/sys/class/thermal")
59 .ok()?
60 .flatten()
61 .map(|e| e.path())
62 .filter(|p| {
63 p.file_name()
64 .and_then(|n| n.to_str())
65 .is_some_and(|n| n.starts_with("thermal_zone"))
66 })
67 .collect();
68 entries.sort_by_key(|p| {
70 p.file_name()
71 .and_then(|n| n.to_str())
72 .and_then(|n| n.trim_start_matches("thermal_zone").parse::<u32>().ok())
73 .unwrap_or(u32::MAX)
74 });
75 Some(
76 entries
77 .iter()
78 .filter_map(|p| {
79 let temp = std::fs::read_to_string(p.join("temp")).ok()?;
80 let kind = std::fs::read_to_string(p.join("type")).ok();
81 parse::thermal_zone(kind.as_deref(), &temp)
82 })
83 .collect(),
84 )
85}
86
87fn cpu_governor() -> Option<String> {
89 read("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor").map(|s| s.trim().to_string())
90}
91
92fn machine() -> MachineIdentity {
93 MachineIdentity {
94 hostname: read("/proc/sys/kernel/hostname")
96 .map(|s| s.trim().to_string())
97 .filter(|s| !s.is_empty()),
98 machine_id: read("/etc/machine-id")
99 .map(|s| s.trim().to_string())
100 .filter(|s| !s.is_empty()),
101 gpu: None,
102 driver: None,
103 }
104}
105
106pub fn collect() -> HardwareState {
110 let mut sources = Vec::new();
111 let mut state = HardwareState {
112 captured_at: now_secs(),
113 machine: machine(),
114 ..HardwareState::default()
115 };
116
117 if let Some(text) = run(
118 "nvidia-smi",
119 &[
120 "--query-gpu=name,driver_version,clocks.sm,clocks.max.sm,temperature.gpu,persistence_mode",
121 "--format=csv,noheader,nounits",
122 ],
123 ) {
124 let q = parse::gpu_query(&text);
125 state.machine.gpu = q.name;
126 state.machine.driver = q.driver;
127 state.sm_clock_mhz = q.sm_clock_mhz;
128 state.sm_clock_max_mhz = q.sm_clock_max_mhz;
129 state.gpu_temp_c = q.gpu_temp_c;
130 state.persistence_mode = q.persistence_mode;
131 sources.push("nvidia-smi".to_string());
132 }
133
134 if let Some(text) = run("nvidia-smi", &["-q", "-d", "PERFORMANCE"]) {
135 let (counters, active) = parse::performance(&text);
136 state.throttle_counters = counters;
137 state.throttle_active = active;
138 if !sources.iter().any(|s| s == "nvidia-smi") {
139 sources.push("nvidia-smi".to_string());
140 }
141 }
142
143 if let Some(text) = run(
147 "nvidia-smi",
148 &[
149 "--query-compute-apps=pid,process_name,used_memory",
150 "--format=csv,noheader,nounits",
151 ],
152 ) {
153 state.gpu_compute_apps = Some(parse::compute_apps(&text));
154 } else if run("nvidia-smi", &["--list-gpus"]).is_some() {
155 state.gpu_compute_apps = Some(Vec::new());
160 }
161
162 if let Some(text) = read("/proc/meminfo") {
163 let m = parse::meminfo(&text);
164 state.mem_total_kb = m.total_kb;
165 state.mem_available_kb = m.available_kb;
166 state.page_cache_kb = m.cached_kb;
167 sources.push("procfs".to_string());
168 }
169
170 state.chassis_temps_c = thermal_zones();
171 if state.chassis_temps_c.is_some() {
172 sources.push("sysfs".to_string());
173 }
174 state.cpu_governor = cpu_governor();
175
176 state.sources = sources;
177 state
178}