Skip to content

feat(memtrack): attach allocator probes on demand#445

Merged
not-matthias merged 9 commits into
mainfrom
cod-1801-only-attach-to-used-libraries-in-memtrack
Jul 20, 2026
Merged

feat(memtrack): attach allocator probes on demand#445
not-matthias merged 9 commits into
mainfrom
cod-1801-only-attach-to-used-libraries-in-memtrack

Conversation

@not-matthias

Copy link
Copy Markdown
Member

Attach allocator uprobes only to libraries the tracked process tree actually maps, instead of pre-attaching every allocator discovered on the system at startup.

A BPF fentry on security_mmap_file signals the first mapping of each unknown executable inode. A background worker SIGSTOPs the mapping processes, classifies the file by its allocator symbols, attaches probes, and resumes them. This:

  • covers dlopen'd allocators and statically linked allocators in spawned children (previously missed entirely)
  • skips libraries the benchmark never loads
  • removes the startup discovery scan (system-wide library glob + build-dir walk), which was slow on nix-heavy systems

The eager discovery path and CODSPEED_MEMTRACK_BINARIES are removed since the watcher supersedes them (breaking: the env var is no longer read).

The crate API is now Tracker (owns the attach worker) and Session (owns the spawned child and its event pipeline); ring buffers are polled through a generic RingBufferPoller with caller-supplied parsing, replacing the keepalive forwarding thread.

Review focus: the stop-the-world attach in attach_worker.rs (fixpoint stop + synchronous ring buffer drain) — its correctness argument is that a full consume() after all producers are stopped cannot miss requests.

Fixes COD-1801

@codspeed-hq

codspeed-hq Bot commented Jul 8, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

✅ 17 untouched benchmarks


Comparing cod-1801-only-attach-to-used-libraries-in-memtrack (6794d28) with main (c17b77f)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the eager startup allocator discovery scan (system-wide glob + CODSPEED_MEMTRACK_BINARIES) with an on-demand watcher: a BPF fentry/security_mmap_file probe triggers a background worker that SIGSTOP-freezes the mapping process tree, classifies the new inode, attaches uprobes, updates known_inodes, then SIGCONTs. The Tracker/Session API split and generic RingBufferPoller are clean improvements.

  • On-demand attach worker (attach_worker.rs, proc_fs.rs, attach.h): fixpoint stop-the-world loop correctly drains the ring buffer only after confirming all known pids are stopped; ContGuard guarantees SIGCONT on every exit path including panics.
  • Spawn stub (spawn.rs): shell-level kill -STOP $$; exec "$@" sidesteps the pre_exec/SIGSTOP deadlock; waitpid(WUNTRACED) blocks until the child stops before tracking is armed.
  • Breaking removal of CODSPEED_MEMTRACK_BINARIES: the env var is no longer read; callers that relied on it for static-allocator discovery now get equivalent coverage from the watcher automatically.

Confidence Score: 4/5

Safe to merge; the stop-the-world drain correctness is sound for the intended use case of well-behaved benchmarks that clean up children before the root exits.

The core attach correctness argument holds: ring-buffer entries are submitted before SIGSTOP is sent, the fixpoint loop stops all observed pids before draining, and ContGuard guarantees resume on every exit path. The wait_all_stopped function has a subtle all_stopped=true default that could prematurely return if all thread-stat reads are skipped, potentially violating the drain guarantee for that pid. The shutdown drain loop in Worker::run has no iteration bound, meaning it could block indefinitely if tracked children outlive the root process.

crates/memtrack/src/ebpf/proc_fs.rs (wait_all_stopped all_stopped default) and crates/memtrack/src/ebpf/attach_worker.rs (shutdown drain loop bound)

Important Files Changed

