webui/topo: attach memory DIMMs to their CPU column in the topology diagram

Memory used to render as one unattached row below the whole diagram
regardless of which socket it belonged to. schema.HardwareMemory has no
NUMANode field (unlike PCIe devices), so CPU affinity is instead read out of
the DIMM's own Locator string: either a CPU number encoded directly in it
("CPU0_DIMM_A1"), or — when the Locator has no CPU number of its own, e.g.
"DIMM000(A)" — a node number from Bank Locator ("_Node1_Channel0_Dimm0"),
read from the persisted dmidecode-type17.txt techdump the same way the
NVLink card already reads extra techdump for visualization only.

DIMMs that can't be attached to a column via either heuristic still fall
back to the old unattached "Memory" row so nothing silently disappears.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-07-09 17:15:06 +03:00
co-authored by Claude Sonnet 5
parent c1f0f7824e
commit 41f683de2b
2 changed files with 370 additions and 19 deletions
+168
View File
@@ -392,6 +392,174 @@ func TestTopoMainDiagramGroupsSameKindSameColumnIntoOneStackedCard(t *testing.T)
}
}
func TestDimmRawNodeFromCPULocatorPrefix(t *testing.T) {
slot := "CPU1_DIMM_A1"
mem := schema.HardwareMemory{Slot: &slot}
node, ok := dimmRawNode(mem, nil)
if !ok || node != 1 {
t.Fatalf("dimmRawNode() = (%d, %v), want (1, true)", node, ok)
}
}
func TestDimmRawNodeFromBankLocatorFallback(t *testing.T) {
// Boards whose Locator carries no CPU number of its own (e.g.
// "DIMM000(A)") still need to be attached to a column via the node
// number in Bank Locator, read separately from techdump.
slot := "DIMM100(A)"
mem := schema.HardwareMemory{Slot: &slot}
bankNodes := map[string]int{"DIMM100(A)": 2}
node, ok := dimmRawNode(mem, bankNodes)
if !ok || node != 2 {
t.Fatalf("dimmRawNode() = (%d, %v), want (2, true)", node, ok)
}
}
func TestDimmRawNodeUnmatched(t *testing.T) {
slot := "SOMETHING_UNRECOGNIZED"
mem := schema.HardwareMemory{Slot: &slot}
if _, ok := dimmRawNode(mem, nil); ok {
t.Fatalf("expected no match for an unrecognized locator")
}
if _, ok := dimmRawNode(schema.HardwareMemory{}, nil); ok {
t.Fatalf("expected no match when Slot is nil")
}
}
func TestParseDIMMBankLocatorNodes(t *testing.T) {
// Real dmidecode -t 17 shape: "Locator" precedes "Bank Locator" within
// each "Memory Device" section.
raw := `Handle 0x0017, DMI type 17, 92 bytes
Memory Device
Size: 64 GB
Locator: DIMM000(A)
Bank Locator: _Node1_Channel0_Dimm0
Type: DDR5
Handle 0x0018, DMI type 17, 92 bytes
Memory Device
Size: No Module Installed
Locator: DIMM001(I)
Bank Locator: _Node1_Channel0_Dimm1
Handle 0x0019, DMI type 17, 92 bytes
Memory Device
Size: 64 GB
Locator: DIMM100(A)
Bank Locator: _Node2_Channel0_Dimm0
`
got := parseDIMMBankLocatorNodes(raw)
want := map[string]int{"DIMM000(A)": 1, "DIMM001(I)": 1, "DIMM100(A)": 2}
if len(got) != len(want) {
t.Fatalf("got=%#v want=%#v", got, want)
}
for k, v := range want {
if got[k] != v {
t.Fatalf("got[%q]=%d want %d (full: %#v)", k, got[k], v, got)
}
}
}
func TestBuildMemoryColumnIndex(t *testing.T) {
// Node numbers observed on real boards are not guaranteed 0-based (Bank
// Locator "NodeN" has been seen starting at 1) — this must rank by
// order, not treat the raw value as a column index.
idx := buildMemoryColumnIndex([]int{1, 2, 1, 2})
if idx[1] != 0 || idx[2] != 1 {
t.Fatalf("idx=%#v want {1:0, 2:1}", idx)
}
}
// TestTopoMainDiagramAttachesMemoryToItsCPUColumn is the regression test for
// the actual feature request: memory used to render as one unattached
// "MEMORY" row below the whole diagram regardless of which CPU it belonged
// to. DIMMs whose Locator carries a CPU number must now render as their own
// box directly under that CPU, wired to it with an edge, the same as
// GPU/NIC/RAID — and must NOT also show up in the leftover flex row.
func TestTopoMainDiagramAttachesMemoryToItsCPUColumn(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
socket0, socket1 := 0, 1
okStatus := "OK"
slot0, slot1 := "CPU0_DIMM_A1", "CPU1_DIMM_A1"
sizeMB := 98304
ingest := schema.HardwareIngestRequest{
CollectedAt: "2026-03-15T00:00:00Z",
Hardware: schema.HardwareSnapshot{
CPUs: []schema.HardwareCPU{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket0},
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket1},
},
Memory: []schema.HardwareMemory{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Slot: &slot0, SizeMB: &sizeMB},
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Slot: &slot1, SizeMB: &sizeMB},
},
},
}
data, err := json.Marshal(ingest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
handler := NewHandler(HandlerOptions{AuditPath: path})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
body := rec.Body.String()
if strings.Count(body, `>Memory<`) != 2 {
t.Fatalf("expected 2 attached Memory boxes (one per CPU column), got body: %s", body)
}
if strings.Contains(body, "Memory</div>") {
t.Fatalf("both DIMMs matched a CPU column — the leftover unattached Memory row must not render: %s", body)
}
if strings.Count(body, "openComponentDetail('memory')") != 2 {
t.Fatalf("expected 2 clickable Memory boxes, got body: %s", body)
}
}
// TestTopoMainDiagramUnattachableMemoryFallsBackToFlexRow guards that a DIMM
// whose Locator can't be parsed into a CPU/node number still shows up
// somewhere (the old unattached row) instead of silently disappearing.
func TestTopoMainDiagramUnattachableMemoryFallsBackToFlexRow(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
socket0 := 0
okStatus := "OK"
slot := "SOMETHING_UNRECOGNIZED"
sizeMB := 32768
ingest := schema.HardwareIngestRequest{
CollectedAt: "2026-03-15T00:00:00Z",
Hardware: schema.HardwareSnapshot{
CPUs: []schema.HardwareCPU{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket0},
},
Memory: []schema.HardwareMemory{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Slot: &slot, SizeMB: &sizeMB},
},
},
}
data, err := json.Marshal(ingest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
handler := NewHandler(HandlerOptions{AuditPath: path})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
body := rec.Body.String()
if !strings.Contains(body, "Memory</div>") {
t.Fatalf("unattachable DIMM should still appear in the leftover flex row: %s", body)
}
}
func TestParseTopoNVLinkStatus(t *testing.T) {
input := `GPU 0: NVIDIA H100 80GB HBM3 (UUID: GPU-a59f6931-c099-8fba-a0b3-08469d86f140)
Link 0: 26.562 GB/s