Skip to main content

xtask/
hardware.rs

1//! Machine description and a disk read sample for benchmark provenance.
2//!
3//! Portable facts come from libc: kernel, memory, CPU count, and the volume
4//! holding the model. GPU and drive identity come from `system_profiler` on
5//! macOS and are absent elsewhere. Only hardware-class fields are recorded;
6//! serial numbers, host names and user paths stay out so a report can be
7//! shared.
8
9use crate::util;
10use anyhow::{Context, Result, ensure};
11use serde_json::{Value, json};
12use std::{
13    ffi::CStr,
14    fs::File,
15    path::{Path, PathBuf},
16    time::Instant,
17};
18
19/// Bytes sampled from the expert store to estimate uncached read bandwidth.
20pub const READ_SAMPLE_BYTES: u64 = 2 * 1024 * 1024 * 1024;
21/// Parallel readers for the sample, matching the engine's per-record fan-out.
22pub const READ_SAMPLE_THREADS: usize = 8;
23const READ_BLOCK: usize = 8 * 1024 * 1024;
24
25/// Describe this machine and the volume holding the prepared artifact `model`.
26/// A probe that fails
27/// records its error in place instead of aborting the benchmark.
28pub fn describe(model: &Path) -> Value {
29    let store = model.join("experts.bin");
30    let read = if store.is_file() {
31        read_rate(&store, READ_SAMPLE_BYTES, READ_SAMPLE_THREADS)
32    } else {
33        Err(anyhow::anyhow!(
34            "no packed expert store at <model>/experts.bin"
35        ))
36    };
37
38    json!({
39        "kernel": kernel(),
40        "os_version": os_version(),
41        "memory_bytes": physical_memory(),
42        "cpu_threads": std::thread::available_parallelism().map(|n| n.get()).ok(),
43        "profile": or_error(profile()),
44        "model_volume": or_error(volume(model)),
45        "store_read": or_error(read),
46    })
47}
48
49fn or_error(result: Result<Value>) -> Value {
50    result.unwrap_or_else(|e| json!({"error": e.to_string()}))
51}
52
53/// `sysname release machine`, the fields of `uname -srm`.
54pub fn kernel() -> Option<String> {
55    let mut name: libc::utsname = unsafe { std::mem::zeroed() };
56
57    if unsafe { libc::uname(&mut name) } != 0 {
58        return None;
59    }
60
61    let field = |bytes: &[libc::c_char]| {
62        unsafe { CStr::from_ptr(bytes.as_ptr()) }
63            .to_string_lossy()
64            .into_owned()
65    };
66
67    Some(format!(
68        "{} {} {}",
69        field(&name.sysname),
70        field(&name.release),
71        field(&name.machine)
72    ))
73}
74
75/// Installed physical memory in bytes.
76pub fn physical_memory() -> Option<u64> {
77    let pages = unsafe { libc::sysconf(libc::_SC_PHYS_PAGES) };
78    let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
79
80    (pages > 0 && page > 0).then(|| pages as u64 * page as u64)
81}
82
83/// Product version of the operating system, where the platform reports one.
84fn os_version() -> Option<String> {
85    #[cfg(target_os = "macos")]
86    {
87        util::output(&["sw_vers", "-productVersion"]).ok()
88    }
89
90    #[cfg(not(target_os = "macos"))]
91    {
92        std::fs::read_to_string("/etc/os-release")
93            .ok()?
94            .lines()
95            .find_map(|line| line.strip_prefix("PRETTY_NAME="))
96            .map(|name| name.trim_matches('"').to_owned())
97    }
98}
99
100/// Chip, GPU and drive identity. macOS exposes these through
101/// `system_profiler`; other platforms record nothing here.
102fn profile() -> Result<Value> {
103    #[cfg(target_os = "macos")]
104    {
105        let text = util::output(&[
106            "system_profiler",
107            "SPHardwareDataType",
108            "SPDisplaysDataType",
109            "SPNVMeDataType",
110            "-json",
111        ])?;
112
113        Ok(profile_fields(
114            &serde_json::from_str(&text).context("system_profiler JSON")?,
115        ))
116    }
117
118    #[cfg(not(target_os = "macos"))]
119    {
120        Ok(Value::Null)
121    }
122}
123
124/// Pick the hardware-class fields from `system_profiler -json` output.
125pub fn profile_fields(value: &Value) -> Value {
126    let hardware = &value["SPHardwareDataType"][0];
127    let gpu = &value["SPDisplaysDataType"][0];
128    let drives: Vec<Value> = value["SPNVMeDataType"]
129        .as_array()
130        .into_iter()
131        .flatten()
132        .filter_map(|controller| controller["_items"].as_array())
133        .flatten()
134        .map(|drive| {
135            json!({
136                "model": drive["device_model"],
137                "size": drive["size"],
138                "size_bytes": drive["size_in_bytes"],
139            })
140        })
141        .collect();
142
143    json!({
144        "machine": hardware["machine_name"],
145        "model_identifier": hardware["machine_model"],
146        "chip": hardware["chip_type"],
147        "cpu_cores": hardware["number_processors"],
148        "memory": hardware["physical_memory"],
149        "gpu": gpu["sppci_model"],
150        "gpu_cores": gpu["sppci_cores"],
151        "metal_family": gpu["spdisplays_mtlgpufamilysupport"],
152        "nvme": drives,
153    })
154}
155
156/// Capacity and free space of the file system holding `path`.
157pub fn volume(path: &Path) -> Result<Value> {
158    let c_path = std::ffi::CString::new(path.as_os_str().as_encoded_bytes())?;
159    let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
160
161    ensure!(
162        unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) } == 0,
163        "statvfs failed: {}",
164        std::io::Error::last_os_error()
165    );
166
167    let fragment = stat.f_frsize as u64;
168
169    Ok(json!({
170        "total_bytes": stat.f_blocks as u64 * fragment,
171        "free_bytes": stat.f_bavail as u64 * fragment,
172    }))
173}
174
175/// Read `bytes` from `path` past the page cache with `threads` parallel
176/// readers at spread offsets, and report the achieved rate in decimal GB/s.
177pub fn read_rate(path: &Path, bytes: u64, threads: usize) -> Result<Value> {
178    ensure!(threads > 0, "read sample needs a reader");
179
180    let len = path.metadata()?.len();
181
182    ensure!(len > 0, "empty file");
183
184    let total = bytes.min(len);
185    let per_thread = (total / threads as u64).max(1);
186    let stride = len / threads as u64;
187    let started = Instant::now();
188    let done: u64 = std::thread::scope(|scope| {
189        let workers: Vec<_> = (0..threads)
190            .map(|i| {
191                let path: PathBuf = path.to_owned();
192                let offset = (stride * i as u64).min(len.saturating_sub(per_thread));
193
194                scope.spawn(move || read_uncached(&path, offset, per_thread))
195            })
196            .collect();
197
198        workers
199            .into_iter()
200            .map(|worker| worker.join().unwrap_or(Ok(0)))
201            .sum::<Result<u64>>()
202    })?;
203    let seconds = started.elapsed().as_secs_f64();
204
205    ensure!(done > 0 && seconds > 0.0, "read sample produced no data");
206
207    Ok(json!({
208        "file": path.file_name().map(|f| f.to_string_lossy().into_owned()),
209        "bytes": done,
210        "seconds": seconds,
211        "gbps": done as f64 / seconds / 1e9,
212        "threads": threads,
213        "method": "uncached 8 MiB reads at spread offsets; a warm-machine sample, not a cold-disk benchmark",
214    }))
215}
216
217fn read_uncached(path: &Path, offset: u64, bytes: u64) -> Result<u64> {
218    use std::os::unix::{fs::FileExt, io::AsRawFd};
219
220    let file = File::open(path)?;
221
222    bypass_cache(file.as_raw_fd(), offset, bytes);
223
224    let mut buffer = vec![0u8; READ_BLOCK];
225    let mut done = 0u64;
226
227    while done < bytes {
228        let want = buffer.len().min((bytes - done) as usize);
229        let read = file.read_at(&mut buffer[..want], offset + done)?;
230
231        if read == 0 {
232            break;
233        }
234
235        done += read as u64;
236    }
237
238    Ok(done)
239}
240
241/// Advisory: keep the sample off the page cache so it measures the device.
242#[cfg(target_os = "macos")]
243fn bypass_cache(fd: libc::c_int, _offset: u64, _bytes: u64) {
244    unsafe {
245        libc::fcntl(fd, libc::F_NOCACHE, 1);
246    }
247}
248
249#[cfg(target_os = "linux")]
250fn bypass_cache(fd: libc::c_int, offset: u64, bytes: u64) {
251    unsafe {
252        libc::posix_fadvise(
253            fd,
254            offset as libc::off_t,
255            bytes as libc::off_t,
256            libc::POSIX_FADV_DONTNEED,
257        );
258    }
259}
260
261#[cfg(not(any(target_os = "macos", target_os = "linux")))]
262fn bypass_cache(_fd: libc::c_int, _offset: u64, _bytes: u64) {}
263
264/// Zip a results directory beside itself for sharing; returns the archive
265/// path. Entries sit under the directory's name so the archive unpacks to a
266/// folder wherever it is sent.
267pub fn archive(out: &Path) -> Result<PathBuf> {
268    use std::io::Write;
269
270    let parent = out.parent().context("results directory parent")?;
271    let name = out
272        .file_name()
273        .context("results directory name")?
274        .to_string_lossy()
275        .into_owned();
276    let zip = parent.join(format!("{name}.zip"));
277    let mut writer = zip::ZipWriter::new(File::create(&zip)?);
278    let options = zip::write::SimpleFileOptions::default()
279        .compression_method(zip::CompressionMethod::Deflated)
280        .large_file(true);
281
282    for path in util::files(out)? {
283        let relative = path.strip_prefix(out)?;
284
285        writer.start_file(format!("{name}/{}", relative.to_string_lossy()), options)?;
286        writer.write_all(&std::fs::read(&path)?)?;
287    }
288
289    writer.finish()?;
290
291    Ok(zip)
292}