From b66b7cad59fb6bab15f246b9ed1c3564718d8f52 Mon Sep 17 00:00:00 2001 From: alu Date: Tue, 18 Aug 2026 10:11:35 -0700 Subject: [PATCH] use linux perf tool for amd power usage, add support for intel DC ssds --- app/disks/drivers/smart.go | 5 +- app/proc/drivers/perf.go | 146 +++++++++++++++++++++++++++++++++++++ app/proc/drivers/rapl.go | 6 +- app/proc/vendor_amd.go | 2 +- 4 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 app/proc/drivers/perf.go diff --git a/app/disks/drivers/smart.go b/app/disks/drivers/smart.go index a9b0ff8..ee046fc 100644 --- a/app/disks/drivers/smart.go +++ b/app/disks/drivers/smart.go @@ -414,17 +414,16 @@ func GetDiskSMART(devicePath string) (*DiskSMART, error) { for _, attr := range smartData.ATAMetrics.Table { switch attr.ID { + // 245: Intel DC case 231, 177, 233, 169, 230, 173: smart.HealthRemaining = attr.Value smart.WearoutPercent = max(100-attr.Value, 0) smart.HasWearData = true - - case 202: + case 202, 245: smart.WearoutPercent = attr.Value smart.HealthRemaining = max(100-attr.Value, 0) smart.HasWearData = true } - if smart.HasWearData { break } diff --git a/app/proc/drivers/perf.go b/app/proc/drivers/perf.go new file mode 100644 index 0000000..4ccec96 --- /dev/null +++ b/app/proc/drivers/perf.go @@ -0,0 +1,146 @@ +package drivers + +import ( + "finally-a-monolithic-linux-hw-monitor/app/common" + "fmt" + "os/exec" + "strconv" + "strings" + "time" +) + +type PerfEvent struct { + EventName string + Label string +} + +type PerfDriver struct { + events []PerfEvent + interval time.Duration +} + +func NewPerfDriver() (*PerfDriver, error) { + if _, err := exec.LookPath("perf"); err != nil { + return nil, fmt.Errorf("'perf' command not found: install linux-perf package") + } + + r := &PerfDriver{ + events: []PerfEvent{}, + interval: 1 * time.Second, + } + + if err := r.detectEvents(); err != nil { + return nil, err + } + + return r, nil +} + +func (r *PerfDriver) detectEvents() error { + cmd := exec.Command("perf", "list") + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to execute 'perf list': %w", err) + } + + perfList := string(out) + + if strings.Contains(perfList, "power/energy-pkg/") { + r.events = append(r.events, PerfEvent{EventName: "power/energy-pkg/", Label: "Package"}) + } + + if strings.Contains(perfList, "power_core/energy-core/") { + r.events = append(r.events, PerfEvent{EventName: "power_core/energy-core/", Label: "Cores"}) + } else if strings.Contains(perfList, "power/energy-cores/") { + r.events = append(r.events, PerfEvent{EventName: "power/energy-cores/", Label: "Cores"}) + } + + if strings.Contains(perfList, "power/energy-ram/") { + r.events = append(r.events, PerfEvent{EventName: "power/energy-ram/", Label: "DRAM"}) + } + + if len(r.events) == 0 { + return fmt.Errorf("no compatible RAPL perf events detected on this system") + } + + return nil +} + +func (r *PerfDriver) Render(term common.Terminal) error { + p, err := r.Read() + if err != nil { + return err + } + + power := p.([]PowerSensor) + term.Subheader("Power (perf)") + + for _, sensor := range power { + term.Print(fmt.Sprintf("%-16s | %.2f W", sensor.Name, sensor.Watts)) + } + + return nil +} + +func (r *PerfDriver) Read() (any, error) { + if len(r.events) == 0 { + return []PowerSensor{}, fmt.Errorf("no perf events configured") + } + + args := []string{"stat"} + for _, event := range r.events { + args = append(args, "-e", event.EventName) + } + intervalSec := fmt.Sprintf("%.1f", r.interval.Seconds()) + args = append(args, "-a", "sleep", intervalSec) + + // perf outputs statistics to stderr, so CombinedOutput captures it + cmd := exec.Command("perf", args...) + out, err := cmd.CombinedOutput() + if err != nil { + return []PowerSensor{}, fmt.Errorf("perf stat execution failed: %w", err) + } + + return parsePerfOutput(string(out), r.events) +} + +func parsePerfOutput(output string, events []PerfEvent) ([]PowerSensor, error) { + sensors := []PowerSensor{} + lines := strings.Split(output, "\n") + + // Parse actual elapsed time from perf output (e.g., "1.001234567 seconds time elapsed") + elapsedSeconds := 1.0 + for _, line := range lines { + if strings.Contains(line, "seconds time elapsed") { + fields := strings.Fields(line) + if len(fields) > 0 { + if val, err := strconv.ParseFloat(fields[0], 64); err == nil && val > 0 { + elapsedSeconds = val + } + } + } + } + + // Extract Joules for each event and calculate Watts = Joules / Seconds + for _, event := range events { + for _, line := range lines { + if strings.Contains(line, event.EventName) { + fields := strings.Fields(line) + if len(fields) > 0 { + // Strip commas from numbers formatted like "1,234.56" + cleanVal := strings.ReplaceAll(fields[0], ",", "") + if joules, err := strconv.ParseFloat(cleanVal, 64); err == nil { + watts := joules / elapsedSeconds + sensors = append(sensors, PowerSensor{ + Name: event.Label, + Watts: watts, + }) + } + } + break + } + } + } + + return sensors, nil +} diff --git a/app/proc/drivers/rapl.go b/app/proc/drivers/rapl.go index e51c348..fdb9195 100644 --- a/app/proc/drivers/rapl.go +++ b/app/proc/drivers/rapl.go @@ -10,7 +10,7 @@ import ( "time" ) -const milliJoulePerJoule = float64(1000000) +const microJoulePerJoule = float64(1000000) type EnergyState struct { lastEnergy uint64 @@ -36,7 +36,7 @@ func (r *RaplDriver) Render(term common.Terminal) error { } power := p.([]PowerSensor) - term.Subheader("Power /sys/class/powercap/intel-rapl") + term.Subheader("Power (rapl)") for _, sensor := range power { // \033[K clears from cursor to end of line (prevents lingering chars) @@ -127,7 +127,7 @@ func (r *RaplDriver) calculateEnergyDelta(inputs []string, out *[]PowerSensor) { // the value will be off for that one measurement, then become correct again energyDiff := float64(microJoules - prevState.lastEnergy) - watts := (energyDiff / milliJoulePerJoule) / timeDiff + watts := (energyDiff / microJoulePerJoule) / timeDiff r.energyState[path] = EnergyState{ lastEnergy: microJoules, diff --git a/app/proc/vendor_amd.go b/app/proc/vendor_amd.go index 271e64f..3d74951 100644 --- a/app/proc/vendor_amd.go +++ b/app/proc/vendor_amd.go @@ -15,7 +15,7 @@ func NewAMDDrivers() ([]common.Driver, error) { } drivers = append(drivers, temperatureDriver) - powerDriver, err := driver.NewRaplDriver() + powerDriver, err := driver.NewPerfDriver() if err != nil { return nil, fmt.Errorf("could not initialize hwmon temperature driver") }