Repository: cilium/ebpf
Pull request #213 (2021-02-10 18:12:35)

Statistics
Signed-off-by: Florian Lehner <dev@der-flo.net>

    BPF can provide kernel internal statistics about how often a program was
    called and its runtime in ns.
    This PR adds the functionallity to enable the collection of these
    statistics and provides a way to fetch them.

florianl: Sorry about being short worded in the commit message.
I've extended the commit message and added the requested feature test.
lmb: I just found out that BPF_STATS_ENABLE returns a file descriptor, which disables statistics collection when it's closed!
ti-mo: > I just found out that BPF_STATS_ENABLE returns a file descriptor, which disables statistics collection when it's closed!

I also suspected this from reading the kernel patches, this seemed the whole point of the syscall. Have you been able to play around with this? Does it only affect BPF programs the current process holds a reference to? What about pinned programs?
lmb: I think it affects all programs, and is reetrant: multiple calls to enable stats can hold an fd at the same time, and collection is only stopped when the last one is closed. pinned programs are no different than regular programs, so those will get stats collected as well.
florianl: Thanks for your feedback :pray: I didn't realize it was possible to disable the collecting of statistics.

I have updated the PR to use `ProgramInfo.Stats()`, added a wrapper function similar to `[context.CancelFunc](https://pkg.go.dev/context#CancelFunc)` to disable the collection of statistics.
And I added a test that fetches statistics before and after `EnableStats` and after disabling the statistics.

Patch:
diff --git a/go.mod b/go.mod
index 2b6e29a..4b392b1 100644
--- a/go.mod
+++ b/go.mod
@@ -4,5 +4,5 @@ go 1.14
 
 require (
 	github.com/google/go-cmp v0.5.4
-	golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9
+	golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c
 )
diff --git a/go.sum b/go.sum
index 5a4d0e8..d7787fc 100644
--- a/go.sum
+++ b/go.sum
@@ -2,5 +2,7 @@ github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk=
 golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/info.go b/info.go
index 169bc07..b95131e 100644
--- a/info.go
+++ b/info.go
@@ -9,6 +9,7 @@ import (
 	"os"
 	"strings"
 	"syscall"
+	"time"
 
 	"github.com/cilium/ebpf/internal"
 )
@@ -70,6 +71,14 @@ func (mi *MapInfo) ID() (MapID, bool) {
 	return mi.id, mi.id > 0
 }
 
