147 lines
3.6 KiB
Go
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
|
|
}
|