Building a Profiler That Tells You When It's Lying

Lost events, attach overhead, and a report that carries its own error bars.

Every profiler we tried had the same flaw: it reported a percentage without saying how many events it dropped to produce it.

We built a small in-house profiler for one workload — read latency on a storage service — with a stricter contract: every report must include the sample count, the lost-event rate, and the measured overhead of the probe itself.

The design constraints

  • Aggregate in the kernel. A histogram per latency bucket, not one event per call. Exporting 200k events/s is how you lose 8% of them.
  • Per-CPU maps. No cross-CPU contention on the hot path.
  • Two paths. Aggregated histograms for the distribution; a small ring buffer for a bounded number of outliers, with an explicit cap.
  • Error bars in the report. The tool prints lost events and an A/B overhead measurement; a report without them is not accepted by our incident reviews.

Kernel-side aggregation

b = BPF(text='''
BPF_HISTOGRAM(lat, u64, 64);
int trace_exit(struct pt_regs *ctx) {
    u64 start = start_ns.lookup(&pid);
    if (!start) return 0;
    u64 delta = bpf_ktime_get_ns() - *start;
    lat.increment(bpf_log2l(delta / 1000));  // microseconds
    start_ns.delete(&pid);
    return 0;
}
''')

The userspace side reads one histogram every second. The event volume at the boundary between kernel and userspace is 1/s, not 200k/s, which is the entire trick.

The overhead harness

We run every probe configuration against a fixed workload for five minutes, with the probe attached and detached, and report the delta:

throughput: 41.2k req/s (probe off) -> 40.1k req/s (probe on)   -2.7%
p99:          18ms               -> 18.4ms                      +2.2%

A 3% overhead is acceptable for a diagnostic run and unacceptable for a permanent agent. We ship the profiler as an on-demand tool for exactly this reason.

What loss looks like, and how we fixed it

Baseline configuration: 64 perf-buffer pages, one event per call. At 200k events/s we lost 8% of events, concentrated in bursts.

ConfigLostOverheadNotes
64 pages, per-event export8.0%3.1%unusable for burst analysis
256 pages, per-event export1.4%3.0%better, still bursty
256 pages, kernel histogram0.2%2.7%our default
256 pages + outlier ring0.2%2.9%outliers retained

The last row is what we run: histograms for the distribution, a 4,096-entry ring buffer for calls over 10ms, with a counter for dropped outliers. The report says "12 outliers dropped", which is honest.

Why this matters

A tracepoint that loses 8% of events in bursts does not have a sampling error; it has a bias toward whatever happened outside the burst. In our case the burst was the incident. The tool reported a calm system because it discarded the evidence.

The same logic applies to sampling profilers. A 99Hz CPU profile over 30 seconds is 2,970 samples; a function that takes 5ms may never appear. That does not mean it is not the problem.

The report format

window          2025-07-30T14:02Z .. 14:07Z
events          61.2M reads
samples lost    0.2% (122k)
outliers kept   4,096 / dropped 12
probe overhead  +2.7% throughput, +2.2% p99
p50             0.8ms
p99             31ms
p99.9           240ms

Nothing exotic, and every line is defensible. If a number cannot be defended, it does not go in the report.


A profiler that hides its own error is worse than no profiler. The first feature we built was the one that says "I dropped 8% of the evidence".