Skip to content

Commit a6075af

Browse files
authored
Merge pull request #281 from dcantah/cg2-reduce-allocs
Cgroup2: Reduce allocations for `manager.Stat`
2 parents 474049b + a8621bd commit a6075af

5 files changed

Lines changed: 130 additions & 52 deletions

File tree

cgroup2/manager.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,10 @@ func (c *Manager) Stat() (*stats.Metrics, error) {
525525
if err != nil {
526526
return nil, err
527527
}
528-
out := make(map[string]uint64)
528+
// Sizing this avoids an allocation to increase the map at runtime;
529+
// currently the default bucket size is 8 and we put 40+ elements
530+
// in it so we'd always end up allocating.
531+
out := make(map[string]uint64, 50)
529532
for _, controller := range controllers {
530533
switch controller {
531534
case "cpu", "memory":
@@ -543,8 +546,8 @@ func (c *Manager) Stat() (*stats.Metrics, error) {
543546
return nil, err
544547
}
545548
}
546-
var metrics stats.Metrics
547549

550+
var metrics stats.Metrics
548551
metrics.Pids = &stats.PidsStat{
549552
Current: getStatFileContentUint64(filepath.Join(c.path, "pids.current")),
550553
Limit: getStatFileContentUint64(filepath.Join(c.path, "pids.max")),

cgroup2/manager_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,20 @@ func TestCgroupType(t *testing.T) {
228228
require.NoError(t, err)
229229
require.Equal(t, cgType, Threaded)
230230
}
231+
232+
func BenchmarkStat(b *testing.B) {
233+
checkCgroupMode(b)
234+
group := "/stat-test-cg"
235+
groupPath := fmt.Sprintf("%s-%d", group, os.Getpid())
236+
c, err := NewManager(defaultCgroup2Path, groupPath, &Resources{})
237+
require.NoErrorf(b, err, "failed to init new cgroup manager")
238+
b.Cleanup(func() {
239+
_ = c.Delete()
240+
})
241+
242+
b.ReportAllocs()
243+
for i := 0; i < b.N; i++ {
244+
_, err := c.Stat()
245+
require.NoError(b, err)
246+
}
247+
}

cgroup2/testutils_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ import (
2727
"golang.org/x/sys/unix"
2828
)
2929

30-
func checkCgroupMode(t *testing.T) {
30+
func checkCgroupMode(tb testing.TB) {
3131
var st unix.Statfs_t
3232
err := unix.Statfs(defaultCgroup2Path, &st)
33-
require.NoError(t, err, "cannot statfs cgroup root")
33+
require.NoError(tb, err, "cannot statfs cgroup root")
3434

3535
isUnified := st.Type == unix.CGROUP2_SUPER_MAGIC
3636
if !isUnified {
37-
t.Skip("System running in hybrid or cgroupv1 mode")
37+
tb.Skip("System running in hybrid or cgroupv1 mode")
3838
}
3939
}
4040

cgroup2/utils.go

Lines changed: 97 additions & 47 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

@@ -236,18 +238,28 @@ func ToResources(spec *specs.LinuxResources) *Resources {
236238

237239
// Gets uint64 parsed content of single value cgroup stat file
238240
func getStatFileContentUint64(filePath string) uint64 {
239-
contents, err := os.ReadFile(filePath)
241+
f, err := os.Open(filePath)
240242
if err != nil {
241243
return 0
242244
}
243-
trimmed := strings.TrimSpace(string(contents))
245+
defer f.Close()
246+
247+
// We expect an unsigned 64 bit integer, or a "max" string
248+
// in some cases.
249+
buf := make([]byte, 32)
250+
n, err := f.Read(buf)
251+
if err != nil {
252+
return 0
253+
}
254+
255+
trimmed := strings.TrimSpace(string(buf[:n]))
244256
if trimmed == "max" {
245257
return math.MaxUint64
246258
}
247259

248260
res, err := parseUint(trimmed, 10, 64)
249261
if err != nil {
250-
logrus.Errorf("unable to parse %q as a uint from Cgroup file %q", string(contents), filePath)
262+
logrus.Errorf("unable to parse %q as a uint from Cgroup file %q", trimmed, filePath)
251263
return res
252264
}
253265

@@ -377,56 +389,94 @@ func systemdUnitFromPath(path string) string {
377389
}
378390

379391
func readHugeTlbStats(path string) []*stats.HugeTlbStat {
380-
usage := []*stats.HugeTlbStat{}
381-
keyUsage := make(map[string]*stats.HugeTlbStat)
382-
f, err := os.Open(path)
383-
if err != nil {
384-
return usage
385-
}
386-
files, err := f.Readdir(-1)
387-
f.Close()
388-
if err != nil {
389-
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+
}
390400
}
401+
return usage
402+
}
391403

392-
for _, file := range files {
393-
if strings.Contains(file.Name(), "hugetlb") &&
394-
(strings.HasSuffix(file.Name(), "max") || strings.HasSuffix(file.Name(), "current")) {
395-
var hugeTlb *stats.HugeTlbStat
396-
var ok bool
397-
fileName := strings.Split(file.Name(), ".")
398-
pageSize := fileName[1]
399-
if hugeTlb, ok = keyUsage[pageSize]; !ok {
400-
hugeTlb = &stats.HugeTlbStat{}
401-
}
402-
hugeTlb.Pagesize = pageSize
403-
out, err := os.ReadFile(filepath.Join(path, file.Name()))
404-
if err != nil {
405-
continue
406-
}
407-
var value uint64
408-
stringVal := strings.TrimSpace(string(out))
409-
if stringVal == "max" {
410-
value = math.MaxUint64
411-
} else {
412-
value, err = strconv.ParseUint(stringVal, 10, 64)
413-
}
414-
if err != nil {
415-
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")`)
416456
}
417-
switch fileName[2] {
418-
case "max":
419-
hugeTlb.Max = value
420-
case "current":
421-
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)
422464
}
423-
keyUsage[pageSize] = hugeTlb
465+
continue
424466
}
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)
425477
}
426-
for _, entry := range keyUsage {
427-
usage = append(usage, entry)
428-
}
429-
return usage
478+
479+
return pageSizes, warn
430480
}
431481

432482
func getSubreaper() (int, error) {

cgroup2/utils_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,11 @@ func TestToResources(t *testing.T) {
5858
v2resources2 := ToResources(&res2)
5959
assert.Equal(t, CPUMax("max 10000"), v2resources2.CPU.Max)
6060
}
61+
62+
func BenchmarkGetStatFileContentUint64(b *testing.B) {
63+
b.ReportAllocs()
64+
65+
for i := 0; i < b.N; i++ {
66+
_ = getStatFileContentUint64("/proc/self/loginuid")
67+
}
68+
}

0 commit comments

Comments
 (0)