Skip to content

Commit 3ece212

Browse files
committed
prog: add Program.Stats() and related ProgramStats struct
This commit lifts runtime statistics out of ProgramInfo to allow them to be queried without fetching all of ProgramInfo, which would otherwise require multiple calls to OBJ_INFO, multiple allocations, as well as parsing fdinfo. Signed-off-by: Timo Beckers <timo@isovalent.com>
1 parent 6bdf7e0 commit 3ece212

File tree

3 files changed

+83
-133
lines changed

3 files changed

+83
-133
lines changed

info.go

Lines changed: 50 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,41 @@ func (mi *MapInfo) Frozen() bool {
185185
return mi.frozen
186186
}
187187

188-
// programStats holds statistics of a program.
189-
type programStats struct {
190-
// Total accumulated runtime of the program ins ns.
191-
runtime time.Duration
192-
// Total number of times the program was called.
193-
runCount uint64
194-
// Total number of times the programm was NOT called.
195-
// Added in commit 9ed9e9ba2337 ("bpf: Count the number of times recursion was prevented").
196-
recursionMisses uint64
188+
// ProgramStats contains runtime statistics for a single [Program], returned by
189+
// [Program.Stats].
190+
//
191+
// Will contain mostly zero values if the collection of statistics is not
192+
// enabled, see [EnableStats].
193+
type ProgramStats struct {
194+
// Total accumulated runtime of the Program.
195+
//
196+
// Requires at least Linux 5.8.
197+
Runtime time.Duration
198+
199+
// Total number of times the Program has executed.
200+
//
201+
// Requires at least Linux 5.8.
202+
RunCount uint64
203+
204+
// Total number of times the program was not executed due to recursion. This
205+
// can happen when another bpf program is already running on the cpu, when bpf
206+
// program execution is interrupted, for example.
207+
//
208+
// Requires at least Linux 5.12.
209+
RecursionMisses uint64
210+
}
211+
212+
func newProgramStatsFromFd(fd *sys.FD) (*ProgramStats, error) {
213+
var info sys.ProgInfo
214+
if err := sys.ObjInfo(fd, &info); err != nil {
215+
return nil, fmt.Errorf("getting program info: %w", err)
216+
}
217+
218+
return &ProgramStats{
219+
Runtime: time.Duration(info.RunTimeNs),
220+
RunCount: info.RunCnt,
221+
RecursionMisses: info.RecursionMisses,
222+
}, nil
197223
}
198224

199225
// programJitedInfo holds information about JITed info of a program.
@@ -230,7 +256,8 @@ type programJitedInfo struct {
230256
numFuncLens uint32
231257
}
232258

233-
// ProgramInfo describes a program.
259+
// ProgramInfo describes a Program's immutable metadata. For runtime statistics,
260+
// see [ProgramStats].
234261
type ProgramInfo struct {
235262
Type ProgramType
236263
id ProgramID
@@ -242,7 +269,6 @@ type ProgramInfo struct {
242269
createdByUID uint32
243270
haveCreatedByUID bool
244271
btf btf.ID
245-
stats *programStats
246272
loadTime time.Duration
247273

248274
maps []MapID
@@ -275,16 +301,11 @@ func newProgramInfoFromFd(fd *sys.FD) (*ProgramInfo, error) {
275301
}
276302

277303
pi := ProgramInfo{
278-
Type: typ,
279-
id: ProgramID(info.Id),
280-
Tag: hex.EncodeToString(info.Tag[:]),
281-
Name: unix.ByteSliceToString(info.Name[:]),
282-
btf: btf.ID(info.BtfId),
283-
stats: &programStats{
284-
runtime: time.Duration(info.RunTimeNs),
285-
runCount: info.RunCnt,
286-
recursionMisses: info.RecursionMisses,
287-
},
304+
Type: typ,
305+
id: ProgramID(info.Id),
306+
Tag: hex.EncodeToString(info.Tag[:]),
307+
Name: unix.ByteSliceToString(info.Name[:]),
308+
btf: btf.ID(info.BtfId),
288309
jitedSize: info.JitedProgLen,
289310
loadTime: time.Duration(info.LoadTime),
290311
verifiedInstructions: info.VerifiedInsns,
@@ -453,38 +474,6 @@ func (pi *ProgramInfo) BTFID() (btf.ID, bool) {
453474
return pi.btf, pi.btf > 0
454475
}
455476

456-
// RunCount returns the total number of times the program was called.
457-
//
458-
// Can return 0 if the collection of statistics is not enabled. See EnableStats().
459-
// The bool return value indicates whether this optional field is available.
460-
func (pi *ProgramInfo) RunCount() (uint64, bool) {
461-
if pi.stats != nil {
462-
return pi.stats.runCount, true
463-
}
464-
return 0, false
465-
}
466-
467-
// Runtime returns the total accumulated runtime of the program.
468-
//
469-
// Can return 0 if the collection of statistics is not enabled. See EnableStats().
470-
// The bool return value indicates whether this optional field is available.
471-
func (pi *ProgramInfo) Runtime() (time.Duration, bool) {
472-
if pi.stats != nil {
473-
return pi.stats.runtime, true
474-
}
475-
return time.Duration(0), false
476-
}
477-
478-
// RecursionMisses returns the total number of times the program was NOT called.
479-
// This can happen when another bpf program is already running on the cpu, which
480-
// is likely to happen for example when you interrupt bpf program execution.
481-
func (pi *ProgramInfo) RecursionMisses() (uint64, bool) {
482-
if pi.stats != nil {
483-
return pi.stats.recursionMisses, true
484-
}
485-
return 0, false
486-
}
487-
488477
// btfSpec returns the BTF spec associated with the program.
489478
func (pi *ProgramInfo) btfSpec() (*btf.Spec, error) {
490479
id, ok := pi.BTFID()
@@ -836,12 +825,16 @@ func zero(arg any) bool {
836825
return v.IsZero()
837826
}
838827

839-
// EnableStats starts the measuring of the runtime
840-
// and run counts of eBPF programs.
828+
// EnableStats starts collecting runtime statistics of eBPF programs, like the
829+
// amount of program executions and the cumulative runtime.
830+
//
831+
// Specify a BPF_STATS_* constant to select which statistics to collect, like
832+
// [unix.BPF_STATS_RUN_TIME]. Closing the returned [io.Closer] will stop
833+
// collecting statistics.
841834
//
842-
// Collecting statistics can have an impact on the performance.
835+
// Collecting statistics may have a performance impact.
843836
//
844-
// Requires at least 5.8.
837+
// Requires at least Linux 5.8.
845838
func EnableStats(which uint32) (io.Closer, error) {
846839
fd, err := sys.EnableStats(&sys.EnableStatsAttr{
847840
Type: which,

info_test.go

Lines changed: 25 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package ebpf
22

33
import (
4-
"errors"
54
"fmt"
65
"os"
76
"reflect"
@@ -404,56 +403,40 @@ func BenchmarkScanFdInfoReader(b *testing.B) {
404403
}
405404
}
406405

407-
// TestStats loads a BPF program once and executes back-to-back test runs
406+
// TestProgramStats loads a BPF program once and executes back-to-back test runs
408407
// of the program. See testStats for details.
409-
func TestStats(t *testing.T) {
408+
func TestProgramStats(t *testing.T) {
410409
testutils.SkipOnOldKernel(t, "5.8", "BPF_ENABLE_STATS")
411410

412411
prog := createBasicProgram(t)
413412

414-
pi, err := prog.Info()
415-
if err != nil {
416-
t.Errorf("failed to get ProgramInfo: %v", err)
417-
}
418-
419-
rc, ok := pi.RunCount()
420-
if !ok {
421-
t.Errorf("expected run count info to be available")
422-
}
423-
if rc != 0 {
424-
t.Errorf("expected a run count of 0 but got %d", rc)
425-
}
413+
s, err := prog.Stats()
414+
qt.Assert(t, qt.IsNil(err))
426415

427-
rt, ok := pi.Runtime()
428-
if !ok {
429-
t.Errorf("expected runtime info to be available")
430-
}
431-
if runtime.GOARCH != "arm64" && rt != 0 {
432-
t.Errorf("expected a runtime of 0ns but got %v", rt)
433-
}
416+
qt.Assert(t, qt.Equals(s.RunCount, 0))
417+
qt.Assert(t, qt.Equals(s.RecursionMisses, 0))
434418

435-
rm, ok := pi.RecursionMisses()
436-
if !ok {
437-
t.Errorf("expected recursion misses info to be available")
438-
}
439-
if rm != 0 {
440-
t.Errorf("expected a recursion misses of 0 but got %v", rm)
419+
if runtime.GOARCH != "arm64" {
420+
// Runtime is flaky on arm64.
421+
qt.Assert(t, qt.Equals(s.Runtime, 0))
441422
}
442423

443-
if err := testStats(prog); err != nil {
424+
if err := testStats(t, prog); err != nil {
444425
testutils.SkipIfNotSupportedOnOS(t, err)
445426
t.Error(err)
446427
}
447428
}
448429

449430
// BenchmarkStats is a benchmark of TestStats. See testStats for details.
450431
func BenchmarkStats(b *testing.B) {
432+
b.ReportAllocs()
433+
451434
testutils.SkipOnOldKernel(b, "5.8", "BPF_ENABLE_STATS")
452435

453436
prog := createBasicProgram(b)
454437

455438
for n := 0; n < b.N; n++ {
456-
if err := testStats(prog); err != nil {
439+
if err := testStats(b, prog); err != nil {
457440
testutils.SkipIfNotSupportedOnOS(b, err)
458441
b.Fatal(fmt.Errorf("iter %d: %w", n, err))
459442
}
@@ -470,7 +453,9 @@ func BenchmarkStats(b *testing.B) {
470453
// resulting in RunCount incrementing by more than one. Expecting RunCount to
471454
// be of a specific value after a call to Test() is therefore not possible.
472455
// See https://golang.org/doc/go1.14#runtime for more details.
473-
func testStats(prog *Program) error {
456+
func testStats(tb testing.TB, prog *Program) error {
457+
tb.Helper()
458+
474459
in := internal.EmptyBPFContext
475460

476461
stats, err := EnableStats(uint32(sys.BPF_STATS_RUN_TIME))
@@ -485,61 +470,25 @@ func testStats(prog *Program) error {
485470
return fmt.Errorf("failed to trigger program: %w", err)
486471
}
487472

488-
pi, err := prog.Info()
489-
if err != nil {
490-
return fmt.Errorf("failed to get ProgramInfo: %w", err)
491-
}
473+
s1, err := prog.Stats()
474+
qt.Assert(tb, qt.IsNil(err))
492475

493-
rc, ok := pi.RunCount()
494-
if !ok {
495-
return errors.New("expected run count info to be available")
496-
}
497-
if rc < 1 {
498-
return fmt.Errorf("expected a run count of at least 1 but got %d", rc)
499-
}
500-
// Store the run count for the next invocation.
501-
lc := rc
476+
qt.Assert(tb, qt.Not(qt.Equals(s1.RunCount, 0)), qt.Commentf("expected run count to be at least 1 after first invocation"))
477+
qt.Assert(tb, qt.Not(qt.Equals(s1.Runtime, 0)), qt.Commentf("expected runtime to be at least 1ns after first invocation"))
502478

503-
rt, ok := pi.Runtime()
504-
if !ok {
505-
return errors.New("expected runtime info to be available")
506-
}
507-
if rt == 0 {
508-
return errors.New("expected a runtime other than 0ns")
509-
}
510-
// Store the runtime value for the next invocation.
511-
lt := rt
512-
513-
if err := stats.Close(); err != nil {
514-
return fmt.Errorf("failed to disable statistics: %w", err)
515-
}
479+
qt.Assert(tb, qt.IsNil(stats.Close()))
516480

517481
// Second program execution, with runtime statistics gathering disabled.
518482
// Total runtime and run counters are not expected to increase.
519483
if _, _, err := prog.Test(in); err != nil {
520484
return fmt.Errorf("failed to trigger program: %w", err)
521485
}
522486

523-
pi, err = prog.Info()
524-
if err != nil {
525-
return fmt.Errorf("failed to get ProgramInfo: %w", err)
526-
}
527-
528-
rc, ok = pi.RunCount()
529-
if !ok {
530-
return errors.New("expected run count info to be available")
531-
}
532-
if rc != lc {
533-
return fmt.Errorf("run count unexpectedly increased over previous value (current: %v, prev: %v)", rc, lc)
534-
}
487+
s2, err := prog.Stats()
488+
qt.Assert(tb, qt.IsNil(err))
535489

536-
rt, ok = pi.Runtime()
537-
if !ok {
538-
return errors.New("expected runtime info to be available")
539-
}
540-
if rt != lt {
541-
return fmt.Errorf("runtime unexpectedly increased over the previous value (current: %v, prev: %v)", rt, lt)
542-
}
490+
qt.Assert(tb, qt.Equals(s2.RunCount, s1.RunCount), qt.Commentf("run count (%d) increased after first invocation (%d)", s2.RunCount, s1.RunCount))
491+
qt.Assert(tb, qt.Equals(s2.Runtime, s1.Runtime), qt.Commentf("runtime (%d) increased after first invocation (%d)", s2.Runtime, s1.Runtime))
543492

544493
return nil
545494
}

prog.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,14 @@ func (p *Program) Info() (*ProgramInfo, error) {
599599
return newProgramInfoFromFd(p.fd)
600600
}
601601

602+
// Stats returns runtime statistics about the Program. Requires BPF statistics
603+
// collection to be enabled, see [EnableStats].
604+
//
605+
// Requires at least Linux 5.8.
606+
func (p *Program) Stats() (*ProgramStats, error) {
607+
return newProgramStatsFromFd(p.fd)
608+
}
609+
602610
// Handle returns a reference to the program's type information in the kernel.
603611
//
604612
// Returns ErrNotSupported if the kernel has no BTF support, or if there is no

0 commit comments

Comments
 (0)