+// programStats holds statistics of a program.
+type programStats struct {
+	// Total accumulated runtime of the program ins ns.
+	runtime time.Duration
+	// Total number of times the program was called.
+	runCount uint64
+}
+
 // ProgramInfo describes a program.
 type ProgramInfo struct {
 	Type ProgramType
@@ -78,6 +87,8 @@ type ProgramInfo struct {
 	Tag string
 	// Name as supplied by user space at load time.
 	Name string
+
+	stats *programStats
 }
 
 func newProgramInfoFromFd(fd *internal.FD) (*ProgramInfo, error) {
@@ -90,12 +101,16 @@ func newProgramInfoFromFd(fd *internal.FD) (*ProgramInfo, error) {
 	}
 
 	return &ProgramInfo{
-		ProgramType(info.prog_type),
-		ProgramID(info.id),
+		Type: ProgramType(info.prog_type),
+		id:   ProgramID(info.id),
 		// tag is available if the kernel supports BPF_PROG_GET_INFO_BY_FD.
-		hex.EncodeToString(info.tag[:]),
+		Tag: hex.EncodeToString(info.tag[:]),
 		// name is available from 4.15.
-		internal.CString(info.name[:]),
+		Name: internal.CString(info.name[:]),
+		stats: &programStats{
+			runtime:  time.Duration(info.run_time_ns),
+			runCount: info.run_cnt,
+		},
 	}, nil
 }
 
@@ -127,6 +142,28 @@ func (pi *ProgramInfo) ID() (ProgramID, bool) {
 	return pi.id, pi.id > 0
 }
 
+// RunCount returns the total number of times the program was called.
+//
+// Can return 0 if the collection of statistics is not enabled. See EnableStats().
+// The bool return value indicates whether this optional field is available.
+func (pi *ProgramInfo) RunCount() (uint64, bool) {
+	if pi.stats != nil {
+		return pi.stats.runCount, true
+	}
+	return 0, false
+}
+
+// Runtime returns the total accumulated runtime of the program.
+//
+// Can return 0 if the collection of statistics is not enabled. See EnableStats().
+// The bool return value indicates whether this optional field is available.
+func (pi *ProgramInfo) Runtime() (time.Duration, bool) {
+	if pi.stats != nil {
+		return pi.stats.runtime, true
+	}
+	return time.Duration(0), false
+}
+
 func scanFdInfo(fd *internal.FD, fields map[string]interface{}) error {
 	raw, err := fd.Value()
 	if err != nil {
@@ -182,3 +219,21 @@ func scanFdInfoReader(r io.Reader, fields map[string]interface{}) error {
 
 	return nil
 }
+
+// EnableStats starts the measuring of the runtime
+// and run counts of eBPF programs.
+//
+// Collecting statistics can have an impact on the performance.
+//
+// Requires at least 5.8.
+func EnableStats(which uint32) (io.Closer, error) {
+	attr := internal.BPFEnableStatsAttr{
+		StatsType: which,
+	}
+
+	fd, err := internal.BPFEnableStats(&attr)
+	if err != nil {
+		return nil, err
+	}
+	return fd, nil
+}
diff --git a/internal/syscall.go b/internal/syscall.go
index 9c5e0b3..c808151 100644
--- a/internal/syscall.go
+++ b/internal/syscall.go
@@ -91,6 +91,19 @@ func BPFProgDetach(attr *BPFProgDetachAttr) error {
 	return err
 }
 
+type BPFEnableStatsAttr struct {
+	StatsType uint32
+}
+
+func BPFEnableStats(attr *BPFEnableStatsAttr) (*FD, error) {
+	ptr, err := BPF(BPF_ENABLE_STATS, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
+	if err != nil {
+		return nil, fmt.Errorf("enable stats: %w", err)
+	}
+	return NewFD(uint32(ptr)), nil
+
+}
+
 type bpfObjAttr struct {
 	fileName  Pointer
 	fd        uint32
diff --git a/internal/unix/types_linux.go b/internal/unix/types_linux.go
index 047ffcb..215f671 100644
--- a/internal/unix/types_linux.go
+++ b/internal/unix/types_linux.go
@@ -43,6 +43,7 @@ const (
 	PERF_FLAG_FD_CLOEXEC     = linux.PERF_FLAG_FD_CLOEXEC
 	RLIM_INFINITY            = linux.RLIM_INFINITY
 	RLIMIT_MEMLOCK           = linux.RLIMIT_MEMLOCK
+	BPF_STATS_RUN_TIME       = linux.BPF_STATS_RUN_TIME
 )
 
 // Statfs_t is a wrapper
diff --git a/internal/unix/types_other.go b/internal/unix/types_other.go
index d897b70..529800d 100644
--- a/internal/unix/types_other.go
+++ b/internal/unix/types_other.go
@@ -44,6 +44,7 @@ const (
 	PERF_FLAG_FD_CLOEXEC     = 0x8
 	RLIM_INFINITY            = 0x7fffffffffffffff
 	RLIMIT_MEMLOCK           = 8
+	BPF_STATS_RUN_TIME       = 0
 )
 
 // Statfs_t is a wrapper


Test patch:
diff --git a/info_test.go b/info_test.go
index ce1392b..6c31b06 100644
--- a/info_test.go
+++ b/info_test.go
@@ -4,6 +4,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/testutils"
 	"github.com/cilium/ebpf/internal/unix"
@@ -139,3 +140,79 @@ func TestScanFdInfoReader(t *testing.T) {
 		}
 	}
 }
+
+func TestStats(t *testing.T) {
+	testutils.SkipOnOldKernel(t, "5.8", "BPF_ENABLE_STATS")
+
+	spec := &ProgramSpec{
+		Type: SocketFilter,
+		Instructions: asm.Instructions{
+			asm.LoadImm(asm.R0, 42, asm.DWord),
+			asm.Return(),
+		},
+		License: "MIT",
+	}
+
+	prog, err := NewProgram(spec)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer prog.Close()
+
+	progInfo, err := prog.Info()
+	if err != nil {
+		t.Errorf("failed to get ProgramInfo: %v", err)
+	}
+
+	if cnt, available := progInfo.RunCount(); cnt != 0 || !available {
+		t.Errorf("expected a run count of 0 but got %d", cnt)
+	}
+
+	if runtime, available := progInfo.Runtime(); runtime != 0 || !available {
+		t.Errorf("expected a runtime of 0ns but got %v", runtime)
+	}
+
+	disableStats, err := EnableStats(uint32(unix.BPF_STATS_RUN_TIME))
+	if err != nil {
+		t.Errorf("failed to enable stats: %v", err)
+	}
+	defer disableStats.Close()
+
+	if _, _, err := prog.Test(make([]byte, 14)); err != nil {
+		t.Errorf("failed to trigger program: %v", err)
+	}
+
+	progInfo2, err := prog.Info()
+	if err != nil {
+		t.Errorf("failed to get ProgramInfo: %v", err)
+	}
+
+	if cnt, available := progInfo2.RunCount(); cnt != 1 || !available {
+		t.Errorf("expected a run count of 1 but got %d", cnt)
+	}
+
+	if runtime, available := progInfo2.Runtime(); runtime == 0 || !available {
+		t.Errorf("expected a runtime other than 0ns")
+	}
+
+	if err := disableStats.Close(); err != nil {
+		t.Errorf("failed to disable statistics: %v", err)
+	}
+
+	if _, _, err := prog.Test(make([]byte, 14)); err != nil {
+		t.Errorf("failed to trigger program: %v", err)
+	}
+
+	progInfo3, err := prog.Info()
+	if err != nil {
+		t.Errorf("failed to get ProgramInfo: %v", err)
+	}
+
+	if cnt, available := progInfo3.RunCount(); cnt != 1 || !available {
+		t.Errorf("expected a run count of 1 but got %d", cnt)
+	}
+
+	if runtime, available := progInfo3.Runtime(); runtime == 0 || !available {
+		t.Errorf("expected a runtime other than 0ns")
+	}
+}