Skip to content

Commit

Permalink
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.
In order to do so, we extract a `memory.Area` struct from `memory.Info`
to describe the basic properties of a memory zone, and
we add it to `topology.Node`.
In the future, we may want to add (or move) more fields
to this basic building block, and we will get them for free
in both system-wide and per-numa memory info.

Fixes: #200
Signed-off-by: Francesco Romani <[email protected]>
  • Loading branch information
ffromani committed Oct 27, 2021
1 parent 17b5707 commit 2b03367
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 57 deletions.
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,24 @@ function.

### Memory

The basic building block of the memory support in ghw is the `ghw.MemoryArea` struct.
A "memory area" is a block of memory which share common properties. In the simplest
case, the whole system memory fits in a single memory area; in more complex scenarios,
like multi-NUMA systems, many memory areas may be present in the system (e.g. one for
each NUMA cell).

The `ghw.MemoryArea` struct contains the following fields:

* `ghw.MemoryInfo.TotalPhysicalBytes` contains the amount of physical memory on
the host
* `ghw.MemoryInfo.TotalUsableBytes` contains the amount of memory the
system can actually use. Usable memory accounts for things like the kernel's
resident memory size and some reserved system bits

Information about the host computer's memory can be retrieved using the
`ghw.Memory()` function which returns a pointer to a `ghw.MemoryInfo` struct.

The `ghw.MemoryInfo` struct contains three fields:
`ghw.MemoryInfo` is a superset of `ghw.MemoryArea`. Thus, it contains all the
fields found in the `ghw.MemoryArea` (replicated for clarity) plus some:

* `ghw.MemoryInfo.TotalPhysicalBytes` contains the amount of physical memory on
the host
Expand Down Expand Up @@ -600,6 +614,8 @@ Each `ghw.TopologyNode` struct contains the following fields:
system
* `ghw.TopologyNode.Distance` is an array of distances between NUMA nodes as reported
by the system.
* `ghw.TopologyNode.Memory` is a struct describing the memory attached to this node.
Please refer to the documentation of `ghw.MemoryArea`.

