aboutsummaryrefslogtreecommitdiff
path: root/cmd/apple-silicon-info/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/apple-silicon-info/main.go')
-rw-r--r--cmd/apple-silicon-info/main.go281
1 files changed, 281 insertions, 0 deletions
diff --git a/cmd/apple-silicon-info/main.go b/cmd/apple-silicon-info/main.go
new file mode 100644
index 0000000..f733d7e
--- /dev/null
+++ b/cmd/apple-silicon-info/main.go
@@ -0,0 +1,281 @@
+package main
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "os/exec"
+ "strconv"
+ "strings"
+)
+
+// Custom error types
+var (
+ ErrParse = errors.New("socinfo parsing error")
+ ErrCommand = errors.New("command execution error")
+ ErrConvert = errors.New("conversion error")
+)
+
+type (
+ Watts = uint32
+ Bandwidth = uint32
+ CoreCount = uint16
+)
+
+// SocInfo represents information about the Silicon chip
+type SocInfo struct {
+ CPUBrandName string `json:"cpu_brand_name"`
+ NumCPUCores CoreCount `json:"num_cpu_cores"`
+ NumGPUCores CoreCount `json:"num_gpu_cores"`
+ CPUMaxPower *Watts `json:"cpu_max_power,omitempty"`
+ GPUMaxPower *Watts `json:"gpu_max_power,omitempty"`
+ CPUMaxBW *Bandwidth `json:"cpu_max_bw,omitempty"`
+ GPUMaxBW *Bandwidth `json:"gpu_max_bw,omitempty"`
+ ECoreCount CoreCount `json:"e_core_count"`
+ PCoreCount CoreCount `json:"p_core_count"`
+}
+
+// AppleChip represents different Apple Silicon variants
+type AppleChip int
+
+const (
+ M1 AppleChip = iota
+ M1Pro
+ M1Max
+ M1Ultra
+ M2
+ M2Pro
+ M2Max
+ M2Ultra
+ M3
+ M3Pro
+ M3Max
+ Unknown
+)
+
+// ChipSpecs holds the specifications for each chip variant
+type ChipSpecs struct {
+ CPUTDP Watts
+ GPUTDP Watts
+ CPUBW Bandwidth
+ GPUBW Bandwidth
+}
+
+// fromBrandString determines the Apple chip type from brand string
+func fromBrandString(brand string) AppleChip {
+ switch {
+ case strings.Contains(brand, "M1 Pro"):
+ return M1Pro
+ case strings.Contains(brand, "M1 Max"):
+ return M1Max
+ case strings.Contains(brand, "M1 Ultra"):
+ return M1Ultra
+ case strings.Contains(brand, "M1"):
+ return M1
+ case strings.Contains(brand, "M2 Pro"):
+ return M2Pro
+ case strings.Contains(brand, "M2 Max"):
+ return M2Max
+ case strings.Contains(brand, "M2 Ultra"):
+ return M2Ultra
+ case strings.Contains(brand, "M2"):
+ return M2
+ case strings.Contains(brand, "M3 Pro"):
+ return M3Pro
+ case strings.Contains(brand, "M3 Max"):
+ return M3Max
+ case strings.Contains(brand, "M3"):
+ return M3
+ default:
+ return Unknown
+ }
+}
+
+// getSpecs returns the specifications for the chip
+func (chip AppleChip) getSpecs() ChipSpecs {
+ switch chip {
+ case M1:
+ return ChipSpecs{CPUTDP: 20, GPUTDP: 20, CPUBW: 70, GPUBW: 70}
+ case M1Pro:
+ return ChipSpecs{CPUTDP: 30, GPUTDP: 30, CPUBW: 200, GPUBW: 200}
+ case M1Max:
+ return ChipSpecs{CPUTDP: 30, GPUTDP: 60, CPUBW: 250, GPUBW: 400}
+ case M1Ultra:
+ return ChipSpecs{CPUTDP: 60, GPUTDP: 120, CPUBW: 500, GPUBW: 800}
+ case M2:
+ return ChipSpecs{CPUTDP: 25, GPUTDP: 15, CPUBW: 100, GPUBW: 100}
+ case M2Pro:
+ return ChipSpecs{CPUTDP: 30, GPUTDP: 35, CPUBW: 0, GPUBW: 0}
+ case M2Max:
+ return ChipSpecs{CPUTDP: 30, GPUTDP: 40, CPUBW: 0, GPUBW: 0}
+ default:
+ return ChipSpecs{CPUTDP: 0, GPUTDP: 0, CPUBW: 0, GPUBW: 0}
+ }
+}
+
+// SystemCommand interface for testable command execution
+type SystemCommand interface {
+ Execute(binary string, args ...string) ([]byte, error)
+}
+
+// RealCommand implements SystemCommand for actual system calls
+type RealCommand struct{}
+
+func (r *RealCommand) Execute(binary string, args ...string) ([]byte, error) {
+ cmd := exec.Command(binary, args...)
+ output, err := cmd.Output()
+ if err != nil {
+ return nil, fmt.Errorf("%w: %v", ErrCommand, err)
+ }
+ return output, nil
+}
+
+const sysctlPath = "/usr/sbin/sysctl"
+
+// getCPUInfo retrieves CPU information using sysctl
+func getCPUInfo(cmd SystemCommand) (string, CoreCount, CoreCount, CoreCount, error) {
+ args := []string{
+ "-n", // don't display the variable name
+ "machdep.cpu.brand_string",
+ "machdep.cpu.core_count",
+ "hw.perflevel0.logicalcpu",
+ "hw.perflevel1.logicalcpu",
+ }
+
+ output, err := cmd.Execute(sysctlPath, args...)
+ if err != nil {
+ return "", 0, 0, 0, err
+ }
+
+ lines := strings.Split(strings.TrimSpace(string(output)), "\n")
+ if len(lines) < 4 {
+ return "", 0, 0, 0, fmt.Errorf("%w: insufficient output lines", ErrParse)
+ }
+
+ cpuBrandName := lines[0]
+
+ numCPUCores, err := strconv.ParseUint(lines[1], 10, 16)
+ if err != nil {
+ return "", 0, 0, 0, fmt.Errorf("%w: parsing cpu cores: %v", ErrConvert, err)
+ }
+
+ numPerformanceCores, err := strconv.ParseUint(lines[2], 10, 16)
+ if err != nil {
+ return "", 0, 0, 0, fmt.Errorf("%w: parsing performance cores: %v", ErrConvert, err)
+ }
+
+ numEfficiencyCores, err := strconv.ParseUint(lines[3], 10, 16)
+ if err != nil {
+ return "", 0, 0, 0, fmt.Errorf("%w: parsing efficiency cores: %v", ErrConvert, err)
+ }
+
+ return cpuBrandName, CoreCount(
+ numCPUCores,
+ ), CoreCount(
+ numPerformanceCores,
+ ), CoreCount(
+ numEfficiencyCores,
+ ), nil
+}
+
+// getGPUInfo retrieves GPU information using system_profiler
+func getGPUInfo(cmd SystemCommand) (CoreCount, error) {
+ args := []string{"-detailLevel", "basic", "SPDisplaysDataType"}
+
+ output, err := cmd.Execute("/usr/sbin/system_profiler", args...)
+ if err != nil {
+ return 0, err
+ }
+
+ scanner := bufio.NewScanner(strings.NewReader(string(output)))
+ for scanner.Scan() {
+ line := scanner.Text()
+ trimmed := strings.TrimSpace(line)
+ if strings.HasPrefix(trimmed, "Total Number of Cores") {
+ parts := strings.Split(trimmed, ": ")
+ if len(parts) == 2 {
+ cores, err := strconv.ParseUint(strings.TrimSpace(parts[1]), 10, 16)
+ if err != nil {
+ return 0, fmt.Errorf("%w: parsing gpu cores: %v", ErrConvert, err)
+ }
+ return CoreCount(cores), nil
+ }
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return 0, fmt.Errorf("%w: scanning output: %v", ErrParse, err)
+ }
+
+ return 0, fmt.Errorf("%w: GPU core count not found", ErrParse)
+}
+
+// NewSocInfo creates a new SocInfo instance by gathering system information
+func NewSocInfo() (*SocInfo, error) {
+ cmd := &RealCommand{}
+ return NewSocInfoWithCommand(cmd)
+}
+
+// NewSocInfoWithCommand creates a new SocInfo instance with a custom command executor (for testing)
+func NewSocInfoWithCommand(cmd SystemCommand) (*SocInfo, error) {
+ cpuBrandName, numCPUCores, eCoreCount, pCoreCount, err := getCPUInfo(cmd)
+ if err != nil {
+ return nil, err
+ }
+
+ numGPUCores, err := getGPUInfo(cmd)
+ if err != nil {
+ return nil, err
+ }
+
+ chip := fromBrandString(cpuBrandName)
+ specs := chip.getSpecs()
+
+ var cpuMaxPower, gpuMaxPower *Watts
+ var cpuMaxBW, gpuMaxBW *Bandwidth
+
+ if specs.CPUTDP > 0 {
+ cpuMaxPower = &specs.CPUTDP
+ }
+ if specs.GPUTDP > 0 {
+ gpuMaxPower = &specs.GPUTDP
+ }
+ if specs.CPUBW > 0 {
+ cpuMaxBW = &specs.CPUBW
+ }
+ if specs.GPUBW > 0 {
+ gpuMaxBW = &specs.GPUBW
+ }
+
+ return &SocInfo{
+ CPUBrandName: cpuBrandName,
+ NumCPUCores: numCPUCores,
+ NumGPUCores: numGPUCores,
+ CPUMaxPower: cpuMaxPower,
+ GPUMaxPower: gpuMaxPower,
+ CPUMaxBW: cpuMaxBW,
+ GPUMaxBW: gpuMaxBW,
+ ECoreCount: eCoreCount,
+ PCoreCount: pCoreCount,
+ }, nil
+}
+
+func main() {
+ socInfo, err := NewSocInfo()
+ if err != nil {
+ fmt.Printf("Error getting SoC info: %v\n", err)
+ return
+ }
+
+ var cpuPower Watts
+ if socInfo.CPUMaxPower != nil {
+ cpuPower = *socInfo.CPUMaxPower
+ }
+
+ fmt.Printf("Our CPU is an %s, and we have %d CPU cores, and %d GPU cores. The TDP is %d.\n",
+ socInfo.CPUBrandName,
+ socInfo.NumCPUCores,
+ socInfo.NumGPUCores,
+ cpuPower,
+ )
+}