A Cache section between the pools and the schedules. One card for the ARC: size against its ceiling with the target and floor, sparklines of size and hit rate from the sampler's history, what it holds (MRU/MFU/dirty, data vs metadata, compression, the ghost lists), the prefetcher and dbuf cache, the memory-pressure counters, and the since-boot breakdown by demand/prefetch and data/metadata. Then a card per pool for L2ARC, SLOG, special vdev, dedup table and spares — the devices and their fill where they exist, and where they don't, plainly none and what that means (the ZIL writing to the pool's own vdevs, with how much it has written). Live numbers come from the sampler, which reads arcstats every tick and publishes zfs.arc.* sensors; the fuller picture is read on request, with zpool list -Hpv and zfs get run without sudo and cached for thirty seconds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
155 lines
5.5 KiB
Go
155 lines
5.5 KiB
Go
package metrics
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const fdinfoA = `pos: 0
|
|
flags: 02100002
|
|
drm-driver: i915
|
|
drm-client-id: 544
|
|
drm-pdev: 0000:00:02.0
|
|
drm-total-system0: 52624 KiB
|
|
drm-resident-system0: 50140 KiB
|
|
drm-resident-stolen-system0: 32 KiB
|
|
drm-engine-render: 1000000000 ns
|
|
drm-engine-copy: 0 ns
|
|
drm-engine-video: 2000000000 ns
|
|
drm-engine-capacity-video: 2
|
|
drm-engine-video-enhance: 0 ns
|
|
`
|
|
|
|
func TestParseFdinfo(t *testing.T) {
|
|
fi := parseFdinfo(fdinfoA)
|
|
if fi.driver != "i915" || fi.pdev != "0000:00:02.0" || fi.client != "544" {
|
|
t.Fatalf("identity: %+v", fi)
|
|
}
|
|
if fi.engines["video"] != 2000000000 || fi.capacity["video"] != 2 {
|
|
t.Errorf("video engine: %+v", fi)
|
|
}
|
|
if fi.mem != (50140+32)<<10 {
|
|
t.Errorf("resident = %d", fi.mem)
|
|
}
|
|
}
|
|
|
|
func TestLspciName(t *testing.T) {
|
|
for in, want := range map[string]string{
|
|
"Intel Corporation DG2 [Arc A380] (rev 05)": "Intel Arc A380",
|
|
"Intel Corporation RocketLake-S GT1 [UHD Graphics 750] (rev 04)": "Intel UHD Graphics 750",
|
|
"Intel Corporation Alder Lake-N [UHD Graphics]": "Intel UHD Graphics",
|
|
"Advanced Micro Devices, Inc. [AMD/ATI] Navi 23 [Radeon RX 6600/6600 XT/6600M] (rev c7)": "AMD Radeon RX 6600/6600 XT/6600M",
|
|
"NVIDIA Corporation GA106 [GeForce RTX 3060 Lite Hash Rate] (rev a1)": "NVIDIA GeForce RTX 3060 Lite Hash Rate",
|
|
"Moore Threads Technology Co.,Ltd MTT S80": "Moore Threads Technology Co.,Ltd MTT S80",
|
|
} {
|
|
if got := lspciName(in); got != want {
|
|
t.Errorf("%q → %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Two scans a second apart: a client that ran its render engine for half of it and both
|
|
// video engines flat out reads 50% render, 100% video, and the device's engines follow.
|
|
func TestGPUClientsShare(t *testing.T) {
|
|
c := NewCollector()
|
|
d := &gpuDev{card: "/nonexistent/card9", dev: "/nonexistent", key: "0000:00:02.0", bus: "0000:00:02.0", driver: "i915", name: "Test GPU", integrated: true}
|
|
c.gpu.devs = []*gpuDev{d}
|
|
c.gpu.listedAt = time.Now().Add(time.Hour) // keep the fake list
|
|
c.gpu.clients = map[string]clientPrev{}
|
|
t0 := time.Now()
|
|
c.gpu.last = []drmFD{{PID: 7, Comm: "ffmpeg", Info: fdinfoA}}
|
|
c.gpu.scanAt = t0 // no walk: the fake list stands
|
|
first := c.gpus(t0, 40)
|
|
if len(first) != 1 || len(first[0].Clients) != 1 || first[0].Clients[0].Usage != 0 {
|
|
t.Fatalf("first pass: %+v", first)
|
|
}
|
|
if first[0].Temp != 40 || first[0].TempSrc != "package" {
|
|
t.Errorf("an integrated GPU wears the package temperature: %+v", first[0])
|
|
}
|
|
later := fdinfoA
|
|
later = replace(later, "drm-engine-render: 1000000000 ns", "drm-engine-render: 1500000000 ns")
|
|
later = replace(later, "drm-engine-video: 2000000000 ns", "drm-engine-video: 4000000000 ns")
|
|
c.gpu.last = []drmFD{{PID: 7, Comm: "ffmpeg", Info: later}}
|
|
c.gpu.scanAt = t0.Add(time.Second)
|
|
got := c.gpus(t0.Add(time.Second), 40)
|
|
cl := got[0].Clients[0]
|
|
if cl.Engines["render"] != 50 || cl.Engines["video"] != 100 || cl.Usage != 100 {
|
|
t.Errorf("client share: %+v", cl)
|
|
}
|
|
if got[0].Engines["video"] != 100 || got[0].Usage != 100 || got[0].UsageSrc != "clients" {
|
|
t.Errorf("device from clients: %+v", got[0])
|
|
}
|
|
if got[0].MemUsed != cl.Mem {
|
|
t.Errorf("resident memory should be the clients' sum: %d vs %d", got[0].MemUsed, cl.Mem)
|
|
}
|
|
// the Processes table joins the client to its row
|
|
c.ring = []Sample{{T: 1, GPUs: got}}
|
|
c.prevProc = nil
|
|
table := c.Processes()
|
|
if len(table.GPUs) != 1 {
|
|
t.Errorf("table carries the GPUs: %+v", table.GPUs)
|
|
}
|
|
}
|
|
|
|
func TestFlattenGPU(t *testing.T) {
|
|
s := Sample{T: 5, GPUs: []GPU{{Name: "Intel Arc A380", Driver: "i915", Bus: "0000:03:00.0", Usage: 12, UsageSrc: "idle", Engines: map[string]float64{"video": 30}, PowerW: 9.5, PowerCapW: 55, MHz: 2450, MaxMHz: 2450, Clients: []GPUClient{{PID: 1}}}}}
|
|
got := map[string]any{}
|
|
for _, x := range flatten(s, nil) {
|
|
got[x.ID] = x.Value
|
|
}
|
|
for id, want := range map[string]any{"gpu.intel_arc_a380.usage": 12.0, "gpu.intel_arc_a380.video": 30.0, "gpu.intel_arc_a380.power": 9.5, "gpu.intel_arc_a380.freq": 2450.0, "gpu.intel_arc_a380.clients": 1.0} {
|
|
if got[id] != want {
|
|
t.Errorf("%s = %v, want %v", id, got[id], want)
|
|
}
|
|
}
|
|
if _, ok := got["gpu.intel_arc_a380.temp"]; ok {
|
|
t.Error("no temperature sensor, no temperature")
|
|
}
|
|
}
|
|
|
|
func replace(s, old, new string) string {
|
|
for i := 0; i+len(old) <= len(s); i++ {
|
|
if s[i:i+len(old)] == old {
|
|
return s[:i] + new + s[i+len(old):]
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// TestLiveGPU reads this machine's GPUs for real: HAMLAB_LIVE=1 go test -run TestLiveGPU -v ./internal/metrics
|
|
func TestLiveGPU(t *testing.T) {
|
|
if os.Getenv("HAMLAB_LIVE") == "" {
|
|
t.Skip("set HAMLAB_LIVE=1 to read the real hardware")
|
|
}
|
|
c := NewCollector()
|
|
c.sample()
|
|
time.Sleep(1100 * time.Millisecond)
|
|
c.sample()
|
|
s, _ := c.Latest()
|
|
b, _ := json.MarshalIndent(s.GPUs, "", " ")
|
|
t.Logf("%s", b)
|
|
for _, x := range flatten(s, func(id string) bool { return len(id) > 4 && id[:4] == "gpu." }) {
|
|
t.Logf("%s = %v %s", x.ID, x.Value, x.Unit)
|
|
}
|
|
}
|
|
|
|
// TestLiveARC: HAMLAB_LIVE=1 go test -run TestLiveARC -v ./internal/metrics
|
|
func TestLiveARC(t *testing.T) {
|
|
if os.Getenv("HAMLAB_LIVE") == "" {
|
|
t.Skip("set HAMLAB_LIVE=1 to read the real machine")
|
|
}
|
|
c := NewCollector()
|
|
c.sample()
|
|
time.Sleep(1100 * time.Millisecond)
|
|
c.sample()
|
|
s, _ := c.Latest()
|
|
b, _ := json.MarshalIndent(s.ARC, "", " ")
|
|
t.Logf("%s", b)
|
|
for _, x := range flatten(s, func(id string) bool { return strings.HasPrefix(id, "zfs.arc.") }) {
|
|
t.Logf("%s = %v %s", x.ID, x.Value, x.Unit)
|
|
}
|
|
}
|