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 }