diff --git a/pkg/memory/memory.go b/pkg/memory/memory.go index 93605d2e..6b424e38 100644 --- a/pkg/memory/memory.go +++ b/pkg/memory/memory.go @@ -25,6 +25,12 @@ type Module struct { Vendor string `json:"vendor"` } +// TODO review the name +type Area struct { + TotalPhysicalBytes int64 `json:"total_physical_bytes"` + TotalUsableBytes int64 `json:"total_usable_bytes"` +} + type Info struct { ctx *context.Context TotalPhysicalBytes int64 `json:"total_physical_bytes"` diff --git a/pkg/memory/memory_linux.go b/pkg/memory/memory_linux.go index 2fb85b71..da41b10f 100644 --- a/pkg/memory/memory_linux.go +++ b/pkg/memory/memory_linux.go @@ -17,6 +17,7 @@ import ( "strconv" "strings" + "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/linuxpath" "github.com/jaypipes/ghw/pkg/unitutil" "github.com/jaypipes/ghw/pkg/util" @@ -56,6 +57,45 @@ func (i *Info) load() error { return nil } +func AreaForNode(ctx *context.Context, nodeID int) (*Area, error) { + paths := linuxpath.New(ctx) + path := filepath.Join( + paths.SysDevicesSystemNode, + fmt.Sprintf("node%d", nodeID), + ) + + blockSizeBytes, err := memoryBlockSizeBytes(paths.SysDevicesSystemMemory) + if err != nil { + return nil, err + } + + totPhys, err := memoryTotalPhysicalBytesFromPath(path, blockSizeBytes) + if err != nil { + return nil, err + } + + totUsable, err := memoryTotalUsableBytesFromPath(filepath.Join(path, "meminfo")) + if err != nil { + return nil, err + } + + return &Area{ + TotalPhysicalBytes: totPhys, + TotalUsableBytes: totUsable, + }, nil +} + +func memoryBlockSizeBytes(dir string) (uint64, error) { + // get the memory block size in byte in hexadecimal notation + blockSize := filepath.Join(dir, "block_size_bytes") + + d, err := ioutil.ReadFile(blockSize) + if err != nil { + return 0, err + } + return strconv.ParseUint(strings.TrimSpace(string(d)), 16, 64) +} + func memTotalPhysicalBytes(paths *linuxpath.Paths) (total int64) { defer func() { // fallback to the syslog file approach in case of error @@ -66,42 +106,48 @@ func memTotalPhysicalBytes(paths *linuxpath.Paths) (total int64) { // detect physical memory from /sys/devices/system/memory dir := paths.SysDevicesSystemMemory - - // get the memory block size in byte in hexadecimal notation - blockSize := filepath.Join(dir, "block_size_bytes") - - d, err := ioutil.ReadFile(blockSize) + blockSizeBytes, err := memoryBlockSizeBytes(dir) if err != nil { - return -1 + total = -1 + return total } - blockSizeBytes, err := strconv.ParseUint(strings.TrimSpace(string(d)), 16, 64) + + total, err = memoryTotalPhysicalBytesFromPath(dir, blockSizeBytes) if err != nil { - return -1 + total = -1 } + return total +} - // iterate over memory's block /sys/devices/system/memory/memory*, +func memoryTotalPhysicalBytesFromPath(dir string, blockSizeBytes uint64) (int64, error) { + // iterate over memory's block /sys/.../memory*, // if the memory block state is 'online' we increment the total // with the memory block size to determine the amount of physical - // memory available on this system + // memory available on this system. + // This works for both system-wide: + // /sys/devices/system/memory/memory* + // and for per-numa-node report: + // /sys/devices/system/node/node*/memory* + sysMemory, err := filepath.Glob(filepath.Join(dir, "memory*")) if err != nil { - return -1 + return -1, err } else if sysMemory == nil { - return -1 + return -1, fmt.Errorf("cannot find memory entries in %q", dir) } + var total int64 for _, path := range sysMemory { s, err := ioutil.ReadFile(filepath.Join(path, "state")) if err != nil { - return -1 + return -1, err } if strings.TrimSpace(string(s)) != "online" { continue } total += int64(blockSizeBytes) } - - return total + return total, nil } func memTotalPhysicalBytesFromSyslog(paths *linuxpath.Paths) int64 { @@ -161,7 +207,17 @@ func memTotalPhysicalBytesFromSyslog(paths *linuxpath.Paths) int64 { } func memTotalUsableBytes(paths *linuxpath.Paths) int64 { - // In Linux, /proc/meminfo contains a set of memory-related amounts, with + amount, err := memoryTotalUsableBytesFromPath(paths.ProcMeminfo) + if err != nil { + return -1 + } + return amount +} + +func memoryTotalUsableBytesFromPath(meminfoPath string) (int64, error) { + // In Linux, /proc/meminfo or its close relative + // /sys/devices/system/node/node*/meminfo + // contains a set of memory-related amounts, with // lines looking like the following: // // $ cat /proc/meminfo @@ -179,36 +235,41 @@ func memTotalUsableBytes(paths *linuxpath.Paths) int64 { // "theoretical" information. For instance, on the above system, I have // 24GB of RAM but MemTotal is indicating only around 23GB. This is because // MemTotal contains the exact amount of *usable* memory after accounting - // for the kernel's resident memory size and a few reserved bits. For more - // information, see: + // for the kernel's resident memory size and a few reserved bits. + // Please note GHW cares about the subset of lines shared between system-wide + // and per-NUMA-node meminfos. For more information, see: // // https://www.kernel.org/doc/Documentation/filesystems/proc.txt - filePath := paths.ProcMeminfo - r, err := os.Open(filePath) + r, err := os.Open(meminfoPath) if err != nil { - return -1 + return -1, err } defer util.SafeClose(r) + splitFields := func(c rune) bool { + return c == ':' + } + scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() - parts := strings.Fields(line) - key := strings.Trim(parts[0], ": \t") - if key != "MemTotal" { + parts := strings.FieldsFunc(line, splitFields) + key := parts[0] + if !strings.Contains(key, "MemTotal") { continue } - value, err := strconv.Atoi(strings.TrimSpace(parts[1])) + rawValue := parts[1] + inKb := strings.HasSuffix(rawValue, "kB") + value, err := strconv.Atoi(strings.TrimSpace(strings.TrimSuffix(rawValue, "kB"))) if err != nil { - return -1 + return -1, err } - inKb := (len(parts) == 3 && strings.TrimSpace(parts[2]) == "kB") if inKb { value = value * int(unitutil.KB) } - return int64(value) + return int64(value), nil } - return -1 + return -1, fmt.Errorf("failed to find MemTotal entry in path %q", meminfoPath) } func memSupportedPageSizes(paths *linuxpath.Paths) []uint64 { diff --git a/pkg/topology/topology.go b/pkg/topology/topology.go index a846584d..1bd93cbe 100644 --- a/pkg/topology/topology.go +++ b/pkg/topology/topology.go @@ -59,6 +59,7 @@ type Node struct { Cores []*cpu.ProcessorCore `json:"cores"` Caches []*memory.Cache `json:"caches"` Distances []int `json:"distances"` + Memory *memory.Area `json:"memory"` } func (n *Node) String() string { diff --git a/pkg/topology/topology_linux.go b/pkg/topology/topology_linux.go index 4c3bbb16..84fbbd2c 100644 --- a/pkg/topology/topology_linux.go +++ b/pkg/topology/topology_linux.go @@ -69,6 +69,13 @@ func topologyNodes(ctx *context.Context) []*Node { } node.Distances = distances + area, err := memory.AreaForNode(ctx, nodeID) + if err != nil { + ctx.Warn("failed to determine memory area for node: %s\n", err) + return nodes + } + node.Memory = area + nodes = append(nodes, node) } return nodes