aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2024-12-29 09:42:36 -0800
committerFranck Cuny <franck@fcuny.net>2024-12-29 09:42:36 -0800
commit36761bad994f435f009415caac011f5972001eeb (patch)
treeafd9611485958a9318a04af52627cc7bfe8fa2dc
parentuse a constant value for the path to sysctl (diff)
downloadx-36761bad994f435f009415caac011f5972001eeb.tar.gz
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.
-rw-r--r--src/apple_silicon/src/main.rs7
-rw-r--r--src/apple_silicon/src/soc.rs190
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<T> = std::result::Result<T, Error>;
+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<Watts>,
+ /// Maximum GPU power in watts (if available)
+ pub gpu_max_power: Option<Watts>,
+ /// Maximum CPU bandwidth in GB/s (if available)
+ pub cpu_max_bw: Option<Bandwidth>,
+ /// Maximum GPU bandwidth in GB/s (if available)
+ pub gpu_max_bw: Option<Bandwidth>,
+ /// Number of efficiency cores
+ pub e_core_count: Option<CoreCount>,
+ /// Number of performance cores
+ pub p_core_count: Option<CoreCount>,
+}
+
+#[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<SocInfo> {
- 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<u16> {
+fn gpu_info(cmd: &impl SystemCommand) -> Result<u16> {
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<u16> {
Ok(num_gpu_cores)
}
+
+/// Trait for system command execution
+pub trait SystemCommand {
+ fn execute(&self, binary: &str, args: &[&str]) -> Result<Output>;
+}
+
+/// Real command executor
+pub struct RealCommand;
+
+impl SystemCommand for RealCommand {
+ fn execute(&self, binary: &str, args: &[&str]) -> Result<Output> {
+ Ok(Command::new(binary).args(args).output()?)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::os::unix::process::ExitStatusExt;
+
+ struct MockCommand {
+ output: Vec<u8>,
+ }
+
+ 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<Output> {
+ 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 { .. })));
+ }
+}