Skip to content

Commit d3d6c55

Browse files
committed
btf: lazy string table
Signed-off-by: Lorenz Bauer <[email protected]>
1 parent c03bd45 commit d3d6c55

File tree

2 files changed

+28
-77
lines changed

2 files changed

+28
-77
lines changed

btf/btf_test.go

-13
Original file line numberDiff line numberDiff line change
@@ -252,19 +252,6 @@ func TestParseCurrentKernelBTF(t *testing.T) {
252252
if len(spec.namedTypes) == 0 {
253253
t.Fatal("Empty kernel BTF")
254254
}
255-
256-
totalBytes := 0
257-
distinct := 0
258-
seen := make(map[string]bool)
259-
for _, str := range spec.strings.strings {
260-
totalBytes += len(str)
261-
if !seen[str] {
262-
distinct++
263-
seen[str] = true
264-
}
265-
}
266-
t.Logf("%d strings total, %d distinct", len(spec.strings.strings), distinct)
267-
t.Logf("Average string size: %.0f", float64(totalBytes)/float64(len(spec.strings.strings)))
268255
}
269256

270257
func TestFindVMLinux(t *testing.T) {

btf/strings.go

+28-64
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
package btf
22

33
import (
4-
"bufio"
54
"bytes"
65
"errors"
76
"fmt"
87
"io"
98
"maps"
10-
"slices"
119
"strings"
1210
)
1311

1412
type stringTable struct {
15-
base *stringTable
16-
offsets []uint32
17-
prevIdx int
18-
strings []string
13+
base *stringTable
14+
bytes []byte
1915
}
2016

2117
// sizedReader is implemented by bytes.Reader, io.SectionReader, strings.Reader, etc.
@@ -29,89 +25,57 @@ func readStringTable(r sizedReader, base *stringTable) (*stringTable, error) {
2925
// from the last entry offset of the base BTF.
3026
firstStringOffset := uint32(0)
3127
if base != nil {
32-
idx := len(base.offsets) - 1
33-
firstStringOffset = base.offsets[idx] + uint32(len(base.strings[idx])) + 1
28+
firstStringOffset = uint32(len(base.bytes))
3429
}
3530

36-
// Derived from vmlinux BTF.
37-
const averageStringLength = 16
38-
39-
n := int(r.Size() / averageStringLength)
40-
offsets := make([]uint32, 0, n)
41-
strings := make([]string, 0, n)
42-
43-
offset := firstStringOffset
44-
scanner := bufio.NewScanner(r)
45-
scanner.Split(splitNull)
46-
for scanner.Scan() {
47-
str := scanner.Text()
48-
offsets = append(offsets, offset)
49-
strings = append(strings, str)
50-
offset += uint32(len(str)) + 1
51-
}
52-
if err := scanner.Err(); err != nil {
31+
bytes := make([]byte, r.Size())
32+
if _, err := io.ReadFull(r, bytes); err != nil {
5333
return nil, err
5434
}
5535

56-
if len(strings) == 0 {
36+
if len(bytes) == 0 {
5737
return nil, errors.New("string table is empty")
5838
}
5939

60-
if firstStringOffset == 0 && strings[0] != "" {
61-
return nil, errors.New("first item in string table is non-empty")
40+
if bytes[len(bytes)-1] != 0 {
41+
return nil, errors.New("string table isn't null terminated")
6242
}
6343

64-
return &stringTable{base, offsets, 0, strings}, nil
65-
}
66-
67-
func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
68-
i := bytes.IndexByte(data, 0)
69-
if i == -1 {
70-
if atEOF && len(data) > 0 {
71-
return 0, nil, errors.New("string table isn't null terminated")
72-
}
73-
return 0, nil, nil
44+
if firstStringOffset == 0 && bytes[0] != 0 {
45+
return nil, errors.New("first item in string table is non-empty")
7446
}
7547

76-
return i + 1, data[:i], nil
48+
return &stringTable{base: base, bytes: bytes}, nil
7749
}
7850

7951
func (st *stringTable) Lookup(offset uint32) (string, error) {
80-
if st.base != nil && offset <= st.base.offsets[len(st.base.offsets)-1] {
81-
return st.base.lookup(offset)
82-
}
83-
return st.lookup(offset)
84-
}
85-
86-
func (st *stringTable) lookup(offset uint32) (string, error) {
8752
// Fast path: zero offset is the empty string, looked up frequently.
88-
if offset == 0 && st.base == nil {
53+
if offset == 0 {
8954
return "", nil
9055
}
9156

92-
// Accesses tend to be globally increasing, so check if the next string is
93-
// the one we want. This skips the binary search in about 50% of cases.
94-
if st.prevIdx+1 < len(st.offsets) && st.offsets[st.prevIdx+1] == offset {
95-
st.prevIdx++
96-
return st.strings[st.prevIdx], nil
97-
}
57+
return st.lookupSlow(offset)
58+
}
9859

99-
i, found := slices.BinarySearch(st.offsets, offset)
100-
if !found {
101-
return "", fmt.Errorf("offset %d isn't start of a string", offset)
60+
func (st *stringTable) lookupSlow(offset uint32) (string, error) {
61+
if st.base != nil {
62+
n := uint32(len(st.base.bytes))
63+
if offset < n {
64+
return st.base.lookupSlow(offset)
65+
}
66+
offset -= n
10267
}
10368

104-
// Set the new increment index, but only if its greater than the current.
105-
if i > st.prevIdx+1 {
106-
st.prevIdx = i
69+
if offset > uint32(len(st.bytes)) {
70+
return "", fmt.Errorf("offset %d is out of bounds of string table", offset)
10771
}
10872

109-
return st.strings[i], nil
110-
}
73+
if offset > 0 && st.bytes[offset-1] != 0 {
74+
return "", fmt.Errorf("offset %d is not the beginning of a string", offset)
75+
}
11176

112-
// Num returns the number of strings in the table.
113-
func (st *stringTable) Num() int {
114-
return len(st.strings)
77+
i := bytes.IndexByte(st.bytes[offset:], 0)
78+
return string(st.bytes[offset : offset+uint32(i)]), nil
11579
}
11680

11781
// stringTableBuilder builds BTF string tables.

0 commit comments

Comments
 (0)