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>
153 lines
4.2 KiB
Go
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
|
|
}
|