Why Orbit’s kernel-uprobe stop was slow
Self-contained report: the kernel lock, why fewer process-wide fds are not the production fix, the shared-tracefs change, and the 10/20/50 attach/detach numbers from this run.
1. The issue
Orbit’s historical kernel-uprobe path opens one uprobe and one uretprobe PMU fd per CPU per function (pid=-1, cpu=0..N-1):
fds = 2 × F × NCPU
Each of those fds is its own create_local_trace_uprobe — not another consumer of one probe. Closing a capture therefore destroys F × NCPU × 2 independent probes.
perf_uprobe_destroy holds the global event_mutex around the entire close:
uprobe_apply(false)uprobe_unregister_nosync— VMA walk underdup_mmap_sem+register_rwsemuprobe_unregister_sync()— RCU tasks-trace and uretprobes SRCUtracepoint_synchronize_unregister
Parallel close() cannot overlap those grace periods; they queue on event_mutex. Wall time stays
O(F × NCPU × 2 × (VMA walk + 2–3 RCU GPs))
Stop time growing with core count is expected from the per-CPU fd model. ParallelFor does not help: it issues more closes at once, but they still serialize on the same mutex.
Old path — every sample fd is a local probe
Two functions × three CPUs shown. Each chip is a PMU fd. Every close takes event_mutex.
2. Why pid=target, cpu=-1 is not the production fix
Opening one uprobe + one uretprobe for the process (2×F fds) was measured. It is not how production samples:
perf_event_openpidis a tid. Samples follow that task only.inheritcovers children created after open, not existing sibling threads.inherit+cpu=-1cannot mmap a sample ring buffer (perf_event_open(2)).
Production keeps pid=-1, cpu=N so every thread on every core is sampled. The bench still times the process-wide experiment; it is a side table, not the fix.
3. The solution
Register each probe once via tracefs (uprobe_events, group orbit) and open the per-CPU fds as TRACEPOINT consumers of that named probe.
- Sample fds (coverage): still
2 × F × NCPU - Named probes / expensive unregisters:
2 × F(last close of each name) Shutdown()disables, unmaps, closes every fd, then deletes the tracefs names. No leaked probes, no fire-and-forget.
New path — many sample fds share two named probes per function
TRACEPOINT fds still deliver per-CPU samples. Only the named probes pay the unregister cost.
close() under event_mutex (historical local PMU)
This is why core count multiplies stop time. Shared-tracefs pays this sequence once per named probe, not once per sample fd.
4. Measured attach/detach (10 / 20 / 50)
Times are this run only. Missing columns are n/a — numbers are never invented. Speedup = percpu_stop / shared_stop. percpu = historical local PMU; shared = production tracefs path.
kernel: not measured on this static copy
ncpus: n/a
pid model: production samples use pid=-1,cpu=N (every thread, every core). pid=target,cpu=-1 is a fewer-fd experiment only (single tid + inherit).
uprobe PMU: n/a
tracefs: n/a
| nfunctions | ncpus | percpu fds | named probes | percpu start | percpu stop | shared start | shared stop | speedup |
|---|---|---|---|---|---|---|---|---|
| 10 | n/a | 2×10×NCPU | 20 | n/a | n/a | n/a | n/a | n/a |
| 20 | n/a | 2×20×NCPU | 40 | n/a | n/a | n/a | n/a | n/a |
| 50 | n/a | 2×50×NCPU | 100 | n/a | n/a | n/a | n/a | n/a |
percpu_fds = 2×F×NCPU; named_probes = 2×F.
pid=target,cpu=-1 (experiment, 2×F fds; not production)
| nfunctions | start | stop |
|---|---|---|
| 10 | n/a | n/a |
| 20 | n/a | n/a |
| 50 | n/a | n/a |
n/a Bench not run on this static copy. Open the HTML written next to cwd after a real run to see timings.
command: not run
legend: percpu = historical local PMU (event_mutex × fds); shared = production (one tracefs unregister per function). Verdict uses the existing policy: FAIL only if shared start/stop is more than 250 ms worse than percpu PMU.
5. How to re-run
Off by default, even as root. The 10/20/50 bench is not part of a normal LinuxTracingIntegrationTests invocation.
./src/LinuxTracingIntegrationTests/run_uprobe_attach_detach_bench.sh
# or: sudo ORBIT_UPROBE_BENCH=1 ./bin/LinuxTracingIntegrationTests --gtest_filter='*UprobeAttachDetachBench*'
The script finds the binary (or takes a path), exports ORBIT_UPROBE_BENCH=1 (true/yes also work), re-execs under sudo, and runs only this test — timings plus the tracer e2e (start/stop/start + function-call check). It writes uprobe_attach_detach_bench.txt and uprobe_attach_detach_bench.html in the current directory.