-
Notifications
You must be signed in to change notification settings - Fork 240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cgroup2: Reduce allocations for manager.Stat
#281
Conversation
I suggest making |
8132977
to
0391449
Compare
@kolyshkin Made a separate commit here for the hugeTlb changes, thanks for the quick review! |
cgroup2/manager.go
Outdated
// Sizing this avoids an allocation to increase the map at runtime; | ||
// currently the default bucket size is 8 and we put 40+ elements | ||
// in it so we'd always end up allocating. | ||
out := make(map[string]string, 50) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps have the size as 0 and capacity as 64?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can't set the capacity of a map or chan, only slices
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, my bad, ENEEDCOFFEE
By looking at the code, I'd rather have it refactored first, then optimize. At least some parts that are crying to be removed, such as readSingleFile -- PTAL #283 |
@kolyshkin ack, there is quite a few places that all do the same thing. I'm fine getting rid of them and then rebasing here |
manager.Stat
manager.Stat
488dbdc
to
f6f6bf0
Compare
f6f6bf0
to
02b1793
Compare
66fce44
to
41646cb
Compare
Okay, updated to include the runc trick @kolyshkin, great suggestion. We're down to 199 now. |
cgroup2/utils.go
Outdated
for _, pagesize := range hugePageSizes() { | ||
usage = append(usage, &stats.HugeTlbStat{ | ||
Max: getStatFileContentUint64(filepath.Join(path, "hugetlb."+pagesize+".max")), | ||
Current: getStatFileContentUint64(filepath.Join(path, "hugetlb."+pagesize+".current")), | ||
Pagesize: pagesize, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So while I do love how this looks now, we'd include N entries (where the N is the amount of hugepages we found in /sys/kernel/mm/hugepages) in this slice now whereas before if we failed to find any hugetlb.x.y files we'd return an empty slice. If someone didn't have the hugetlb controller on it may be odd to see multiple entries all with zero stats. There's not a great way to handle this if we use getStatFileContentUint64
as it doesn't error, it just returns 0, which are valid numbers for these. I don't want to make getStatFileContentUint64
error either as it makes the other 7 uses of it more annoying. We could just make an anonymous function here that does what getStatFileContentUint64
does but errors on failures and we'll just continue
in the loop, or leave if no one has concerns
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm! Can we do soemthing like
hugePageSizeSlice := hugePageSizes()
var usage = make(*stats.HugeTlbStat{}, len(hugePageSizeSlice)
and then iterate like an array
for idx, pageSize := range hugePageSizeSlice {
usage[idx] = ....
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the same outcome though right? We still have a slice with empty entries, where the number of them is tied to how many huge page sizes we found in /sys/kernel/mm/hugepages
. Before this we'd return an empty slice which I think is ideal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh okay! yes. what I suggested matches your current code; but I was looking from an allocation perspective. If you pre-allocate; the slice won't have to expand while looping and appending.
I do not have an opinion on functionality as I do not know how users use it. I would try and keep the previous behavior though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @manugupt1 that pre-allocating the usage
slice is better than growing it dynamically. Even if it won't shave off any allocations, this is more optimized overall, and since we know the size, it makes total sense to do so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I agree about pre-allocating, more the point I was trying to get across still stands: this behavior is different than whats here today. We'll have entries in hugetlb stats regardless of if someone even has the controller enabled, as long as there's entries in /sys/kernel/mm/hugepages
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right! I am not the best judge of this tbh!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets see what others think, I don't think this is too crazy a departure
41646cb
to
71447da
Compare
cgroup2/utils.go
Outdated
for _, pagesize := range hugePageSizes() { | ||
usage = append(usage, &stats.HugeTlbStat{ | ||
Max: getStatFileContentUint64(filepath.Join(path, "hugetlb."+pagesize+".max")), | ||
Current: getStatFileContentUint64(filepath.Join(path, "hugetlb."+pagesize+".current")), | ||
Pagesize: pagesize, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm! Can we do soemthing like
hugePageSizeSlice := hugePageSizes()
var usage = make(*stats.HugeTlbStat{}, len(hugePageSizeSlice)
and then iterate like an array
for idx, pageSize := range hugePageSizeSlice {
usage[idx] = ....
}
71447da
to
86bf59d
Compare
This change works towards bringing down allocations for manager.Stat(). There's two things this change does: 1. Swap to opening a file and issuing a single read for uint64/"max" values. Previously we were doing os.ReadFile's which returns a byte slice, so it needs to allocate. 2. Sizing the map we store {controller}.stat values in. We store 40+ stats in this map and the default bucket size for Go seems to be smaller than this, so we'd incur a couple allocs at runtime when adding these. Signed-off-by: Danny Canter <danny@dcantah.dev>
86bf59d
to
3216f01
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM overall, except for preallocating the usage slice would be better.
@dcantah also, if you like, you can add Co-authored-by: Kir Kolyshkin <kolyshkin@gmail.com>
to the second commit, as I wrote that venerable /sys/kernel/mm/hugepages/* parsing code.
Oh, and if you can use |
@kolyshkin Sure thing! I didn't even realize you wrote that, so you've already gone through this pain 🤣 |
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>
3216f01
to
a8621bd
Compare
No wonder you recommended, I fat fingered 47 from 38.. No more programming past 12 🐸 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Related to #280. There's still some spots we can fix, as the majority of allocs are occurring in bufio.Scan in readKVStatsFile. cc @kolyshkin @manugupt1 seeming as how we were all there for the last one
This change works towards bringing down allocations for manager.Stat(). There's quite a few things we can do:
as f.Readdir(-1) was being called everytime we went to collect HugeTlb stats (it returns a slice of interfaces, that all we were doing is using the Name() method of anyways). To optimize this we can 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.
All in all on Go 1.19.4 this drops allocations in Stat from 322 to 198