From 131f8a34c9c4c021939dcba51e7ff7c660611136 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Thu, 23 Jul 2026 20:03:05 +0200 Subject: [PATCH] feat: add --track-allocators toggle for memory mode Add a --track-allocators flag (default on, env CODSPEED_TRACK_ALLOCATORS) to the memtrack track subcommand. When disabled, memtrack skips the allocator uprobe machinery (exec watcher + attach worker) and only emits coarse mmap/munmap/brk events, reducing overhead on allocation-heavy programs. The mmap/munmap/brk syscall tracepoints are now always attached in every memory run. The runner does not add a CLI flag for this: it relies on the CODSPEED_TRACK_ALLOCATORS environment variable being inherited by the memtrack subprocess, keeping the runner decoupled from the installed memtrack version. Standalone memtrack can still use the CLI flag. --- crates/memtrack/src/ebpf/memtrack/tracking.rs | 10 +++++ crates/memtrack/src/ebpf/tracker.rs | 4 +- crates/memtrack/src/main.rs | 19 ++++++-- crates/memtrack/testdata/malloc_mmap.c | 15 +++++++ crates/memtrack/tests/c_tests.rs | 45 +++++++++++++++++++ crates/memtrack/tests/shared.rs | 31 ++++++++----- 6 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 crates/memtrack/testdata/malloc_mmap.c diff --git a/crates/memtrack/src/ebpf/memtrack/tracking.rs b/crates/memtrack/src/ebpf/memtrack/tracking.rs index 5f97fbd7..3bcbbd0e 100644 --- a/crates/memtrack/src/ebpf/memtrack/tracking.rs +++ b/crates/memtrack/src/ebpf/memtrack/tracking.rs @@ -7,6 +7,11 @@ impl MemtrackBpf { attach_tracepoint!(task_newtask); attach_tracepoint!(sched_process_exec); attach_tracepoint!(sched_process_exit); + attach_tracepoint!(sys_enter_mmap); + attach_tracepoint!(sys_exit_mmap); + attach_tracepoint!(sys_enter_munmap); + attach_tracepoint!(sys_enter_brk); + attach_tracepoint!(sys_exit_brk); fn rmap_target_enabled(&self, target: &str) -> bool { !self.btf_disabled_rmap_targets.contains(&target) @@ -16,6 +21,11 @@ impl MemtrackBpf { self.attach_task_newtask()?; self.attach_sched_process_exec()?; self.attach_sched_process_exit()?; + self.attach_sys_enter_mmap()?; + self.attach_sys_exit_mmap()?; + self.attach_sys_enter_munmap()?; + self.attach_sys_enter_brk()?; + self.attach_sys_exit_brk()?; if let Err(e) = self.attach_rss_stat() { warn!("Failed to attach rss_stat tracepoint, RSS collection disabled: {e:#}"); } diff --git a/crates/memtrack/src/ebpf/tracker.rs b/crates/memtrack/src/ebpf/tracker.rs index aec6c5f2..fea4af59 100644 --- a/crates/memtrack/src/ebpf/tracker.rs +++ b/crates/memtrack/src/ebpf/tracker.rs @@ -17,9 +17,9 @@ pub struct Tracker { impl Tracker { /// Create a new tracker. The exec-mapping watcher discovers and attaches /// allocator probes as the tracked process tree maps executable files. - pub fn new() -> Result { + pub fn new(track_allocators: bool) -> Result { let track_rmap = std::env::var("CODSPEED_MEMTRACK_TRACK_RMAP").is_ok_and(|v| v == "1"); - Self::build(track_rmap, true) + Self::build(track_rmap, track_allocators) } /// Track per-process RSS via the rss_stat tracepoint and folio rmap fentry diff --git a/crates/memtrack/src/main.rs b/crates/memtrack/src/main.rs index 283cff19..38bb042c 100644 --- a/crates/memtrack/src/main.rs +++ b/crates/memtrack/src/main.rs @@ -30,6 +30,17 @@ enum Commands { /// Optional IPC server name for receiving control commands #[arg(long)] ipc_server: Option, + + /// Track individual heap allocations (malloc/free/calloc/...). Disable to + /// reduce overhead on allocation-heavy programs; mmap/brk events are still + /// collected either way. + #[arg( + long, + env = "CODSPEED_TRACK_ALLOCATORS", + default_value_t = true, + action = clap::ArgAction::Set + )] + track_allocators: bool, }, } @@ -54,11 +65,12 @@ fn main() -> Result<()> { command, output: out_dir, ipc_server, + track_allocators, } => { debug!("Starting memtrack for command: {command}"); - let status = - track_command(&command, ipc_server, &out_dir).context("Failed to track command")?; + let status = track_command(&command, ipc_server, &out_dir, track_allocators) + .context("Failed to track command")?; std::process::exit(status.code().unwrap_or(1)); } @@ -69,6 +81,7 @@ fn track_command( cmd_string: &str, ipc_server_name: Option, out_dir: &Path, + track_allocators: bool, ) -> anyhow::Result { // First, establish IPC connection if needed to avoid timeouts on the runner because // creating the Tracker instance takes some time. @@ -84,7 +97,7 @@ fn track_command( None }; - let tracker = Arc::new(Tracker::new()?); + let tracker = Arc::new(Tracker::new(track_allocators)?); // Spawn IPC handler thread with the now-available tracker let ipc_handle = if let Some(rx) = ipc_channel { diff --git a/crates/memtrack/testdata/malloc_mmap.c b/crates/memtrack/testdata/malloc_mmap.c new file mode 100644 index 00000000..26fb2b5b --- /dev/null +++ b/crates/memtrack/testdata/malloc_mmap.c @@ -0,0 +1,15 @@ +#include +#include +#include + +int main(void) { + void *p = malloc(1 << 20); + memset(p, 1, 1 << 20); + free(p); + + void *m = mmap(NULL, 4 << 20, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + memset(m, 1, 4 << 20); + munmap(m, 4 << 20); + return 0; +} diff --git a/crates/memtrack/tests/c_tests.rs b/crates/memtrack/tests/c_tests.rs index 2e29ae16..c12d61cf 100644 --- a/crates/memtrack/tests/c_tests.rs +++ b/crates/memtrack/tests/c_tests.rs @@ -69,3 +69,48 @@ fn test_allocation_tracking( Ok(()) } + +#[test_with::env(GITHUB_ACTIONS)] +#[test_log::test] +fn test_track_allocators_disabled_skips_allocations() -> Result<(), Box> { + use runner_shared::artifacts::MemtrackEventKind; + + let temp_dir = TempDir::new()?; + let binary = shared::compile_c_source( + include_str!("../testdata/malloc_mmap.c"), + "malloc_mmap", + temp_dir.path(), + )?; + + let (events, thread_handle) = + shared::track_command_with_opts(std::process::Command::new(&binary), false)?; + + let has_mmap = events + .iter() + .any(|e| matches!(e.kind, MemtrackEventKind::Mmap { .. })); + assert!( + has_mmap, + "expected at least one Mmap event with allocators disabled" + ); + + let alloc_events: Vec<_> = events + .iter() + .filter(|e| { + matches!( + e.kind, + MemtrackEventKind::Malloc { .. } + | MemtrackEventKind::Free + | MemtrackEventKind::Calloc { .. } + | MemtrackEventKind::Realloc { .. } + | MemtrackEventKind::AlignedAlloc { .. } + ) + }) + .collect(); + assert!( + alloc_events.is_empty(), + "expected no allocation events with allocators disabled, got {alloc_events:?}" + ); + + thread_handle.join().unwrap(); + Ok(()) +} diff --git a/crates/memtrack/tests/shared.rs b/crates/memtrack/tests/shared.rs index 5d9982ff..c1f5a1e3 100644 --- a/crates/memtrack/tests/shared.rs +++ b/crates/memtrack/tests/shared.rs @@ -24,18 +24,19 @@ macro_rules! assert_events_snapshot { use runner_shared::artifacts::MemtrackEventKind; use std::mem::discriminant; - // Dedup events by address and type to remove duplicates + // Keep only allocator events. mmap/munmap/brk sizes reflect allocator + // arena reservations that vary per run, so including them here would + // make these snapshots nondeterministic. let events = $events .iter() .filter(|e| { - // Allocation snapshots track only allocator events; RSS and - // process-lifecycle events are asserted by dedicated tests. - !matches!( + matches!( e.kind, - MemtrackEventKind::Rss { .. } - | MemtrackEventKind::Fork { .. } - | MemtrackEventKind::Exec - | MemtrackEventKind::Exit + MemtrackEventKind::Malloc { .. } + | MemtrackEventKind::Free + | MemtrackEventKind::Calloc { .. } + | MemtrackEventKind::Realloc { .. } + | MemtrackEventKind::AlignedAlloc { .. } ) }) .sorted_by_key(|e| e.timestamp) @@ -182,19 +183,25 @@ pub fn compile_c_source( /// No allocators are pre-attached: the exec-mapping watcher discovers and /// attaches them as the tracked tree maps executable files. pub fn track_command(command: Command) -> TrackResult { - track_command_impl(command, false) + track_command_impl(command, true, false) } /// Track a command with folio rmap hooks enabled, reconstructing per-process RSS. pub fn track_command_with_rmap(command: Command) -> TrackResult { - track_command_impl(command, true) + track_command_impl(command, false, true) } -fn track_command_impl(command: Command, track_rmap: bool) -> TrackResult { +/// Track a command, collecting all memory events, with allocator tracking +/// optionally disabled. +pub fn track_command_with_opts(command: Command, track_allocators: bool) -> TrackResult { + track_command_impl(command, track_allocators, false) +} + +fn track_command_impl(command: Command, track_allocators: bool, track_rmap: bool) -> TrackResult { let tracker = if track_rmap { Tracker::new_without_allocators_with_rmap(true)? } else { - Tracker::new()? + Tracker::new(track_allocators)? }; tracker.enable_tracking()?;