Skip to content

Commit a8621bd

Browse files
dcantahkolyshkin
andcommitted
Cgroup2: Reduce allocations in readHugeTlbStats
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>
1 parent 92a7d65 commit a8621bd

1 file changed

Lines changed: 84 additions & 44 deletions

File tree

cgroup2/utils.go

Lines changed: 84 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ package cgroup2
1818

1919
import (
2020
"bufio"
21+
"errors"
2122
"fmt"
2223
"io"
2324
"math"
2425
"os"
2526
"path/filepath"
2627
"strconv"
2728
"strings"
29+
"sync"
2830
"time"
2931
"unsafe"
3032

@@ -387,56 +389,94 @@ func systemdUnitFromPath(path string) string {
387389
}
388390

389391
func readHugeTlbStats(path string) []*stats.HugeTlbStat {
390-
usage := []*stats.HugeTlbStat{}
391-
keyUsage := make(map[string]*stats.HugeTlbStat)
392-
f, err := os.Open(path)
393-
if err != nil {
394-
return usage
395-
}
396-
files, err := f.Readdir(-1)
397-
f.Close()
398-
if err != nil {
399-
return usage
392+
hpSizes := hugePageSizes()
393+
usage := make([]*stats.HugeTlbStat, len(hpSizes))
394+
for idx, pagesize := range hpSizes {
395+
usage[idx] = &stats.HugeTlbStat{
396+
Max: getStatFileContentUint64(filepath.Join(path, "hugetlb."+pagesize+".max")),
397+
Current: getStatFileContentUint64(filepath.Join(path, "hugetlb."+pagesize+".current")),
398+
Pagesize: pagesize,
399+
}
400400
}
401+
return usage
402+
}
401403

402-
for _, file := range files {
403-
if strings.Contains(file.Name(), "hugetlb") &&
404-
(strings.HasSuffix(file.Name(), "max") || strings.HasSuffix(file.Name(), "current")) {
405-
var hugeTlb *stats.HugeTlbStat
406-
var ok bool
407-
fileName := strings.Split(file.Name(), ".")
408-
pageSize := fileName[1]
409-
if hugeTlb, ok = keyUsage[pageSize]; !ok {
410-
hugeTlb = &stats.HugeTlbStat{}
411-
}
412-
hugeTlb.Pagesize = pageSize
413-
out, err := os.ReadFile(filepath.Join(path, file.Name()))
414-
if err != nil {
415-
continue
416-
}
417-
var value uint64
418-
stringVal := strings.TrimSpace(string(out))
419-
if stringVal == "max" {
420-
value = math.MaxUint64
421-
} else {
422-
value, err = strconv.ParseUint(stringVal, 10, 64)
423-
}
424-
if err != nil {
425-
continue
404+
var (
405+
hPageSizes []string
406+
initHPSOnce sync.Once
407+
)
408+
409+
// The following idea and implementation is taken pretty much line for line from
410+
// runc. Because the hugetlb files are well known, and the only variable thrown in
411+
// the mix is what huge page sizes you have on your host, this lends itself well
412+
// to doing the work to find the files present once, and then re-using this. This
413+
// saves a os.Readdirnames(0) call to search for hugeltb files on every `manager.Stat`
414+
// call.
415+
// https://github.com/opencontainers/runc/blob/3a2c0c2565644d8a7e0f1dd594a060b21fa96cf1/libcontainer/cgroups/utils.go#L301
416+
func hugePageSizes() []string {
417+
initHPSOnce.Do(func() {
418+
dir, err := os.OpenFile("/sys/kernel/mm/hugepages", unix.O_DIRECTORY|unix.O_RDONLY, 0)
419+
if err != nil {
420+
return
421+
}
422+
files, err := dir.Readdirnames(0)
423+
dir.Close()
424+
if err != nil {
425+
return
426+
}
427+
428+
hPageSizes, err = getHugePageSizeFromFilenames(files)
429+
if err != nil {
430+
logrus.Warnf("hugePageSizes: %s", err)
431+
}
432+
})
433+
434+
return hPageSizes
435+
}
436+
437+
func getHugePageSizeFromFilenames(fileNames []string) ([]string, error) {
438+
pageSizes := make([]string, 0, len(fileNames))
439+
var warn error
440+
441+
for _, file := range fileNames {
442+
// example: hugepages-1048576kB
443+
val := strings.TrimPrefix(file, "hugepages-")
444+
if len(val) == len(file) {
445+
// Unexpected file name: no prefix found, ignore it.
446+
continue
447+
}
448+
// In all known versions of Linux up to 6.3 the suffix is always
449+
// "kB". If we find something else, produce an error but keep going.
450+
eLen := len(val) - 2
451+
val = strings.TrimSuffix(val, "kB")
452+
if len(val) != eLen {
453+
// Highly unlikely.
454+
if warn == nil {
455+
warn = errors.New(file + `: invalid suffix (expected "kB")`)
426456
}
427-
switch fileName[2] {
428-
case "max":
429-
hugeTlb.Max = value
430-
case "current":
431-
hugeTlb.Current = value
457+
continue
458+
}
459+
size, err := strconv.Atoi(val)
460+
if err != nil {
461+
// Highly unlikely.
462+
if warn == nil {
463+
warn = fmt.Errorf("%s: %w", file, err)
432464
}
433-
keyUsage[pageSize] = hugeTlb
465+
continue
434466
}
467+
// Model after https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/hugetlb_cgroup.c?id=eff48ddeab782e35e58ccc8853f7386bbae9dec4#n574
468+
// but in our case the size is in KB already.
469+
if size >= (1 << 20) {
470+
val = strconv.Itoa(size>>20) + "GB"
471+
} else if size >= (1 << 10) {
472+
val = strconv.Itoa(size>>10) + "MB"
473+
} else {
474+
val += "KB"
475+
}
476+
pageSizes = append(pageSizes, val)
435477
}
436-
for _, entry := range keyUsage {
437-
usage = append(usage, entry)
438-
}
439-
return usage
478+
479+
return pageSizes, warn
440480
}
441481

442482
func getSubreaper() (int, error) {

0 commit comments

Comments
 (0)