From 41b33927891d64e0716e299bb8523105bd45951c Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 14:35:24 +0200 Subject: [PATCH] Count the same meters the power module's sampler reads (hq ADR 0255) An APU's integrated graphics and Intel's second (MMIO) view of a package made a machine report a power meter the sampler would not read, so its bar could only say not measured. --- internal/profile/power.go | 24 ++++++++++++++------- internal/profile/power_test.go | 38 ++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/internal/profile/power.go b/internal/profile/power.go index 8c9474f..48efcc6 100644 --- a/internal/profile/power.go +++ b/internal/profile/power.go @@ -24,7 +24,8 @@ const ( CapBattery = "battery" // CapPowerMeter is something here that measures the power the machine, or part of it, draws: // the machine's battery, the processor package's energy counter (powercap, Intel's and AMD's - // RAPL), or a graphics device that reports its own power. + // RAPL), or a discrete graphics device that reports its own power — the same meters the power + // module's sampler reads, so a machine never gets a block that can only say "not measured". CapPowerMeter = "power-meter" ) @@ -89,24 +90,33 @@ func (powerMeter) Name() string { return CapPowerMeter } // reader's question — powercap's energy counter is root's to read, and the power module samples it // as root and publishes only a rate. func (powerMeter) Detect(context.Context) Verdict { - const how = "a system battery; /sys/class/powercap/*/name starting package; a graphics device's " + - "hwmon power1_average or power1_input; /proc/driver/nvidia/gpus" + const how = "a system battery; /sys/class/powercap/*/name starting package, each name once; a discrete " + + "graphics device's (mem_info_vram_vendor) hwmon power1_average or power1_input; /proc/driver/nvidia/gpus" var found []string for _, b := range systemBatteries() { found = append(found, "battery "+b) } + // Each package once by its name: Intel shows some twice, as intel-rapl:N and intel-rapl-mmio:N. if entries, err := os.ReadDir(PowercapSource); err == nil { + seen := map[string]bool{} for _, e := range entries { dir := filepath.Join(PowercapSource, e.Name()) - if strings.HasPrefix(readTrimmed(filepath.Join(dir, "name")), "package") { - if _, err := os.Stat(filepath.Join(dir, "energy_uj")); err == nil { - found = append(found, "processor package "+e.Name()) - } + name := readTrimmed(filepath.Join(dir, "name")) + if !strings.HasPrefix(name, "package") || seen[name] { + continue + } + if _, err := os.Stat(filepath.Join(dir, "energy_uj")); err == nil { + seen[name] = true + found = append(found, "processor "+name) } } } if cards, err := filepath.Glob(filepath.Join(SeatSource, "card*", "device", "hwmon", "hwmon*")); err == nil { for _, hw := range cards { + // Integrated graphics report the package the processor's counter already holds. + if _, err := os.Stat(filepath.Join(filepath.Dir(filepath.Dir(hw)), "mem_info_vram_vendor")); err != nil { + continue + } for _, f := range []string{"power1_average", "power1_input"} { if _, err := os.Stat(filepath.Join(hw, f)); err == nil { found = append(found, "graphics "+strings.Split(strings.TrimPrefix(hw, SeatSource+"/"), "/")[0]) diff --git a/internal/profile/power_test.go b/internal/profile/power_test.go index c232f5c..aebea24 100644 --- a/internal/profile/power_test.go +++ b/internal/profile/power_test.go @@ -49,7 +49,7 @@ func TestALaptopHasABatteryAndAMeter(t *testing.T) { t.Fatalf("battery: %+v", b) } m := powerMeter{}.Detect(context.Background()) - if !m.Present || m.Detail != "battery BAT1, processor package intel-rapl:0" { + if !m.Present || m.Detail != "battery BAT1, processor package-0" { t.Fatalf("power meter: %+v", m) } } @@ -79,13 +79,14 @@ func TestADesktopMeasuresItsProcessorAndItsGraphics(t *testing.T) { write(t, dir+"/powercap/intel-rapl:0:0/name", "core") write(t, dir+"/powercap/intel-rapl:0:0/energy_uj", "1") write(t, dir+"/drm/card1/device/hwmon/hwmon4/power1_average", "8000000") + write(t, dir+"/drm/card1/device/mem_info_vram_vendor", "samsung") write(t, dir+"/nvidia/0000:01:00.0/information", "x") if b := (battery{}).Detect(context.Background()); b.Present { t.Fatalf("a desktop has a battery: %+v", b) } m := powerMeter{}.Detect(context.Background()) - if !m.Present || m.Detail != "processor package intel-rapl:0, graphics card1, graphics nvidia" { + if !m.Present || m.Detail != "processor package-0, graphics card1, graphics nvidia" { t.Fatalf("power meter: %+v", m) } } @@ -97,3 +98,36 @@ func TestAMachineWithNothingToReadHasNeither(t *testing.T) { t.Fatalf("%+v %+v", b, m) } } + +func TestAPackageShownTwiceIsOneMeterAndIntegratedGraphicsAreNone(t *testing.T) { + dir := sysfs(t) + write(t, dir+"/powercap/intel-rapl:0/name", "package-0") + write(t, dir+"/powercap/intel-rapl:0/energy_uj", "1") + write(t, dir+"/powercap/intel-rapl-mmio:0/name", "package-0") + write(t, dir+"/powercap/intel-rapl-mmio:0/energy_uj", "1") + // An APU's graphics: power reported, no memory of its own. + write(t, dir+"/drm/card1/device/hwmon/hwmon5/power1_average", "19000000") + m := powerMeter{}.Detect(context.Background()) + if !m.Present || m.Detail != "processor package-0" { + t.Fatalf("power meter: %+v", m) + } +} + +func TestOnlyIntegratedGraphicsMeasureNothing(t *testing.T) { + dir := sysfs(t) + write(t, dir+"/drm/card0/device/hwmon/hwmon1/power1_input", "5000000") + if m := (powerMeter{}).Detect(context.Background()); m.Present { + t.Fatalf("integrated graphics were taken for a meter: %+v", m) + } +} + +func TestABatteryOfUnknownScopeIsTheMachines(t *testing.T) { + // Many laptops' firmware says nothing of scope; the kernel reports Unknown. Only Device is a + // peripheral's. + dir := sysfs(t) + write(t, dir+"/power_supply/BAT0/type", "Battery") + write(t, dir+"/power_supply/BAT0/scope", "Unknown") + if b := (battery{}).Detect(context.Background()); !b.Present || b.Detail != "BAT0" { + t.Fatalf("battery: %+v", b) + } +}