Skip to content

Commit cfab21b

Browse files
committed
Reduce heap allocations in SubsystemMountpoints()
1 parent b95cadf commit cfab21b

File tree

2 files changed

+153
-22
lines changed

2 files changed

+153
-22
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package stringutil
19+
20+
import (
21+
"strings"
22+
"unsafe"
23+
)
24+
25+
var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}
26+
27+
// FieldsN splits the string s around each instance of one or more consecutive space
28+
// characters, filling f with substrings of s.
29+
// If s contains more fields than n, the last element of f is set to the
30+
// unparsed remainder of s starting with the first non-space character.
31+
// f will stay untouched if s is empty or contains only white space.
32+
// if n is greater than len(f), 0 is returned without doing any parsing.
33+
//
34+
// Apart from the mentioned differences, FieldsN is like an allocation-free strings.Fields.
35+
func FieldsN(s string, f []string) int {
36+
n := len(f)
37+
si := 0
38+
for i := 0; i < n-1; i++ {
39+
// Find the start of the next field.
40+
for si < len(s) && asciiSpace[s[si]] != 0 {
41+
si++
42+
}
43+
fieldStart := si
44+
45+
// Find the end of the field.
46+
for si < len(s) && asciiSpace[s[si]] == 0 {
47+
si++
48+
}
49+
if fieldStart >= si {
50+
return i
51+
}
52+
53+
f[i] = s[fieldStart:si]
54+
}
55+
56+
// Find the start of the next field.
57+
for si < len(s) && asciiSpace[s[si]] != 0 {
58+
si++
59+
}
60+
61+
// Put the remainder of s as last element of f.
62+
if si < len(s) {
63+
f[n-1] = s[si:]
64+
return n
65+
}
66+
67+
return n - 1
68+
}
69+
70+
// SplitN splits the string around each instance of sep, filling f with substrings of s.
71+
// If s contains more fields than n, the last element of f is set to the
72+
// unparsed remainder of s starting with the first non-space character.
73+
// f will stay untouched if s is empty or contains only white space.
74+
// if n is greater than len(f), 0 is returned without doing any parsing.
75+
//
76+
// Apart from the mentioned differences, SplitN is like an allocation-free strings.SplitN.
77+
func SplitN(s, sep string, f []string) int {
78+
n := len(f)
79+
i := 0
80+
for ; i < n-1 && s != ""; i++ {
81+
fieldEnd := strings.Index(s, sep)
82+
if fieldEnd < 0 {
83+
f[i] = s
84+
return i + 1
85+
}
86+
f[i] = s[:fieldEnd]
87+
s = s[fieldEnd+len(sep):]
88+
}
89+
90+
// Put the remainder of s as last element of f.
91+
f[i] = s
92+
return i + 1
93+
}
94+
95+
// ByteSlice2String converts a byte slice into a string without a heap allocation.
96+
// Be aware that the byte slice and the string share the same memory - which makes
97+
// the string mutable.
98+
func ByteSlice2String(b []byte) string {
99+
return *(*string)(unsafe.Pointer(&b))
100+
}
101+
102+
func SplitInline(s, sep string) []string {
103+
// Use a fixed-size slice to avoid allocations.
104+
fields := make([]string, strings.Count(s, sep)+1)
105+
SplitN(s, sep, fields)
106+
return fields
107+
}

metric/system/cgroup/util.go

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import (
3030
"time"
3131

3232
"github.com/elastic/elastic-agent-libs/logp"
33+
34+
"github.com/elastic/elastic-agent-system-metrics/metric/system/cgroup/stringutil"
3335
"github.com/elastic/elastic-agent-system-metrics/metric/system/resolve"
3436
)
3537

