fix(exporter): fix memory dedup collapsing distinct DIMMs, disambiguate slots

dedupeMemory keyed on slot before serial, unlike dedupeStorage/dedupePSUs
which both prefer serial. Some collectors (observed on MSI CG480-S6053 with
older BEE-SP versions — confirmed reproducible across multiple servers,
fixed in newer BEE-SP versions) report every memory module at the same slot
label ("DIMM 0") even though each has a distinct real serial. Slot-first
keying collapsed all of them into a single record on export — 16 real 32GB
DIMMs (512GB) reduced to 1, discarding 15 physical modules' worth of data.

Serial now takes priority, matching storage/PSU. Also add
disambiguateMemorySlots (same pattern as the existing PSU slot fix): when
multiple already-distinct DIMMs still share a slot label, renumber them to
"DIMM {n}" by ascending serial order so Reanimator can track them by
position and results are deterministic across re-imports.

Updated the existing dedup test (was asserting the old collapse-by-slot
behavior on a same-slot/different-serial case, which was exactly the bug)
and added a dedicated regression test for the collision case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-08-17 11:05:19 +03:00
co-authored by Claude Sonnet 5
parent 6827c1fe20
commit 2c3072cf10
2 changed files with 83 additions and 4 deletions
+39 -2
View File
@@ -46,7 +46,7 @@ func ConvertToReanimator(result *models.AnalysisResult) (*ReanimatorExport, erro
Board: convertBoard(result.Hardware.BoardInfo),
Firmware: dedupeFirmware(convertFirmware(result.Hardware.Firmware)),
CPUs: dedupeCPUs(convertCPUsFromDevices(devices, collectedAt, result.Hardware.BoardInfo.SerialNumber, buildCPUMicrocodeBySocket(result.Hardware.Firmware))),
Memory: dedupeMemory(convertMemoryFromDevices(devices, collectedAt)),
Memory: disambiguateMemorySlots(dedupeMemory(convertMemoryFromDevices(devices, collectedAt))),
Storage: dedupeStorage(convertStorageFromDevices(devices, collectedAt)),
PCIeDevices: dedupePCIe(convertPCIeFromDevices(devices, collectedAt)),
PowerSupplies: disambiguatePSUSlots(dedupePSUs(convertPSUsFromDevices(devices, collectedAt))),
@@ -2130,7 +2130,15 @@ func dedupeMemory(items []ReanimatorMemory) []ReanimatorMemory {
seen := make(map[string]struct{}, len(items))
result := make([]ReanimatorMemory, 0, len(items))
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(item.Slot))
// Serial identifies one physical DIMM; some collectors (observed on
// MSI CG480-S6053 with older BEE-SP versions) report every module at
// the same slot label ("DIMM 0"). Keying on slot first would collapse
// all of them into one record and silently drop the rest even though
// each has a distinct, real serial number.
key := strings.ToLower(strings.TrimSpace(item.SerialNumber))
if key == "" {
key = strings.ToLower(strings.TrimSpace(item.Slot))
}
if key == "" {
key = strings.ToLower(strings.TrimSpace(item.Location))
}
@@ -2215,6 +2223,35 @@ func disambiguatePSUSlots(items []ReanimatorPSU) []ReanimatorPSU {
return items
}
// disambiguateMemorySlots renumbers memory slots when two or more distinct
// DIMMs (different serials, already survived dedupeMemory) report the same
// slot string from the source — observed on MSI CG480-S6053 with older
// BEE-SP versions, which label every module "DIMM 0" regardless of its real
// channel/slot. As with disambiguatePSUSlots, positions within a colliding
// group are assigned by ascending serial number for a deterministic result.
func disambiguateMemorySlots(items []ReanimatorMemory) []ReanimatorMemory {
if len(items) < 2 {
return items
}
groups := make(map[string][]int, len(items))
for i, it := range items {
key := strings.ToLower(strings.TrimSpace(it.Slot))
groups[key] = append(groups[key], i)
}
for _, idxs := range groups {
if len(idxs) < 2 {
continue
}
sort.Slice(idxs, func(a, b int) bool {
return strings.ToLower(items[idxs[a]].SerialNumber) < strings.ToLower(items[idxs[b]].SerialNumber)
})
for pos, idx := range idxs {
items[idx].Slot = fmt.Sprintf("DIMM %d", pos)
}
}
return items
}
func dedupePCIe(items []ReanimatorPCIe) []ReanimatorPCIe {
if len(items) < 2 {
return items