Skip to content

Commit 4ad6b8c

Browse files
committed
btf: fix slow LoadKernelSpec by making Spec.Copy lazy
LoadKernelSpec is currently very slow, on the order of tens of milliseconds. The reason for this is that we copy all ~150k types before returning the Spec to the caller. This is necessary since we don't want independent callers of LoadKernelSpec to be able to influence each other. So far we've put up with the slowness in case of features that are not used that much yet (kfuncs) or worked around it by moving APIs into the btf package where we can avoid the copy. I've made multiple attempts at fixing this problem. In no particlar order: * Have an internal non-copying API and an external copying one. Users have to make sure they only call LoadKernelSpec once since it's expensive. The problem here is that we'd still like the library internal code to use a single copy of the kernel Spec, but this is very difficult due to import cycles. * Decode raw BTF lazily instead of slurping all types into memory. This saves heap memory since we keep less inflated types around and only needs to do minimal upfront work. However, the upfront work still takes 10ish ms to complete and the decoding logic becomes much more complicated since we need to lazily apply fixups. Both Dylan and I have written implementations for this, which go into the 500-600 lines of changes. Finally, I've decided to dust of a third approach: we still parse kernel BTF eagerly but make copying a Spec lazy. This makes LoadKernelSpec basically free, at the cost of more expensive Spec.TypeByID, etc. Doing CO-RE and reading split BTF also slows down, although it probably ends up faster overall since we save a lot by not copying types. Finally, we still pin one copy of the kernel spec in memory, so FlushKernelSpec() is here to stay. It's hard to estimate how much of an issue this really is since Go's tooling for heap analysis is really poor. Signed-off-by: Lorenz Bauer <lmb@isovalent.com>
1 parent 6be83e2 commit 4ad6b8c

9 files changed

+316
-136
lines changed

btf/btf.go