@@ -120,35 +122,36 @@ func (pl PathList) Flatten() []ControllerPath {
120122
func parseMountinfoLine(line string) (mountinfo, error) {
121123
mount := mountinfo{}
122124

123-
fields := strings.Fields(line)
124-
if len(fields) < 10 {
125+
var fields [10 + 6]string // support up to 6 optional fields
126+
127+
nFields := stringutil.FieldsN(line, fields[:])
128+
if nFields < 10 {
125129
return mount, fmt.Errorf("invalid mountinfo line, expected at least "+
126-
"10 fields but got %d from line='%s'", len(fields), line)
130+
"10 fields but got %d from line='%s'", nFields, line)
127131
}
128132

129-
mount.mountpoint = fields[4]
130-
131133
var separatorIndex int
132-
for i, value := range fields {
133-
if value == "-" {
134+
for i := 6; i < nFields; i++ {
135+
if fields[i] == "-" {
134136
separatorIndex = i
135137
break
136138
}
137139
}
138-
if fields[separatorIndex] != "-" {
140+
if separatorIndex == 0 {
139141
return mount, fmt.Errorf("invalid mountinfo line, separator ('-') not "+
140142
"found in line='%s'", line)
141143
}
142-
143-
if len(fields)-separatorIndex-1 < 3 {
144+
if nFields-separatorIndex-1 < 3 {
144145
return mount, fmt.Errorf("invalid mountinfo line, expected at least "+
145146
"3 fields after separator but got %d from line='%s'",
146-
len(fields)-separatorIndex-1, line)
147+
nFields-separatorIndex-1, line)
147148
}
148149

149-
fields = fields[separatorIndex+1:]
150-
mount.filesystemType = fields[0]
151-
mount.superOptions = strings.Split(fields[2], ",")
150+
mount.mountpoint = fields[4]
151+
mount.filesystemType = fields[separatorIndex+1]
152+
if mount.filesystemType == "cgroup" {
153+
mount.superOptions = stringutil.SplitInline(fields[separatorIndex+3], ",")
154+
}
152155
return mount, nil
153156
}
154157

@@ -210,6 +213,7 @@ func SubsystemMountpoints(rootfs resolve.Resolver, subsystems map[string]struct{
210213
}
211214
defer mountinfo.Close()
212215

216+
hostFS := rootfs.ResolveHostFS("")
213217
mounts := map[string]string{}
214218
mountInfo := Mountpoints{}
215219
sc := bufio.NewScanner(mountinfo)
@@ -218,7 +222,11 @@ func SubsystemMountpoints(rootfs resolve.Resolver, subsystems map[string]struct{
218222
// https://www.kernel.org/doc/Documentation/filesystems/proc.txt
219223
// Example:
220224
// 25 21 0:20 / /cgroup/cpu rw,relatime - cgroup cgroup rw,cpu
221-
line := strings.TrimSpace(sc.Text())
225+
226+
// Avoid heap allocation by not using scanner.Text().
227+
// NOTE: The underlying bytes will change with the next call to scanner.Scan(),
228+
// so make sure to not keep any references after the end of the loop iteration.
229+
line := strings.TrimSpace(stringutil.ByteSlice2String(sc.Bytes()))
222230
if line == "" {
223231
continue
224232
}
@@ -229,32 +237,32 @@ func SubsystemMountpoints(rootfs resolve.Resolver, subsystems map[string]struct{
229237
}
230238

231239
// if the mountpoint from the subsystem has a different root than ours, it probably belongs to something else.
232-
if !strings.HasPrefix(mount.mountpoint, rootfs.ResolveHostFS("")) {
240+
if !strings.HasPrefix(mount.mountpoint, hostFS) {
233241
continue
234242
}
235243

236244
// cgroupv1 option
237245
if mount.filesystemType == "cgroup" {
238246
for _, opt := range mount.superOptions {
239247
// Sometimes the subsystem name is written like "name=blkio".
240-
fields := strings.SplitN(opt, "=", 2)
241-
if len(fields) > 1 {
248+
var fields [2]string
249+
if n := stringutil.SplitN(opt, "=", fields[:]); n > 1 {
242250
opt = fields[1]
243251
}
244252

245253
// Test if option is a subsystem name.
246254
if _, found := subsystems[opt]; found {
247255
// Add the subsystem mount if it does not already exist.
248256
if _, exists := mounts[opt]; !exists {
249-
mounts[opt] = mount.mountpoint
257+
mounts[opt] = strings.Clone(mount.mountpoint)
250258
}
251259
}
252260
}
253261
}
254262

255263
// V2 option
256264
if mount.filesystemType == "cgroup2" {
257-
possibleV2Paths = append(possibleV2Paths, mount.mountpoint)
265+
possibleV2Paths = append(possibleV2Paths, strings.Clone(mount.mountpoint))
258266
}
259267

260268
}
@@ -290,9 +298,25 @@ func isCgroupNSPrivate() bool {
290298
}
291299
// if we have a path of just "/" that means we're in our own private namespace
292300
// if it's something else, we're probably in a host namespace
293-
segments := strings.Split(strings.TrimSpace(string(raw)), ":")
294-
return segments[len(segments)-1] == "/"
301+
colons := 2
302+
for i := range trimBytes(raw) {
303+
if raw[i] == ':' {
304+
colons++
305+
if colons == 2 {
306+
// if we have a second colon, check if the next character is a slash
307+
return len(raw) == i+2 && raw[i+1] == '/'
308+
}
309+
}
310+
}
311+
return false
312+
}
295313

314+
func trimBytes(b []byte) []byte {
315+
// trim the trailing newlines
316+
for i := len(b) - 1; i >= 0 && b[i] == '\n'; i-- {
317+
b = b[:i]
318+
}
319+
return b
296320
}
297321

298322
// tries to find the cgroup path for the currently-running container,

0 commit comments

Comments
 (0)