Skip to content

Commit

Permalink
Cgroup2: Reduce allocations in readHugeTlbStats
Browse files Browse the repository at this point in the history
In the journey of continuing to reduce allocations in manager.Stat,
this sets its eyes on optimizing readHugeTlbStats. The number of allocs
shaved off here is tied to the number of files in the cgroup directory
as f.Readdir(-1) was being called which returns a slice of interfaces.
To optimize this we can use a trick runc's cgroup code does which is to
take advantage of the fact that we know exactly what files we need to
open as they're fixed, the only variable portion is the hugepage sizes
available on the host. To get around this we can compute the hugepage
sizes in a sync.Once, and all future manager.Stat calls can reap the
benefits as we don't need to Readdir() the entire cgroup path to search
for hugetlb.pagesize.foobar's.

This brings the total number of allocations on Go 1.19.4 down to 199 from
the starting point of 322.

Signed-off-by: Danny Canter <danny@dcantah.dev>
Co-authored-by: Kir Kolyshkin <kolyshkin@gmail.com>
  • Loading branch information
dcantah and kolyshkin committed Apr 28, 2023
1 parent 92a7d65 commit a8621bd
Showing 1 changed file with 84 additions and 44 deletions.
128 changes: 84 additions & 44 deletions cgroup2/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ package cgroup2

import (
"bufio"
"errors"
"fmt"
"io"
"math"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"unsafe"

Expand Down Expand Up @@ -387,56 +389,94 @@ func systemdUnitFromPath(path string) string {
}

func readHugeTlbStats(path string) []*stats.HugeTlbStat {
usage := []*stats.HugeTlbStat{}
keyUsage := make(map[string]*stats.HugeTlbStat)
f, err := os.Open(path)
if err != nil {
return usage
}
files, err := f.Readdir(-1)
f.Close()
if err != nil {
return usage
hpSizes := hugePageSizes()
usage := make([]*stats.HugeTlbStat, len(hpSizes))
for idx, pagesize := range hpSizes {
usage[idx] = &stats.HugeTlbStat{
Max: getStatFileContentUint64(filepath.Join(path, "hugetlb."+pagesize+".max")),
Current: getStatFileContentUint64(filepath.Join(path, "hugetlb."+pagesize+".current")),
Pagesize: pagesize,
}
}
return usage
}

for _, file := range files {
if strings.Contains(file.Name(), "hugetlb") &&
(strings.HasSuffix(file.Name(), "max") || strings.HasSuffix(file.Name(), "current")) {
var hugeTlb *stats.HugeTlbStat
var ok bool
fileName := strings.Split(file.Name(), ".")
pageSize := fileName[1]
if hugeTlb, ok = keyUsage[pageSize]; !ok {
hugeTlb = &stats.HugeTlbStat{}
}
hugeTlb.Pagesize = pageSize
out, err := os.ReadFile(filepath.Join(path, file.Name()))
if err != nil {
continue
}
var value uint64
stringVal := strings.TrimSpace(string(out))
if stringVal == "max" {
value = math.MaxUint64
} else {
value, err = strconv.ParseUint(stringVal, 10, 64)
}
if err != nil {
continue
var (
hPageSizes []string
initHPSOnce sync.Once
)

// The following idea and implementation is taken pretty much line for line from
// runc. Because the hugetlb files are well known, and the only variable thrown in
// the mix is what huge page sizes you have on your host, this lends itself well
// to doing the work to find the files present once, and then re-using this. This
// saves a os.Readdirnames(0) call to search for hugeltb files on every `manager.Stat`
// call.
// https://github.com/opencontainers/runc/blob/3a2c0c2565644d8a7e0f1dd594a060b21fa96cf1/libcontainer/cgroups/utils.go#L301
func hugePageSizes() []string {
initHPSOnce.Do(func() {
dir, err := os.OpenFile("/sys/kernel/mm/hugepages", unix.O_DIRECTORY|unix.O_RDONLY, 0)
if err != nil {
return
}
files, err := dir.Readdirnames(0)
dir.Close()
if err != nil {
return
}

hPageSizes, err = getHugePageSizeFromFilenames(files)
if err != nil {
logrus.Warnf("hugePageSizes: %s", err)
}
})

return hPageSizes
}

func getHugePageSizeFromFilenames(fileNames []string) ([]string, error) {
pageSizes := make([]string, 0, len(fileNames))
var warn error

for _, file := range fileNames {
// example: hugepages-1048576kB
val := strings.TrimPrefix(file, "hugepages-")
if len(val) == len(file) {
// Unexpected file name: no prefix found, ignore it.
continue
}
// In all known versions of Linux up to 6.3 the suffix is always
// "kB". If we find something else, produce an error but keep going.
eLen := len(val) - 2
val = strings.TrimSuffix(val, "kB")
if len(val) != eLen {
// Highly unlikely.
if warn == nil {
warn = errors.New(file + `: invalid suffix (expected "kB")`)
}
switch fileName[2] {
case "max":
hugeTlb.Max = value
case "current":
hugeTlb.Current = value
continue
}
size, err := strconv.Atoi(val)
if err != nil {
// Highly unlikely.
if warn == nil {
warn = fmt.Errorf("%s: %w", file, err)
}
keyUsage[pageSize] = hugeTlb
continue
}
// Model after https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/hugetlb_cgroup.c?id=eff48ddeab782e35e58ccc8853f7386bbae9dec4#n574
// but in our case the size is in KB already.
if size >= (1 << 20) {
val = strconv.Itoa(size>>20) + "GB"
} else if size >= (1 << 10) {
val = strconv.Itoa(size>>10) + "MB"
} else {
val += "KB"
}
pageSizes = append(pageSizes, val)
}
for _, entry := range keyUsage {
usage = append(usage, entry)
}
return usage

return pageSizes, warn
}

func getSubreaper() (int, error) {
Expand Down

0 comments on commit a8621bd

Please sign in to comment.