See above in the [CPU](#cpu) section for information about the
`ghw.ProcessorCore` struct and how to use and query it.
Expand Down
1 change: 1 addition & 0 deletions alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ var (
CPU = cpu.New
)

type MemoryArea = memory.Area
type MemoryInfo = memory.Info
type MemoryCacheType = memory.CacheType
type MemoryModule = memory.Module
Expand Down
5 changes: 5 additions & 0 deletions cmd/ghwc/commands/topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ func showTopology(cmd *cobra.Command, args []string) error {
for _, cache := range node.Caches {
fmt.Printf(" %v\n", cache)
}
fmt.Printf(" %v\n", node.Memory)
fmt.Printf(" distances\n")
for nodeID, dist := range node.Distances {
fmt.Printf(" to node #%d %v\n", nodeID, dist)
}
}
case outputFormatJSON:
fmt.Printf("%s\n", topology.JSONString(pretty))
Expand Down
44 changes: 26 additions & 18 deletions pkg/memory/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,37 @@ type Module struct {
Vendor string `json:"vendor"`
}

type Info struct {
ctx *context.Context
type Area struct {
TotalPhysicalBytes int64 `json:"total_physical_bytes"`
TotalUsableBytes int64 `json:"total_usable_bytes"`
// An array of sizes, in bytes, of memory pages supported by the host
// An array of sizes, in bytes, of memory pages supported in this area
SupportedPageSizes []uint64 `json:"supported_page_sizes"`
Modules []*Module `json:"modules"`
}

func (a *Area) String() string {
tpbs := util.UNKNOWN
if a.TotalPhysicalBytes > 0 {
tpb := a.TotalPhysicalBytes
unit, unitStr := unitutil.AmountString(tpb)
tpb = int64(math.Ceil(float64(a.TotalPhysicalBytes) / float64(unit)))
tpbs = fmt.Sprintf("%d%s", tpb, unitStr)
}
tubs := util.UNKNOWN
if a.TotalUsableBytes > 0 {
tub := a.TotalUsableBytes
unit, unitStr := unitutil.AmountString(tub)
tub = int64(math.Ceil(float64(a.TotalUsableBytes) / float64(unit)))
tubs = fmt.Sprintf("%d%s", tub, unitStr)
}
return fmt.Sprintf("memory (%s physical, %s usable)", tpbs, tubs)
}

type Info struct {
ctx *context.Context
Area
}

func New(opts ...*option.Option) (*Info, error) {
ctx := context.New(opts...)
info := &Info{ctx: ctx}
Expand All @@ -44,21 +66,7 @@ func New(opts ...*option.Option) (*Info, error) {
}

func (i *Info) String() string {
tpbs := util.UNKNOWN
if i.TotalPhysicalBytes > 0 {
tpb := i.TotalPhysicalBytes
unit, unitStr := unitutil.AmountString(tpb)
tpb = int64(math.Ceil(float64(i.TotalPhysicalBytes) / float64(unit)))
tpbs = fmt.Sprintf("%d%s", tpb, unitStr)
}
tubs := util.UNKNOWN
if i.TotalUsableBytes > 0 {
tub := i.TotalUsableBytes
unit, unitStr := unitutil.AmountString(tub)
tub = int64(math.Ceil(float64(i.TotalUsableBytes) / float64(unit)))
tubs = fmt.Sprintf("%d%s", tub, unitStr)
}
return fmt.Sprintf("memory (%s physical, %s usable)", tpbs, tubs)
return i.Area.String()
}

// simple private struct used to encapsulate memory information in a top-level
Expand Down
135 changes: 99 additions & 36 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 @@ -52,10 +53,56 @@ func (i *Info) load() error {
i.ctx.Warn(_WARN_CANNOT_DETERMINE_PHYSICAL_MEMORY)
i.TotalPhysicalBytes = tub
}
i.SupportedPageSizes = memSupportedPageSizes(paths)
i.SupportedPageSizes, _ = memorySupportedPageSizes(paths.SysKernelMMHugepages)
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
}

supportedHP, err := memorySupportedPageSizes(filepath.Join(path, "hugepages"))
if err != nil {
fmt.Fprintf(os.Stderr, "err=%v\n", err)
return nil, err
}

return &Area{
TotalPhysicalBytes: totPhys,
TotalUsableBytes: totUsable,
SupportedPageSizes: supportedHP,
}, 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 +113,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 +214,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,48 +242,48 @@ 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)

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.Split(line, ":")
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 {
func memorySupportedPageSizes(hpDir string) ([]uint64, error) {
// In Linux, /sys/kernel/mm/hugepages contains a directory per page size
// supported by the kernel. The directory name corresponds to the pattern
// 'hugepages-{pagesize}kb'
dir := paths.SysKernelMMHugepages
out := make([]uint64, 0)

files, err := ioutil.ReadDir(dir)
files, err := ioutil.ReadDir(hpDir)
if err != nil {
return out
return out, err
}
for _, file := range files {
parts := strings.Split(file.Name(), "-")
Expand All @@ -229,9 +292,9 @@ func memSupportedPageSizes(paths *linuxpath.Paths) []uint64 {
sizeStr = sizeStr[0 : len(sizeStr)-2]
size, err := strconv.Atoi(sizeStr)
if err != nil {
return out
return out, err
}
out = append(out, uint64(size*int(unitutil.KB)))
}
return out
return out, nil
}
7 changes: 6 additions & 1 deletion pkg/memory/memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ func TestMemory(t *testing.T) {

tpb := mem.TotalPhysicalBytes
tub := mem.TotalUsableBytes

if tpb == 0 {
t.Fatalf("Total physical bytes reported zero")
}
if tub == 0 {
t.Fatalf("Total usable bytes reported zero")
}
if tpb < tub {
t.Fatalf("Total physical bytes < total usable bytes. %d < %d",
tpb, tub)
Expand Down
3 changes: 3 additions & 0 deletions pkg/snapshot/clonetree_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ func ExpectedCloneStaticContent() []string {
"/sys/devices/system/node/possible",
"/sys/devices/system/node/node*/cpu*",
"/sys/devices/system/node/node*/distance",
"/sys/devices/system/node/node*/meminfo",
"/sys/devices/system/node/node*/memory*",
"/sys/devices/system/node/node*/hugepages/hugepages-*/*",
}
}

Expand Down
1 change: 1 addition & 0 deletions pkg/topology/topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,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
Loading

0 comments on commit 2b03367

Please sign in to comment.