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

143 lines
3.0 KiB
Go

package drivers
import (
"finally-a-monolithic-linux-hw-monitor/app/common"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
const microJoulePerJoule = float64(1000000)
type EnergyState struct {
lastEnergy uint64
lastTime time.Time
}
type RaplDriver struct {
energyState map[string]EnergyState
}
func NewRaplDriver() (*RaplDriver, error) {
r := RaplDriver{
energyState: map[string]EnergyState{},
}
_, err := r.Read()
return &r, err
}
func (r *RaplDriver) Render(term common.Terminal) error {
p, err := r.Read()
if err != nil {
return err
}
power := p.([]PowerSensor)
term.Subheader("Power (rapl)")
for _, sensor := range power {
// \033[K clears from cursor to end of line (prevents lingering chars)
term.Print(fmt.Sprintf("%-16s | %.2f W", sensor.Name, sensor.Watts))
}
return nil
}
func (r *RaplDriver) Read() (any, error) {
powerSensors := []PowerSensor{}
patterns := []string{
"/sys/class/powercap/intel-rapl*/energy_uj",
"/sys/class/powercap/intel-rapl/intel-rapl*/energy_uj",
"/sys/class/powercap/intel-rapl/intel-rapl*/*/energy_uj",
"/sys/class/powercap/intel-rapl:*/energy_uj",
"/sys/class/powercap/intel-rapl:*/*/energy_uj",
}
energyInputs := []string{}
unique := make(map[string]bool)
for _, pattern := range patterns {
matches, _ := filepath.Glob(pattern)
for _, match := range matches {
realPath, err := filepath.EvalSymlinks(match)
if err != nil {
realPath = match
}
if !unique[realPath] {
unique[realPath] = true
energyInputs = append(energyInputs, realPath)
}
}
}
if len(energyInputs) == 0 {
return powerSensors, fmt.Errorf("found no compatible rapl drivers")
}
r.calculateEnergyDelta(energyInputs, &powerSensors)
return powerSensors, nil
}
func getRaplLabel(inputPath string) string {
dir := filepath.Dir(inputPath)
if nameBytes, err := os.ReadFile(filepath.Join(dir, "name")); err == nil {
return strings.TrimSpace(string(nameBytes))
}
return filepath.Base(dir)
}
func (r *RaplDriver) calculateEnergyDelta(inputs []string, out *[]PowerSensor) {
for _, path := range inputs {
now := time.Now()
valBytes, err := os.ReadFile(path)
if err != nil {
continue
}
microJoules, err := strconv.ParseUint(strings.TrimSpace(string(valBytes)), 10, 64)
if err != nil {
continue
}
sensorLabel := getRaplLabel(path)
prevState, exists := r.energyState[path]
if !exists {
r.energyState[path] = EnergyState{
lastEnergy: microJoules,
lastTime: now,
}
*out = append(*out, PowerSensor{
Name: sensorLabel,
Watts: 0.0,
})
continue
}
timeDiff := now.Sub(prevState.lastTime).Seconds()
// technically the energy counter could overflow, at ~18 TerraJoules
// the value will be off for that one measurement, then become correct again
energyDiff := float64(microJoules - prevState.lastEnergy)
watts := (energyDiff / microJoulePerJoule) / timeDiff
r.energyState[path] = EnergyState{
lastEnergy: microJoules,
lastTime: now,
}
*out = append(*out, PowerSensor{
Name: sensorLabel,
Watts: watts,
})
}
}