Context
I'm building a continuous profiling function of (ccfos/huatuo) that samples CPU stack traces via perf events. This is a follow-up discussion from #1910 — I want to propose a less intrusive API that addresses the same underlying need.
Problem
My workflow requires a clear batch boundary when consuming perf events:
- Wait for events (with a controlled timeout)
- Drain all available records from all rings as a single batch
- Aggregate the batch (deduplicate stack IDs, accumulate counts)
- Compare the BPF-side sample counter with records received — if there's a gap, do a short re-poll and drain again
ReadInto in a loop doesn't cleanly express this because:
-
No "batch complete" signal. After ReadInto returns, the caller cannot distinguish "more data pending in loaded rings" from "rings exhausted, next call will epoll_wait." The only way to detect the boundary is to set a short deadline and treat ErrDeadlineExceeded as "batch done" — which conflates "no data available right now" with "timeout expired while waiting for new data."
-
Unpredictable deadline behavior. SetDeadline only takes effect when epollRings is empty (since ReadInto skips Wait when rings have data). From the caller's perspective, the configured deadline may or may not be used on any given call, making timeout-based flow control unreliable.
-
Forced external buffering. To aggregate a batch, I must buffer records outside the library and guess when to stop calling ReadInto. This effectively reimplements the drain logic in user code.
Proposal
Add a single method:
// DrainAll waits for at least one event (respecting SetDeadline), then
// reads all available records from all rings and returns them as a batch.
// When it returns, all rings are empty — giving the caller a clear batch boundary.
func (pr *Reader) DrainAll() ([]*Record, error)
Why this is minimal:
- Single method, no new types or options
- Reuses
SetDeadline for timeout control (no new timeout parameter)
- Does not expose internal state (
epollRings, poller, etc.)
- Semantically equivalent to "ReadInto in a loop until all rings are empty" — but packaged so the caller gets the batch boundary for free
Why not BPF ring buffer:
We use perf buffers because our deployment includes older kernels where BPF_MAP_TYPE_RINGBUF is not available. Even on newer kernels, perf buffers remain a valid choice for per-CPU isolation without cross-CPU contention.
Intended usage
for {
reader.SetDeadline(time.Now().Add(500 * time.Millisecond))
batch, err := reader.DrainAll()
if errors.Is(err, os.ErrDeadlineExceeded) {
continue
}
if err != nil {
return err
}
process(batch)
// Short re-poll if BPF counter indicates missed events
if bpfSampleCount > uint64(len(batch)) {
reader.SetDeadline(time.Now().Add(100 * time.Millisecond))
more, _ := reader.DrainAll()
if len(more) > 0 {
process(more)
}
}
}
I'm happy to submit a PR if there's interest. The implementation is straightforward — ~30 lines composing existing internals (poller.Wait, loadHead, readRecordFromRing).
cc @lmb @florianl
Context
I'm building a continuous profiling function of (ccfos/huatuo) that samples CPU stack traces via perf events. This is a follow-up discussion from #1910 — I want to propose a less intrusive API that addresses the same underlying need.
Problem
My workflow requires a clear batch boundary when consuming perf events:
ReadIntoin a loop doesn't cleanly express this because:No "batch complete" signal. After
ReadIntoreturns, the caller cannot distinguish "more data pending in loaded rings" from "rings exhausted, next call will epoll_wait." The only way to detect the boundary is to set a short deadline and treatErrDeadlineExceededas "batch done" — which conflates "no data available right now" with "timeout expired while waiting for new data."Unpredictable deadline behavior.
SetDeadlineonly takes effect whenepollRingsis empty (sinceReadIntoskipsWaitwhen rings have data). From the caller's perspective, the configured deadline may or may not be used on any given call, making timeout-based flow control unreliable.Forced external buffering. To aggregate a batch, I must buffer records outside the library and guess when to stop calling
ReadInto. This effectively reimplements the drain logic in user code.Proposal
Add a single method:
Why this is minimal:
SetDeadlinefor timeout control (no new timeout parameter)epollRings,poller, etc.)Why not BPF ring buffer:
We use perf buffers because our deployment includes older kernels where
BPF_MAP_TYPE_RINGBUFis not available. Even on newer kernels, perf buffers remain a valid choice for per-CPU isolation without cross-CPU contention.Intended usage
I'm happy to submit a PR if there's interest. The implementation is straightforward — ~30 lines composing existing internals (
poller.Wait,loadHead,readRecordFromRing).cc @lmb @florianl