+152-71
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ var (
2929
// ID represents the unique ID of a BTF object.
3030
type ID = sys.BTFID
3131

32-
// Spec allows querying a set of Types and loading the set into the
33-
// kernel.
34-
type Spec struct {
32+
// immutableTypes is a set of types which musn't be changed.
33+
type immutableTypes struct {
3534
// All types contained by the spec, not including types from the base in
3635
// case the spec was parsed from split BTF.
3736
types []Type
@@ -44,13 +43,132 @@ type Spec struct {
4443

4544
// Types indexed by essential name.
4645
// Includes all struct flavors and types with the same name.
47-
namedTypes map[essentialName][]Type
46+
namedTypes map[essentialName][]TypeID
47+
48+
// Byte order of the types. This affects things like struct member order
49+
// when using bitfields.
50+
byteOrder binary.ByteOrder
51+
}
52+
53+
func (s *immutableTypes) typeByID(id TypeID) (Type, bool) {
54+
if id < s.firstTypeID {
55+
return nil, false
56+
}
57+
58+
index := int(id - s.firstTypeID)
59+
if index >= len(s.types) {
60+
return nil, false
61+
}
62+
63+
return s.types[index], true
64+
}
65+
66+
// mutableTypes is a set of types which may be changed.
67+
type mutableTypes struct {
68+
imm immutableTypes
69+
copies map[Type]Type // map[orig]copy
70+
copiedTypeIDs map[Type]TypeID //map[copy]origID
71+
}
72+
73+
// add a type to the set of mutable types.
74+
//
75+
// Copies type and all of its children once. Repeated calls with the same type
76+
// do not copy again.
77+
func (mt *mutableTypes) add(typ Type, typeIDs map[Type]TypeID) Type {
78+
return modifyGraphPreorder(typ, func(t Type) (Type, bool) {
79+
cpy, ok := mt.copies[t]
80+
if ok {
81+
// This has been copied previously, no need to continue.
82+
return cpy, false
83+
}
84+
85+
cpy = t.copy()
86+
mt.copies[t] = cpy
87+
88+
if id, ok := typeIDs[t]; ok {
89+
mt.copiedTypeIDs[cpy] = id
90+
}
91+
92+
// This is a new copy, keep copying children.
93+
return cpy, true
94+
})
95+
}
96+
97+
// copy a set of mutable types.
98+
func (mt *mutableTypes) copy() mutableTypes {
99+
mtCopy := mutableTypes{
100+
mt.imm,
101+
make(map[Type]Type, len(mt.copies)),
102+
make(map[Type]TypeID, len(mt.copiedTypeIDs)),
103+
}
104+
105+
copies := make(map[Type]Type, len(mt.copies))
106+
for orig, copy := range mt.copies {
107+
// NB: We make a copy of copy, not orig, so that changes to mutable types
108+
// are preserved.
109+
copyOfCopy := mtCopy.add(copy, mt.copiedTypeIDs)
110+
copies[orig] = copyOfCopy
111+
}
112+
113+
// mtCopy.copies is currently map[copy]copyOfCopy, replace it with
114+
// map[orig]copyOfCopy.
115+
mtCopy.copies = copies
116+
return mtCopy
117+
}
118+
119+
func (mt *mutableTypes) typeID(typ Type) (TypeID, error) {
120+
if _, ok := typ.(*Void); ok {
121+
// Equality is weird for void, since it is a zero sized type.
122+
return 0, nil
123+
}
124+
125+
id, ok := mt.copiedTypeIDs[typ]
126+
if !ok {
127+
return 0, fmt.Errorf("no ID for type %s: %w", typ, ErrNotFound)
128+
}
129+
130+
return id, nil
131+
}
132+
133+
func (mt *mutableTypes) typeByID(id TypeID) (Type, bool) {
134+
immT, ok := mt.imm.typeByID(id)
135+
if !ok {
136+
return nil, false
137+
}
138+
139+
return mt.add(immT, mt.imm.typeIDs), true
140+
}
141+
142+
func (mt *mutableTypes) anyTypesByName(name string) ([]Type, error) {
143+
immTypes := mt.imm.namedTypes[newEssentialName(name)]
144+
if len(immTypes) == 0 {
145+
return nil, fmt.Errorf("type name %s: %w", name, ErrNotFound)
146+
}
147+
148+
// Return a copy to prevent changes to namedTypes.
149+
result := make([]Type, 0, len(immTypes))
150+
for _, id := range immTypes {
151+
immT, ok := mt.imm.typeByID(id)
152+
if !ok {
153+
return nil, fmt.Errorf("no type with ID %d", id)
154+
}
155+
156+
// Match against the full name, not just the essential one
157+
// in case the type being looked up is a struct flavor.
158+
if immT.TypeName() == name {
159+
result = append(result, mt.add(immT, mt.imm.typeIDs))
160+
}
161+
}
162+
return result, nil
163+
}
164+
165+
// Spec allows querying a set of Types and loading the set into the
166+
// kernel.
167+
type Spec struct {
168+
mutableTypes
48169

49170
// String table from ELF.
50171
strings *stringTable
51-
52-
// Byte order of the ELF we decoded the spec from, may be nil.
53-
byteOrder binary.ByteOrder
54172
}
55173

56174
// LoadSpec opens file and calls LoadSpecFromReader on it.
@@ -181,7 +299,7 @@ func loadSpecFromELF(file *internal.SafeELFFile) (*Spec, error) {
181299
return nil, err
182300
}
183301

184-
err = fixupDatasec(spec.types, sectionSizes, offsets)
302+
err = fixupDatasec(spec.imm.types, sectionSizes, offsets)
185303
if err != nil {
186304
return nil, err
187305
}
@@ -197,7 +315,7 @@ func loadRawSpec(btf io.ReaderAt, bo binary.ByteOrder, base *Spec) (*Spec, error
197315
)
198316

199317
if base != nil {
200-
if base.firstTypeID != 0 {
318+
if base.imm.firstTypeID != 0 {
201319
return nil, fmt.Errorf("can't use split BTF as base")
202320
}
203321

@@ -217,16 +335,22 @@ func loadRawSpec(btf io.ReaderAt, bo binary.ByteOrder, base *Spec) (*Spec, error
217335
typeIDs, typesByName := indexTypes(types, firstTypeID)
218336

219337
return &Spec{
220-
namedTypes: typesByName,
221-
typeIDs: typeIDs,
222-
types: types,
223-
firstTypeID: firstTypeID,
224-
strings: rawStrings,
225-
byteOrder: bo,
338+
mutableTypes{
339+
immutableTypes{
340+
types,
341+
typeIDs,
342+
firstTypeID,
343+
typesByName,
344+
bo,
345+
},
346+
make(map[Type]Type),
347+
make(map[Type]TypeID),
348+
},
349+
rawStrings,
226350
}, nil
227351
}
228352

229-
func indexTypes(types []Type, firstTypeID TypeID) (map[Type]TypeID, map[essentialName][]Type) {
353+
func indexTypes(types []Type, firstTypeID TypeID) (map[Type]TypeID, map[essentialName][]TypeID) {
230354
namedTypes := 0
231355
for _, typ := range types {
232356
if typ.TypeName() != "" {
@@ -238,13 +362,15 @@ func indexTypes(types []Type, firstTypeID TypeID) (map[Type]TypeID, map[essentia
238362
}
239363

240364
typeIDs := make(map[Type]TypeID, len(types))
241-
typesByName := make(map[essentialName][]Type, namedTypes)
365+
typesByName := make(map[essentialName][]TypeID, namedTypes)
242366

243367
for i, typ := range types {
368+
id := firstTypeID + TypeID(i)
369+
typeIDs[typ] = id
370+
244371
if name := newEssentialName(typ.TypeName()); name != "" {
245-
typesByName[name] = append(typesByName[name], typ)
372+
typesByName[name] = append(typesByName[name], id)
246373
}
247-
typeIDs[typ] = firstTypeID + TypeID(i)
248374
}
249375

250376
return typeIDs, typesByName
@@ -492,17 +618,9 @@ func fixupDatasecLayout(ds *Datasec) error {
492618

493619
// Copy creates a copy of Spec.
494620
func (s *Spec) Copy() *Spec {
495-
types := copyTypes(s.types, nil)
496-
typeIDs, typesByName := indexTypes(types, s.firstTypeID)
497-
498-
// NB: Other parts of spec are not copied since they are immutable.
499621
return &Spec{
500-
types,
501-
typeIDs,
502-
s.firstTypeID,
503-
typesByName,
622+
s.mutableTypes.copy(),
504623
s.strings,
505-
s.byteOrder,
506624
}
507625
}
508626

@@ -519,8 +637,8 @@ func (sw sliceWriter) Write(p []byte) (int, error) {
519637
// nextTypeID returns the next unallocated type ID or an error if there are no
520638
// more type IDs.
521639
func (s *Spec) nextTypeID() (TypeID, error) {
522-
id := s.firstTypeID + TypeID(len(s.types))
523-
if id < s.firstTypeID {
640+
id := s.imm.firstTypeID + TypeID(len(s.imm.types))
641+
if id < s.imm.firstTypeID {
524642
return 0, fmt.Errorf("no more type IDs")
525643
}
526644
return id, nil
@@ -533,40 +651,17 @@ func (s *Spec) nextTypeID() (TypeID, error) {
533651
func (s *Spec) TypeByID(id TypeID) (Type, error) {
534652
typ, ok := s.typeByID(id)
535653
if !ok {
536-
return nil, fmt.Errorf("look up type with ID %d (first ID is %d): %w", id, s.firstTypeID, ErrNotFound)
654+
return nil, fmt.Errorf("look up type with ID %d (first ID is %d): %w", id, s.imm.firstTypeID, ErrNotFound)
537655
}
538656

539657
return typ, nil
540658
}
541659

542-
func (s *Spec) typeByID(id TypeID) (Type, bool) {
543-
if id < s.firstTypeID {
544-
return nil, false
545-
}
546-
547-
index := int(id - s.firstTypeID)
548-
if index >= len(s.types) {
549-
return nil, false
550-
}
551-
552-
return s.types[index], true
553-
}
554-
555660
// TypeID returns the ID for a given Type.
556661
//
557662
// Returns an error wrapping ErrNoFound if the type isn't part of the Spec.
558663
func (s *Spec) TypeID(typ Type) (TypeID, error) {
559-
if _, ok := typ.(*Void); ok {
560-
// Equality is weird for void, since it is a zero sized type.
561-
return 0, nil
562-
}
563-
564-
id, ok := s.typeIDs[typ]
565-
if !ok {
566-
return 0, fmt.Errorf("no ID for type %s: %w", typ, ErrNotFound)
567-
}
568-
569-
return id, nil
664+
return s.mutableTypes.typeID(typ)
570665
}
571666

572667
// AnyTypesByName returns a list of BTF Types with the given name.
@@ -577,21 +672,7 @@ func (s *Spec) TypeID(typ Type) (TypeID, error) {
577672
//
578673
// Returns an error wrapping ErrNotFound if no matching Type exists in the Spec.
579674
func (s *Spec) AnyTypesByName(name string) ([]Type, error) {
580-
types := s.namedTypes[newEssentialName(name)]
581-
if len(types) == 0 {
582-
return nil, fmt.Errorf("type name %s: %w", name, ErrNotFound)
583-
}
584-
585-
// Return a copy to prevent changes to namedTypes.
586-
result := make([]Type, 0, len(types))
587-
for _, t := range types {
588-
// Match against the full name, not just the essential one
589-
// in case the type being looked up is a struct flavor.
590-
if t.TypeName() == name {
591-
result = append(result, t)
592-
}
593-
}
594-
return result, nil
675+
return s.mutableTypes.anyTypesByName(name)
595676
}
596677

597678
// AnyTypeByName returns a Type with the given name.
@@ -689,7 +770,7 @@ type TypesIterator struct {
689770

690771
// Iterate returns the types iterator.
691772
func (s *Spec) Iterate() *TypesIterator {
692-
return &TypesIterator{spec: s, id: s.firstTypeID}
773+
return &TypesIterator{spec: s, id: s.imm.firstTypeID}
693774
}
694775

695776
// Next returns true as long as there are any remaining types.

btf/btf_test.go

+29-9
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ func BenchmarkParseVmlinux(b *testing.B) {
226226
func TestParseCurrentKernelBTF(t *testing.T) {
227227
spec := vmlinuxSpec(t)
228228

229-
if len(spec.namedTypes) == 0 {
229+
if len(spec.imm.namedTypes) == 0 {
230230
t.Fatal("Empty kernel BTF")
231231
}
232232

@@ -257,7 +257,7 @@ func TestFindVMLinux(t *testing.T) {
257257
t.Fatal("Can't load BTF:", err)
258258
}
259259

260-
if len(spec.namedTypes) == 0 {
260+
if len(spec.imm.namedTypes) == 0 {
261261
t.Fatal("Empty kernel BTF")
262262
}
263263
}
@@ -339,10 +339,10 @@ func TestSpecCopy(t *testing.T) {
339339
spec := parseELFBTF(t, "../testdata/loader-el.elf")
340340
cpy := spec.Copy()
341341

342-
have := typesFromSpec(t, spec)
343-
qt.Assert(t, qt.IsTrue(len(spec.types) > 0))
342+
have := typesFromSpec(spec)
343+
qt.Assert(t, qt.IsTrue(len(have) > 0))
344344

345-
want := typesFromSpec(t, cpy)
345+
want := typesFromSpec(cpy)
346346
qt.Assert(t, qt.HasLen(want, len(have)))
347347

348348
for i := range want {
@@ -359,6 +359,28 @@ func TestSpecCopy(t *testing.T) {
359359
}
360360
}
361361

362+
func TestSpecCopyModifications(t *testing.T) {
363+
spec := specFromTypes(t, []Type{&Int{Name: "a", Size: 4}})
364+
365+
typ, err := spec.TypeByID(1)
366+
qt.Assert(t, qt.IsNil(err))
367+
368+
i := typ.(*Int)
369+
i.Name = "b"
370+
i.Size = 2
371+
372+
cpy := spec.Copy()
373+
typ2, err := cpy.TypeByID(1)
374+
qt.Assert(t, qt.IsNil(err))
375+
i2 := typ2.(*Int)
376+
377+
qt.Assert(t, qt.Not(qt.Equals(i2, i)), qt.Commentf("Types are distinct"))
378+
qt.Assert(t, qt.DeepEquals(i2, i), qt.Commentf("Modifications are preserved"))
379+
380+
i.Name = "bar"
381+
qt.Assert(t, qt.Equals(i2.Name, "b"))
382+
}
383+
362384
func TestSpecTypeByID(t *testing.T) {
363385
spec := specFromTypes(t, nil)
364386

@@ -459,10 +481,8 @@ func TestLoadSplitSpecFromReader(t *testing.T) {
459481
t.Fatal("'int' is not supposed to be found in the split BTF")
460482
}
461483

462-
if fnProto.Return != intType {
463-
t.Fatalf("Return type of 'bpf_testmod_init()' (%s) does not match 'int' type (%s)",
464-
fnProto.Return, intType)
465-
}
484+
qt.Assert(t, qt.Not(qt.Equals(fnProto.Return, intType)),
485+
qt.Commentf("types found in base of split spec should be copies"))
466486

467487
// Check that copied split-BTF's spec has correct type indexing
468488
splitSpecCopy := splitSpec.Copy()

0 commit comments

Comments
 (0)