A drive is a drive: NVMe namespaces fold into their controller

An Apple SSD presents three namespaces and the kernel lists each as a whole disk, so the
dashboard was showing one drive as three. The Disks page has always folded them; now the
dashboard, the Top tab and the disk.* sensors do too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-22 09:11:03 -07:00
co-authored by Claude Opus 5
parent 5761ee09db
commit 4b255fe63f
4 changed files with 100 additions and 1 deletions
+44
View File
@@ -97,3 +97,47 @@ func TestKeepSpacing(t *testing.T) {
t.Errorf("at the default window every sample is kept, got %d of 50", len(c.ring))
}
}
func TestFoldNVMe(t *testing.T) {
// an Apple SSD as the kernel lists it: one controller, three namespaces
got := foldNVMe([]Disk{
{Name: "nvme0n1", Model: "APPLE SSD AP0512Z", Size: 500 << 30, ReadBps: 100, WriteBps: 200, UtilPct: 12, Temp: 37},
{Name: "nvme0n2", Model: "APPLE SSD AP0512Z", Size: 3 << 20, ReadBps: 10, WriteBps: 20, UtilPct: 40},
{Name: "nvme0n3", Model: "APPLE SSD AP0512Z", Size: 128 << 20, ReadBps: 1, WriteBps: 2, UtilPct: 5},
{Name: "sda", Size: 100 << 30, ReadBps: 7, UtilPct: 3},
{Name: "nvme1n1", Size: 200 << 30, ReadBps: 5},
})
if len(got) != 3 {
t.Fatalf("three namespaces of one controller are one drive: %v", names(got))
}
d := got[0]
if d.Name != "nvme0" {
t.Errorf("the drive is the controller, got %q", d.Name)
}
if d.Size != 500<<30+3<<20+128<<20 {
t.Errorf("its size is all of its namespaces, got %d", d.Size)
}
if d.ReadBps != 111 || d.WriteBps != 222 {
t.Errorf("rates add up, got %v/%v", d.ReadBps, d.WriteBps)
}
if d.UtilPct != 40 {
t.Errorf("busy is the longest of them, not the sum: got %v", d.UtilPct)
}
if d.Temp != 37 {
t.Errorf("a temperature from any namespace is the drive's, got %v", d.Temp)
}
if got[1].Name != "sda" || got[1].UtilPct != 3 || got[2].Name != "nvme1" {
t.Errorf("everything else is left alone: %v", names(got))
}
if n := len(foldNVMe(nil)); n != 0 {
t.Errorf("no disks, no drives, got %d", n)
}
}
func names(d []Disk) []string {
out := make([]string, len(d))
for i, x := range d {
out[i] = x.Name
}
return out
}
+43
View File
@@ -715,10 +715,53 @@ func (c *Collector) disks(dt float64) []Disk {
c.prevDisk[name] = [3]uint64{rd, wr, ticks}
out = append(out, d)
}
out = foldNVMe(out)
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
var nsRe = regexp.MustCompile(`^(nvme\d+)n\d+$`)
// foldNVMe merges nvme0n1, nvme0n2, … into the drive they are: one controller presenting
// several namespaces, which the kernel lists as whole disks apiece. An Apple SSD shows three —
// the 466 GiB of storage and two of firmware, a few MiB each — and a page that lists disks was
// listing all three as separate drives. The Disks page has always folded them (internal/storage
// does the same); this is what the dashboard, the Top tab and the disk.* sensors read.
//
// Rates add up across namespaces. Busy does not: they share one controller, so the time it
// spent working is the longest of them, never the sum.
func foldNVMe(devs []Disk) []Disk {
byCtrl := map[string]int{}
out := devs[:0]
for _, d := range devs {
m := nsRe.FindStringSubmatch(d.Name)
if m == nil {
out = append(out, d)
continue
}
ctrl := m[1]
if i, ok := byCtrl[ctrl]; ok {
out[i].Size += d.Size
out[i].ReadBps = round1(out[i].ReadBps + d.ReadBps)
out[i].WriteBps = round1(out[i].WriteBps + d.WriteBps)
if d.UtilPct > out[i].UtilPct {
out[i].UtilPct = d.UtilPct
}
if out[i].Temp == 0 {
out[i].Temp = d.Temp
}
continue
}
d.Name = ctrl
if mdl := strings.TrimSpace(readStr("/sys/class/nvme/" + ctrl + "/model")); mdl != "" {
d.Model = mdl
}
byCtrl[ctrl] = len(out)
out = append(out, d)
}
return out
}
// ---- filesystems ----
// Kernel/virtual filesystems that aren't storage. Anything else that's mounted is