use super::common::{clean_value, extract_dmidecode_value, parse_key_value};
use crate::domain::{CpuInfo, CpuTopology};
use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
static ref CPU_SPEED_RE: Regex = Regex::new(r"(\d+(?:\.\d+)?)\s*(MHz|GHz)").unwrap();
static ref CORE_COUNT_RE: Regex = Regex::new(r"(\d+)").unwrap();
}
pub fn parse_sysfs_freq_khz(content: &str) -> Result<u32, String> {
let khz: u64 = content
.trim()
.parse()
.map_err(|e| format!("Failed to parse frequency: {}", e))?;
Ok((khz / 1000) as u32)
}
pub fn parse_sysfs_cache_size(content: &str) -> Result<u32, String> {
let trimmed = content.trim();
if trimmed.ends_with('K') || trimmed.ends_with('k') {
let value: u32 = trimmed[..trimmed.len() - 1]
.parse()
.map_err(|e| format!("Failed to parse cache size: {}", e))?;
Ok(value)
} else if trimmed.ends_with('M') || trimmed.ends_with('m') {
let value: u32 = trimmed[..trimmed.len() - 1]
.parse()
.map_err(|e| format!("Failed to parse cache size: {}", e))?;
Ok(value * 1024)
} else {
let value: u32 = trimmed
.parse()
.map_err(|e| format!("Failed to parse cache size: {}", e))?;
Ok(value / 1024)
}
}
pub fn parse_proc_cpuinfo(content: &str) -> Result<CpuInfo, String> {
let mut cpu_info = CpuInfo::default();
for line in content.lines() {
if let Some((key, value)) = line.split_once(':') {
let key = key.trim();
let value = value.trim();
match key {
"vendor_id" => cpu_info.vendor = value.to_string(),
"model name" if cpu_info.model.is_empty() => {
cpu_info.model = value.to_string();
}
"flags" | "Features" => {
cpu_info.flags = value.split_whitespace().map(|s| s.to_string()).collect();
}
"CPU implementer" if cpu_info.vendor.is_empty() => {
cpu_info.vendor = match value {
"0x41" => "ARM".to_string(),
"0x4e" => "NVIDIA".to_string(),
"0x51" => "Qualcomm".to_string(),
"0x61" => "Apple".to_string(),
_ => value.to_string(),
};
}
_ => {}
}
}
}
Ok(cpu_info)
}
pub fn parse_lscpu_output(lscpu_output: &str) -> Result<CpuInfo, String> {
let mut model = "Unknown CPU".to_string();
let mut cores = 1u32;
let mut threads = 1u32;
let mut sockets = 1u32;
let mut speed = "Unknown".to_string();
for line in lscpu_output.lines() {
if let Ok((key, value)) = parse_key_value(line, ':') {
match key.as_str() {
"Model name" => {
model = clean_value(&value);
}
"CPU(s)" => {
if let Ok(total_cpus) = value.parse::<u32>() {
threads = total_cpus;
}
}
"Core(s) per socket" => {
if let Ok(cores_per_socket) = value.parse::<u32>() {
cores = cores_per_socket;
}
}
"Socket(s)" => {
if let Ok(socket_count) = value.parse::<u32>() {
sockets = socket_count;
}
}
"Thread(s) per core" => {
if let Ok(threads_per_core) = value.parse::<u32>() {
threads = threads_per_core;
}
}
"CPU MHz" | "CPU max MHz" => {
speed = format!("{} MHz", clean_value(&value));
}
_ => {}
}
}
}
Ok(CpuInfo {
model,
cores,
threads,
sockets,
speed,
..Default::default()
})
}
pub fn parse_dmidecode_cpu(dmidecode_output: &str) -> Result<CpuInfo, String> {
let model = extract_dmidecode_value(dmidecode_output, "Version")
.unwrap_or_else(|_| "Unknown CPU".to_string());
let speed = extract_dmidecode_value(dmidecode_output, "Current Speed")
.or_else(|_| extract_dmidecode_value(dmidecode_output, "Max Speed"))
.unwrap_or_else(|_| "Unknown".to_string());
let core_count_str =
extract_dmidecode_value(dmidecode_output, "Core Count").unwrap_or_else(|_| "1".to_string());
let cores = core_count_str.parse::<u32>().unwrap_or(1);
let thread_count_str = extract_dmidecode_value(dmidecode_output, "Thread Count")
.unwrap_or_else(|_| "1".to_string());
let threads = thread_count_str.parse::<u32>().unwrap_or(1);
Ok(CpuInfo {
model: clean_value(&model),
cores,
threads,
sockets: 1, speed: clean_value(&speed),
..Default::default()
})
}
pub fn parse_macos_cpu_info(system_profiler_output: &str) -> Result<CpuInfo, String> {
let mut model = "Unknown CPU".to_string();
let mut cores = 1u32;
let mut speed = "Unknown".to_string();
for line in system_profiler_output.lines() {
let trimmed = line.trim();
if trimmed.starts_with("Chip:") {
model = trimmed
.split(':')
.nth(1)
.unwrap_or("Unknown CPU")
.trim()
.to_string();
} else if trimmed.starts_with("Processor Name:") {
model = trimmed
.split(':')
.nth(1)
.unwrap_or("Unknown CPU")
.trim()
.to_string();
} else if trimmed.starts_with("Total Number of Cores:") {
let core_str = trimmed
.split(':')
.nth(1)
.unwrap_or("1")
.split_whitespace()
.next()
.unwrap_or("1");
cores = core_str.parse::<u32>().unwrap_or(1);
} else if trimmed.starts_with("Processor Speed:") {
speed = trimmed
.split(':')
.nth(1)
.unwrap_or("Unknown")
.trim()
.to_string();
}
}
Ok(CpuInfo {
model: clean_value(&model),
cores,
threads: 1, sockets: 1, speed: clean_value(&speed),
..Default::default()
})
}
pub fn combine_cpu_info(primary: CpuInfo, secondary: CpuInfo) -> CpuInfo {
CpuInfo {
model: if primary.model != "Unknown CPU" && !primary.model.is_empty() {
primary.model
} else {
secondary.model
},
cores: if primary.cores > 0 {
primary.cores
} else {
secondary.cores
},
threads: if primary.threads > 0 {
primary.threads
} else {
secondary.threads
},
sockets: if primary.sockets > 0 {
primary.sockets
} else {
secondary.sockets
},
speed: if primary.speed != "Unknown" && !primary.speed.is_empty() {
primary.speed
} else {
secondary.speed
},
..Default::default()
}
}
pub fn create_cpu_topology(cpu_info: &CpuInfo, numa_nodes: Option<u32>) -> CpuTopology {
let total_cores = cpu_info.cores * cpu_info.sockets;
let total_threads = total_cores * cpu_info.threads;
CpuTopology {
total_cores,
total_threads,
sockets: cpu_info.sockets,
cores_per_socket: cpu_info.cores,
threads_per_core: cpu_info.threads,
numa_nodes: numa_nodes.unwrap_or(1),
cpu_model: cpu_info.model.clone(),
}
}
pub fn create_cpu_summary(cpu_topology: &CpuTopology) -> String {
format!(
"{} ({} Socket{}, {} Core{}/Socket, {} Thread{}/Core, {} NUMA Node{})",
cpu_topology.cpu_model,
cpu_topology.sockets,
if cpu_topology.sockets == 1 { "" } else { "s" },
cpu_topology.cores_per_socket,
if cpu_topology.cores_per_socket == 1 {
""
} else {
"s"
},
cpu_topology.threads_per_core,
if cpu_topology.threads_per_core == 1 {
""
} else {
"s"
},
cpu_topology.numa_nodes,
if cpu_topology.numa_nodes == 1 {
""
} else {
"s"
}
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_lscpu_output() {
let lscpu_output = r#"Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 39 bits physical, 48 bits virtual
CPU(s): 16
On-line CPU(s) list: 0-15
Thread(s) per core: 2
Core(s) per socket: 8
Socket(s): 1
Model name: Intel(R) Core(TM) i7-10875H CPU @ 2.30GHz
CPU family: 6
Model: 165
Stepping: 2
CPU MHz: 2300.000"#;
let cpu_info = parse_lscpu_output(lscpu_output).unwrap();
assert_eq!(cpu_info.model, "Intel(R) Core(TM) i7-10875H CPU @ 2.30GHz");
assert_eq!(cpu_info.cores, 8);
assert_eq!(cpu_info.threads, 2);
assert_eq!(cpu_info.sockets, 1);
assert_eq!(cpu_info.speed, "2300.000 MHz");
}
#[test]
fn test_parse_macos_cpu_info() {
let macos_output = r#"Hardware Overview:
Model Name: MacBook Pro
Model Identifier: MacBookPro18,2
Chip: Apple M1 Max
Total Number of Cores: 10 (8 performance and 2 efficiency)
Memory: 32 GB
System Firmware Version: 8419.121.2
OS Loader Version: 8419.121.2"#;
let cpu_info = parse_macos_cpu_info(macos_output).unwrap();
assert_eq!(cpu_info.model, "Apple M1 Max");
assert_eq!(cpu_info.cores, 10);
assert_eq!(cpu_info.sockets, 1);
}
#[test]
fn test_combine_cpu_info() {
let primary = CpuInfo {
model: "Intel Core i7".to_string(),
cores: 8,
threads: 2,
sockets: 1,
speed: "Unknown".to_string(),
..Default::default()
};
let secondary = CpuInfo {
model: "Unknown CPU".to_string(),
cores: 0,
threads: 0,
sockets: 0,
speed: "2.3 GHz".to_string(),
..Default::default()
};
let combined = combine_cpu_info(primary, secondary);
assert_eq!(combined.model, "Intel Core i7");
assert_eq!(combined.cores, 8);
assert_eq!(combined.speed, "2.3 GHz");
}
#[test]
fn test_create_cpu_topology() {
let cpu_info = CpuInfo {
model: "Intel Core i7".to_string(),
cores: 8,
threads: 2,
sockets: 1,
speed: "2.3 GHz".to_string(),
..Default::default()
};
let topology = create_cpu_topology(&cpu_info, Some(1));
assert_eq!(topology.total_cores, 8);
assert_eq!(topology.total_threads, 16);
assert_eq!(topology.sockets, 1);
assert_eq!(topology.cores_per_socket, 8);
assert_eq!(topology.threads_per_core, 2);
assert_eq!(topology.numa_nodes, 1);
}
#[test]
fn test_create_cpu_summary() {
let topology = CpuTopology {
total_cores: 16,
total_threads: 32,
sockets: 2,
cores_per_socket: 8,
threads_per_core: 2,
numa_nodes: 2,
cpu_model: "Intel Xeon Gold 6226R".to_string(),
};
let summary = create_cpu_summary(&topology);
assert!(summary.contains("Intel Xeon Gold 6226R"));
assert!(summary.contains("2 Sockets"));
assert!(summary.contains("8 Cores/Socket"));
assert!(summary.contains("2 Threads/Core"));
assert!(summary.contains("2 NUMA Nodes"));
}
}