Processes: CPU power beside the clock, from the packages' energy counters
New sensor cpu.power: watts from the RAPL energy counters (Intel, and AMD Zen through the same driver), summed over every package and averaged into history. Parts of a package, psys and the MMIO duplicate are left out. The counters are root-only, so a daemon that isn't root reads them through `hamlab cpu-energy`, a root helper started once with sudo or doas and asked over a pipe each sample, the same shape as the GPU client walk's helper. It reads only the counters it finds itself. No counters or no root means no reading, and the CPU box shows nothing extra. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d5680e8e41
commit
bfedf622be
@@ -0,0 +1,106 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A fake /sys/class/powercap: a package with its parts, the platform domain, and the MMIO
|
||||
// copy of the same package. Only the package counts.
|
||||
func fakeRAPL(t *testing.T, domains map[string][2]string) string {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
for dir, v := range domains {
|
||||
d := filepath.Join(root, dir)
|
||||
if err := os.MkdirAll(d, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.WriteFile(filepath.Join(d, "name"), []byte(v[0]+"\n"), 0o644)
|
||||
os.WriteFile(filepath.Join(d, "energy_uj"), []byte(v[1]+"\n"), 0o644)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
func TestRAPLPackagesOnlyCountsPackages(t *testing.T) {
|
||||
root := fakeRAPL(t, map[string][2]string{
|
||||
"intel-rapl:0": {"package-0", "1000"},
|
||||
"intel-rapl:0:0": {"core", "600"},
|
||||
"intel-rapl:0:1": {"uncore", "10"},
|
||||
"intel-rapl:1": {"psys", "300"},
|
||||
"intel-rapl-mmio:0": {"package-0", "1000"},
|
||||
})
|
||||
if got := raplPackages(root); len(got) != 1 || got[0] != "intel-rapl:0" {
|
||||
t.Fatalf("packages = %v, want [intel-rapl:0]", got)
|
||||
}
|
||||
|
||||
two := fakeRAPL(t, map[string][2]string{"intel-rapl:0": {"package-0", "5"}, "intel-rapl:1": {"package-1", "7"}})
|
||||
got := raplPackages(two)
|
||||
if strings.Join(got, ",") != "intel-rapl:0,intel-rapl:1" {
|
||||
t.Fatalf("two sockets: packages = %v", got)
|
||||
}
|
||||
cs, ok := readRAPL(two, got)
|
||||
if !ok || len(cs) != 2 || cs[0].UJ != 5 || cs[1].UJ != 7 {
|
||||
t.Fatalf("readRAPL = %+v, %v", cs, ok)
|
||||
}
|
||||
if got := raplPackages(filepath.Join(two, "nope")); len(got) != 0 {
|
||||
t.Fatalf("no powercap: packages = %v, want none", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRAPLWatts(t *testing.T) {
|
||||
c := func(d string, uj uint64) raplCount { return raplCount{d, uj} }
|
||||
max := map[string]uint64{"intel-rapl:0": 1_000_000_000}
|
||||
cases := []struct {
|
||||
name string
|
||||
prev, cur []raplCount
|
||||
dt float64
|
||||
want float64
|
||||
ok bool
|
||||
}{
|
||||
{"steady", []raplCount{c("intel-rapl:0", 0)}, []raplCount{c("intel-rapl:0", 10_000_000)}, 2, 5, true},
|
||||
{"two packages add", []raplCount{c("intel-rapl:0", 0), c("intel-rapl:1", 0)}, []raplCount{c("intel-rapl:0", 4_000_000), c("intel-rapl:1", 6_000_000)}, 1, 10, true},
|
||||
{"wrapped", []raplCount{c("intel-rapl:0", 999_000_000)}, []raplCount{c("intel-rapl:0", 1_000_000)}, 1, 2, true},
|
||||
{"backwards, range unknown", []raplCount{c("intel-rapl:1", 5_000_000)}, []raplCount{c("intel-rapl:1", 1)}, 1, 0, false},
|
||||
{"counters changed", []raplCount{c("intel-rapl:0", 0)}, []raplCount{c("intel-rapl:1", 9)}, 1, 0, false},
|
||||
{"first reading", nil, []raplCount{c("intel-rapl:0", 9)}, 1, 0, false},
|
||||
{"no time passed", []raplCount{c("intel-rapl:0", 0)}, []raplCount{c("intel-rapl:0", 9)}, 0, 0, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got, ok := raplWatts(tc.prev, tc.cur, max, tc.dt)
|
||||
if ok != tc.ok || (ok && (got < tc.want-1e-9 || got > tc.want+1e-9)) {
|
||||
t.Errorf("%s: got %v, %v; want %v, %v", tc.name, got, ok, tc.want, tc.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The helper answers every newline with the counters it found itself, and stops at EOF.
|
||||
func TestCPUEnergyHelper(t *testing.T) {
|
||||
root := fakeRAPL(t, map[string][2]string{"intel-rapl:0": {"package-0", "42"}, "intel-rapl:0:0": {"core", "7"}})
|
||||
var out bytes.Buffer
|
||||
if code := cpuEnergyHelper(root, strings.NewReader("\n\n"), &out); code != 0 {
|
||||
t.Fatalf("exit %d", code)
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
|
||||
if len(lines) != 2 {
|
||||
t.Fatalf("got %d lines, want 2: %q", len(lines), out.String())
|
||||
}
|
||||
var cs []raplCount
|
||||
if err := json.Unmarshal([]byte(lines[1]), &cs); err != nil || len(cs) != 1 || cs[0] != (raplCount{"intel-rapl:0", 42}) {
|
||||
t.Fatalf("line = %s (%v)", lines[1], err)
|
||||
}
|
||||
}
|
||||
|
||||
// History keeps the average of the samples that had a reading.
|
||||
func TestFoldCPUPower(t *testing.T) {
|
||||
b := []Sample{{T: 1, CPUPower: 4}, {T: 2}, {T: 3, CPUPower: 8}}
|
||||
if got := fold(b).CPUPower; got != 6 {
|
||||
t.Fatalf("folded CPU power = %v, want 6", got)
|
||||
}
|
||||
if got := fold([]Sample{{T: 1}, {T: 2}}).CPUPower; got != 0 {
|
||||
t.Fatalf("no readings folded to %v, want 0", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user