Skip to content

Commit d5d48c0

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 b9a7efe commit d5d48c0

File tree

5 files changed

+94
-30
lines changed

5 files changed

+94
-30
lines changed

internal/pkg/instrumentation/manager.go

+2-16
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,6 @@ func NewManager(logger *slog.Logger, otelController *opentelemetry.Controller, p
8282
return nil, err
8383
}
8484

85-
alloc, err := process.Allocate(logger, pid)
86-
if err != nil {
87-
return nil, err
88-
}
89-
m.proc.Allocation = alloc
90-
9185
m.logger.Info("loaded process info", "process", m.proc)
9286

9387
m.filterUnusedProbes()
@@ -362,7 +356,8 @@ func (m *Manager) loadProbes() error {
362356
}
363357
m.exe = exe
364358

365-
if err := m.mount(); err != nil {
359+
m.logger.Debug("Mounting bpffs")
360+
if err := bpffsMount(m.proc); err != nil {
366361
return err
367362
}
368363

@@ -382,15 +377,6 @@ func (m *Manager) loadProbes() error {
382377
return nil
383378
}
384379

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

internal/pkg/instrumentation/probe/probe.go

+34-3
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

-4
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

+2-2
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/analyze.go

+56-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
"debug/elf"
88
"errors"
99
"fmt"
10+
"log/slog"
11+
"sync"
12+
"sync/atomic"
1013

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

@@ -15,11 +18,26 @@ import (
1518

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

2543
// GetFunctionOffset returns the offset for of the function with name.
@@ -104,3 +122,36 @@ func (a *Analyzer) findFunctions(elfF *elf.File, relevantFuncs map[string]interf
104122

105123
return result, nil
106124
}
125+
126+
// onceResult is an object that will perform exactly one action if that action
127+
// does not error. For errors, no state is stored and subsequent attempts will
128+
// be tried.
129+
type onceResult[T any] struct {
130+
done atomic.Bool
131+
mutex sync.Mutex
132+
val T
133+
}
134+
135+
// Do runs f only once, and only stores the result if f returns a nil error.
136+
// Subsequent calls to Do will return the stored value or they will re-attempt
137+
// to run f and store the result if an error had been returned.
138+
func (o *onceResult[T]) Do(f func() (T, error)) (T, error) {
139+
if o.done.Load() {
140+
o.mutex.Lock()
141+
defer o.mutex.Unlock()
142+
return o.val, nil
143+
}
144+
145+
o.mutex.Lock()
146+
defer o.mutex.Unlock()
147+
if o.done.Load() {
148+
return o.val, nil
149+
}
150+
151+
var err error
152+
o.val, err = f()
153+
if err == nil {
154+
o.done.Store(true)
155+
}
156+
return o.val, err
157+
}

0 commit comments

Comments
 (0)