Skip to content

Commit 8ddb2aa

Browse files
committed
Lazy allocate memory for process
Only allocate memory for a process if a probe requires the allocation. Move the allocation functionality to be a method of the process.Info. This is intrinsically linked to a single process, therefore, encapsulate the functionality as a method.
1 parent c7371e5 commit 8ddb2aa

File tree

5 files changed

+96
-30
lines changed

5 files changed

+96
-30
lines changed

internal/pkg/instrumentation/manager.go

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,6 @@ func NewManager(logger *slog.Logger, otelController *opentelemetry.Controller, p
8080
return nil, err
8181
}
8282

83-
alloc, err := process.Allocate(logger, pid)
84-
if err != nil {
85-
return nil, err
86-
}
87-
m.proc.Allocation = alloc
88-
8983
m.logger.Info("loaded process info", "process", m.proc)
9084

9185
m.filterUnusedProbes()
@@ -360,7 +354,8 @@ func (m *Manager) loadProbes() error {
360354
}
361355
m.exe = exe
362356

363-
if err := m.mount(); err != nil {
357+
m.logger.Debug("Mounting bpffs")
358+
if err := bpffsMount(m.proc); err != nil {
364359
return err
365360
}
366361

@@ -380,15 +375,6 @@ func (m *Manager) loadProbes() error {
380375
return nil
381376
}
382377

383-
func (m *Manager) mount() error {
384-
if m.proc.Allocation != nil {
385-
m.logger.Debug("Mounting bpffs", "allocation", m.proc.Allocation)
386-
} else {
387-
m.logger.Debug("Mounting bpffs")
388-
}
389-
return bpffsMount(m.proc)
390-
}
391-
392378
func (m *Manager) cleanup() error {
393379
ctx := context.Background()
394380
err := m.cp.Shutdown(context.Background())

internal/pkg/instrumentation/probe/probe.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package probe
66

77
import (
88
"bytes"
9+
"context"
910
"encoding/binary"
1011
"errors"
1112
"fmt"
@@ -572,16 +573,46 @@ func (c StructFieldConstMinVersion) InjectOption(info *process.Info) (inject.Opt
572573

573574
// AllocationConst is a [Const] for all the allocation details that need to be
574575
// injected into an eBPF program.
575-
type AllocationConst struct{}
576+
type AllocationConst struct {
577+
l *slog.Logger
578+
}
579+
580+
// SetLogger sets the Logger for AllocationConst operations.
581+
func (c AllocationConst) SetLogger(l *slog.Logger) Const {
582+
c.l = l
583+
return c
584+
}
585+
586+
func (c AllocationConst) logger() *slog.Logger {
587+
l := c.l
588+
if l == nil {
589+
return slog.New(discardHandlerIntance)
590+
}
591+
return l
592+
}
593+
594+
var discardHandlerIntance = discardHandler{}
595+
596+
// Copy of slog.DiscardHandler. Remove when support for Go < 1.24 is dropped.
597+
type discardHandler struct{}
598+
599+
func (dh discardHandler) Enabled(context.Context, slog.Level) bool { return false }
600+
func (dh discardHandler) Handle(context.Context, slog.Record) error { return nil }
601+
func (dh discardHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return dh }
602+
func (dh discardHandler) WithGroup(name string) slog.Handler { return dh }
576603

577604
// InjectOption returns the appropriately configured
578605
// [inject.WithAllocation] if the [process.Allocation] within td
579606
// are not nil. An error is returned if [process.Allocation] is nil.
580607
func (c AllocationConst) InjectOption(info *process.Info) (inject.Option, error) {
581-
if info.Allocation == nil {
608+
alloc, err := info.Alloc(c.logger())
609+
if err != nil {
610+
return nil, err
611+
}
612+
if alloc == nil {
582613
return nil, errors.New("no allocation details")
583614
}
584-
return inject.WithAllocation(*info.Allocation), nil
615+
return inject.WithAllocation(*alloc), nil
585616
}
586617

587618
// KeyValConst is a [Const] for a generic key-value pair.

internal/pkg/instrumentation/testutils/testutils.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ func ProbesLoad(t *testing.T, p TestProbe, libs map[string]*semver.Version) {
3232

3333
info := &process.Info{
3434
ID: 1,
35-
Allocation: &process.Allocation{
36-
StartAddr: 140434497441792,
37-
EndAddr: 140434497507328,
38-
},
3935
Modules: map[string]*semver.Version{
4036
"std": testGoVersion,
4137
},

internal/pkg/process/allocate.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ type Allocation struct {
2121
NumCPU uint64
2222
}
2323

24-
// Allocate allocates memory for the instrumented process.
25-
func Allocate(logger *slog.Logger, id ID) (*Allocation, error) {
24+
// allocate allocates memory for the instrumented process.
25+
func allocate(logger *slog.Logger, id ID) (*Allocation, error) {
2626
// runtime.NumCPU doesn't query any kind of hardware or OS state,
2727
// but merely uses affinity APIs to count what CPUs the given go process is available to run on.
2828
// Go's implementation of runtime.NumCPU (https://github.com/golang/go/blob/48d899dcdbed4534ed942f7ec2917cf86b18af22/src/runtime/os_linux.go#L97)

internal/pkg/process/info.go

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import (
77
"debug/elf"
88
"errors"
99
"fmt"
10+
"log/slog"
1011
"runtime/debug"
12+
"sync"
13+
"sync/atomic"
1114

1215
"github.com/Masterminds/semver/v3"
1316

@@ -16,11 +19,26 @@ import (
1619

1720
// Info are the details about a target process.
1821
type Info struct {
19-
ID ID
20-
Functions []*binary.Func
21-
GoVersion *semver.Version
22-
Modules map[string]*semver.Version
23-
Allocation *Allocation
22+
ID ID
23+
Functions []*binary.Func
24+
GoVersion *semver.Version
25+
Modules map[string]*semver.Version
26+
27+
allocOnce onceResult[*Allocation]
28+
}
29+
30+
// Alloc allocates memory for the process described by Info i.
31+
//
32+
// The underlying memory allocation is only successfully performed once for the
33+
// instance i. Meaning, it is safe to call this multiple times. The first
34+
// successful result will be returned to all subsequent calls. If an error is
35+
// returned, subsequent calls will re-attempt to perform the allocation.
36+
//
37+
// It is safe to call this method concurrently.
38+
func (i *Info) Alloc(logger *slog.Logger) (*Allocation, error) {
39+
return i.allocOnce.Do(func() (*Allocation, error) {
40+
return allocate(logger, i.ID)
41+
})
2442
}
2543

2644
// NewInfo returns a new Info with information about the process identified by
@@ -118,3 +136,38 @@ func (i *Info) GetFunctionReturns(name string) ([]uint64, error) {
118136

119137
return nil, fmt.Errorf("could not find returns for function %s", name)
120138
}
139+
140+
// onceResult is an object that will perform exactly one action if that action
141+
// does not error. For errors, no state is stored and subsequent attempts will
142+
// be tried.
143+
type onceResult[T any] struct {
144+
done atomic.Bool
145+
mu sync.Mutex
146+
val T
147+
}
148+
149+
// Do runs f only once, and only stores the result if f returns a nil error.
150+
// Subsequent calls to Do will return the stored value or they will re-attempt
151+
// to run f and store the result if an error had been returned.
152+
func (o *onceResult[T]) Do(f func() (T, error)) (T, error) {
153+
if !o.done.Load() {
154+
// Outlined complex-path to allow inlining here.
155+
return o.do(f)
156+
}
157+
return o.val, nil
158+
}
159+
160+
func (o *onceResult[T]) do(f func() (T, error)) (T, error) {
161+
o.mu.Lock()
162+
defer o.mu.Unlock()
163+
164+
if !o.done.Load() {
165+
v, err := f()
166+
if err != nil {
167+
return v, err
168+
}
169+
o.val = v
170+
o.done.Store(true)
171+
}
172+
return o.val, nil
173+
}

0 commit comments

Comments
 (0)