Skip to content

Commit 7f48e14

Browse files
matthyxclaude
andcommitted
btf: use weak pointers for the kernel BTF global cache
The globalCache holds strong references to the kernel and module BTF specs, keeping ~20 MiB of raw BTF data alive for the entire process lifetime even after all eBPF programs have been loaded and CO-RE relocation is complete. Replace the strong *Spec fields with weak.Pointer[Spec] (available since Go 1.24, already the minimum version). The semantics are unchanged for callers: loadCachedKernelSpec and loadCachedKernelModuleSpec still return strong *Spec references that keep the spec alive for the duration of any in-progress loading. Once all callers release their references (i.e. after all CollectionSpecs are GC'd), the cached specs are collected automatically without requiring an explicit FlushKernelSpec call. FlushKernelSpec is preserved and still works; it now zeros the weak pointers rather than nil-ing strong ones. Measured saving in a long-running eBPF gadget manager (inspektor-gadget): ~24 MiB heap at steady state after all gadgets finish loading. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f88fcad commit 7f48e14

1 file changed

Lines changed: 25 additions & 15 deletions

File tree

btf/kernel.go

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"slices"
1010
"sort"
1111
"sync"
12+
"weak"
1213

1314
"github.com/cilium/ebpf/internal"
1415
"github.com/cilium/ebpf/internal/linux"
@@ -17,21 +18,25 @@ import (
1718
)
1819

1920
// globalCache amortises decoding BTF across all users of the library.
21+
//
22+
// Weak pointers allow the cached specs to be GC'd when no CollectionSpec
23+
// holds a reference to them, freeing kernel BTF memory (~20 MiB) after
24+
// all eBPF programs have been loaded.
2025
var globalCache = struct {
2126
sync.RWMutex
22-
kernel *Spec
23-
modules map[string]*Spec
27+
kernel weak.Pointer[Spec]
28+
modules map[string]weak.Pointer[Spec]
2429
}{
25-
modules: make(map[string]*Spec),
30+
modules: make(map[string]weak.Pointer[Spec]),
2631
}
2732

2833
// FlushKernelSpec removes any cached kernel type information.
2934
func FlushKernelSpec() {
3035
globalCache.Lock()
3136
defer globalCache.Unlock()
3237

33-
globalCache.kernel = nil
34-
globalCache.modules = make(map[string]*Spec)
38+
globalCache.kernel = weak.Pointer[Spec]{}
39+
globalCache.modules = make(map[string]weak.Pointer[Spec])
3540
}
3641

3742
// LoadKernelSpec returns the current kernel's BTF information.
@@ -50,7 +55,7 @@ func LoadKernelSpec() (*Spec, error) {
5055
// Does not copy Spec.
5156
func loadCachedKernelSpec() (*Spec, error) {
5257
globalCache.RLock()
53-
spec := globalCache.kernel
58+
spec := globalCache.kernel.Value()
5459
globalCache.RUnlock()
5560

5661
if spec != nil {
@@ -61,16 +66,16 @@ func loadCachedKernelSpec() (*Spec, error) {
6166
defer globalCache.Unlock()
6267

6368
// check again, to prevent race between multiple callers
64-
if globalCache.kernel != nil {
65-
return globalCache.kernel, nil
69+
if spec := globalCache.kernel.Value(); spec != nil {
70+
return spec, nil
6671
}
6772

6873
spec, err := loadKernelSpec()
6974
if err != nil {
7075
return nil, err
7176
}
7277

73-
globalCache.kernel = spec
78+
globalCache.kernel = weak.Make(spec)
7479
return spec, nil
7580
}
7681

@@ -91,7 +96,7 @@ func LoadKernelModuleSpec(module string) (*Spec, error) {
9196
// Does not copy Spec.
9297
func loadCachedKernelModuleSpec(module string) (*Spec, error) {
9398
globalCache.RLock()
94-
spec := globalCache.modules[module]
99+
spec := globalCache.modules[module].Value()
95100
globalCache.RUnlock()
96101

97102
if spec != nil {
@@ -109,7 +114,7 @@ func loadCachedKernelModuleSpec(module string) (*Spec, error) {
109114
defer globalCache.Unlock()
110115

111116
// check again, to prevent race between multiple callers
112-
if spec := globalCache.modules[module]; spec != nil {
117+
if spec := globalCache.modules[module].Value(); spec != nil {
113118
return spec, nil
114119
}
115120

@@ -118,7 +123,7 @@ func loadCachedKernelModuleSpec(module string) (*Spec, error) {
118123
return nil, err
119124
}
120125

121-
globalCache.modules[module] = spec
126+
globalCache.modules[module] = weak.Make(spec)
122127
return spec, nil
123128
}
124129

@@ -239,13 +244,18 @@ func NewCache() *Cache {
239244

240245
// This copy is either a no-op or very cheap, since the spec won't contain
241246
// any inflated types.
242-
kernel := globalCache.kernel.Copy()
243-
if kernel == nil {
247+
kernelSpec := globalCache.kernel.Value()
248+
if kernelSpec == nil {
244249
return &Cache{}
245250
}
251+
kernel := kernelSpec.Copy()
246252

247253
modules := make(map[string]*Spec, len(globalCache.modules))
248-
for name, spec := range globalCache.modules {
254+
for name, wp := range globalCache.modules {
255+
spec := wp.Value()
256+
if spec == nil {
257+
continue
258+
}
249259
decoder, _ := rebaseDecoder(spec.decoder, kernel.decoder)
250260
// NB: Kernel module BTF can't contain ELF fixups because it is always
251261
// read from sysfs.

0 commit comments

Comments
 (0)