aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/apple-silicon-info/README.org15
-rw-r--r--cmd/apple-silicon-info/main.go281
-rw-r--r--cmd/apple-silicon-info/main_test.go197
3 files changed, 493 insertions, 0 deletions
diff --git a/cmd/apple-silicon-info/README.org b/cmd/apple-silicon-info/README.org
new file mode 100644
index 0000000..682a11c
--- /dev/null
+++ b/cmd/apple-silicon-info/README.org
@@ -0,0 +1,15 @@
+* socinfo
+
+A lightweight macOS utility that gathers detailed information about Apple Silicon System-on-Chip (SoC) specifications.
+
+** What it does
+
+=socinfo= queries your Mac's hardware through system utilities to retrieve:
+
+- CPU brand name and core configuration
+- GPU core count
+- Performance and efficiency core breakdown
+- Thermal Design Power (TDP) specifications
+- Memory bandwidth specifications
+
+The tool automatically detects your specific Apple Silicon variant (M1, M1 Pro/Max/Ultra, M2, M2 Pro/Max/Ultra, M3, M3 Pro/Max) and provides the corresponding technical specifications.
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,
+ )
+}
diff --git a/cmd/apple-silicon-info/main_test.go b/cmd/apple-silicon-info/main_test.go
new file mode 100644
index 0000000..a9b7caf
--- /dev/null
+++ b/cmd/apple-silicon-info/main_test.go
@@ -0,0 +1,197 @@
+package main
+
+import (
+ "errors"
+ "testing"
+)
+
+type MockCommand struct {
+ output string
+ err error
+}
+
+func NewMockCommand(output string) *MockCommand {
+ return &MockCommand{output: output}
+}
+
+func NewMockCommandWithError(err error) *MockCommand {
+ return &MockCommand{err: err}
+}
+
+func (m *MockCommand) Execute(binary string, args ...string) ([]byte, error) {
+ if m.err != nil {
+ return nil, m.err
+ }
+ return []byte(m.output), nil
+}
+
+func TestGetGPUInfo(t *testing.T) {
+ mockOutput := `Graphics/Displays:
+ Apple M2:
+ Total Number of Cores: 10`
+
+ cmd := NewMockCommand(mockOutput)
+
+ result, err := getGPUInfo(cmd)
+ if err != nil {
+ t.Fatalf("Expected no error, got: %v", err)
+ }
+
+ expected := CoreCount(10)
+ if result != expected {
+ t.Errorf("Expected %d GPU cores, got %d", expected, result)
+ }
+}
+
+func TestGetGPUInfoNotFound(t *testing.T) {
+ mockOutput := `Graphics/Displays:
+ Apple M2:
+ Some other field: 10`
+
+ cmd := NewMockCommand(mockOutput)
+
+ _, err := getGPUInfo(cmd)
+ if err == nil {
+ t.Fatal("Expected error when GPU cores not found, got nil")
+ }
+}
+
+func TestGetCPUInfoSuccess(t *testing.T) {
+ mockOutput := "Apple M2\n8\n4\n4\n"
+ cmd := NewMockCommand(mockOutput)
+
+ brand, cores, pCores, eCores, err := getCPUInfo(cmd)
+ if err != nil {
+ t.Fatalf("Expected no error, got: %v", err)
+ }
+
+ if brand != "Apple M2" {
+ t.Errorf("Expected brand 'Apple M2', got '%s'", brand)
+ }
+ if cores != 8 {
+ t.Errorf("Expected 8 cores, got %d", cores)
+ }
+ if pCores != 4 {
+ t.Errorf("Expected 4 performance cores, got %d", pCores)
+ }
+ if eCores != 4 {
+ t.Errorf("Expected 4 efficiency cores, got %d", eCores)
+ }
+}
+
+func TestGetCPUInfoMissingCoreCount(t *testing.T) {
+ mockOutput := "Apple M2\n"
+ cmd := NewMockCommand(mockOutput)
+
+ _, _, _, _, err := getCPUInfo(cmd)
+ if err == nil {
+ t.Fatal("Expected error with insufficient output, got nil")
+ }
+}
+
+func TestGetCPUInfoInvalidCoreCount(t *testing.T) {
+ mockOutput := "Apple M2\ninvalid\n4\n4\n"
+ cmd := NewMockCommand(mockOutput)
+
+ _, _, _, _, err := getCPUInfo(cmd)
+ if err == nil {
+ t.Fatal("Expected error with invalid core count, got nil")
+ }
+}
+
+func TestFromBrandString(t *testing.T) {
+ tests := []struct {
+ brand string
+ expected AppleChip
+ }{
+ {"Apple M1", M1},
+ {"Apple M1 Pro", M1Pro},
+ {"Apple M1 Max", M1Max},
+ {"Apple M1 Ultra", M1Ultra},
+ {"Apple M2", M2},
+ {"Apple M2 Pro", M2Pro},
+ {"Apple M2 Max", M2Max},
+ {"Apple M2 Ultra", M2Ultra},
+ {"Apple M3", M3},
+ {"Apple M3 Pro", M3Pro},
+ {"Apple M3 Max", M3Max},
+ {"Intel Core i7", Unknown},
+ }
+
+ for _, test := range tests {
+ result := fromBrandString(test.brand)
+ if result != test.expected {
+ t.Errorf("For brand '%s', expected %v, got %v", test.brand, test.expected, result)
+ }
+ }
+}
+
+func TestChipGetSpecs(t *testing.T) {
+ tests := []struct {
+ chip AppleChip
+ expected ChipSpecs
+ }{
+ {M1, ChipSpecs{CPUTDP: 20, GPUTDP: 20, CPUBW: 70, GPUBW: 70}},
+ {M1Pro, ChipSpecs{CPUTDP: 30, GPUTDP: 30, CPUBW: 200, GPUBW: 200}},
+ {M2, ChipSpecs{CPUTDP: 25, GPUTDP: 15, CPUBW: 100, GPUBW: 100}},
+ {Unknown, ChipSpecs{CPUTDP: 0, GPUTDP: 0, CPUBW: 0, GPUBW: 0}},
+ }
+
+ for _, test := range tests {
+ result := test.chip.getSpecs()
+ if result != test.expected {
+ t.Errorf("For chip %v, expected %+v, got %+v", test.chip, test.expected, result)
+ }
+ }
+}
+
+func TestNewSocInfoWithCommand(t *testing.T) {
+ sysctlOutput := "Apple M2\n8\n4\n4\n"
+
+ profilerOutput := `Graphics/Displays:
+ Apple M2:
+ Total Number of Cores: 10`
+
+ cmd := &MockCommandMultiple{
+ outputs: map[string]string{
+ "/usr/sbin/sysctl": sysctlOutput,
+ "/usr/sbin/system_profiler": profilerOutput,
+ },
+ }
+
+ socInfo, err := NewSocInfoWithCommand(cmd)
+ if err != nil {
+ t.Fatalf("Expected no error, got: %v", err)
+ }
+
+ if socInfo.CPUBrandName != "Apple M2" {
+ t.Errorf("Expected CPU brand 'Apple M2', got '%s'", socInfo.CPUBrandName)
+ }
+ if socInfo.NumCPUCores != 8 {
+ t.Errorf("Expected 8 CPU cores, got %d", socInfo.NumCPUCores)
+ }
+ if socInfo.NumGPUCores != 10 {
+ t.Errorf("Expected 10 GPU cores, got %d", socInfo.NumGPUCores)
+ }
+ if socInfo.CPUMaxPower == nil || *socInfo.CPUMaxPower != 25 {
+ t.Errorf("Expected CPU max power 25W, got %v", socInfo.CPUMaxPower)
+ }
+}
+
+type MockCommandMultiple struct {
+ outputs map[string]string
+ err error
+}
+
+func (m *MockCommandMultiple) Execute(binary string, args ...string) ([]byte, error) {
+ if m.err != nil {
+ return nil, m.err
+ }
+
+ output, exists := m.outputs[binary]
+ if !exists {
+ return nil, errors.New("binary not found in mock")
+ }
+
+ return []byte(output), nil
+}