1use wowlab_types::{
7 constants::{BYTES_PER_MB, HUNDRED},
8 sim::FastSet,
9};
10
11const LOAD_AVG_COUNT: usize = 3;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub struct CpuTicks {
16 total: u64,
17 idle: u64,
18}
19
20impl CpuTicks {
21 #[must_use]
23 pub const fn new(total: u64, idle: u64) -> Self {
24 Self { total, idle }
25 }
26
27 #[must_use]
29 pub const fn total(self) -> u64 {
30 self.total
31 }
32
33 #[must_use]
35 pub const fn idle(self) -> u64 {
36 self.idle
37 }
38}
39
40#[must_use]
42pub fn cpu_usage_from_ticks(before: CpuTicks, after: CpuTicks) -> f64 {
43 let total_delta = after.total.saturating_sub(before.total);
44
45 if total_delta == 0 {
46 return 0.0;
47 }
48
49 let idle_delta = after.idle.saturating_sub(before.idle);
50 let active_delta = total_delta.saturating_sub(idle_delta);
51
52 HUNDRED * wowlab_types::numeric::u64_to_f64(active_delta)
53 / wowlab_types::numeric::u64_to_f64(total_delta)
54}
55
56#[must_use]
58pub fn logical_cores() -> usize {
59 std::thread::available_parallelism().map_or(1, std::num::NonZero::get)
60}
61
62#[must_use]
64pub fn optimal_concurrency() -> usize {
65 #[cfg(target_arch = "aarch64")]
66 {
67 aarch64_performance_cores().unwrap_or_else(physical_cores)
68 }
69
70 #[cfg(target_arch = "x86_64")]
71 {
72 physical_cores()
73 }
74
75 #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
76 {
77 logical_cores()
78 }
79}
80
81#[must_use]
83pub fn physical_cores() -> usize {
84 #[cfg(target_os = "macos")]
85 {
86 let n = sysctl_usize("hw.physicalcpu");
87
88 if n > 0 { n } else { logical_cores() }
89 }
90
91 #[cfg(target_os = "linux")]
92 {
93 let Ok(cpuinfo) = wowlab_fs::file::read_text(wowlab_fs::path::Path::new("/proc/cpuinfo"))
94 else {
95 return logical_cores();
96 };
97 let mut ids = FastSet::default();
98
99 for line in cpuinfo.lines() {
100 if line.starts_with("core id")
101 && let Some(id) = line
102 .split(':')
103 .nth(1)
104 .and_then(|v| v.trim().parse::<u32>().ok())
105 {
106 ids.insert(id);
107 }
108 }
109
110 if ids.is_empty() {
111 logical_cores()
112 } else {
113 ids.len()
114 }
115 }
116
117 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
118 {
119 logical_cores()
120 }
121}
122
123#[cfg(target_arch = "aarch64")]
124fn aarch64_performance_cores() -> Option<usize> {
125 #[cfg(target_os = "macos")]
126 {
127 macos_perflevel_cores(0)
128 }
129
130 #[cfg(target_os = "linux")]
131 {
132 linux_big_cores()
133 }
134
135 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
136 {
137 None
138 }
139}
140
141#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
142fn macos_perflevel_cores(level: u32) -> Option<usize> {
143 use std::{
144 ffi::CString,
145 mem::MaybeUninit,
146 os::raw::{c_int, c_void},
147 };
148
149 extern "C" {
150 fn sysctlbyname(
151 name: *const i8,
152 oldp: *mut c_void,
153 oldlenp: *mut usize,
154 newp: *mut c_void,
155 newlen: usize,
156 ) -> c_int;
157 }
158
159 let name = CString::new(format!("hw.perflevel{level}.logicalcpu")).ok()?;
160 let mut value = MaybeUninit::<i32>::uninit();
161 let mut size = std::mem::size_of::<i32>();
162
163 let result = unsafe {
165 sysctlbyname(
166 name.as_ptr(),
167 value.as_mut_ptr().cast(),
168 &mut size,
169 std::ptr::null_mut(),
170 0,
171 )
172 };
173
174 if result == 0 && size == std::mem::size_of::<i32>() {
175 let cores = unsafe { value.assume_init() };
177
178 if cores > 0 {
179 return Some(cores as usize);
180 }
181 }
182
183 None
184}
185
186#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
187const BIG_CORE_CAPACITY_THRESHOLD: u32 = 900;
188
189#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
190fn linux_big_cores() -> Option<usize> {
191 use wowlab_fs::{directory, file, path::Path};
192
193 let cpu_base = Path::new("/sys/devices/system/cpu");
194
195 if directory::inspect(&cpu_base.join("cpu0/cpu_capacity"))
196 .ok()?
197 .is_none()
198 {
199 return None;
200 }
201
202 let mut big_cores = 0;
203 let mut found_any = false;
204
205 for entry in directory::entries(cpu_base).ok()? {
206 let name = entry.path().file_name()?;
207 let name_str = name.to_string_lossy();
208
209 if !name_str.starts_with("cpu") {
210 continue;
211 }
212
213 if name_str
214 .strip_prefix("cpu")
215 .and_then(|s| s.parse::<u32>().ok())
216 .is_none()
217 {
218 continue;
219 }
220
221 let capacity_path = entry.path().join("cpu_capacity");
222
223 if let Ok(content) = file::read_text(&capacity_path) {
224 if let Ok(capacity) = content.trim().parse::<u32>() {
225 found_any = true;
226
227 if capacity >= BIG_CORE_CAPACITY_THRESHOLD {
228 big_cores += 1;
229 }
230 }
231 }
232 }
233
234 if found_any && big_cores > 0 {
235 Some(big_cores)
236 } else {
237 None
238 }
239}
240
241#[cfg(target_os = "linux")]
242#[path = "linux.rs"]
243mod inner;
244
245#[cfg(target_os = "macos")]
246#[path = "macos.rs"]
247mod inner;
248
249#[cfg(target_os = "windows")]
250#[path = "windows.rs"]
251mod inner;
252
253#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
254#[path = "fallback.rs"]
255mod inner;
256
257pub use inner::{cpu_model, read_cpu_ticks, read_load_average, read_memory_mb, read_os_memory_mb};
258
259#[must_use]
261pub fn performance_cores() -> usize {
262 optimal_concurrency()
263}
264
265#[must_use]
267pub fn efficiency_cores() -> usize {
268 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
269 {
270 macos_perflevel_cores(1).unwrap_or(0)
271 }
272
273 #[cfg(all(target_arch = "aarch64", target_os = "linux"))]
274 {
275 let big = optimal_concurrency();
276 let total = physical_cores();
277
278 total.saturating_sub(big)
279 }
280
281 #[cfg(not(target_arch = "aarch64"))]
282 {
283 0
284 }
285}
286
287#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
289pub struct OsProfile {
290 pub cpu_model: String,
291 pub logical_cores: usize,
292 pub physical_cores: usize,
293 pub optimal_cores: usize,
294 pub p_cores: usize,
295 pub e_cores: usize,
296 pub total_memory_mb: u64,
297 pub available_memory_mb: u64,
298 pub os: String,
299 pub arch: String,
300}
301
302#[must_use]
304pub fn os_profile() -> OsProfile {
305 let (total_mem, avail_mem) = read_os_memory_mb();
306
307 OsProfile {
308 cpu_model: cpu_model(),
309 logical_cores: logical_cores(),
310 physical_cores: physical_cores(),
311 optimal_cores: optimal_concurrency(),
312 p_cores: performance_cores(),
313 e_cores: efficiency_cores(),
314 total_memory_mb: wowlab_types::numeric::f64_to_u64_saturating_trunc(total_mem),
315 available_memory_mb: wowlab_types::numeric::f64_to_u64_saturating_trunc(avail_mem),
316 os: std::env::consts::OS.to_string(),
317 arch: std::env::consts::ARCH.to_string(),
318 }
319}
320
321#[cfg(target_os = "macos")]
322fn sysctl_string(name: &str) -> String {
323 use std::ffi::CString;
324
325 let Ok(cname) = CString::new(name) else {
326 return String::new();
327 };
328
329 let mut size: usize = 0;
330 let ret = unsafe {
332 libc::sysctlbyname(
333 cname.as_ptr(),
334 std::ptr::null_mut(),
335 &mut size,
336 std::ptr::null_mut(),
337 0,
338 )
339 };
340
341 if ret != 0 || size == 0 {
342 return String::new();
343 }
344
345 let mut buf = vec![0u8; size];
346 let ret = unsafe {
348 libc::sysctlbyname(
349 cname.as_ptr(),
350 buf.as_mut_ptr().cast(),
351 &mut size,
352 std::ptr::null_mut(),
353 0,
354 )
355 };
356
357 if ret != 0 {
358 return String::new();
359 }
360
361 if buf.last() == Some(&0) {
362 buf.pop();
363 }
364
365 String::from_utf8_lossy(&buf).trim().to_string()
366}
367
368#[cfg(target_os = "macos")]
369fn sysctl_usize(name: &str) -> usize {
370 use std::ffi::CString;
371
372 let Ok(cname) = CString::new(name) else {
373 return 0;
374 };
375
376 let mut value: usize = 0;
377 let mut size = std::mem::size_of::<usize>();
378 let ret = unsafe {
380 libc::sysctlbyname(
381 cname.as_ptr(),
382 &mut value as *mut _ as *mut libc::c_void,
383 &mut size,
384 std::ptr::null_mut(),
385 0,
386 )
387 };
388
389 if ret == 0 { value } else { 0 }
390}
391
392#[cfg(test)]
393mod tests {
394 use googletest::prelude::*;
395 use rstest::rstest;
396
397 use super::*;
398
399 #[gtest]
400 #[rstest]
401 #[case::zero_total_delta(CpuTicks::new(0, 0), CpuTicks::new(0, 0), 0.0)]
402 #[case::fully_active(CpuTicks::new(0, 0), CpuTicks::new(100, 0), 100.0)]
403 #[case::fully_idle(CpuTicks::new(0, 0), CpuTicks::new(100, 100), 0.0)]
404 #[case::half_active(CpuTicks::new(0, 0), CpuTicks::new(100, 50), 50.0)]
405 #[case::quarter_active_nonzero_base(CpuTicks::new(100, 50), CpuTicks::new(300, 100), 75.0)]
406 #[case::after_before_saturates_total(CpuTicks::new(100, 50), CpuTicks::new(50, 20), 0.0)]
407 #[case::idle_gt_total_saturates_active(CpuTicks::new(0, 0), CpuTicks::new(100, 200), 0.0)]
408 fn cpu_usage_cases(
409 #[case] before: CpuTicks,
410 #[case] after: CpuTicks,
411 #[case] expected: f64,
412 ) -> Result<()> {
413 verify_that!(cpu_usage_from_ticks(before, after), near(expected, 1e-9))
414 }
415
416 #[gtest]
417 fn logical_cores_positive() -> Result<()> {
418 verify_that!(logical_cores(), gt(0))
419 }
420
421 #[gtest]
422 fn optimal_le_logical() -> Result<()> {
423 verify_that!(optimal_concurrency(), le(logical_cores()))
424 }
425
426 #[gtest]
427 fn physical_cores_positive() -> Result<()> {
428 verify_that!(physical_cores(), gt(0))
429 }
430
431 #[gtest]
432 fn load_average_non_negative() -> Result<()> {
433 let avg = read_load_average();
434
435 for v in avg {
436 verify_that!(v, ge(0.0))?;
437 }
438
439 Ok(())
440 }
441
442 #[gtest]
443 fn memory_mb_non_negative() -> Result<()> {
444 verify_that!(read_memory_mb(), ge(0.0))
445 }
446
447 #[gtest]
448 fn os_memory_non_negative() -> Result<()> {
449 let (total, available) = read_os_memory_mb();
450
451 verify_that!(total, ge(0.0))?;
452
453 verify_that!(available, ge(0.0))
454 }
455
456 #[gtest]
457 fn cpu_ticks_some() -> Result<()> {
458 if cfg!(any(
459 target_os = "linux",
460 target_os = "macos",
461 target_os = "windows"
462 )) {
463 verify_that!(read_cpu_ticks(), some(anything()))?;
464 }
465
466 Ok(())
467 }
468}