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>
173 lines
6.2 KiB
Go
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)
|
|
}
|
|
}
|