aboutsummaryrefslogtreecommitdiff
path: root/src/apple_silicon/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/apple_silicon/src/bin/apple_silicon.rs12
-rw-r--r--src/apple_silicon/src/error.rs23
-rw-r--r--src/apple_silicon/src/lib.rs2
-rw-r--r--src/apple_silicon/src/soc.rs302
4 files changed, 339 insertions, 0 deletions
diff --git a/src/apple_silicon/src/bin/apple_silicon.rs b/src/apple_silicon/src/bin/apple_silicon.rs
new file mode 100644
index 0000000..0f71eeb
--- /dev/null
+++ b/src/apple_silicon/src/bin/apple_silicon.rs
@@ -0,0 +1,12 @@
+use apple_silicon::soc::SocInfo;
+
+fn main() {
+ let cpu_info = SocInfo::new().unwrap();
+ println!(
+ "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/error.rs b/src/apple_silicon/src/error.rs
new file mode 100644
index 0000000..a70b9b5
--- /dev/null
+++ b/src/apple_silicon/src/error.rs
@@ -0,0 +1,23 @@
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("socinfo parsing error: `{0}`")]
+ Parse(String),
+
+ #[error("I/O error: `{source}`")]
+ Io {
+ #[from]
+ source: std::io::Error,
+ },
+
+ #[error("utf8 conversion error: `{source}`")]
+ Utf8Conversion {
+ #[from]
+ source: std::string::FromUtf8Error,
+ },
+
+ #[error("integer parsing error: `{source}`")]
+ ParseInt {
+ #[from]
+ source: std::num::ParseIntError,
+ },
+}
diff --git a/src/apple_silicon/src/lib.rs b/src/apple_silicon/src/lib.rs
new file mode 100644
index 0000000..74bd62c
--- /dev/null
+++ b/src/apple_silicon/src/lib.rs
@@ -0,0 +1,2 @@
+pub mod error;
+pub mod soc;
diff --git a/src/apple_silicon/src/soc.rs b/src/apple_silicon/src/soc.rs
new file mode 100644
index 0000000..0e2bf5c
--- /dev/null
+++ b/src/apple_silicon/src/soc.rs
@@ -0,0 +1,302 @@
+use crate::error::Error;
+
+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: CoreCount,
+ /// Number of GPU cores
+ 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: CoreCount,
+ /// Number of performance cores
+ pub p_core_count: 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,
+ cpu_bw: Bandwidth,
+ gpu_bw: Bandwidth,
+}
+
+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,
+ cpu_bw: 70,
+ gpu_bw: 70,
+ },
+ AppleChip::M1Pro => ChipSpecs {
+ cpu_tdp: 30,
+ gpu_tdp: 30,
+ cpu_bw: 200,
+ gpu_bw: 200,
+ },
+ AppleChip::M1Max => ChipSpecs {
+ cpu_tdp: 30,
+ gpu_tdp: 60,
+ cpu_bw: 250,
+ gpu_bw: 400,
+ },
+ AppleChip::M1Ultra => ChipSpecs {
+ cpu_tdp: 60,
+ gpu_tdp: 120,
+ cpu_bw: 500,
+ gpu_bw: 800,
+ },
+ AppleChip::M2 => ChipSpecs {
+ cpu_tdp: 25,
+ gpu_tdp: 15,
+ cpu_bw: 100,
+ gpu_bw: 100,
+ },
+ AppleChip::M2Pro => ChipSpecs {
+ cpu_tdp: 30,
+ gpu_tdp: 35,
+ cpu_bw: 0,
+ gpu_bw: 0,
+ },
+ AppleChip::M2Max => ChipSpecs {
+ cpu_tdp: 30,
+ gpu_tdp: 40,
+ cpu_bw: 0,
+ gpu_bw: 0,
+ },
+ // Add more variants as needed
+ _ => ChipSpecs {
+ cpu_tdp: 0,
+ gpu_tdp: 0,
+ cpu_bw: 0,
+ gpu_bw: 0,
+ },
+ }
+ }
+}
+
+impl SocInfo {
+ pub fn new() -> Result<SocInfo> {
+ let (cpu_brand_name, num_cpu_cores, e_core_count, p_core_count) = cpu_info(&RealCommand)?;
+ let num_gpu_cores = gpu_info(&RealCommand)?;
+
+ 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: Some(specs.cpu_bw),
+ gpu_max_bw: Some(specs.gpu_bw),
+ e_core_count,
+ p_core_count,
+ })
+ }
+}
+
+// https://github.com/tlkh/asitop/blob/74ebe2cbc23d5b1eec874aebb1b9bacfe0e670cd/asitop/utils.py#L94
+const SYSCTL_PATH: &str = "/usr/sbin/sysctl";
+
+fn cpu_info(cmd: &impl SystemCommand) -> Result<(String, u16, u16, u16)> {
+ let binary = SYSCTL_PATH;
+ let args = &[
+ // don't display the variable name
+ "-n",
+ "machdep.cpu.brand_string",
+ "machdep.cpu.core_count",
+ "hw.perflevel0.logicalcpu",
+ "hw.perflevel1.logicalcpu",
+ ];
+
+ let output = cmd.execute(binary, args)?;
+ let buffer = String::from_utf8(output.stdout)?;
+
+ let mut iter = buffer.split('\n');
+ let cpu_brand_name = match iter.next() {
+ Some(s) => s.to_string(),
+ None => return Err(Error::Parse(buffer.to_string())),
+ };
+
+ let num_cpu_cores = match iter.next() {
+ Some(s) => s.parse::<u16>()?,
+ None => return Err(Error::Parse(buffer.to_string())),
+ };
+
+ let num_performance_cores = match iter.next() {
+ Some(s) => s.parse::<u16>()?,
+ None => return Err(Error::Parse(buffer.to_string())),
+ };
+
+ let num_efficiency_cores = match iter.next() {
+ Some(s) => s.parse::<u16>()?,
+ None => return Err(Error::Parse(buffer.to_string())),
+ };
+
+ Ok((
+ cpu_brand_name,
+ num_cpu_cores,
+ num_performance_cores,
+ num_efficiency_cores,
+ ))
+}
+
+// https://github.com/tlkh/asitop/blob/74ebe2cbc23d5b1eec874aebb1b9bacfe0e670cd/asitop/utils.py#L120
+fn gpu_info(cmd: &impl SystemCommand) -> Result<u16> {
+ let binary = "/usr/sbin/system_profiler";
+ let args = &["-detailLevel", "basic", "SPDisplaysDataType"];
+
+ let output = cmd.execute(binary, args)?;
+ let buffer = String::from_utf8(output.stdout)?;
+
+ let num_gpu_cores_line = buffer
+ .lines()
+ .find(|&line| line.trim_start().starts_with("Total Number of Cores"));
+
+ let num_gpu_cores = match num_gpu_cores_line {
+ Some(s) => match s.split(": ").last() {
+ Some(s) => s.parse::<u16>()?,
+ None => return Err(Error::Parse(buffer.to_string())),
+ },
+ None => return Err(Error::Parse(buffer.to_string())),
+ };
+
+ 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\n4\n4\n";
+ let cmd = MockCommand::new(mock_output);
+
+ let result = cpu_info(&cmd);
+ assert!(result.is_ok());
+ let (brand, cores, p_cores, e_cores) = result.unwrap();
+ assert_eq!(brand, "Apple M2");
+ assert_eq!(cores, 8);
+ assert_eq!(p_cores, 4);
+ assert_eq!(e_cores, 4);
+ }
+
+ #[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 { .. })));
+ }
+}