Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions internal/perfevent/perfevent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Package perfevent opens per-CPU software perf_event_open events and
// attaches a BPF program to each. It exists to deduplicate the boilerplate
// that profile.NewProfiler and dwarfagent.NewProfilerWithMode each used to
// spell out independently — same PerfEventAttr, same per-CPU loop, same
// "tolerate ESRCH on offline CPU" rule, same cleanup-on-error.
package perfevent

import (
"errors"
"fmt"
"syscall"
"unsafe"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"golang.org/x/sys/unix"
)

// Set is a bundle of per-CPU perf events with their attached BPF links.
// Close releases everything in the right order; safe to call once.
type Set struct {
fds []int
links []link.Link
}

// FDs returns the underlying perf_event file descriptors. Callers should
// treat the returned slice as read-only — the Set still owns these fds and
// will close them via Close. Used by collectors that need to read counter
// values directly (currently none, but PMU-side collectors might).
func (s *Set) FDs() []int { return s.fds }

// Close closes every attached link and then every fd. Errors are
// best-effort — the first non-nil error is returned, but cleanup proceeds
// regardless.
func (s *Set) Close() error {
if s == nil {
return nil
}
var firstErr error
for _, l := range s.links {
if err := l.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
for _, fd := range s.fds {
if err := unix.Close(fd); err != nil && firstErr == nil {
firstErr = err
}
}
s.links = nil
s.fds = nil
return firstErr
}

// Option configures OpenAll.
type Option func(*config)

type config struct {
// deferEnable causes OpenAll to set PerfBitDisabled at perf_event_open
// time and call PERF_EVENT_IOC_ENABLE after the BPF link is attached.
// Eliminates the (tiny) race window where the event fires between
// open and attach. dwarfagent uses this; profile historically did not.
deferEnable bool
}

// WithDeferredEnable opens each event disabled and enables it after the
// BPF program is attached. Recommended for new call sites.
func WithDeferredEnable() Option { return func(c *config) { c.deferEnable = true } }

// OpenAll opens one PERF_TYPE_SOFTWARE / PERF_COUNT_SW_CPU_CLOCK event per
// CPU at the given sample rate (treated as Hz via PerfBitFreq), attaches
// prog to each via link.AttachRawLink, and returns the resulting Set.
//
// Offline CPUs (ESRCH from perf_event_open) are skipped silently — they
// come back online or stay offline, neither is an error here. If every CPU
// is offline / no event was attached, OpenAll returns an error.
//
// On any failure mid-loop, every fd and link opened so far is released
// before returning. Caller never has to clean up after a non-nil error.
func OpenAll(prog *ebpf.Program, cpus []uint, sampleRate int, opts ...Option) (*Set, error) {
cfg := config{}
for _, o := range opts {
o(&cfg)
}

bits := uint64(unix.PerfBitFreq)
if cfg.deferEnable {
bits |= unix.PerfBitDisabled
}
attr := &unix.PerfEventAttr{
Type: unix.PERF_TYPE_SOFTWARE,
Config: unix.PERF_COUNT_SW_CPU_CLOCK,
Size: uint32(unsafe.Sizeof(unix.PerfEventAttr{})),
Sample: uint64(sampleRate),
Bits: bits,
}

s := &Set{}
for _, cpu := range cpus {
fd, err := unix.PerfEventOpen(attr, -1, int(cpu), -1, unix.PERF_FLAG_FD_CLOEXEC)
if err != nil {
if errors.Is(err, syscall.ESRCH) {
continue
}
_ = s.Close()
return nil, fmt.Errorf("perf_event_open cpu=%d: %w", cpu, err)
}
s.fds = append(s.fds, fd)

rl, err := link.AttachRawLink(link.RawLinkOptions{
Target: fd,
Program: prog,
Attach: ebpf.AttachPerfEvent,
})
if err != nil {
_ = s.Close()
return nil, fmt.Errorf("attach perf event cpu=%d: %w", cpu, err)
}
s.links = append(s.links, rl)

if cfg.deferEnable {
if err := unix.IoctlSetInt(fd, unix.PERF_EVENT_IOC_ENABLE, 0); err != nil {
_ = s.Close()
return nil, fmt.Errorf("enable perf event cpu=%d: %w", cpu, err)
}
}
}

if len(s.fds) == 0 {
return nil, fmt.Errorf("no perf events attached (cpus=%d, all offline?)", len(cpus))
}
return s, nil
}
88 changes: 9 additions & 79 deletions profile/profiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ import (
"io"
"log"
"os"
"unsafe"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"golang.org/x/sys/unix"

blazesym "github.com/libbpf/blazesym/go"

"github.com/dpsoft/perf-agent/internal/bpfstack"
"github.com/dpsoft/perf-agent/internal/perfevent"
"github.com/dpsoft/perf-agent/pprof"
"github.com/dpsoft/perf-agent/unwind/procmap"
)
Expand All @@ -24,17 +22,11 @@ type Profiler struct {
objs *perfObjects
symbolizer *blazesym.Symbolizer
resolver *procmap.Resolver
perfEvents []*perfEvent
perfSet *perfevent.Set
tags []string
sampleRate int
}

// perfEvent wraps a Linux perf event for CPU sampling
type perfEvent struct {
fd int
link *link.RawLink
}

// stackBuilder accumulates symbolized stack frames
type stackBuilder struct {
stack []pprof.Frame
Expand Down Expand Up @@ -103,37 +95,18 @@ func NewProfiler(pid int, systemWide bool, cpus []uint, tags []string, sampleRat
}
}

var perfEvents []*perfEvent
for _, id := range cpus {
pe, err := newPerfEvent(int(id), sampleRate)
if err != nil {
for _, pe := range perfEvents {
_ = pe.Close()
}
_ = objs.Close()
return nil, fmt.Errorf("create perf event on CPU %d: %w", id, err)
}

if err := pe.attachPerfEvent(objs.Profile); err != nil {
_ = pe.Close()
for _, pe := range perfEvents {
_ = pe.Close()
}
_ = objs.Close()
return nil, fmt.Errorf("attach eBPF to perf event on CPU %d: %w", id, err)
}

perfEvents = append(perfEvents, pe)
perfSet, err := perfevent.OpenAll(objs.Profile, cpus, sampleRate)
if err != nil {
_ = objs.Close()
return nil, err
}

symbolizer, err := blazesym.NewSymbolizer(
blazesym.SymbolizerWithCodeInfo(true),
blazesym.SymbolizerWithInlinedFns(true),
)
if err != nil {
for _, pe := range perfEvents {
_ = pe.Close()
}
_ = perfSet.Close()
_ = objs.Close()
return nil, fmt.Errorf("create symbolizer: %w", err)
}
Expand All @@ -142,7 +115,7 @@ func NewProfiler(pid int, systemWide bool, cpus []uint, tags []string, sampleRat
objs: objs,
symbolizer: symbolizer,
resolver: procmap.NewResolver(),
perfEvents: perfEvents,
perfSet: perfSet,
tags: tags,
sampleRate: sampleRate,
}, nil
Expand All @@ -152,9 +125,7 @@ func NewProfiler(pid int, systemWide bool, cpus []uint, tags []string, sampleRat
func (pr *Profiler) Close() {
pr.symbolizer.Close()
pr.resolver.Close()
for _, pe := range pr.perfEvents {
_ = pe.Close()
}
_ = pr.perfSet.Close()
_ = pr.objs.Close()
}

Expand Down Expand Up @@ -286,44 +257,3 @@ func (pr *Profiler) createSample(sb *stackBuilder, value uint64, pid int) pprof.
}
}

// perfEvent helpers

func newPerfEvent(cpu int, sampleRate int) (*perfEvent, error) {
attr := unix.PerfEventAttr{
Type: unix.PERF_TYPE_SOFTWARE,
Size: uint32(unsafe.Sizeof(unix.PerfEventAttr{})),
Config: unix.PERF_COUNT_SW_CPU_CLOCK,
Sample: uint64(sampleRate),
Bits: unix.PerfBitFreq, // Enable frequency mode
}

fd, err := unix.PerfEventOpen(&attr, -1, cpu, -1, unix.PERF_FLAG_FD_CLOEXEC)
if err != nil {
return nil, os.NewSyscallError("perf_event_open", err)
}

return &perfEvent{fd: fd}, nil
}

func (pe *perfEvent) Close() error {
if pe.link != nil {
_ = pe.link.Close()
}
if pe.fd >= 0 {
return unix.Close(pe.fd)
}
return nil
}

func (pe *perfEvent) attachPerfEvent(prog *ebpf.Program) error {
rawLink, err := link.AttachRawLink(link.RawLinkOptions{
Target: pe.fd,
Program: prog,
Attach: ebpf.AttachPerfEvent,
})
if err != nil {
return fmt.Errorf("attach raw link: %w", err)
}
pe.link = rawLink
return nil
}
Loading
Loading