diff --git a/Cargo.lock b/Cargo.lock index 5dc55c1..1a26061 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,7 +271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -513,6 +513,12 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + [[package]] name = "parse_datetime" version = "0.7.0" @@ -783,7 +789,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -923,7 +929,7 @@ dependencies = [ "getrandom", "once_cell", "rustix 0.38.44", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1113,6 +1119,7 @@ name = "uu_lscpu" version = "0.0.1" dependencies = [ "clap", + "parse-size", "regex", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index f2864ca..9222a85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ dns-lookup = "2.0.4" libc = "0.2.152" linux-raw-sys = { version = "0.7.0", features = ["ioctl"] } nix = { version = "0.29", default-features = false } +parse-size = "1.1.0" phf = "0.11.2" phf_codegen = "0.11.2" rand = { version = "0.9.0", features = ["small_rng"] } diff --git a/src/uu/lscpu/Cargo.toml b/src/uu/lscpu/Cargo.toml index 8d45f12..bb8c86a 100644 --- a/src/uu/lscpu/Cargo.toml +++ b/src/uu/lscpu/Cargo.toml @@ -17,3 +17,4 @@ uucore = { workspace = true } clap = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +parse-size = { workspace = true } diff --git a/src/uu/lscpu/src/lscpu.rs b/src/uu/lscpu/src/lscpu.rs index bc532f4..71bbe1d 100644 --- a/src/uu/lscpu/src/lscpu.rs +++ b/src/uu/lscpu/src/lscpu.rs @@ -6,7 +6,7 @@ use clap::{crate_version, Arg, ArgAction, Command}; use regex::RegexBuilder; use serde::Serialize; -use std::{cmp, fs}; +use std::{cmp, collections::HashMap, fs}; use uucore::{error::UResult, format_usage, help_about, help_usage}; mod options { @@ -92,15 +92,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { arch_info.add_child(CpuInfo::new("Byte Order", byte_order, None)); } - let cpu_topology = sysfs::read_cpu_topology(); - cpu_infos.push(arch_info); - cpu_infos.push(CpuInfo::new( - "CPU(s)", - &format!("{}", cpu_topology.cpus.len()), + + let cpu_topology = sysfs::read_cpu_topology(); + let mut cores_info = CpuInfo::new("CPU(s)", &format!("{}", cpu_topology.cpus.len()), None); + + cores_info.add_child(CpuInfo::new( + "On-line CPU(s) list", + &sysfs::read_online_cpus(), None, )); + cpu_infos.push(cores_info); + // TODO: This is currently quite verbose and doesn't strictly respect the hierarchy of `/proc/cpuinfo` contents // ie. the file might contain multiple sections, each with their own vendor_id/model name etc. but right now // we're just taking whatever our regex matches first and using that @@ -132,6 +136,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { cpu_infos.push(vendor_info); } + if let Some(cache_info) = calculate_cache_totals(cpu_topology.cpus) { + cpu_infos.push(cache_info); + } + let vulns = sysfs::read_cpu_vulnerabilities(); if !vulns.is_empty() { let mut vuln_info = CpuInfo::new("Vulnerabilities", "", None); @@ -146,6 +154,53 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Ok(()) } +fn calculate_cache_totals(cpus: Vec<sysfs::Cpu>) -> Option<CpuInfo> { + let mut by_levels: HashMap<String, Vec<&sysfs::CpuCache>> = HashMap::new(); + let all_caches: Vec<_> = cpus.iter().flat_map(|cpu| &cpu.caches).collect(); + + if all_caches.is_empty() { + return None; + } + + for cache in all_caches { + let type_suffix = match cache.typ { + sysfs::CacheType::Instruction => "i", + sysfs::CacheType::Data => "d", + _ => "", + }; + let level_key = format!("L{}{}", cache.level, type_suffix); + + if let Some(caches) = by_levels.get_mut(&level_key) { + caches.push(cache); + } else { + by_levels.insert(level_key, vec![cache]); + } + } + + let mut cache_info = CpuInfo::new("Caches (sum of all)", "", None); + + for (level, caches) in by_levels.iter_mut() { + // Cache instances that are shared across multiple CPUs should have the same `shared_cpu_map` value + // Deduplicating the list on a per-level basic using the CPU map ensures that we don't count any shared caches multiple times + caches.sort_by(|a, b| a.shared_cpu_map.cmp(&b.shared_cpu_map)); + caches.dedup_by_key(|c| &c.shared_cpu_map); + + let count = caches.len(); + let size_total = caches.iter().fold(0_u64, |acc, c| acc + c.size); + cache_info.add_child(CpuInfo::new( + level, + // TODO: Format sizes using `KiB`, `MiB` etc. + &format!("{} bytes ({} instances)", size_total, count), + None, + )); + } + + // Make sure caches get printed in alphabetical order + cache_info.children.sort_by(|a, b| a.field.cmp(&b.field)); + + Some(cache_info) +} + fn print_output(infos: CpuInfos, out_opts: OutputOptions) { if out_opts.json { println!("{}", infos.to_json()); diff --git a/src/uu/lscpu/src/sysfs.rs b/src/uu/lscpu/src/sysfs.rs index 8536e52..f4b1102 100644 --- a/src/uu/lscpu/src/sysfs.rs +++ b/src/uu/lscpu/src/sysfs.rs @@ -1,4 +1,5 @@ -use std::fs; +use parse_size::parse_size; +use std::{fs, path::PathBuf}; pub struct CpuVulnerability { pub name: String, @@ -9,49 +10,127 @@ pub struct CpuTopology { pub cpus: Vec<Cpu>, } +#[derive(Debug)] pub struct Cpu { _index: usize, - _caches: Vec<CpuCache>, + pub _pkg_id: usize, + pub caches: Vec<CpuCache>, } +#[derive(Debug)] pub struct CpuCache { - _index: usize, - _typ: String, - _level: String, - _size: String, + pub typ: CacheType, + pub level: usize, + pub size: u64, + pub shared_cpu_map: String, +} + +#[derive(Debug)] +pub enum CacheType { + Data, + Instruction, + Unified, +} + +// TODO: respect `--hex` option and output the bitmask instead of human-readable range +pub fn read_online_cpus() -> String { + fs::read_to_string("/sys/devices/system/cpu/online") + .expect("Could not read sysfs") + .trim() + .to_string() +} + +// Takes in a human-readable list of CPUs, and returns a list of indices parsed from that list +// These can come in the form of a plain range like `X-Y`, or a comma-separated ranges and indices ie. `1,3-4,7-8,10` +// Kernel docs with examples: https://www.kernel.org/doc/html/latest/admin-guide/cputopology.html +fn parse_cpu_list(list: &str) -> Vec<usize> { + let mut out: Vec<usize> = vec![]; + + for part in list.trim().split(",") { + if part.contains("-") { + let bounds: Vec<_> = part.split("-").flat_map(|x| x.parse::<usize>()).collect(); + assert_eq!(bounds.len(), 2); + for idx in bounds[0]..bounds[1] + 1 { + out.push(idx) + } + } else { + let idx = part.parse::<usize>().expect("Invalid CPU index value"); + out.push(idx); + } + } + + out } -// TODO: This should go through each CPU in sysfs and calculate things such as cache sizes and physical topology -// For now it just returns a list of CPUs which are enabled pub fn read_cpu_topology() -> CpuTopology { let mut out: Vec<Cpu> = vec![]; - // NOTE: All examples I could find was where this file contains a CPU index range in the form of `<start>-<end>` - // Theoretically, there might be a situation where some cores are disabled, so that `enabled` cannot be represented - // as a continuous range. For now we just assume it's always `X-Y` and use those as our bounds to read CPU information - let enabled_cpus = match fs::read_to_string("/sys/devices/system/cpu/enabled") { - Ok(content) => { - let parts: Vec<_> = content - .trim() - .split("-") - .flat_map(|part| part.parse::<usize>()) - .collect(); - assert_eq!(parts.len(), 2); - (parts[0], parts[1]) - } - Err(e) => panic!("Could not read sysfs: {}", e), - }; + let online_cpus = parse_cpu_list(&read_online_cpus()); + + for cpu_index in online_cpus { + let cpu_dir = PathBuf::from(format!("/sys/devices/system/cpu/cpu{}/", cpu_index)); + + let physical_pkg_id = fs::read_to_string(cpu_dir.join("topology/physical_package_id")) + .unwrap() + .trim() + .parse::<usize>() + .unwrap(); + + let caches = read_cpu_caches(cpu_index); - for cpu_index in enabled_cpus.0..(enabled_cpus.1 + 1) { out.push(Cpu { _index: cpu_index, - _caches: vec![], + _pkg_id: physical_pkg_id, + caches, }) } CpuTopology { cpus: out } } +fn read_cpu_caches(cpu_index: usize) -> Vec<CpuCache> { + let cpu_dir = PathBuf::from(format!("/sys/devices/system/cpu/cpu{}/", cpu_index)); + let cache_dir = fs::read_dir(cpu_dir.join("cache")).unwrap(); + let cache_paths = cache_dir + .flatten() + .filter(|x| x.path().is_dir()) + .map(|x| x.path()); + + let mut caches: Vec<CpuCache> = vec![]; + + for cache_path in cache_paths { + let type_string = fs::read_to_string(cache_path.join("type")).unwrap(); + + let c_type = match type_string.trim() { + "Unified" => CacheType::Unified, + "Data" => CacheType::Data, + "Instruction" => CacheType::Instruction, + _ => panic!("Unrecognized cache type: {}", type_string), + }; + + let c_level = fs::read_to_string(cache_path.join("level")) + .map(|s| s.trim().parse::<usize>().unwrap()) + .unwrap(); + + let size_string = fs::read_to_string(cache_path.join("size")).unwrap(); + let c_size = parse_size(size_string.trim()).unwrap(); + + let shared_cpu_map = fs::read_to_string(cache_path.join("shared_cpu_map")) + .unwrap() + .trim() + .to_string(); + + caches.push(CpuCache { + level: c_level, + size: c_size, + typ: c_type, + shared_cpu_map, + }); + } + + caches +} + pub fn read_freq_boost_state() -> Option<bool> { match fs::read_to_string("/sys/devices/system/cpu/cpufreq/boost") { Ok(content) => Some(content.trim() == "1"),