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 if 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
111 if ids.is_empty() {
112 logical_cores()
113 } else {
114 ids.len()
115 }
116 }
117
118 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
119 {
120 logical_cores()
121 }
122}
123
124#[cfg(target_arch = "aarch64")]
125fn aarch64_performance_cores() -> Option<usize> {
126 #[cfg(target_os = "macos")]
127 {
128 macos_perflevel_cores(0)
129 }
130
131 #[cfg(target_os = "linux")]
132 {
133 linux_big_cores()
134 }
135
136 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
137 {
138 None
139 }
140}
141
142#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
143fn macos_perflevel_cores(level: u32) -> Option<usize> {
144 use std::{
145 ffi::CString,
146 mem::MaybeUninit,
147 os::raw::{c_int, c_void},
148 };
149
150 extern "C" {
151 fn sysctlbyname(
152 name: *const i8,
153 oldp: *mut c_void,
154 oldlenp: *mut usize,
155 newp: *mut c_void,
156 newlen: usize,
157 ) -> c_int;
158 }
159
160 let name = CString::new(format!("hw.perflevel{level}.logicalcpu")).ok()?;
161 let mut value = MaybeUninit::<i32>::uninit();
162 let mut size = std::mem::size_of::<i32>();
163
164 let result = unsafe {
166 sysctlbyname(
167 name.as_ptr(),
168 value.as_mut_ptr().cast(),
169 &mut size,
170 std::ptr::null_mut(),
171 0,
172 )
173 };
174
175 if result == 0 && size == std::mem::size_of::<i32>() {
176 let cores = unsafe { value.assume_init() };
178
179 if cores > 0 {
180 return Some(cores as usize);
181 }
182 }
183
184 None
185}
186
187#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
188const BIG_CORE_CAPACITY_THRESHOLD: u32 = 900;
189
190#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
191fn linux_big_cores() -> Option<usize> {
192 use wowlab_fs::{directory, file, path::Path};
193
194 let cpu_base = Path::new("/sys/devices/system/cpu");
195
196 if directory::inspect(&cpu_base.join("cpu0/cpu_capacity"))
197 .ok()?
198 .is_none()
199 {
200 return None;
201 }
202
203 let mut big_cores = 0;
204 let mut found_any = false;
205
206 for entry in directory::entries(cpu_base).ok()? {
207 let name = entry.path().file_name()?;
208 let name_str = name.to_string_lossy();
209
210 if !name_str.starts_with("cpu") {
211 continue;
212 }
213
214 if name_str
215 .strip_prefix("cpu")
216 .and_then(|s| s.parse::<u32>().ok())
217 .is_none()
218 {
219 continue;
220 }
221
222 let capacity_path = entry.path().join("cpu_capacity");
223
224 if let Ok(content) = file::read_text(&capacity_path) {
225 if let Ok(capacity) = content.trim().parse::<u32>() {
226 found_any = true;
227
228 if capacity >= BIG_CORE_CAPACITY_THRESHOLD {
229 big_cores += 1;
230 }
231 }
232 }
233 }
234
235 if found_any && big_cores > 0 {
236 Some(big_cores)
237 } else {
238 None
239 }
240}
241
242#[cfg(target_os = "linux")]
243#[path = "linux.rs"]
244mod inner;
245
246#[cfg(target_os = "macos")]
247#[path = "macos.rs"]
248mod inner;
249
250#[cfg(target_os = "windows")]
251#[path = "windows.rs"]
252mod inner;
253
254#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
255#[path = "fallback.rs"]
256mod inner;
257
258pub use inner::{cpu_model, read_cpu_ticks, read_load_average, read_memory_mb, read_os_memory_mb};
259
260#[must_use]
262pub fn performance_cores() -> usize {
263 optimal_concurrency()
264}
265
266#[must_use]
268pub fn efficiency_cores() -> usize {
269 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
270 {
271 macos_perflevel_cores(1).unwrap_or(0)
272 }
273
274 #[cfg(all(target_arch = "aarch64", target_os = "linux"))]
275 {
276 let big = optimal_concurrency();
277 let total = physical_cores();
278
279 total.saturating_sub(big)
280 }
281
282 #[cfg(not(target_arch = "aarch64"))]
283 {
284 0
285 }
286}
287
288#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
290pub struct OsProfile {
291 pub cpu_model: String,
292 pub logical_cores: usize,
293 pub physical_cores: usize,
294 pub optimal_cores: usize,
295 pub p_cores: usize,
296 pub e_cores: usize,
297 pub total_memory_mb: u64,
298 pub available_memory_mb: u64,
299 pub os: String,
300 pub arch: String,
301}
302
303#[must_use]
305pub fn os_profile() -> OsProfile {
306 let (total_mem, avail_mem) = read_os_memory_mb();
307
308 OsProfile {
309 cpu_model: cpu_model(),
310 logical_cores: logical_cores(),
311 physical_cores: physical_cores(),
312 optimal_cores: optimal_concurrency(),
313 p_cores: performance_cores(),
314 e_cores: efficiency_cores(),
315 total_memory_mb: wowlab_types::numeric::f64_to_u64_saturating_trunc(total_mem),
316 available_memory_mb: wowlab_types::numeric::f64_to_u64_saturating_trunc(avail_mem),
317 os: std::env::consts::OS.to_string(),
318 arch: std::env::consts::ARCH.to_string(),
319 }
320}
321
322#[cfg(target_os = "macos")]
323fn sysctl_string(name: &str) -> String {
324 use std::ffi::CString;
325
326 let Ok(cname) = CString::new(name) else {
327 return String::new();
328 };
329
330 let mut size: usize = 0;
331 let ret = unsafe {
333 libc::sysctlbyname(
334 cname.as_ptr(),
335 std::ptr::null_mut(),
336 &mut size,
337 std::ptr::null_mut(),
338 0,
339 )
340 };
341
342 if ret != 0 || size == 0 {
343 return String::new();
344 }
345
346 let mut buf = vec![0u8; size];
347 let ret = unsafe {
349 libc::sysctlbyname(
350 cname.as_ptr(),
351 buf.as_mut_ptr().cast(),
352 &mut size,
353 std::ptr::null_mut(),
354 0,
355 )
356 };
357
358 if ret != 0 {
359 return String::new();
360 }
361
362 if buf.last() == Some(&0) {
363 buf.pop();
364 }
365
366 String::from_utf8_lossy(&buf).trim().to_string()
367}
368
369#[cfg(target_os = "macos")]
370fn sysctl_usize(name: &str) -> usize {
371 use std::ffi::CString;
372
373 let Ok(cname) = CString::new(name) else {
374 return 0;
375 };
376
377 let mut value: usize = 0;
378 let mut size = std::mem::size_of::<usize>();
379 let ret = unsafe {
381 libc::sysctlbyname(
382 cname.as_ptr(),
383 &mut value as *mut _ as *mut libc::c_void,
384 &mut size,
385 std::ptr::null_mut(),
386 0,
387 )
388 };
389
390 if ret == 0 { value } else { 0 }
391}
392
393#[cfg(test)]
394mod tests {
395 use googletest::prelude::*;
396 use rstest::rstest;
397
398 use super::*;
399
400 #[gtest]
401 #[rstest]
402 #[case::zero_total_delta(CpuTicks::new(0, 0), CpuTicks::new(0, 0), 0.0)]
403 #[case::fully_active(CpuTicks::new(0, 0), CpuTicks::new(100, 0), 100.0)]
404 #[case::fully_idle(CpuTicks::new(0, 0), CpuTicks::new(100, 100), 0.0)]
405 #[case::half_active(CpuTicks::new(0, 0), CpuTicks::new(100, 50), 50.0)]
406 #[case::quarter_active_nonzero_base(CpuTicks::new(100, 50), CpuTicks::new(300, 100), 75.0)]
407 #[case::after_before_saturates_total(CpuTicks::new(100, 50), CpuTicks::new(50, 20), 0.0)]
408 #[case::idle_gt_total_saturates_active(CpuTicks::new(0, 0), CpuTicks::new(100, 200), 0.0)]
409 fn cpu_usage_cases(
410 #[case] before: CpuTicks,
411 #[case] after: CpuTicks,
412 #[case] expected: f64,
413 ) -> Result<()> {
414 verify_that!(cpu_usage_from_ticks(before, after), near(expected, 1e-9))
415 }
416
417 #[gtest]
418 fn logical_cores_positive() -> Result<()> {
419 verify_that!(logical_cores(), gt(0))
420 }
421
422 #[gtest]
423 fn optimal_le_logical() -> Result<()> {
424 verify_that!(optimal_concurrency(), le(logical_cores()))
425 }
426
427 #[gtest]
428 fn physical_cores_positive() -> Result<()> {
429 verify_that!(physical_cores(), gt(0))
430 }
431
432 #[gtest]
433 fn load_average_non_negative() -> Result<()> {
434 let avg = read_load_average();
435
436 for v in avg {
437 verify_that!(v, ge(0.0))?;
438 }
439
440 Ok(())
441 }
442
443 #[gtest]
444 fn memory_mb_non_negative() -> Result<()> {
445 verify_that!(read_memory_mb(), ge(0.0))
446 }
447
448 #[gtest]
449 fn os_memory_non_negative() -> Result<()> {
450 let (total, available) = read_os_memory_mb();
451
452 verify_that!(total, ge(0.0))?;
453
454 verify_that!(available, ge(0.0))
455 }
456
457 #[gtest]
458 fn cpu_ticks_some() -> Result<()> {
459 if cfg!(any(
460 target_os = "linux",
461 target_os = "macos",
462 target_os = "windows"
463 )) {
464 verify_that!(read_cpu_ticks(), some(anything()))?;
465 }
466
467 Ok(())
468 }
469}