Filename Overview
crates/memtrack/src/ebpf/attach_worker.rs New background worker that stop-the-world attaches allocator uprobes on demand; fixpoint stop logic looks correct, ContGuard SIGCONT-on-drop guarantees resume on all exit paths, panic/drain error handling wired up
crates/memtrack/src/ebpf/proc_fs.rs New /proc helpers; wait_all_stopped has a subtle all_stopped=true default that prematurely declares success when no thread stat is readable; resolve_mapping and dev-encoding logic otherwise correct
crates/memtrack/src/ebpf/c/attach.h New BPF fentry on security_mmap_file; correctly submits ring-buffer entry before bpf_send_signal(SIGSTOP), drop counter on overflow handled, known_inodes dedup prevents repeated requests
crates/memtrack/src/ebpf/poller.rs RingBufferPoller refactored to generic parse-fn + mpsc::Sender; drain() protocol via ctl channel is sound; shutdown via ctl drop triggers final consume() before thread exit
crates/memtrack/src/ebpf/tracker.rs Tracker now owns Arc<Mutex> and AttachWorker; spawn() atomically arms tracking while process is stopped then resumes; API simplified
crates/memtrack/src/ebpf/spawn.rs stopped_command shell-stub sidesteps the pre_exec SIGSTOP deadlock; spawn_stopped uses waitpid(WUNTRACED) to detect the stop before returning
crates/memtrack/src/ebpf/events.rs New AttachRequest::parse uses same unsafe pointer-cast pattern as parse_event; relies on BPF ring-buffer 8-byte alignment guarantee for the u64 fields in struct attach_request
crates/memtrack/src/session.rs New Session owns the child process and event pipeline; dropping it stops the event poller and closes the channel, which unblocks rx.iter() collect in tests
crates/memtrack/src/main.rs Removed Mutex in favour of Arc; session/tracker shutdown order correct; finish() called after encode pipeline drains
crates/memtrack/src/ebpf/c/utils/process_tracking.h is_enabled() now fails closed (returns 0) on unexpected map-lookup failure instead of open; correct improvement
crates/memtrack/tests/dlopen_tests.rs New dlopen tests compile fake allocator shared libs at runtime and verify all 100 alloc/free events are captured after on-demand attach
src/cli/exec/mod.rs CODSPEED_MEMTRACK_BINARIES wiring removed; execute_config no longer needs to resolve binary paths since the watcher supersedes it

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant CLI as main.rs
    participant T as Tracker
    participant W as AttachWorker (bg thread)
    participant BPF as BPF (kernel)
    participant S as Stopped Process

    CLI->>T: Tracker::new()
    T->>BPF: attach fentry/security_mmap_file + tracepoints
    T->>W: AttachWorker::start()
    Note over W: poll_attach ring buffer → mpsc channel

    CLI->>T: tracker.spawn(cmd)
    T->>S: spawn_stopped (sh stub → SIGSTOP)
    T->>BPF: add_tracked_pid(pid)
    T->>S: resume(SIGCONT)

    S->>BPF: mmap(libfoo.so, PROT_EXEC)
    BPF->>BPF: submit attach_request(pid,dev,ino)
    BPF->>S: bpf_send_signal(SIGSTOP)
    Note over S: process stops

    W->>W: recv attach_request batch
    W->>S: wait_all_stopped(pid, 1s)
    W->>W: poller.drain() → consume ring buffer
    W->>W: resolve_mapping(pid, dev, ino)
    W->>BPF: attach_allocator_probes(libjemalloc)
    W->>BPF: insert_known_inode(dev, ino)
    W->>S: ContGuard drop → SIGCONT

    Note over S: process resumes, allocations now tracked

    CLI->>T: tracker.finish()
    T->>W: "shutdown=true"
    W->>W: final drain + process remaining batch
    W-->>T: "Ok() or Err (dropped count > 0)"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant CLI as main.rs
    participant T as Tracker
    participant W as AttachWorker (bg thread)
    participant BPF as BPF (kernel)
    participant S as Stopped Process

    CLI->>T: Tracker::new()
    T->>BPF: attach fentry/security_mmap_file + tracepoints
    T->>W: AttachWorker::start()
    Note over W: poll_attach ring buffer → mpsc channel

    CLI->>T: tracker.spawn(cmd)
    T->>S: spawn_stopped (sh stub → SIGSTOP)
    T->>BPF: add_tracked_pid(pid)
    T->>S: resume(SIGCONT)

    S->>BPF: mmap(libfoo.so, PROT_EXEC)
    BPF->>BPF: submit attach_request(pid,dev,ino)
    BPF->>S: bpf_send_signal(SIGSTOP)
    Note over S: process stops

    W->>W: recv attach_request batch
    W->>S: wait_all_stopped(pid, 1s)
    W->>W: poller.drain() → consume ring buffer
    W->>W: resolve_mapping(pid, dev, ino)
    W->>BPF: attach_allocator_probes(libjemalloc)
    W->>BPF: insert_known_inode(dev, ino)
    W->>S: ContGuard drop → SIGCONT

    Note over S: process resumes, allocations now tracked

    CLI->>T: tracker.finish()
    T->>W: "shutdown=true"
    W->>W: final drain + process remaining batch
    W-->>T: "Ok() or Err (dropped count > 0)"
Loading

Reviews (9): Last reviewed commit: "fix(memtrack): join attach worker thread..." | Re-trigger Greptile

Comment thread crates/memtrack/src/ebpf/attach_worker.rs Outdated
@not-matthias
not-matthias force-pushed the cod-1801-only-attach-to-used-libraries-in-memtrack branch from 8bce092 to 16ffc38 Compare July 9, 2026 16:28
@not-matthias
not-matthias marked this pull request as ready for review July 9, 2026 16:29
@not-matthias
not-matthias force-pushed the cod-1801-only-attach-to-used-libraries-in-memtrack branch from 16ffc38 to 5b76788 Compare July 9, 2026 16:50
Comment thread crates/memtrack/src/ebpf/attach_worker.rs

