Approach (overview)- Run each benchmark run pinned to one CPU, create perf hardware-event counters with perf_event_open, use a group leader to aggregate across threads, sample via mmap ring buffer to minimize syscalls, disable unrelated kernel activity (isolate CPU, irq affinity) and post-process sample IPs to source lines to build flamegraphs.Key syscalls & APIs- sched_setaffinity (via syscall.SchedSetaffinity or runtime.LockOSThread + syscall.SchedSetaffinity) — pin process/thread to a core.- perf_event_open (syscall.Syscall with SYS_PERF_EVENT_OPEN) — create counters. Use PERF_TYPE_HARDWARE with PERF_COUNT_HW_CPU_CYCLES, PERF_COUNT_HW_INSTRUCTIONS, PERF_COUNT_HW_CACHE_MISSES. Use PERF_FLAG_FD_CLOEXEC.- ioctl: PERF_EVENT_IOC_ENABLE / PERF_EVENT_IOC_DISABLE to control counting.- mmap/read: map the perf ring buffer for samples, read efficiently.- read(2) for reading group counter values (if using perf read format) or use read on mapped buffer to consume samples.Permissions & kernel config- Either run as root or ensure perf_event_paranoid <= 1 (write /proc/sys/kernel/perf_event_paranoid) or grant CAP_SYS_ADMIN as needed for system-wide sampling.- For cgroup-based sampling, ensure proper cgroup and permissions.- Consider setting /proc/sys/kernel/kptr_restrict appropriately so symbol resolution is possible.Minimize kernel noise / environment hardening- Pin process to isolated CPU (kernel boot option isolcpus) or use cpuset; set IRQ affinities away from that core (echo mask to /proc/irq/*/smp_affinity).- Disable scheduler migration for the thread (use sched_setaffinity per-thread).- Set CPU governor to performance, disable turbo if reproducibility important.- Isolate userspace by setting rcu_nocbs and other tunables; run benchmark with minimal other processes.- Use mmap ring buffer (PERF_SAMPLE_IP | PERF_SAMPLE_TID | PERF_SAMPLE_TIME | PERF_SAMPLE_CALLCHAIN) and sample_period (not overflow) to avoid frequent signals/syscalls.Aggregating counters across threads- Create a group leader perf fd (perf_event_attr.group_fd = -1), then open per-thread counters with group_fd set to leader fd; enabling/disabling leader affects group atomically.- Alternatively open CPU-wide counters (pid = -1, cpu = core) to count everything on that CPU across threads; for per-thread attribution open per-thread and aggregate in userland.- Use PERF_FORMAT_GROUP | PERF_FORMAT_ID in read() so kernel returns all counters in one read; this avoids multiple syscalls and provides atomic snapshot.Mapping samples to source lines for flamegraphs- Configure PERF_SAMPLE_CALLCHAIN (or sample ip and use callchain) in perf_event_attr.- Collect sample IPs from ring buffer. For Go programs use runtime/pprof? Prefer native addresses: - Ensure binaries have inlined frame info and build with -gcflags="-N -l" for unobfuscated mapping when needed. - Use addr2line or flamegraph tools: pass IPs + binary + /proc/<pid>/exe to addr2line or llvm-symbolizer. - For Go specifically, use runtime.FuncForPC or the debug/elf and debug/gosym packages to translate PCs to file:line; use symbolization that understands Go inlined frames.- Convert callchains into folded stack format (func;caller;callee count) for flamegraph.pl.Example Go snippets (essentials)go
// wrapper for perf_event_open
package perf
import "syscall"
import "unsafe"
func perfEventOpen(attr *PerfEventAttr, pid, cpu, groupFd, flags int) (fd int, err error) {
r0, _, e1 := syscall.Syscall6(syscall.SYS_PERF_EVENT_OPEN,
uintptr(unsafe.Pointer(attr)),
uintptr(pid), uintptr(cpu), uintptr(groupFd), uintptr(flags), 0)
if e1 != 0 {
return -1, e1
}
return int(r0), nil
}
Plus: runtime.LockOSThread(); set affinity; open group leader on CPU, open samples (callchain), mmap the ring buffer, ioctl ENABLE, run benchmark, ioctl DISABLE, parse samples, map IPs to source lines, aggregate.Complexity & trade-offs- Using group fds + PERF_FORMAT_GROUP gives atomic snapshots with low overhead.- mmap ring buffer reduces syscalls vs. signal-based sampling but requires parsing ring buffer.- CPU-wide counters capture all activity on core (simpler) but include kernel/interrupt noise; per-thread excludes unrelated tasks but needs opening fds per thread and careful lifecycle management.Edge cases- Go scheduler moves goroutines across threads; lock hot goroutine to OS thread for per-thread attribution or prefer CPU-wide counters.- Kernel/native symbol resolution failures: strip/unstrip binaries, use debug symbols.- High sample rates can perturb workload; benchmark with different sample_periods and validate stability.This approach yields low-noise, high-fidelity hardware-counter samples mapped to source lines suitable for flamegraphs in production-like runs.