diff --git a/crates/asm/src/lib.rs b/crates/asm/src/lib.rs index 19abc0e..04db2db 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -731,6 +731,80 @@ pub unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 { } } +/// Direct `sched_getaffinity(2)` syscall +/// +/// # Returns +/// +/// On success, the number of bytes written to the mask buffer (always a +/// multiple of `sizeof(long)`). On error, a negative errno. +/// +/// # Safety +/// +/// The caller must ensure that `mask` points to a buffer of at least +/// `mask_size` bytes. +#[inline] +pub unsafe fn sys_sched_getaffinity( + pid: i32, + mask_size: usize, + mask: *mut u8, +) -> i32 { + #[cfg(target_arch = "x86_64")] + unsafe { + let ret: i64; + core::arch::asm!( + "syscall", + in("rax") 204i64, // __NR_sched_getaffinity + in("rdi") pid, + in("rsi") mask_size, + in("rdx") mask, + lateout("rax") ret, + lateout("rcx") _, + lateout("r11") _, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } + } + + #[cfg(target_arch = "aarch64")] + unsafe { + let ret: i64; + core::arch::asm!( + "svc #0", + in("x8") 123i64, // __NR_sched_getaffinity + in("x0") pid, + in("x1") mask_size, + in("x2") mask, + lateout("x0") ret, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } + } + + #[cfg(target_arch = "riscv64")] + unsafe { + let ret: i64; + core::arch::asm!( + "ecall", + in("a7") 123i64, // __NR_sched_getaffinity + in("a0") pid, + in("a1") mask_size, + in("a2") mask, + lateout("a0") ret, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } + } +} + /// Direct syscall to exit the process /// /// # Safety diff --git a/crates/lib/src/cpu.rs b/crates/lib/src/cpu.rs new file mode 100644 index 0000000..c6ed8a0 --- /dev/null +++ b/crates/lib/src/cpu.rs @@ -0,0 +1,286 @@ +use alloc::string::String; + +use crate::{Error, syscall::read_file_fast, system::write_u64}; + +/// Gets CPU model name (trimmed), or empty string if unavailable. +#[cfg_attr(feature = "hotpath", hotpath::measure)] +pub fn get_cpu_name() -> String { + get_model_name().unwrap_or_default() +} + +/// Gets CPU core/thread info as a string. +/// +/// Format: `{cores} cores ({p}p/{e}e), {threads} threads` on hybrid Intel, +/// `{cores} cores, {threads} threads` otherwise. +/// +/// # Errors +/// +/// Returns an error if the thread count cannot be determined. +#[cfg_attr(feature = "hotpath", hotpath::measure)] +pub fn get_cpu_cores() -> Result { + let threads = get_thread_count()?; + let cores = get_core_count(threads); + + let mut result = String::new(); + + write_u64(&mut result, u64::from(cores)); + result.push_str(" cores"); + + if let Some((p, e)) = get_pe_cores() { + result.push_str(" ("); + write_u64(&mut result, u64::from(p)); + result.push_str("p/"); + write_u64(&mut result, u64::from(e)); + result.push_str("e)"); + } + + if threads != cores { + result.push_str(", "); + write_u64(&mut result, u64::from(threads)); + result.push_str(" threads"); + } + + Ok(result) +} + +/// Count online threads via `sched_getaffinity(2)`. +fn get_thread_count() -> Result { + let mut mask = [0u8; 128]; + let ret = unsafe { + crate::syscall::sys_sched_getaffinity(0, mask.len(), mask.as_mut_ptr()) + }; + if ret < 0 { + return Err(Error::from_raw_os_error(-ret)); + } + + #[allow(clippy::cast_sign_loss)] + let bytes = ret as usize; + let mut count = 0u32; + for &byte in &mask[..bytes] { + count += byte.count_ones(); + } + Ok(count) +} + +/// Derive physical core count from thread count and topology. +fn get_core_count(threads: u32) -> u32 { + let Some(smt_width) = + count_cpulist("/sys/devices/system/cpu/cpu0/topology/thread_siblings_list") + else { + return threads; + }; + if smt_width == 0 { + return threads; + } + threads / smt_width +} + +/// Detect P-core and E-core counts via sysfs PMU device files, which is done +/// by reading `/sys/devices/cpu_core/cpus` and `/sys/devices/cpu_atom/cpus`. +fn get_pe_cores() -> Option<(u32, u32)> { + let p = count_cpulist("/sys/devices/cpu_core/cpus")?; + let e = count_cpulist("/sys/devices/cpu_atom/cpus").unwrap_or(0); + if p > 0 || e > 0 { Some((p, e)) } else { None } +} + +/// Parse a cpulist file and count listed CPUs. +fn count_cpulist(path: &str) -> Option { + let mut buf = [0u8; 64]; + let n = read_file_fast(path, &mut buf).ok()?; + let data = &buf[..n]; + + let mut count = 0u32; + let mut i = 0; + while i < data.len() { + // Parse start number + let start = parse_num(data, &mut i); + if i < data.len() && data[i] == b'-' { + i += 1; + let end = parse_num(data, &mut i); + // The Kernel always emits ascending ranges, so end is always >= start + // https://github.com/torvalds/linux/blob/v6.19/lib/vsprintf.c#L1276-L1303 + count += end - start + 1; + } else { + count += 1; + } + // Skip comma or newline + if i < data.len() && (data[i] == b',' || data[i] == b'\n') { + i += 1; + } + } + Some(count) +} + +/// Parse a decimal number from a byte slice, advancing the index. +fn parse_num(data: &[u8], i: &mut usize) -> u32 { + let mut n = 0u32; + while *i < data.len() && data[*i].is_ascii_digit() { + n = n * 10 + u32::from(data[*i] - b'0'); + *i += 1; + } + n +} + +/// Read CPU frequency in MHz. Tries sysfs first, then cpuinfo fields. +fn get_cpu_freq_mhz() -> Option { + // Try sysfs cpuinfo_max_freq (in kHz) + let mut buf = [0u8; 32]; + if let Ok(n) = read_file_fast( + "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq", + &mut buf, + ) { + let mut khz = 0u32; + for &b in &buf[..n] { + if b.is_ascii_digit() { + khz = khz * 10 + u32::from(b - b'0'); + } + } + if khz > 0 { + return Some(khz / 1000); + } + } + // Fall back to cpuinfo fields + let mut buf2 = [0u8; 2048]; + let n = read_file_fast("/proc/cpuinfo", &mut buf2).ok()?; + let data = &buf2[..n]; + for key in &[b"cpu MHz" as &[u8], b"cpu MHz dynamic", b"CPU MHz"] { + if let Some(val) = extract_field(data, key) { + // Parse integer part of the MHz value (e.g. "5200.00" -> 5200) + let mut mhz = 0u32; + for &b in val.as_bytes() { + if b == b'.' { + break; + } + if b.is_ascii_digit() { + mhz = mhz * 10 + u32::from(b - b'0'); + } + } + if mhz > 0 { + return Some(mhz); + } + } + } + None +} + +/// Parse CPU model name from `/proc/cpuinfo` and append frequency. +fn get_model_name() -> Option { + let mut buf = [0u8; 2048]; + let n = read_file_fast("/proc/cpuinfo", &mut buf).ok()?; + let data = &buf[..n]; + + for key in &[ + b"model name" as &[u8], + b"Model Name", + b"uarch", + b"isa", + b"cpu", + b"machine", + b"vendor_id", + ] { + if let Some(val) = extract_field(data, key) { + let trimmed = trim(val); + if !trimmed.is_empty() { + let mut name = String::from(trimmed); + if let Some(mhz) = get_cpu_freq_mhz() { + name.push_str(" @ "); + // Round to nearest 0.01 GHz, then split so carries (e.g. 1999 MHz) + // roll into the integer part instead of overflowing the fraction. + let rounded_centesimal = (mhz + 5) / 10; + let ghz_int = rounded_centesimal / 100; + let ghz_frac = rounded_centesimal % 100; + write_u64(&mut name, u64::from(ghz_int)); + name.push('.'); + if ghz_frac < 10 { + name.push('0'); + } + write_u64(&mut name, u64::from(ghz_frac)); + name.push_str(" GHz"); + } + return Some(name); + } + } + } + + None +} + +/// Extract value of first occurrence of `key` in cpuinfo. +fn extract_field<'a>(data: &'a [u8], key: &[u8]) -> Option<&'a str> { + let mut i = 0; + while i < data.len() { + let remaining = &data[i..]; + let eol = remaining + .iter() + .position(|&b| b == b'\n') + .unwrap_or(remaining.len()); + let line = &remaining[..eol]; + + if line.starts_with(key) { + let mut p = key.len(); + while p < line.len() && (line[p] == b'\t' || line[p] == b' ') { + p += 1; + } + if p < line.len() && line[p] == b':' { + p += 1; + while p < line.len() && line[p] == b' ' { + p += 1; + } + return core::str::from_utf8(&line[p..]).ok(); + } + } + + i += eol + 1; + } + None +} + +/// Strip noise from model names. +fn trim(name: &str) -> &str { + let b = name.as_bytes(); + let mut end = b.len(); + + while end > 0 && b[end - 1].is_ascii_whitespace() { + end -= 1; + } + + if end >= 10 && &b[end - 10..end] == b" Processor" { + end -= 10; + } else if end >= 4 && &b[end - 4..end] == b" CPU" { + end -= 4; + } + while end > 0 && b[end - 1].is_ascii_whitespace() { + end -= 1; + } + + if end >= 3 && &b[end - 3..end] == b"(R)" { + end -= 3; + } else if end >= 4 + && (&b[end - 4..end] == b"(TM)" || &b[end - 4..end] == b"(tm)") + { + end -= 4; + } + while end > 0 && b[end - 1].is_ascii_whitespace() { + end -= 1; + } + + if end > 7 && &b[end - 5..end] == b"-Core" { + let mut p = end - 5; + while p > 0 && b[p - 1].is_ascii_digit() { + p -= 1; + } + if p > 0 && b[p - 1] == b' ' { + end = p - 1; + } + } + while end > 0 && b[end - 1].is_ascii_whitespace() { + end -= 1; + } + + let mut start = 0; + while start < end && b[start].is_ascii_whitespace() { + start += 1; + } + + &name[start..end] +} diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 1d89f8d..1f74569 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -2,6 +2,7 @@ extern crate alloc; pub mod colors; +pub mod cpu; pub mod desktop; pub mod release; pub mod system; @@ -23,6 +24,7 @@ pub use microfetch_asm::{ sys_close, sys_open, sys_read, + sys_sched_getaffinity, sys_statfs, sys_sysinfo, sys_uname, @@ -281,6 +283,8 @@ struct Fields { user_info: String, os_name: String, kernel_version: String, + cpu_name: String, + cpu_cores: String, shell: String, uptime: String, desktop: String, @@ -320,7 +324,7 @@ impl core::fmt::Write for StackWriter<'_> { } /// Custom logo art embedded at compile time via the `MICROFETCH_LOGO` -/// environment variable. Set it to 9 newline-separated lines of ASCII/Unicode +/// environment variable. Set it to 11 newline-separated lines of ASCII/Unicode /// art when building to replace the default NixOS logo: /// /// `MICROFETCH_LOGO="$(cat my_logo.txt)"` cargo build --release @@ -338,6 +342,8 @@ fn print_system_info(fields: &Fields) -> Result<(), Error> { user_info, os_name, kernel_version, + cpu_name, + cpu_cores, shell, uptime, desktop, @@ -349,7 +355,7 @@ fn print_system_info(fields: &Fields) -> Result<(), Error> { let no_color = colors::is_no_color(); let c = colors::Colors::new(no_color); - let mut buf = [0u8; 2048]; + let mut buf = [0u8; 2560]; let mut w = StackWriter::new(&mut buf); if CUSTOM_LOGO.is_empty() { @@ -357,24 +363,25 @@ fn print_system_info(fields: &Fields) -> Result<(), Error> { core::fmt::write( &mut w, format_args!( - "\n {b} ▟█▖ {cy}▝█▙ ▗█▛ {user_info} ~{rs}\n {b} \ - ▗▄▄▟██▄▄▄▄▄{cy}▝█▙█▛ {b}▖ {cy}\u{F313} {b}System{rs} \ - {os_name}\n {b} ▀▀▀▀▀▀▀▀▀▀▀▘{cy}▝██ {b}▟█▖ {cy}\u{E712} \ - {b}Kernel{rs} {kernel_version}\n {cy} ▟█▛ \ - {cy}▝█▘{b}▟█▛ {cy}\u{E795} {b}Shell{rs} {shell}\n \ - {cy}▟█████▛ {b}▟█████▛ {cy}\u{F017} {b}Uptime{rs} \ - {uptime}\n {cy} ▟█▛{b}▗█▖ {b}▟█▛ {cy}\u{F2D2} \ - {b}Desktop{rs} {desktop}\n {cy} ▝█▛ \ - {b}██▖{cy}▗▄▄▄▄▄▄▄▄▄▄▄ {cy}\u{F035B} {b}Memory{rs} \ - {memory_usage}\n {cy} ▝ {b}▟█▜█▖{cy}▀▀▀▀▀██▛▀▀▘ \ - {cy}\u{F194E} {b}Storage (/){rs} {storage}\n {b} ▟█▘ ▜█▖ \ - {cy}▝█▛ {cy}\u{E22B} {b}Colors{rs} {colors}\n\n", + "\n {b}⠀⠀⠀⠀⠀⠀⢼⣿⣄⠀⠀⠀{cy}⠹⣿⣷⡀⠀⣠⣿⡧⠀⠀⠀⠀⠀⠀{rs} {user_info} ~{rs}\ + \n {b}⠀⠀⠀⠀⠀⠀⠈⢿⣿⣆⠀⠀⠀{cy}⠘⣿⣿⣴⣿⡿⠁⠀⠀⠀⠀⠀⠀{rs} {cy}\u{F313} {b}System{rs} \u{E621} {os_name}\ + \n {b}⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡜{cy}⢿⣿⣟⠀⠀⠀{b}⢀⡄⠀⠀⠀{rs} {cy}\u{E712} {b}Kernel{rs} \u{E621} {kernel_version}\ + \n {b}⠀⠀⠀⠉⠉⠉⠉{cy}⣩⣭⡭{b}⠉⠉⠉⠉⠉{cy}⠈⢿⣿⣆⠀{b}⢠⣿⣿⠂⠀⠀{rs} {cy}\u{F2DB} {b}CPU{rs} \u{E621} {cpu_name}\ + \n {cy}⠀⠀⠀⠀⠀⠀⣼⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⢻⡟{b}⣡⣿⣿⠃⠀⠀⠀{rs} {cy}\u{F4BC} {b}Topology{rs} \u{E621} {cpu_cores}\ + \n {cy}⢸⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀{b}⣰⣿⣿⣿⣿⣿⣿⡇{rs} {cy}\u{E795} {b}Shell{rs} \u{E621} {shell}\ + \n {cy}⠀⠀⠀⢠⣿⣿⢋{b}⣼⣧⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⡟⠀⠀⠀⠀⠀⠀{rs} {cy}\u{F017} {b}Uptime{rs} \u{E621} {uptime}\ + \n {cy}⠀⠀⠠⣿⣿⠃⠀{b}⠹⣿⣷⡀{cy}⣀⣀⣀⣀⣀{b}⣚⣛⣋{cy}⣀⣀⣀⣀⠀⠀⠀{rs} {cy}\u{F2D2} {b}Desktop{rs} \u{E621} {desktop}\ + \n {cy}⠀⠀⠀⠘⠁⠀⠀⠀{b}⣽⣿⣷⡜{cy}⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀{rs} {cy}\u{F035B} {b}Memory{rs} \u{E621} {memory_usage}\ + \n {b}⠀⠀⠀⠀⠀⠀⢀⣾⣿⠟⣿⣿⡄⠀⠀⠀{cy}⠹⣿⣷⡀⠀⠀⠀⠀⠀⠀{rs} {cy}\u{F194E} {b}Storage (/){rs} \u{E621} {storage}\ + \n {b}⠀⠀⠀⠀⠀⠀⢺⣿⠋⠀⠈⢿⣿⣆⠀⠀⠀{cy}⠙⣿⡗⠀⠀⠀⠀⠀⠀{rs} {cy}\u{E22B} {b}Colors{rs} \u{E621} {colors}\n\n", b = c.blue, cy = c.cyan, rs = c.reset, user_info = user_info, os_name = os_name, kernel_version = kernel_version, + cpu_name = cpu_name, + cpu_cores = cpu_cores, shell = shell, uptime = uptime, desktop = desktop, @@ -385,39 +392,41 @@ fn print_system_info(fields: &Fields) -> Result<(), Error> { ) .ok(); } else { - // Custom logo is 9 lines from MICROFETCH_LOGO env var, one per info row. - // Lines beyond 9 are ignored; missing lines render as empty. + // Custom logo is 11 lines from MICROFETCH_LOGO env var, one per info row. + // Lines beyond 11 are ignored; missing lines render as empty. let mut lines = CUSTOM_LOGO.split('\n'); - let logo_rows: [&str; 9] = + let logo_rows: [&str; 11] = core::array::from_fn(|_| lines.next().unwrap_or("")); // Row format mirrors the default logo path exactly. - let rows: [(&str, &str, &str, &str, &str); 9] = [ + let rows: [(&str, &str, &str, &str, &str); 11] = [ ("", "", user_info.as_str(), " ", " ~"), - ("\u{F313} ", "System", os_name.as_str(), " ", ""), + ("\u{F313} ", "System", os_name.as_str(), " \u{E621} ", ""), ( "\u{E712} ", "Kernel", kernel_version.as_str(), - " ", + " \u{E621} ", "", ), - ("\u{E795} ", "Shell", shell.as_str(), " ", ""), - ("\u{F017} ", "Uptime", uptime.as_str(), " ", ""), - ("\u{F2D2} ", "Desktop", desktop.as_str(), " ", ""), + ("\u{F2DB} ", "CPU", cpu_name.as_str(), " \u{E621} ", ""), + ("\u{F4BC} ", "Topology", cpu_cores.as_str(), " \u{E621} ", ""), + ("\u{E795} ", "Shell", shell.as_str(), " \u{E621} ", ""), + ("\u{F017} ", "Uptime", uptime.as_str(), " \u{E621} ", ""), + ("\u{F2D2} ", "Desktop", desktop.as_str(), " \u{E621} ", ""), ( "\u{F035B} ", "Memory", memory_usage.as_str(), - " ", + " \u{E621} ", "", ), - ("\u{F194E} ", "Storage (/)", storage.as_str(), " ", ""), - ("\u{E22B} ", "Colors", colors.as_str(), " ", ""), + ("\u{F194E} ", "Storage (/)", storage.as_str(), " \u{E621} ", ""), + ("\u{E22B} ", "Colors", colors.as_str(), " \u{E621} ", ""), ]; core::fmt::write(&mut w, format_args!("\n")).ok(); - for i in 0..9 { + for i in 0..11 { let (icon, key, value, spacing, suffix) = rows[i]; if key.is_empty() { // Row 1 has no icon/key, just logo + user_info @@ -534,6 +543,8 @@ pub unsafe fn run(argc: i32, argv: *const *const u8) -> Result<(), Error> { user_info: system::get_username_and_hostname(&utsname), os_name: release::get_os_pretty_name()?, kernel_version: release::get_system_info(&utsname), + cpu_name: cpu::get_cpu_name(), + cpu_cores: cpu::get_cpu_cores()?, shell: system::get_shell(), desktop: desktop::get_desktop_info(), uptime: uptime::get_current()?, diff --git a/crates/lib/src/system.rs b/crates/lib/src/system.rs index b902dac..1db8510 100644 --- a/crates/lib/src/system.rs +++ b/crates/lib/src/system.rs @@ -149,7 +149,7 @@ fn round_f64(x: f64) -> f64 { } /// Write a u64 to string -fn write_u64(s: &mut String, mut n: u64) { +pub fn write_u64(s: &mut String, mut n: u64) { if n == 0 { s.push('0'); return;