@GuillaumeLagrange GuillaumeLagrange left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stopping here becuase I want us to dig the TRACEME thing before leaning in the stop wrapper

Comment thread crates/memtrack/src/ebpf/c/attach.bpf.h Outdated
Comment thread crates/memtrack/src/ebpf/c/memtrack.bpf.c Outdated
Comment thread crates/memtrack/src/ebpf/c/memtrack.bpf.c Outdated
Comment thread crates/memtrack/src/ebpf/events.rs
Comment thread crates/memtrack/src/ebpf/tracker.rs Outdated
Comment thread crates/memtrack/src/ebpf/tracker.rs
Comment thread crates/memtrack/src/ebpf/tracker.rs
Comment thread crates/memtrack/src/ebpf/spawn.rs
@not-matthias
not-matthias force-pushed the cod-1801-only-attach-to-used-libraries-in-memtrack branch from a0a194d to 4d3fbb0 Compare July 16, 2026 16:56

@GuillaumeLagrange GuillaumeLagrange left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, I recommend you rebase on top of #455 before merging though 😅

Copy link
Copy Markdown
Member Author

Yeah, that was the plan :)

An ARRAY-map lookup on a valid index cannot fail, but if one ever does
is_enabled() now returns 0 (disabled) instead of 1, so tracking never
runs unguarded.
…tcher

Instead of pre-attaching every allocator found on the system, a BPF
fentry on security_mmap_file signals the first mapping of each unknown
executable inode. A background worker stops the mapping processes,
classifies the file by its symbols, attaches probes, and resumes them.
This covers dlopen'd and statically linked allocators in spawned
children, and skips libraries the benchmark never loads.

The public API shrinks to Tracker (owns the attach worker) and Session
(owns the spawned child and its event pipeline); ring buffers are
polled through a generic RingBufferPoller with caller-supplied parsing.

Fixes COD-1801
On-demand attach supersedes the startup scan: delete the system-wide
library glob, the build-dir walk, and the CODSPEED_MEMTRACK_BINARIES
env var (the runner no longer resolves exec target binaries for it).

BREAKING CHANGE: CODSPEED_MEMTRACK_BINARIES is no longer read

Refs COD-1801
Distro libjemalloc is built without the je_ symbol prefix, so symbol
classification missed it and fell through to libc++ (jemalloc exports
operator new), leaving plain malloc/aligned_alloc unprobed. Match on
mallocx, which keeps its name in unprefixed builds and is unique to
jemalloc.
@not-matthias
not-matthias force-pushed the cod-1801-only-attach-to-used-libraries-in-memtrack branch from 4d3fbb0 to a7e1666 Compare July 20, 2026 10:11
…pings

The /proc/<pid>/maps range column is zero-padded to a minimum of 8 hex
digits, but /proc/<pid>/map_files/ dirents use the kernel's canonical
no-leading-zero hex. Reusing the padded range built a non-existent path
for any mapping starting below 0x10000000 (e.g. non-PIE executables at
0x400000), so its allocator probes were silently never attached.

Normalize each half of the range before building the map_files path, via
an extracted map_files_name helper with a deterministic unit test.
…ve process

The watcher fires once per inode, so a mapping it could not resolve is
never retried. Treating that as a benign miss (Ok(false)) silently
resumed the process with no allocator probes and let the run succeed with
incomplete coverage.

Split resolve_mapping's outcome into a Resolution enum: ProcessGone (the
process exited before classification, non-fatal) versus Unresolved (maps
was read but no row matches the watcher's (dev, ino)). Since the process
is stopped with the mapping necessarily present, an Unresolved mapping is
a real coverage gap (e.g. an overlayfs inode-namespace mismatch) and now
aborts the run via record_fatal instead of failing silently.

The deeper overlayfs correlation fix (matching new exec VMAs directly
rather than the watcher's overlay inode) still needs an overlayfs test
environment and is left as a follow-up; this change makes the failure
loud and diagnosable rather than a silent empty trace.
finish() was the only path that set the shutdown flag and joined the
worker thread. On any error path that drops the Tracker without calling
finish() (e.g. an early return in main before teardown), the worker kept
spinning on its recv loop and held an Arc<Mutex<MemtrackBpf>> clone alive,
so probe links were never detached and fell back to the slow serial
close-at-exit path this worker exists to eliminate.

Add a Drop impl that sets shutdown and joins the taken handle; it is
idempotent with finish() (which already take()s the handle first).
@not-matthias
not-matthias force-pushed the cod-1801-only-attach-to-used-libraries-in-memtrack branch from 1a7255c to 6794d28 Compare July 20, 2026 12:40
@not-matthias
not-matthias merged commit 6794d28 into main Jul 20, 2026
23 checks passed
@not-matthias
not-matthias deleted the cod-1801-only-attach-to-used-libraries-in-memtrack branch July 20, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants