diff --git a/app/app.go b/app/app.go index 61c0413..3630cf6 100644 --- a/app/app.go +++ b/app/app.go @@ -2,6 +2,7 @@ package app import ( "finally-a-monolithic-linux-hw-monitor/app/cpu" + "finally-a-monolithic-linux-hw-monitor/app/cpu/proc" "fmt" "os" "os/signal" @@ -24,11 +25,18 @@ func Run() { os.Exit(0) }() + proc, err := proc.GetProc() + ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { console := &strings.Builder{} - cpu.RenderCPU(console) + + if err != nil { + return + } + + cpu.RenderCPU(console, &proc) console.WriteString("\nPress Ctrl+C to exit.\033[K\n") fmt.Print(console.String()) diff --git a/app/cpu/cpu.go b/app/cpu/cpu.go index e08a14a..ada7863 100644 --- a/app/cpu/cpu.go +++ b/app/cpu/cpu.go @@ -6,12 +6,7 @@ import ( "strings" ) -func RenderCPU(console *strings.Builder) { - proc, err := proc.GetProc() - if err != nil { - return - } - +func RenderCPU(console *strings.Builder, proc *proc.Proc) { // Move cursor to top-left (0,0) without clearing full buffer (flicker-free) console.WriteString("\033[H") console.WriteString(fmt.Sprintf("=== %s ===\n", proc.Model())) @@ -20,7 +15,7 @@ func RenderCPU(console *strings.Builder) { RenderPowerSensors(console, proc) } -func RenderTemperatureSensors(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 { @@ -31,7 +26,7 @@ func RenderTemperatureSensors(console *strings.Builder, proc proc.Proc) { } } -func RenderPowerSensors(console *strings.Builder, proc proc.Proc) { +func RenderPowerSensors(console *strings.Builder, proc *proc.Proc) { console.WriteString("=== Power ===\n") power, _ := proc.SensorReader.ReadPowerSensors() for _, sensor := range power { diff --git a/app/cpu/proc/vendor_amd.go b/app/cpu/proc/vendor_amd.go index 2f09154..3aceadd 100644 --- a/app/cpu/proc/vendor_amd.go +++ b/app/cpu/proc/vendor_amd.go @@ -2,6 +2,7 @@ package proc import ( "fmt" + "math" "os" "path/filepath" "strconv" @@ -12,7 +13,7 @@ import ( type AMDReader struct { hwmonDir string driver string - energyState map[string]EnergyState // tracks (path -> energy state) for energy*_input calculations + energyState map[string]EnergyState } type EnergyState struct { @@ -21,10 +22,9 @@ type EnergyState struct { } func NewAMDReader() (*AMDReader, error) { - // Locate AMD k10temp or zenpower hwmon path matches, err := filepath.Glob("/sys/class/hwmon/hwmon*") if err != nil { - return nil, err + return nil, fmt.Errorf("glob hwmon failed: %w", err) } for _, dir := range matches { @@ -35,8 +35,9 @@ func NewAMDReader() (*AMDReader, error) { drv := strings.TrimSpace(string(b)) if drv == "k10temp" || drv == "zenpower" { return &AMDReader{ - hwmonDir: dir, - driver: drv, + hwmonDir: dir, + driver: drv, + energyState: make(map[string]EnergyState), }, nil } } @@ -50,7 +51,7 @@ func (r *AMDReader) ReadTemperatureSensors() ([]TempSensor, error) { return nil, fmt.Errorf("no temperature inputs found in %s", r.hwmonDir) } - temps := []TempSensor{} + temps := make([]TempSensor, 0, len(inputs)) for _, inputPath := range inputs { valBytes, err := os.ReadFile(inputPath) @@ -129,11 +130,12 @@ func (r *AMDReader) readHwmonPower() []PowerSensor { 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", + "/sys/class/powercap/intel-rapl:*/energy_uj", + "/sys/class/powercap/intel-rapl:*/*/energy_uj", } var energyInputs []string @@ -141,8 +143,8 @@ func (r *AMDReader) readRaplPower() []PowerSensor { 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 @@ -172,18 +174,18 @@ func (r *AMDReader) readRaplPower() []PowerSensor { 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) + for _, rawPath := range inputs { + key, err := filepath.EvalSymlinks(rawPath) if err != nil { - realPath = inputPath + key = filepath.Clean(rawPath) } - valBytes, err := os.ReadFile(realPath) + valBytes, err := os.ReadFile(key) if err != nil { continue } @@ -193,26 +195,29 @@ func (r *AMDReader) calculateEnergyDelta(inputs []string, getLabel func(string) continue } - prevState, exists := r.energyState[realPath] - r.energyState[realPath] = EnergyState{ - lastEnergy: microJoules, - lastTime: now, - } + prevState, exists := r.energyState[key] - // Initial seed step: power delta requires two distinct reads 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 { + if timeDiff < 0.05 { continue } - maxRange := uint64(1<<64 - 1) - rangeBytes, err := os.ReadFile(filepath.Join(filepath.Dir(realPath), "max_energy_range_uj")) + 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 { + if parsedMax, err := strconv.ParseUint(strings.TrimSpace(string(rangeBytes)), 10, 64); err == nil && parsedMax > 0 { maxRange = parsedMax } } @@ -221,12 +226,18 @@ func (r *AMDReader) calculateEnergyDelta(inputs []string, getLabel func(string) if microJoules >= prevState.lastEnergy { energyDiff = microJoules - prevState.lastEnergy } else { - energyDiff = maxRange - prevState.lastEnergy + microJoules + 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(realPath), + Name: getLabel(key), Watts: watts, }) }