From 04b4f530920798f1373ea8207de3c223936054ca Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Tue, 4 Aug 2026 20:47:11 +0000 Subject: [PATCH] imrplement amd power --- app/cpu/cpu.go | 15 +++- app/cpu/proc/proc.go | 6 ++ app/cpu/proc/vendor_amd.go | 159 ++++++++++++++++++++++++++++++++++- app/cpu/proc/vendor_intel.go | 4 + 4 files changed, 179 insertions(+), 5 deletions(-) diff --git a/app/cpu/cpu.go b/app/cpu/cpu.go index 56f11d0..e08a14a 100644 --- a/app/cpu/cpu.go +++ b/app/cpu/cpu.go @@ -16,11 +16,13 @@ func RenderCPU(console *strings.Builder) { console.WriteString("\033[H") console.WriteString(fmt.Sprintf("=== %s ===\n", proc.Model())) - RenderTemperatures(console, proc) + RenderTemperatureSensors(console, proc) + RenderPowerSensors(console, proc) } -func RenderTemperatures(console *strings.Builder, proc proc.Proc) { +func RenderTemperatureSensors(console *strings.Builder, proc proc.Proc) { temps, _ := proc.SensorReader.ReadTemperatureSensors() + console.WriteString("=== Temperature ===\n") for _, sensor := range temps { tempC := sensor.TempC tempF := (tempC * 9 / 5) + 32 @@ -28,3 +30,12 @@ func RenderTemperatures(console *strings.Builder, proc proc.Proc) { console.WriteString(fmt.Sprintf("Sensor: %-25s | %5.1f°C (%5.1f°F)\033[K\n", sensor.Name, tempC, tempF)) } } + +func RenderPowerSensors(console *strings.Builder, proc proc.Proc) { + console.WriteString("=== Power ===\n") + power, _ := proc.SensorReader.ReadPowerSensors() + for _, sensor := range power { + // \033[K clears from cursor to end of line (prevents lingering chars) + console.WriteString(fmt.Sprintf("Sensor: %-25s | %5.1fW\033[K\n", sensor.Name, sensor.Watts)) + } +} diff --git a/app/cpu/proc/proc.go b/app/cpu/proc/proc.go index 17a846a..52cca4b 100644 --- a/app/cpu/proc/proc.go +++ b/app/cpu/proc/proc.go @@ -14,6 +14,7 @@ type Proc struct { type CPUSensorReader interface { ReadTemperatureSensors() ([]TempSensor, error) + ReadPowerSensors() ([]PowerSensor, error) } type TempSensor struct { @@ -21,6 +22,11 @@ type TempSensor struct { TempC float64 } +type PowerSensor struct { + Name string + Watts float64 +} + const ( VendorIntel string = "GenuineIntel" VendorAMD string = "AuthenticAMD" diff --git a/app/cpu/proc/vendor_amd.go b/app/cpu/proc/vendor_amd.go index 185da90..2f09154 100644 --- a/app/cpu/proc/vendor_amd.go +++ b/app/cpu/proc/vendor_amd.go @@ -10,11 +10,12 @@ import ( ) type AMDReader struct { - hwmonDir string - driver string + hwmonDir string + driver string + energyState map[string]EnergyState // tracks (path -> energy state) for energy*_input calculations } -type PowerState struct { +type EnergyState struct { lastEnergy uint64 lastTime time.Time } @@ -78,3 +79,155 @@ func (r *AMDReader) ReadTemperatureSensors() ([]TempSensor, error) { return temps, nil } + +func (r *AMDReader) ReadPowerSensors() ([]PowerSensor, error) { + hwmonSensors := r.readHwmonPower() + if len(hwmonSensors) > 0 { + return hwmonSensors, nil + } + + return r.readRaplPower(), nil +} + +func (r *AMDReader) readHwmonPower() []PowerSensor { + var powerSensors []PowerSensor + + getLabel := func(inputPath string) string { + labelPath := strings.TrimSuffix(inputPath, "_input") + "_label" + if lBytes, err := os.ReadFile(labelPath); err == nil { + return strings.TrimSpace(string(lBytes)) + } + return filepath.Base(strings.TrimSuffix(inputPath, "_input")) + } + + powerInputs, _ := filepath.Glob(filepath.Join(r.hwmonDir, "power*_input")) + for _, inputPath := range powerInputs { + valBytes, err := os.ReadFile(inputPath) + if err != nil { + continue + } + + microWatts, err := strconv.ParseFloat(strings.TrimSpace(string(valBytes)), 64) + if err != nil { + continue + } + + powerSensors = append(powerSensors, PowerSensor{ + Name: getLabel(inputPath), + Watts: microWatts / 1_000_000.0, + }) + } + + energyInputs, _ := filepath.Glob(filepath.Join(r.hwmonDir, "energy*_input")) + if len(energyInputs) > 0 { + r.calculateEnergyDelta(energyInputs, getLabel, &powerSensors) + } + + return powerSensors +} + +func (r *AMDReader) readRaplPower() []PowerSensor { + var powerSensors []PowerSensor + + // Match both top-level and nested RAPL zone directories + patterns := []string{ + "/sys/class/powercap/intel-rapl*/energy_uj", + "/sys/class/powercap/intel-rapl/intel-rapl*/energy_uj", + "/sys/class/powercap/intel-rapl/intel-rapl*/*/energy_uj", + } + + var energyInputs []string + seen := make(map[string]bool) + + for _, pattern := range patterns { + matches, _ := filepath.Glob(pattern) + for _, match := range matches { + // Resolve symlinks to get real sysfs path + realPath, err := filepath.EvalSymlinks(match) + if err != nil { + realPath = match + } + if !seen[realPath] { + seen[realPath] = true + energyInputs = append(energyInputs, realPath) + } + } + } + + if len(energyInputs) == 0 { + return powerSensors + } + + getLabel := func(inputPath string) string { + dir := filepath.Dir(inputPath) + if nameBytes, err := os.ReadFile(filepath.Join(dir, "name")); err == nil { + return strings.TrimSpace(string(nameBytes)) + } + return filepath.Base(dir) + } + + r.calculateEnergyDelta(energyInputs, getLabel, &powerSensors) + return powerSensors +} + +func (r *AMDReader) calculateEnergyDelta(inputs []string, getLabel func(string) string, out *[]PowerSensor) { + now := time.Now() + if r.energyState == nil { + r.energyState = make(map[string]EnergyState) + } + + for _, inputPath := range inputs { + // Canonicalize path to resolve symlinks and prevent map key duplication + realPath, err := filepath.EvalSymlinks(inputPath) + if err != nil { + realPath = inputPath + } + + valBytes, err := os.ReadFile(realPath) + if err != nil { + continue + } + + microJoules, err := strconv.ParseUint(strings.TrimSpace(string(valBytes)), 10, 64) + if err != nil { + continue + } + + prevState, exists := r.energyState[realPath] + r.energyState[realPath] = EnergyState{ + lastEnergy: microJoules, + lastTime: now, + } + + // Initial seed step: power delta requires two distinct reads + if !exists { + continue + } + + timeDiff := now.Sub(prevState.lastTime).Seconds() + if timeDiff <= 0 { + continue + } + + maxRange := uint64(1<<64 - 1) + rangeBytes, err := os.ReadFile(filepath.Join(filepath.Dir(realPath), "max_energy_range_uj")) + if err == nil { + if parsedMax, err := strconv.ParseUint(strings.TrimSpace(string(rangeBytes)), 10, 64); err == nil { + maxRange = parsedMax + } + } + + var energyDiff uint64 + if microJoules >= prevState.lastEnergy { + energyDiff = microJoules - prevState.lastEnergy + } else { + energyDiff = maxRange - prevState.lastEnergy + microJoules + } + + watts := (float64(energyDiff) / 1_000_000.0) / timeDiff + *out = append(*out, PowerSensor{ + Name: getLabel(realPath), + Watts: watts, + }) + } +} diff --git a/app/cpu/proc/vendor_intel.go b/app/cpu/proc/vendor_intel.go index 4333fb0..2d3367d 100644 --- a/app/cpu/proc/vendor_intel.go +++ b/app/cpu/proc/vendor_intel.go @@ -74,3 +74,7 @@ func (r *IntelReader) ReadTemperatureSensors() ([]TempSensor, error) { return temps, nil } + +func (r *IntelReader) ReadPowerSensors() ([]PowerSensor, error) { + return []PowerSensor{}, nil +}