From 36761bad994f435f009415caac011f5972001eeb Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 29 Dec 2024 09:42:36 -0800 Subject: report TDP for GPU and CPU Add the TDP for the GPUs and CPUs, for the models we know about. Apple does not publish information about TDP so we're going to have to gather that information by going over various sites. Add some unit tests. --- src/apple_silicon/src/main.rs | 7 +- src/apple_silicon/src/soc.rs | 190 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 186 insertions(+), 11 deletions(-) diff --git a/src/apple_silicon/src/main.rs b/src/apple_silicon/src/main.rs index a1b6ba1..18c9747 100644 --- a/src/apple_silicon/src/main.rs +++ b/src/apple_silicon/src/main.rs @@ -4,7 +4,10 @@ mod soc; fn main() { let cpu_info = soc::SocInfo::new().unwrap(); println!( - "our CPU is an {}, and we have {} CPU cores, and {} GPU cores", - cpu_info.cpu_brand_name, cpu_info.num_cpu_cores, cpu_info.num_gpu_cores, + "our CPU is an {}, and we have {} CPU cores, and {} GPU cores. The TDP is {}.", + cpu_info.cpu_brand_name, + cpu_info.num_cpu_cores, + cpu_info.num_gpu_cores, + cpu_info.cpu_max_power.unwrap(), ); } diff --git a/src/apple_silicon/src/soc.rs b/src/apple_silicon/src/soc.rs index 43a29ba..8db2e1f 100644 --- a/src/apple_silicon/src/soc.rs +++ b/src/apple_silicon/src/soc.rs @@ -1,29 +1,118 @@ use crate::error::Error; -use std::process::Command; +use std::process::{Command, Output}; pub type Result = std::result::Result; +pub type Watts = u32; +pub type Bandwidth = u32; +pub type CoreCount = u16; /// Information about the Silicon chip pub struct SocInfo { /// The CPU brand name string pub cpu_brand_name: String, /// Number of CPU cores - pub num_cpu_cores: u16, + pub num_cpu_cores: CoreCount, /// Number of GPU cores - pub num_gpu_cores: u16, + pub num_gpu_cores: CoreCount, + /// Maximum CPU power in watts (if available) + pub cpu_max_power: Option, + /// Maximum GPU power in watts (if available) + pub gpu_max_power: Option, + /// Maximum CPU bandwidth in GB/s (if available) + pub cpu_max_bw: Option, + /// Maximum GPU bandwidth in GB/s (if available) + pub gpu_max_bw: Option, + /// Number of efficiency cores + pub e_core_count: Option, + /// Number of performance cores + pub p_core_count: Option, +} + +#[derive(Debug, PartialEq)] +enum AppleChip { + M1, + M1Pro, + M1Max, + M1Ultra, + M2, + M2Pro, + M2Max, + M2Ultra, + M3, + M3Pro, + M3Max, + Unknown, +} + +struct ChipSpecs { + cpu_tdp: Watts, + gpu_tdp: Watts, +} + +impl AppleChip { + fn from_brand_string(brand: &str) -> Self { + match brand { + s if s.contains("M1 Pro") => AppleChip::M1Pro, + s if s.contains("M1 Max") => AppleChip::M1Max, + s if s.contains("M1 Ultra") => AppleChip::M1Ultra, + s if s.contains("M1") => AppleChip::M1, + s if s.contains("M2 Pro") => AppleChip::M2Pro, + s if s.contains("M2 Max") => AppleChip::M2Max, + s if s.contains("M2 Ultra") => AppleChip::M2Ultra, + s if s.contains("M2") => AppleChip::M2, + s if s.contains("M3 Pro") => AppleChip::M3Pro, + s if s.contains("M3 Max") => AppleChip::M3Max, + s if s.contains("M3") => AppleChip::M3, + _ => AppleChip::Unknown, + } + } + + fn get_specs(&self) -> ChipSpecs { + match self { + AppleChip::M1 => ChipSpecs { + cpu_tdp: 20, + gpu_tdp: 20, + }, + AppleChip::M2 => ChipSpecs { + cpu_tdp: 20, + gpu_tdp: 22, + }, + AppleChip::M2Pro => ChipSpecs { + cpu_tdp: 30, + gpu_tdp: 35, + }, + AppleChip::M2Max => ChipSpecs { + cpu_tdp: 30, + gpu_tdp: 40, + }, + // Add more variants as needed + _ => ChipSpecs { + cpu_tdp: 0, + gpu_tdp: 0, + }, + } + } } impl SocInfo { pub fn new() -> Result { - let (cpu_brand_name, num_cpu_cores) = cpu_info()?; + let (cpu_brand_name, num_cpu_cores) = cpu_info(&RealCommand)?; + let num_gpu_cores = gpu_info(&RealCommand)?; - let num_gpu_cores = gpu_info()?; + let chip = AppleChip::from_brand_string(&cpu_brand_name); + let specs = chip.get_specs(); Ok(SocInfo { cpu_brand_name, num_cpu_cores, num_gpu_cores, + cpu_max_power: Some(specs.cpu_tdp), + gpu_max_power: Some(specs.gpu_tdp), + cpu_max_bw: None, + gpu_max_bw: None, + e_core_count: None, + p_core_count: None, }) } } @@ -31,7 +120,7 @@ impl SocInfo { // https://github.com/tlkh/asitop/blob/74ebe2cbc23d5b1eec874aebb1b9bacfe0e670cd/asitop/utils.py#L94 const SYSCTL_PATH: &str = "/usr/sbin/sysctl"; -fn cpu_info() -> Result<(String, u16)> { +fn cpu_info(cmd: &impl SystemCommand) -> Result<(String, u16)> { let binary = SYSCTL_PATH; let args = &[ // don't display the variable name @@ -40,7 +129,7 @@ fn cpu_info() -> Result<(String, u16)> { "machdep.cpu.core_count", ]; - let output = Command::new(binary).args(args).output()?; + let output = cmd.execute(binary, args)?; let buffer = String::from_utf8(output.stdout)?; let mut iter = buffer.split('\n'); @@ -58,11 +147,11 @@ fn cpu_info() -> Result<(String, u16)> { } // https://github.com/tlkh/asitop/blob/74ebe2cbc23d5b1eec874aebb1b9bacfe0e670cd/asitop/utils.py#L120 -fn gpu_info() -> Result { +fn gpu_info(cmd: &impl SystemCommand) -> Result { let binary = "/usr/sbin/system_profiler"; let args = &["-detailLevel", "basic", "SPDisplaysDataType"]; - let output = Command::new(binary).args(args).output()?; + let output = cmd.execute(binary, args)?; let buffer = String::from_utf8(output.stdout)?; let num_gpu_cores_line = buffer @@ -79,3 +168,86 @@ fn gpu_info() -> Result { Ok(num_gpu_cores) } + +/// Trait for system command execution +pub trait SystemCommand { + fn execute(&self, binary: &str, args: &[&str]) -> Result; +} + +/// Real command executor +pub struct RealCommand; + +impl SystemCommand for RealCommand { + fn execute(&self, binary: &str, args: &[&str]) -> Result { + Ok(Command::new(binary).args(args).output()?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::os::unix::process::ExitStatusExt; + + struct MockCommand { + output: Vec, + } + + impl MockCommand { + fn new(output: &str) -> Self { + Self { + output: output.as_bytes().to_vec(), + } + } + } + + impl SystemCommand for MockCommand { + fn execute(&self, _binary: &str, _args: &[&str]) -> Result { + Ok(Output { + status: std::process::ExitStatus::from_raw(0), + stdout: self.output.clone(), + stderr: Vec::new(), + }) + } + } + + #[test] + fn test_gpu_info() { + let mock_output = r#"Graphics/Displays: + Apple M2: + Total Number of Cores: 10"#; + let cmd = MockCommand::new(mock_output); + + let result = gpu_info(&cmd); + assert_eq!(result.unwrap(), 10); + } + + #[test] + fn test_cpu_info_success() { + let mock_output = "Apple M2\n8\n"; + let cmd = MockCommand::new(mock_output); + + let result = cpu_info(&cmd); + assert!(result.is_ok()); + let (brand, cores) = result.unwrap(); + assert_eq!(brand, "Apple M2"); + assert_eq!(cores, 8); + } + + #[test] + fn test_cpu_info_missing_core_count() { + let mock_output = "Apple M2\n"; + let cmd = MockCommand::new(mock_output); + + let result = cpu_info(&cmd); + assert!(matches!(result, Err(Error::ParseInt { .. }))); + } + + #[test] + fn test_cpu_info_invalid_core_count() { + let mock_output = "Apple M2\ninvalid\n"; + let cmd = MockCommand::new(mock_output); + + let result = cpu_info(&cmd); + assert!(matches!(result, Err(Error::ParseInt { .. }))); + } +} -- cgit v1.2.3