Files
alu b66b7cad59 use linux perf tool for amd power usage,
add support for intel DC ssds
2026-08-18 10:16:06 -07:00

147 lines
3.6 KiB
Go

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
}