From afbae3d3d450d8f61660c0bc76105d0b9b33f434 Mon Sep 17 00:00:00 2001 From: alu Date: Tue, 4 Aug 2026 14:33:55 -0700 Subject: [PATCH] implement intel power stats --- app/cpu/proc/proc.go | 6 ++ app/cpu/proc/vendor_amd.go | 47 ------------- app/cpu/proc/vendor_intel.go | 125 ++++++++++++++++++++++++++++++++++- 3 files changed, 128 insertions(+), 50 deletions(-) diff --git a/app/cpu/proc/proc.go b/app/cpu/proc/proc.go index ba288b0..b926eae 100644 --- a/app/cpu/proc/proc.go +++ b/app/cpu/proc/proc.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strings" + "time" ) type Proc struct { @@ -27,6 +28,11 @@ type PowerSensor struct { Watts float64 } +type EnergyState struct { + lastEnergy uint64 + lastTime time.Time +} + const ( VendorIntel string = "GenuineIntel" VendorAMD string = "AuthenticAMD" diff --git a/app/cpu/proc/vendor_amd.go b/app/cpu/proc/vendor_amd.go index 3ceb24e..a826b8a 100644 --- a/app/cpu/proc/vendor_amd.go +++ b/app/cpu/proc/vendor_amd.go @@ -16,11 +16,6 @@ type AMDReader struct { energyState map[string]EnergyState } -type EnergyState struct { - lastEnergy uint64 - lastTime time.Time -} - func NewAMDReader() (*AMDReader, error) { matches, err := filepath.Glob("/sys/class/hwmon/hwmon*") if err != nil { @@ -82,51 +77,9 @@ func (r *AMDReader) ReadTemperatureSensors() ([]TempSensor, error) { } 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 { - 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 { powerSensors := []PowerSensor{} diff --git a/app/cpu/proc/vendor_intel.go b/app/cpu/proc/vendor_intel.go index 2d3367d..9c14add 100644 --- a/app/cpu/proc/vendor_intel.go +++ b/app/cpu/proc/vendor_intel.go @@ -2,15 +2,18 @@ package proc import ( "fmt" + "math" "os" "path/filepath" "strconv" "strings" + "time" ) type IntelReader struct { - hwmonDir string - driver string + hwmonDir string + driver string + energyState map[string]EnergyState } func NewIntelReader() (*IntelReader, error) { @@ -76,5 +79,121 @@ func (r *IntelReader) ReadTemperatureSensors() ([]TempSensor, error) { } func (r *IntelReader) ReadPowerSensors() ([]PowerSensor, error) { - return []PowerSensor{}, nil + return r.readRaplPower(), nil +} + +func (r *IntelReader) readRaplPower() []PowerSensor { + powerSensors := []PowerSensor{} + + 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", + "/sys/class/powercap/intel-rapl:*/energy_uj", + "/sys/class/powercap/intel-rapl:*/*/energy_uj", + } + + energyInputs := []string{} + seen := make(map[string]bool) + + for _, pattern := range patterns { + matches, _ := filepath.Glob(pattern) + + for _, match := range matches { + 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 *IntelReader) calculateEnergyDelta(inputs []string, getLabel func(string) string, out *[]PowerSensor) { + now := time.Now() + + if r.energyState == nil { + r.energyState = make(map[string]EnergyState) + } + + for _, rawPath := range inputs { + key, err := filepath.EvalSymlinks(rawPath) + if err != nil { + key = filepath.Clean(rawPath) + } + + valBytes, err := os.ReadFile(key) + if err != nil { + continue + } + + microJoules, err := strconv.ParseUint(strings.TrimSpace(string(valBytes)), 10, 64) + if err != nil { + continue + } + + prevState, exists := r.energyState[key] + + if !exists { + r.energyState[key] = EnergyState{ + lastEnergy: microJoules, + lastTime: now, + } + *out = append(*out, PowerSensor{ + Name: getLabel(key), + Watts: 0.0, + }) + continue + } + + timeDiff := now.Sub(prevState.lastTime).Seconds() + if timeDiff < 0.05 { + continue + } + + maxRange := uint64(math.MaxUint64) + rangeBytes, err := os.ReadFile(filepath.Join(filepath.Dir(key), "max_energy_range_uj")) + if err == nil { + if parsedMax, err := strconv.ParseUint(strings.TrimSpace(string(rangeBytes)), 10, 64); err == nil && parsedMax > 0 { + 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 + + r.energyState[key] = EnergyState{ + lastEnergy: microJoules, + lastTime: now, + } + + *out = append(*out, PowerSensor{ + Name: getLabel(key), + Watts: watts, + }) + } }