Skip to main content

lean_ctx/core/
system_info.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Host facts (OS, CPU, memory, versions) recorded in benchmark reports.
3
4use std::fmt;
5
6#[derive(Debug, Clone)]
7pub struct SystemInfo {
8    pub os: String,
9    pub arch: String,
10    pub cpu_brand: String,
11    pub cpu_cores: usize,
12    pub memory_gb: f64,
13}
14
15impl fmt::Display for SystemInfo {
16    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17        write!(
18            f,
19            "{} {} | {} ({} cores) | {:.1} GB RAM",
20            self.os, self.arch, self.cpu_brand, self.cpu_cores, self.memory_gb
21        )
22    }
23}
24
25pub fn collect() -> SystemInfo {
26    SystemInfo {
27        os: std::env::consts::OS.to_string(),
28        arch: std::env::consts::ARCH.to_string(),
29        cpu_brand: read_cpu_brand(),
30        cpu_cores: std::thread::available_parallelism().map_or(1, std::num::NonZero::get),
31        memory_gb: read_memory_gb(),
32    }
33}
34
35fn read_cpu_brand() -> String {
36    #[cfg(target_os = "macos")]
37    {
38        std::process::Command::new("sysctl")
39            .args(["-n", "machdep.cpu.brand_string"])
40            .output()
41            .ok()
42            .and_then(|o| String::from_utf8(o.stdout).ok())
43            .map_or_else(|| "Unknown CPU".to_string(), |s| s.trim().to_string())
44    }
45    #[cfg(target_os = "linux")]
46    {
47        std::fs::read_to_string("/proc/cpuinfo")
48            .ok()
49            .and_then(|s| {
50                s.lines()
51                    .find(|l| l.starts_with("model name"))
52                    .and_then(|l| l.split(':').nth(1))
53                    .map(|v| v.trim().to_string())
54            })
55            .unwrap_or_else(|| "Unknown CPU".to_string())
56    }
57    #[cfg(target_os = "windows")]
58    {
59        std::process::Command::new("powershell")
60            .args([
61                "-NoProfile",
62                "-Command",
63                "(Get-CimInstance Win32_Processor).Name",
64            ])
65            .output()
66            .ok()
67            .and_then(|o| String::from_utf8(o.stdout).ok())
68            .map(|s| s.trim().to_string())
69            .filter(|s| !s.is_empty())
70            .unwrap_or_else(|| "Unknown CPU".to_string())
71    }
72    #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
73    {
74        "Unknown CPU".to_string()
75    }
76}
77
78fn read_memory_gb() -> f64 {
79    #[cfg(target_os = "macos")]
80    {
81        std::process::Command::new("sysctl")
82            .args(["-n", "hw.memsize"])
83            .output()
84            .ok()
85            .and_then(|o| String::from_utf8(o.stdout).ok())
86            .and_then(|s| s.trim().parse::<u64>().ok())
87            .map_or(0.0, |bytes| bytes as f64 / (1024.0 * 1024.0 * 1024.0))
88    }
89    #[cfg(target_os = "linux")]
90    {
91        std::fs::read_to_string("/proc/meminfo")
92            .ok()
93            .and_then(|s| {
94                s.lines().find(|l| l.starts_with("MemTotal")).and_then(|l| {
95                    l.split_whitespace()
96                        .nth(1)
97                        .and_then(|v| v.parse::<u64>().ok())
98                })
99            })
100            .map_or(0.0, |kb| kb as f64 / (1024.0 * 1024.0))
101    }
102    #[cfg(target_os = "windows")]
103    {
104        // Query total physical memory (bytes) via CIM; reliable on all
105        // supported Windows versions and present on CI runners.
106        std::process::Command::new("powershell")
107            .args([
108                "-NoProfile",
109                "-Command",
110                "(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory",
111            ])
112            .output()
113            .ok()
114            .and_then(|o| String::from_utf8(o.stdout).ok())
115            .and_then(|s| s.trim().parse::<u64>().ok())
116            .map_or(0.0, |bytes| bytes as f64 / (1024.0 * 1024.0 * 1024.0))
117    }
118    #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
119    {
120        0.0
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn collect_returns_valid_info() {
130        let info = collect();
131        assert!(!info.os.is_empty(), "`info.os` is empty");
132        assert!(!info.arch.is_empty(), "`info.arch` is empty");
133        assert!(info.cpu_cores >= 1);
134    }
135
136    #[test]
137    fn display_is_readable() {
138        let info = collect();
139        let s = format!("{info}");
140        assert!(s.contains(&info.os));
141        assert!(s.contains("cores"));
142    }
143}