Modules talk over the bus. The power module's bundle puts each fresh reading of its own sampler in its state draw, keyed by the machine; the bar's block shows that state, which the bar's bundle watches under the read the mesh grants it. A new catalogue check refuses a module naming another module's path under /run or /var/lib.
302 lines
9.3 KiB
Go
302 lines
9.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The power the machine draws, measured honestly and said with what was measured (novox/hq ADR 0255).
|
|
//
|
|
// - **On battery,** the battery's discharge rate. That is the whole machine.
|
|
// - **Otherwise** — on mains, or on a machine without a battery — the processor package's power from
|
|
// the kernel's powercap energy counter (RAPL, on Intel and AMD alike: a rate from the counter's
|
|
// change), plus a discrete graphics device's own power where it reports one. That is the processor
|
|
// (and graphics) part, never called the machine's.
|
|
// - **The machine at the wall** needs a metered plug or a UPS, and is not measured here.
|
|
//
|
|
// The energy counter is root's to read, on purpose: read often, it leaks what the processor is doing
|
|
// (the PLATYPUS attack). So the counter is never opened to the operator's account. `power draw` runs as
|
|
// root, as the module's process, samples every SampleEvery, and writes only the averaged rate to
|
|
// DrawFile, too coarse to leak anything. That file is this module's alone: its bundle reads it and puts
|
|
// the reading on the bus as the state `draw`, keyed by the machine (publish.go), and nothing of another
|
|
// module reads it (novox/hq ADR 0260: modules talk over the bus).
|
|
|
|
// DrawFile is where the sampler leaves the reading for the module's bundle, and SampleEvery how often.
|
|
const (
|
|
DrawFile = "/run/mesh-power/draw"
|
|
SampleEvery = 5 * time.Second
|
|
)
|
|
|
|
// Draw is one reading: each part measured, in watts, and what it covers.
|
|
type Draw struct {
|
|
// Machine is the battery's discharge, when on battery: the whole machine.
|
|
Machine *float64
|
|
// CPU is the processor packages' power, from the energy counters.
|
|
CPU *float64
|
|
// GPU is the discrete graphics devices' own power, where they report it; asleep counts as none.
|
|
GPU *float64
|
|
}
|
|
|
|
// Reading is a draw as the state `draw` holds it: the line shown, the watts, what they cover and when.
|
|
type Reading struct {
|
|
Text string `json:"text"`
|
|
Watts float64 `json:"watts,omitempty"`
|
|
Measures string `json:"measures,omitempty"`
|
|
At string `json:"at"`
|
|
}
|
|
|
|
// Reading is the draw as it is published, at a moment.
|
|
func (d Draw) Reading(at time.Time) Reading {
|
|
r := Reading{Text: d.Line(), At: at.UTC().Format(time.RFC3339)}
|
|
switch {
|
|
case d.Machine != nil:
|
|
r.Watts, r.Measures = *d.Machine, "machine"
|
|
case d.CPU != nil && d.GPU != nil:
|
|
r.Watts, r.Measures = *d.CPU+*d.GPU, "cpu+gpu"
|
|
case d.CPU != nil:
|
|
r.Watts, r.Measures = *d.CPU, "cpu"
|
|
case d.GPU != nil:
|
|
r.Watts, r.Measures = *d.GPU, "gpu"
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Line is the reading as the bar shows it.
|
|
func (d Draw) Line() string {
|
|
switch {
|
|
case d.Machine != nil:
|
|
return fmt.Sprintf("%.1f W machine", *d.Machine)
|
|
case d.CPU != nil && d.GPU != nil:
|
|
return fmt.Sprintf("%.1f W CPU+GPU", *d.CPU+*d.GPU)
|
|
case d.CPU != nil:
|
|
return fmt.Sprintf("%.1f W CPU", *d.CPU)
|
|
case d.GPU != nil:
|
|
return fmt.Sprintf("%.1f W GPU", *d.GPU)
|
|
}
|
|
return "power not measured"
|
|
}
|
|
|
|
// counter is one energy counter's reading, in microjoules, with the range it wraps at.
|
|
type counter struct {
|
|
uj, max int64
|
|
}
|
|
|
|
// MaxPackageWatts is more than any processor package draws. A rate above it is a counter that was
|
|
// reset — after a resume, say — not power, and that sample gives no reading.
|
|
const MaxPackageWatts = 1000
|
|
|
|
// packageCounters is every processor package's energy counter, by the package's name, each counted
|
|
// once. Intel exposes some packages twice, through MSRs (`intel-rapl:N`) and through MMIO
|
|
// (`intel-rapl-mmio:N`), under one name; the MSR one is read, and the second never adds to it.
|
|
func (m *Machine) packageCounters() map[string]counter {
|
|
out := map[string]counter{}
|
|
from := map[string]string{}
|
|
for _, dir := range m.glob("/sys/class/powercap/*") {
|
|
name := m.read(dir + "/name")
|
|
if !strings.HasPrefix(name, "package") {
|
|
continue
|
|
}
|
|
if was, seen := from[name]; seen && !strings.Contains(filepath.Base(was), "mmio") {
|
|
continue
|
|
}
|
|
uj, ok := m.readInt(dir + "/energy_uj")
|
|
if !ok {
|
|
continue
|
|
}
|
|
max, _ := m.readInt(dir + "/max_energy_range_uj")
|
|
out[name] = counter{uj: uj, max: max}
|
|
from[name] = dir
|
|
}
|
|
return out
|
|
}
|
|
|
|
// cpuWatts is the packages' power between two readings `elapsed` apart; nil when no counter was read
|
|
// both times. A counter that wrapped is counted across its range; one that gives a rate no package
|
|
// draws was reset, and the sample gives nothing rather than a number that is not power.
|
|
func cpuWatts(before, after map[string]counter, elapsed time.Duration) *float64 {
|
|
if elapsed <= 0 {
|
|
return nil
|
|
}
|
|
var joules float64
|
|
read := false
|
|
for dir, a := range after {
|
|
b, ok := before[dir]
|
|
if !ok {
|
|
continue
|
|
}
|
|
delta := a.uj - b.uj
|
|
if delta < 0 {
|
|
if a.max <= 0 {
|
|
continue
|
|
}
|
|
delta += a.max
|
|
}
|
|
if float64(delta)/1e6/elapsed.Seconds() > MaxPackageWatts {
|
|
return nil
|
|
}
|
|
joules += float64(delta) / 1e6
|
|
read = true
|
|
}
|
|
if !read {
|
|
return nil
|
|
}
|
|
w := joules / elapsed.Seconds()
|
|
return &w
|
|
}
|
|
|
|
// batteryWatts is the system battery's discharge, when it is discharging; nil otherwise.
|
|
func (m *Machine) batteryWatts() *float64 {
|
|
var total float64
|
|
found := false
|
|
for _, s := range m.Supplies() {
|
|
if s.Type != "Battery" || s.Device || s.Status != "Discharging" {
|
|
continue
|
|
}
|
|
dir := "/sys/class/power_supply/" + s.Name
|
|
if p, ok := m.readInt(dir + "/power_now"); ok {
|
|
total += float64(abs(p)) / 1e6
|
|
found = true
|
|
continue
|
|
}
|
|
i, iok := m.readInt(dir + "/current_now")
|
|
v, vok := m.readInt(dir + "/voltage_now")
|
|
if iok && vok {
|
|
total += float64(abs(i)) * float64(v) / 1e12
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return nil
|
|
}
|
|
return &total
|
|
}
|
|
|
|
func abs(n int64) int64 {
|
|
if n < 0 {
|
|
return -n
|
|
}
|
|
return n
|
|
}
|
|
|
|
// gpuWatts is the discrete graphics devices' own power, where they report it; nil when none does.
|
|
//
|
|
// - An AMD device's hwmon power counts only when it has its own memory (`mem_info_vram_vendor`): on
|
|
// an APU the integrated graphics report the whole package, which the processor's counter already
|
|
// holds, and counting it twice would be a lie.
|
|
// - An NVIDIA device is asked through nvidia-smi only while it is awake: asking wakes a suspended GPU,
|
|
// which on a hybrid laptop costs more than the answer is worth. Asleep, it draws next to nothing and
|
|
// counts as none.
|
|
func (m *Machine) gpuWatts(ctx context.Context) *float64 {
|
|
var total float64
|
|
found := false
|
|
for _, card := range m.glob("/sys/class/drm/card[0-9]*") {
|
|
if strings.Contains(filepath.Base(card), "-") || !m.exists(card+"/device/mem_info_vram_vendor") {
|
|
continue
|
|
}
|
|
// Asleep, it is not read: reading its sensors wakes it, as asking nvidia-smi would.
|
|
if status := m.read(card + "/device/power/runtime_status"); status != "" && status != "active" {
|
|
continue
|
|
}
|
|
for _, hw := range m.glob(card + "/device/hwmon/hwmon*") {
|
|
uw, ok := m.readInt(hw + "/power1_average")
|
|
if !ok {
|
|
uw, ok = m.readInt(hw + "/power1_input")
|
|
}
|
|
if ok {
|
|
total += float64(uw) / 1e6
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
for _, gpu := range m.glob("/proc/driver/nvidia/gpus/*") {
|
|
address := filepath.Base(gpu)
|
|
if m.read("/sys/bus/pci/devices/"+address+"/power/runtime_status") != "active" {
|
|
continue
|
|
}
|
|
out, err := m.Run(ctx, "nvidia-smi", "--query-gpu=power.draw", "--format=csv,noheader,nounits", "-i", address)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if w, err := strconv.ParseFloat(strings.TrimSpace(out), 64); err == nil {
|
|
total += w
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return nil
|
|
}
|
|
return &total
|
|
}
|
|
|
|
// Sample is one reading over `every`: the battery's discharge if the machine is on battery, else the
|
|
// processor's and the graphics' parts.
|
|
func (m *Machine) Sample(ctx context.Context, every time.Duration, wait func(time.Duration) bool) Draw {
|
|
if w := m.batteryWatts(); w != nil {
|
|
return Draw{Machine: w}
|
|
}
|
|
before, start := m.packageCounters(), time.Now()
|
|
if !wait(every) {
|
|
return Draw{}
|
|
}
|
|
return Draw{CPU: cpuWatts(before, m.packageCounters(), time.Since(start)), GPU: m.gpuWatts(ctx)}
|
|
}
|
|
|
|
// Publish writes the reading where the module's bundle reads it, whole, replacing the last.
|
|
func (m *Machine) Publish(r Reading) error {
|
|
raw, err := json.Marshal(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
line := string(raw)
|
|
path := m.path(DrawFile)
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
tmp := path + ".new"
|
|
if err := os.WriteFile(tmp, []byte(line+"\n"), 0o644); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Chmod(tmp, 0o644); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, path)
|
|
}
|
|
|
|
// RunDraw samples and publishes until the context ends.
|
|
func RunDraw(ctx context.Context, m *Machine) error {
|
|
return runDraw(ctx, m, SampleEvery, m.Publish)
|
|
}
|
|
|
|
// runDraw publishes one reading every `every`. A battery reading is instant, so it waits the interval
|
|
// itself; a counter reading waits inside its sample. It returns as soon as the context ends.
|
|
func runDraw(ctx context.Context, m *Machine, every time.Duration, publish func(Reading) error) error {
|
|
wait := func(d time.Duration) bool {
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case <-time.After(d):
|
|
return true
|
|
}
|
|
}
|
|
for ctx.Err() == nil {
|
|
d := m.Sample(ctx, every, wait)
|
|
if ctx.Err() != nil {
|
|
break
|
|
}
|
|
if err := publish(d.Reading(time.Now())); err != nil {
|
|
return fmt.Errorf("publishing the draw: %w", err)
|
|
}
|
|
if d.Machine != nil && !wait(every) {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|