Skip to content

Commit

Permalink
WIP: memory: topology: expose per-numa memory
Browse files Browse the repository at this point in the history
This patch wants to add memory informations on topology.Node,
so we can have per-NUMA-zone memory information.

WIP: tests are broken (and more needs to come), doc is missing,
API is provisional. The basic concepts are there, though.

next steps, once this PR is finalized, is to expose hugepage stats
per-NUMA (and per-system?)

Signed-off-by: Francesco Romani <[email protected]>
  • Loading branch information
ffromani committed Aug 26, 2021
1 parent 22c410e commit df10a08
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 29 deletions.
6 changes: 6 additions & 0 deletions pkg/memory/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
119 changes: 90 additions & 29 deletions pkg/memory/memory_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions pkg/topology/topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions pkg/topology/topology_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit df10a08

Please sign in to comment.