use linux perf tool for amd power usage,

add support for intel DC ssds
This commit is contained in:
alu
2026-08-18 10:16:06 -07:00
parent a474ff2511
commit b66b7cad59
4 changed files with 152 additions and 7 deletions
+2 -3
View File
@@ -414,17 +414,16 @@ func GetDiskSMART(devicePath string) (*DiskSMART, error) {
for _, attr := range smartData.ATAMetrics.Table { for _, attr := range smartData.ATAMetrics.Table {
switch attr.ID { switch attr.ID {
// 245: Intel DC
case 231, 177, 233, 169, 230, 173: case 231, 177, 233, 169, 230, 173:
smart.HealthRemaining = attr.Value smart.HealthRemaining = attr.Value
smart.WearoutPercent = max(100-attr.Value, 0) smart.WearoutPercent = max(100-attr.Value, 0)
smart.HasWearData = true smart.HasWearData = true
case 202, 245:
case 202:
smart.WearoutPercent = attr.Value smart.WearoutPercent = attr.Value
smart.HealthRemaining = max(100-attr.Value, 0) smart.HealthRemaining = max(100-attr.Value, 0)
smart.HasWearData = true smart.HasWearData = true
} }
if smart.HasWearData { if smart.HasWearData {
break break
} }
+146
View File
@@ -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
}
+3 -3
View File
@@ -10,7 +10,7 @@ import (
"time" "time"
) )
const milliJoulePerJoule = float64(1000000) const microJoulePerJoule = float64(1000000)
type EnergyState struct { type EnergyState struct {
lastEnergy uint64 lastEnergy uint64
@@ -36,7 +36,7 @@ func (r *RaplDriver) Render(term common.Terminal) error {
} }
power := p.([]PowerSensor) power := p.([]PowerSensor)
term.Subheader("Power /sys/class/powercap/intel-rapl") term.Subheader("Power (rapl)")
for _, sensor := range power { for _, sensor := range power {
// \033[K clears from cursor to end of line (prevents lingering chars) // \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 // the value will be off for that one measurement, then become correct again
energyDiff := float64(microJoules - prevState.lastEnergy) energyDiff := float64(microJoules - prevState.lastEnergy)
watts := (energyDiff / milliJoulePerJoule) / timeDiff watts := (energyDiff / microJoulePerJoule) / timeDiff
r.energyState[path] = EnergyState{ r.energyState[path] = EnergyState{
lastEnergy: microJoules, lastEnergy: microJoules,
+1 -1
View File
@@ -15,7 +15,7 @@ func NewAMDDrivers() ([]common.Driver, error) {
} }
drivers = append(drivers, temperatureDriver) drivers = append(drivers, temperatureDriver)
powerDriver, err := driver.NewRaplDriver() powerDriver, err := driver.NewPerfDriver()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not initialize hwmon temperature driver") return nil, fmt.Errorf("could not initialize hwmon temperature driver")
} }