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,
)
}