1#[derive(Debug, Clone)]
5pub struct HardwareInfo {
6 pub cpu_model: String,
8 pub cpu_cores: usize,
10 pub simd_type: &'static str,
12 pub gpu_name: Option<String>,
14 pub memory_gb: f64,
16}
17
18impl HardwareInfo {
19 pub fn detect() -> Self {
21 let cpu_cores = std::thread::available_parallelism()
22 .map(|p| p.get())
23 .unwrap_or(1);
24
25 let simd_type = Self::detect_simd();
27
28 let cpu_model = batuta_common::sys::get_cpu_info();
30
31 let gpu_name = Self::detect_gpu();
33
34 let memory_gb = Self::read_memory_gb();
36
37 Self {
38 cpu_model,
39 cpu_cores,
40 simd_type,
41 gpu_name,
42 memory_gb,
43 }
44 }
45
46 fn detect_simd() -> &'static str {
47 #[cfg(target_arch = "x86_64")]
48 {
49 if std::arch::is_x86_feature_detected!("avx512f") {
50 return "AVX-512";
51 }
52 if std::arch::is_x86_feature_detected!("avx2") {
53 return "AVX2";
54 }
55 if std::arch::is_x86_feature_detected!("avx") {
56 return "AVX";
57 }
58 if std::arch::is_x86_feature_detected!("sse4.2") {
59 return "SSE4.2";
60 }
61 "SSE2"
62 }
63 #[cfg(target_arch = "aarch64")]
64 {
65 "NEON"
66 }
67 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
68 {
69 "Scalar"
70 }
71 }
72
73 fn detect_gpu() -> Option<String> {
74 #[cfg(target_os = "linux")]
75 {
76 Self::detect_gpu_linux()
77 }
78 #[cfg(target_os = "macos")]
79 {
80 return Self::detect_gpu_macos();
81 }
82 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
83 {
84 None
85 }
86 }
87
88 #[cfg(target_os = "linux")]
89 fn detect_gpu_linux() -> Option<String> {
90 let output = std::process::Command::new("nvidia-smi")
91 .args(["--query-gpu=name", "--format=csv,noheader"])
92 .output()
93 .ok()?;
94 if !output.status.success() {
95 return None;
96 }
97 String::from_utf8(output.stdout)
98 .ok()
99 .map(|s| s.lines().next().unwrap_or("").trim().to_string())
100 .filter(|s| !s.is_empty())
101 }
102
103 #[cfg(target_os = "macos")]
104 fn detect_gpu_macos() -> Option<String> {
105 let output = std::process::Command::new("system_profiler")
106 .args(["SPDisplaysDataType"])
107 .output()
108 .ok()?;
109 if !output.status.success() {
110 return None;
111 }
112 let text = String::from_utf8_lossy(&output.stdout);
113 text.lines()
114 .find(|line| line.contains("Chipset Model:"))
115 .and_then(|line| line.split(':').nth(1))
116 .map(|s| s.trim().to_string())
117 }
118
119 fn read_memory_gb() -> f64 {
120 #[cfg(target_os = "linux")]
121 {
122 Self::read_memory_gb_linux()
123 }
124 #[cfg(target_os = "macos")]
125 {
126 return Self::read_memory_gb_macos();
127 }
128 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
129 {
130 0.0
131 }
132 }
133
134 #[cfg(target_os = "linux")]
135 fn read_memory_gb_linux() -> f64 {
136 let contents = match std::fs::read_to_string("/proc/meminfo") {
137 Ok(c) => c,
138 Err(_) => return 0.0,
139 };
140 contents
141 .lines()
142 .find(|line| line.starts_with("MemTotal:"))
143 .and_then(|line| line.split_whitespace().nth(1))
144 .and_then(|kb_str| kb_str.parse::<u64>().ok())
145 .map_or(0.0, |kb| kb as f64 / 1_048_576.0)
146 }
147
148 #[cfg(target_os = "macos")]
149 fn read_memory_gb_macos() -> f64 {
150 let output = match std::process::Command::new("sysctl")
151 .args(["-n", "hw.memsize"])
152 .output()
153 {
154 Ok(o) => o,
155 Err(_) => return 0.0,
156 };
157 String::from_utf8(output.stdout)
158 .ok()
159 .and_then(|s| s.trim().parse::<u64>().ok())
160 .map_or(0.0, |bytes| bytes as f64 / 1_073_741_824.0)
161 }
162}
163
164#[derive(Debug, Clone, Default)]
166pub struct MemoryBreakdown {
167 pub total_kb: u64,
169 pub used_kb: u64,
171 pub cached_kb: u64,
173 pub buffers_kb: u64,
175 pub available_kb: u64,
177}
178
179impl MemoryBreakdown {
180 pub fn usage_percent(&self) -> f64 {
182 if self.total_kb > 0 {
183 ((self.total_kb - self.available_kb) as f64 / self.total_kb as f64) * 100.0
184 } else {
185 0.0
186 }
187 }
188
189 pub fn format_kb(kb: u64) -> String {
191 if kb >= 1_048_576 {
192 format!("{:.1}G", kb as f64 / 1_048_576.0)
193 } else if kb >= 1024 {
194 format!("{:.1}M", kb as f64 / 1024.0)
195 } else {
196 format!("{}K", kb)
197 }
198 }
199}
200
201#[derive(Debug, Clone, Default)]
203pub struct NetworkMetrics {
204 pub rx_bytes: u64,
206 pub tx_bytes: u64,
208 pub rx_rate: f64,
210 pub tx_rate: f64,
212}
213
214impl NetworkMetrics {
215 pub fn format_rate(bytes_per_sec: f64) -> String {
217 if bytes_per_sec >= 1_073_741_824.0 {
218 format!("{:.1} GB/s", bytes_per_sec / 1_073_741_824.0)
219 } else if bytes_per_sec >= 1_048_576.0 {
220 format!("{:.1} MB/s", bytes_per_sec / 1_048_576.0)
221 } else if bytes_per_sec >= 1024.0 {
222 format!("{:.1} KB/s", bytes_per_sec / 1024.0)
223 } else {
224 format!("{:.0} B/s", bytes_per_sec)
225 }
226 }
227}
228
229#[derive(Debug, Clone, Default)]
231pub struct DiskMetrics {
232 pub mount: String,
234 pub total_bytes: u64,
236 pub used_bytes: u64,
238 pub usage_percent: f64,
240}
241
242impl DiskMetrics {
243 pub fn format_bytes(bytes: u64) -> String {
245 batuta_common::fmt::format_bytes_compact(bytes)
246 }
247}
248
249#[derive(Debug, Clone, Default)]
251pub struct LoadMetrics {
252 pub bricks_per_second: f64,
254 pub total_bricks: u64,
256 pub avg_latency_us: f64,
258 pub cpu_usage: f64,
260 pub per_core_usage: Vec<f64>,
262 pub ops_per_second: f64,
264 pub bytes_per_second: f64,
266 pub memory: MemoryBreakdown,
268 pub network: NetworkMetrics,
270 pub disks: Vec<DiskMetrics>,
272}