Skip to content

Commit 865a715

Browse files
ShimmerGlassandrewkrohefd6
authoredAug 16, 2023
providers/linux: optimize parseKeyValue (#186)
This optimizes `parseKeyValue()` function in the linux provider. It reduces the CPU cost and completely eliminates memory allocations. ``` │ before.txt │ after.txt │ │ sec/op │ sec/op vs base │ ParseKeyValue-10 3540.0n ± ∞ ¹ 972.6n ± ∞ ¹ -72.53% (p=0.008 n=5) │ B/op │ B/op vs base │ ParseKeyValue-10 6.672Ki ± ∞ ¹ 0.000Ki ± ∞ ¹ -100.00% (p=0.008 n=5) │ allocs/op │ allocs/op vs base │ ParseKeyValue-10 58.00 ± ∞ ¹ 0.00 ± ∞ ¹ -100.00% (p=0.008 n=5) ``` --------- Co-authored-by: Andrew Kroh <andrew.kroh@elastic.co> Co-authored-by: Dan Kortschak <90160302+efd6@users.noreply.github.com>
1 parent e4ac65c commit 865a715

File tree

8 files changed

+186
-13
lines changed

8 files changed

+186
-13
lines changed
 

‎.changelog/186.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
linux: optimize linux key value parsing (ie: /proc files)
3+
```

‎providers/linux/capabilities_linux.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func capabilityName(num int) string {
8686
func readCapabilities(content []byte) (*types.CapabilityInfo, error) {
8787
var cap types.CapabilityInfo
8888

89-
err := parseKeyValue(content, ":", func(key, value []byte) error {
89+
err := parseKeyValue(content, ':', func(key, value []byte) error {
9090
var err error
9191
switch string(key) {
9292
case "CapInh":

‎providers/linux/memory_linux.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func parseMemInfo(content []byte) (*types.HostMemoryInfo, error) {
2929
}
3030

3131
hasAvailable := false
32-
err := parseKeyValue(content, ":", func(key, value []byte) error {
32+
err := parseKeyValue(content, ':', func(key, value []byte) error {
3333
num, err := parseBytesOrNumber(value)
3434
if err != nil {
3535
return fmt.Errorf("failed to parse %v value of %v: %w", string(key), string(value), err)

‎providers/linux/process_linux.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ func (p *process) User() (types.UserInfo, error) {
229229
}
230230

231231
var user types.UserInfo
232-
err = parseKeyValue(content, ":", func(key, value []byte) error {
232+
err = parseKeyValue(content, ':', func(key, value []byte) error {
233233
// See proc(5) for the format of /proc/[pid]/status
234234
switch string(key) {
235235
case "Uid":

‎providers/linux/seccomp_linux.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func (m SeccompMode) String() string {
4747
func readSeccompFields(content []byte) (*types.SeccompInfo, error) {
4848
var seccomp types.SeccompInfo
4949

50-
err := parseKeyValue(content, ":", func(key, value []byte) error {
50+
err := parseKeyValue(content, ':', func(key, value []byte) error {
5151
switch string(key) {
5252
case "Seccomp":
5353
mode, err := strconv.ParseUint(string(value), 10, 8)

‎providers/linux/util.go

+16-8
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,28 @@ import (
2626
"strconv"
2727
)
2828

29-
func parseKeyValue(content []byte, separator string, callback func(key, value []byte) error) error {
30-
sc := bufio.NewScanner(bytes.NewReader(content))
31-
for sc.Scan() {
32-
parts := bytes.SplitN(sc.Bytes(), []byte(separator), 2)
33-
if len(parts) != 2 {
29+
// parseKeyValue parses key/val pairs separated by the provided separator from
30+
// each line in content and invokes the callback. White-space is trimmed from
31+
// val. Empty lines are ignored. All non-empty lines must contain the separator
32+
// otherwise an error is returned.
33+
func parseKeyValue(content []byte, separator byte, callback func(key, value []byte) error) error {
34+
var line []byte
35+
36+
for len(content) > 0 {
37+
line, content, _ = bytes.Cut(content, []byte{'\n'})
38+
if len(line) == 0 {
3439
continue
3540
}
3641

37-
if err := callback(parts[0], bytes.TrimSpace(parts[1])); err != nil {
38-
return err
42+
key, value, ok := bytes.Cut(line, []byte{separator})
43+
if !ok {
44+
return fmt.Errorf("separator %q not found", separator)
3945
}
46+
47+
callback(key, bytes.TrimSpace(value))
4048
}
4149

42-
return sc.Err()
50+
return nil
4351
}
4452

4553
func findValue(filename, separator, key string) (string, error) {

‎providers/linux/util_test.go

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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 linux
19+
20+
import (
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
)
25+
26+
func TestParseKeyValueNoEOL(t *testing.T) {
27+
vals := [][2]string{}
28+
err := parseKeyValue([]byte(
29+
"Name: zsh\nUmask: 0022\nState: S (sleeping)\nUid: 1000 1000 1000 1000",
30+
), ':', func(key, value []byte) error {
31+
vals = append(vals, [2]string{string(key), string(value)})
32+
return nil
33+
})
34+
assert.NoError(t, err)
35+
36+
assert.Equal(t, [][2]string{
37+
{"Name", "zsh"},
38+
{"Umask", "0022"},
39+
{"State", "S (sleeping)"},
40+
{"Uid", "1000\t1000\t1000\t1000"},
41+
}, vals)
42+
}
43+
44+
func TestParseKeyValueEmptyLine(t *testing.T) {
45+
vals := [][2]string{}
46+
err := parseKeyValue([]byte(
47+
"Name: zsh\nUmask: 0022\nState: S (sleeping)\n\nUid: 1000 1000 1000 1000",
48+
), ':', func(key, value []byte) error {
49+
vals = append(vals, [2]string{string(key), string(value)})
50+
return nil
51+
})
52+
assert.NoError(t, err)
53+
54+
assert.Equal(t, [][2]string{
55+
{"Name", "zsh"},
56+
{"Umask", "0022"},
57+
{"State", "S (sleeping)"},
58+
{"Uid", "1000\t1000\t1000\t1000"},
59+
}, vals)
60+
}
61+
62+
func TestParseKeyValueEOL(t *testing.T) {
63+
vals := [][2]string{}
64+
err := parseKeyValue([]byte(
65+
"Name: zsh\nUmask: 0022\nState: S (sleeping)\nUid: 1000 1000 1000 1000\n",
66+
), ':', func(key, value []byte) error {
67+
vals = append(vals, [2]string{string(key), string(value)})
68+
return nil
69+
})
70+
assert.NoError(t, err)
71+
72+
assert.Equal(t, [][2]string{
73+
{"Name", "zsh"},
74+
{"Umask", "0022"},
75+
{"State", "S (sleeping)"},
76+
{"Uid", "1000\t1000\t1000\t1000"},
77+
}, vals)
78+
}
79+
80+
// from cat /proc/$$/status
81+
var testProcStatus = []byte(`Name: zsh
82+
Umask: 0022
83+
State: S (sleeping)
84+
Tgid: 4023363
85+
Ngid: 0
86+
Pid: 4023363
87+
PPid: 4023357
88+
TracerPid: 0
89+
Uid: 1000 1000 1000 1000
90+
Gid: 1000 1000 1000 1000
91+
FDSize: 64
92+
Groups: 24 25 27 29 30 44 46 102 109 112 116 119 131 998 1000
93+
NStgid: 4023363
94+
NSpid: 4023363
95+
NSpgid: 4023363
96+
NSsid: 4023363
97+
VmPeak: 15596 kB
98+
VmSize: 15144 kB
99+
VmLck: 0 kB
100+
VmPin: 0 kB
101+
VmHWM: 9060 kB
102+
VmRSS: 8716 kB
103+
RssAnon: 3828 kB
104+
RssFile: 4888 kB
105+
RssShmem: 0 kB
106+
VmData: 3500 kB
107+
VmStk: 328 kB
108+
VmExe: 600 kB
109+
VmLib: 2676 kB
110+
VmPTE: 68 kB
111+
VmSwap: 0 kB
112+
HugetlbPages: 0 kB
113+
CoreDumping: 0
114+
THP_enabled: 1
115+
Threads: 1
116+
SigQ: 0/126683
117+
SigPnd: 0000000000000000
118+
ShdPnd: 0000000000000000
119+
SigBlk: 0000000000000002
120+
SigIgn: 0000000000384000
121+
SigCgt: 0000000008013003
122+
CapInh: 0000000000000000
123+
CapPrm: 0000000000000000
124+
CapEff: 0000000000000000
125+
CapBnd: 000001ffffffffff
126+
CapAmb: 0000000000000000
127+
NoNewPrivs: 0
128+
Seccomp: 0
129+
Seccomp_filters: 0
130+
Speculation_Store_Bypass: thread vulnerable
131+
Cpus_allowed: fff
132+
Cpus_allowed_list: 0-11
133+
Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
134+
Mems_allowed_list: 0
135+
voluntary_ctxt_switches: 223
136+
nonvoluntary_ctxt_switches: 25
137+
`)
138+
139+
func BenchmarkParseKeyValue(b *testing.B) {
140+
for i := 0; i < b.N; i++ {
141+
_ = parseKeyValue(testProcStatus, ':', func(key, value []byte) error {
142+
return nil
143+
})
144+
}
145+
}
146+
147+
func FuzzParseKeyValue(f *testing.F) {
148+
testcases := []string{
149+
"no_separator",
150+
"no_value:",
151+
"empty_value: ",
152+
"normal: 223",
153+
}
154+
for _, tc := range testcases {
155+
f.Add(tc)
156+
}
157+
f.Fuzz(func(t *testing.T, orig string) {
158+
_ = parseKeyValue([]byte(orig), ':', func(key, value []byte) error {
159+
return nil
160+
})
161+
})
162+
}

‎providers/linux/vmstat.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func parseVMStat(content []byte) (*types.VMStatInfo, error) {
4545
var vmStat types.VMStatInfo
4646
refValues := reflect.ValueOf(&vmStat).Elem()
4747

48-
err := parseKeyValue(content, " ", func(key, value []byte) error {
48+
err := parseKeyValue(content, ' ', func(key, value []byte) error {
4949
// turn our []byte value into an int
5050
val, err := parseBytesOrNumber(value)
5151
if err != nil {

0 commit comments

Comments
 (0)
Failed to load comments.