Files
Mitchell-JohnsonandClaude Opus 5.5 bfedf622be 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>
2026-10-10 23:58:35 -07:00

153 lines
4.2 KiB
Go

package metrics
// One kept history point is the average of the samples behind it. At the default ten-minute
// window that bucket is a single sample and nothing is averaged; at a day it is five minutes
// of them, and the difference matters — a graph of the last day drawn from whichever instant
// landed on each five-minute mark would show spikes that never happened and miss the ones
// that did.
//
// The newest sample of the bucket is the template: its shape, labels, totals and slow
// sections (filesystems, UPS) are the current ones. Only the moving numbers are averaged,
// and only where an entry is present in that sample — a disk plugged in halfway through the
// bucket is averaged over the samples that have it.
func fold(b []Sample) Sample {
out := b[len(b)-1]
if len(b) == 1 {
return out
}
n := float64(len(b))
sum := func(get func(Sample) float64) float64 {
var t float64
for _, s := range b {
t += get(s)
}
return t / n
}
sumU := func(get func(Sample) uint64) uint64 {
var t uint64
for _, s := range b {
t += get(s)
}
return t / uint64(len(b))
}
out.CPU = sum(func(s Sample) float64 { return s.CPU })
// CPU power: over the samples that have a reading, so a helper restart is not a dip to zero
var pw, pn float64
for _, s := range b {
if s.CPUPower > 0 {
pw, pn = pw+s.CPUPower, pn+1
}
}
out.CPUPower = 0
if pn > 0 {
out.CPUPower = round1(pw / pn)
}
out.Load[0] = sum(func(s Sample) float64 { return s.Load[0] })
out.Load[1] = sum(func(s Sample) float64 { return s.Load[1] })
out.Load[2] = sum(func(s Sample) float64 { return s.Load[2] })
out.Cores = append([]float64(nil), out.Cores...)
for i := range out.Cores {
var t, c float64
for _, s := range b {
if i < len(s.Cores) {
t, c = t+s.Cores[i], c+1
}
}
if c > 0 {
out.Cores[i] = t / c
}
}
out.Mem.Used = sumU(func(s Sample) uint64 { return s.Mem.Used })
out.Mem.Avail = sumU(func(s Sample) uint64 { return s.Mem.Avail })
out.Mem.Free = sumU(func(s Sample) uint64 { return s.Mem.Free })
out.Mem.Cached = sumU(func(s Sample) uint64 { return s.Mem.Cached })
out.Swap.Used = sumU(func(s Sample) uint64 { return s.Swap.Used })
// the rest are lists whose entries are matched by name across the bucket
temps := avgBy(b, func(s Sample) map[string][]float64 {
m := make(map[string][]float64, len(s.Temps))
for _, x := range s.Temps {
m[x.Chip+"/"+x.Label] = []float64{x.C}
}
return m
})
out.Temps = append([]Temp(nil), out.Temps...)
for i, x := range out.Temps {
if v, ok := temps[x.Chip+"/"+x.Label]; ok {
out.Temps[i].C = v[0]
}
}
fans := avgBy(b, func(s Sample) map[string][]float64 {
m := make(map[string][]float64, len(s.Fans))
for _, x := range s.Fans {
m[x.Chip+"/"+x.Label] = []float64{float64(x.RPM)}
}
return m
})
out.Fans = append([]Fan(nil), out.Fans...)
for i, x := range out.Fans {
if v, ok := fans[x.Chip+"/"+x.Label]; ok {
out.Fans[i].RPM = int(v[0] + .5)
}
}
nets := avgBy(b, func(s Sample) map[string][]float64 {
m := make(map[string][]float64, len(s.Net))
for _, x := range s.Net {
m[x.Iface] = []float64{x.RxBps, x.TxBps}
}
return m
})
out.Net = append([]Net(nil), out.Net...)
for i, x := range out.Net {
if v, ok := nets[x.Iface]; ok {
out.Net[i].RxBps, out.Net[i].TxBps = v[0], v[1]
}
}
disks := avgBy(b, func(s Sample) map[string][]float64 {
m := make(map[string][]float64, len(s.Disks))
for _, x := range s.Disks {
m[x.Name] = []float64{x.ReadBps, x.WriteBps, x.UtilPct}
}
return m
})
out.Disks = append([]Disk(nil), out.Disks...)
for i, x := range out.Disks {
if v, ok := disks[x.Name]; ok {
out.Disks[i].ReadBps, out.Disks[i].WriteBps, out.Disks[i].UtilPct = v[0], v[1], v[2]
}
}
return out
}
// avgBy averages each sample's named numbers across the bucket, per name.
func avgBy(b []Sample, of func(Sample) map[string][]float64) map[string][]float64 {
sums := map[string][]float64{}
count := map[string]float64{}
for _, s := range b {
for k, vs := range of(s) {
acc := sums[k]
if acc == nil {
acc = make([]float64, len(vs))
}
for i, v := range vs {
if i < len(acc) {
acc[i] += v
}
}
sums[k], count[k] = acc, count[k]+1
}
}
for k, acc := range sums {
for i := range acc {
acc[i] /= count[k]
}
}
return sums
}