Files
hamlab/internal/metrics/gpu_test.go
Mitchell-JohnsonandClaude Fable 5.1 3a58e265c6 A GPU is named from an embedded PCI ID table, the same on every machine
The router has no lspci, so its GeminiLake chip came up as 'Intel GPU
3185' beside the identical chip's 'Intel UHD Graphics 600' on hamtv.
A trimmed pci.ids — the graphics devices of Intel, AMD and NVIDIA, and the
virtual GPUs, 115 KB — rides in the binary and is consulted first; lspci
is the fallback for a card newer than the table. scripts/pci-ids-gpu.sh
regenerates it from the hub's copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 23:48:05 -07:00

173 lines
6.2 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)
}
}
// The embedded table names the lab's four GPUs the way lspci would, so a machine without
// pciutils (the router) agrees with one that has it.
func TestPCIName(t *testing.T) {
for id, want := range map[string]string{"8086:3185": "Intel UHD Graphics 600", "8086:56a5": "Intel Arc A380", "8086:46d1": "Intel UHD Graphics", "8086:4c8a": "Intel UHD Graphics 750", "10de:2684": "NVIDIA GeForce RTX 4090"} {
v, d := pciName(id[:4], id[5:])
if d == "" {
t.Errorf("%s: not in the table", id)
continue
}
if got := lspciName(v + " " + d); got != want {
t.Errorf("%s → %q, want %q", id, got, want)
}
}
if _, d := pciName("8086", "ffff"); d != "" {
t.Errorf("an unknown device should be empty, got %q", d)
}
}