From 2dd053ef217b7d1eeb3b031b3980dc5bfdc43235 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 7 Jul 2026 18:03:23 +0200 Subject: [PATCH 1/4] refactor(memtrack): attach uprobes at resolved file offsets instead of func_name Resolve every defined symbol's libbpf-compatible file offset once per library (st_value - sh_addr + sh_offset) and attach uprobes by offset, replacing the per-probe func_name lookup that reparsed the ELF each time. --- crates/memtrack/src/ebpf/memtrack.rs | 355 +++++++++++++++++++-------- crates/memtrack/src/ebpf/mod.rs | 2 +- crates/memtrack/src/ebpf/tracker.rs | 7 +- 3 files changed, 258 insertions(+), 106 deletions(-) diff --git a/crates/memtrack/src/ebpf/memtrack.rs b/crates/memtrack/src/ebpf/memtrack.rs index 3a1bfa26..b497b358 100644 --- a/crates/memtrack/src/ebpf/memtrack.rs +++ b/crates/memtrack/src/ebpf/memtrack.rs @@ -4,10 +4,12 @@ use libbpf_rs::skel::OpenSkel; use libbpf_rs::skel::SkelBuilder; use libbpf_rs::{MapCore, UprobeOpts}; use paste::paste; +use std::collections::HashMap; +use std::collections::HashSet; use std::mem::MaybeUninit; use std::path::Path; -use crate::allocators::AllocatorKind; +use crate::allocators::{AllocatorKind, AllocatorLib}; use crate::ebpf::poller::RingBufferPoller; pub mod memtrack_skel { @@ -15,15 +17,15 @@ pub mod memtrack_skel { } pub use memtrack_skel::*; -/// Resolve symbol offset from .symtab to ensure that libbpf can find it. Otherwise -/// it will print a warning at runtime. -fn ensure_symbol_exists(lib_path: &Path, symbol_name: &str) -> Result<()> { +/// Resolve libbpf attach targets for every defined symbol in `lib_path`. +pub fn resolve_symbol_offsets(lib_path: &Path) -> Result { use object::{Object, ObjectSymbol}; let data = std::fs::read(lib_path)?; let file = object::File::parse(&*data)?; + let mut offsets = HashMap::new(); + let mut unresolved = HashSet::new(); - // Check both regular and dynamic symbols for symbol in file.symbols().chain(file.dynamic_symbols()) { if !symbol.is_definition() { continue; @@ -33,28 +35,65 @@ fn ensure_symbol_exists(lib_path: &Path, symbol_name: &str) -> Result<()> { continue; }; - if name == symbol_name { - let addr = symbol.address(); - if addr != 0 { - return Ok(()); + match symbol_file_offset(&file, &symbol) { + Some(file_offset) => { + offsets.insert(name.to_owned(), file_offset); + } + None => { + unresolved.insert(name.to_owned()); } } } - bail!("Symbol {symbol_name} not found in {}", lib_path.display()) + unresolved.retain(|name| !offsets.contains_key(name)); + + Ok(ResolvedSymbols { + offsets, + unresolved, + }) +} + +/// The libbpf file offset for `symbol`, or `None` when it has no address in a +/// file-backed section (absolute, `SHT_NOBITS`, ...). +fn symbol_file_offset<'a>( + file: &object::File, + symbol: &impl object::ObjectSymbol<'a>, +) -> Option { + use object::{Object, ObjectSection}; + + let address = symbol.address(); + if address == 0 { + return None; + } + + let section = file.section_by_index(symbol.section_index()?).ok()?; + let (sh_offset, _) = section.file_range()?; + Some((address - section.address() + sh_offset) as usize) +} + +/// Attach targets resolved from a library's symbol tables. +pub struct ResolvedSymbols { + offsets: HashMap, + unresolved: HashSet, +} + +impl ResolvedSymbols { + fn offset(&self, symbol: &str) -> Option { + self.offsets.get(symbol).copied() + } + + fn is_defined_without_offset(&self, symbol: &str) -> bool { + self.unresolved.contains(symbol) + } } -/// Macro to attach a function with both entry and return probes. -/// Also generates a `try_attach_*` variant that logs errors instead of returning them. -/// -/// Uses offset-based attachment by resolving symbols from .symtab. -/// Fails if the symbol is not found. +/// Macro to attach a function with both entry and return probes at a resolved +/// file offset. Also generates a `try_attach_*` variant that looks the symbol up +/// in the resolved offset table, skips it if absent, and logs errors instead of +/// returning them. macro_rules! attach_uprobe_uretprobe { ($name:ident, $prog_entry:ident, $prog_return:ident) => { - fn $name(&mut self, lib_path: &Path, symbol: &str) -> Result<()> { - ensure_symbol_exists(lib_path, symbol)?; - - // Attach entry probe at function entry via func_name + fn $name(&mut self, lib_path: &Path, offset: usize) -> Result<()> { let link = self .skel .progs @@ -62,21 +101,19 @@ macro_rules! attach_uprobe_uretprobe { .attach_uprobe_with_opts( -1, lib_path, - 0, + offset, UprobeOpts { retprobe: false, - func_name: Some(symbol.to_owned()), ..Default::default() }, ) .context(format!( - "Failed to attach {} uprobe in {}", - symbol, + "Failed to attach uprobe at offset {:#x} in {}", + offset, lib_path.display() ))?; self.probes.push(link); - // Attach return probe at function entry via func_name let link = self .skel .progs @@ -84,16 +121,15 @@ macro_rules! attach_uprobe_uretprobe { .attach_uprobe_with_opts( -1, lib_path, - 0, + offset, UprobeOpts { retprobe: true, - func_name: Some(symbol.to_owned()), ..Default::default() }, ) .context(format!( - "Failed to attach {} uretprobe in {}", - symbol, + "Failed to attach uretprobe at offset {:#x} in {}", + offset, lib_path.display() ))?; self.probes.push(link); @@ -102,24 +138,78 @@ macro_rules! attach_uprobe_uretprobe { } paste! { - fn [](&mut self, lib_path: &Path, symbol: &str) { - let result = self.$name(lib_path, symbol); + fn [<$name _by_name>](&mut self, lib_path: &Path, symbol: &str) -> Result<()> { + let link = self + .skel + .progs + .$prog_entry + .attach_uprobe_with_opts( + -1, + lib_path, + 0, + UprobeOpts { + retprobe: false, + func_name: Some(symbol.to_owned()), + ..Default::default() + }, + ) + .context(format!( + "Failed to attach uprobe to {} in {}", + symbol, + lib_path.display() + ))?; + self.probes.push(link); + + let link = self + .skel + .progs + .$prog_return + .attach_uprobe_with_opts( + -1, + lib_path, + 0, + UprobeOpts { + retprobe: true, + func_name: Some(symbol.to_owned()), + ..Default::default() + }, + ) + .context(format!( + "Failed to attach uretprobe to {} in {}", + symbol, + lib_path.display() + ))?; + self.probes.push(link); + + Ok(()) + } + + fn []( + &mut self, + lib_path: &Path, + symbol: &str, + symbols: &ResolvedSymbols, + ) { + let result = if let Some(offset) = symbols.offset(symbol) { + self.$name(lib_path, offset) + } else if symbols.is_defined_without_offset(symbol) { + self.[<$name _by_name>](lib_path, symbol) + } else { + return; + }; log::trace!("{} uprobe attach result: {:?}", symbol, result); } } }; } -/// Macro to attach a function with only an entry probe (no return probe). -/// Also generates a `try_attach_*` variant that logs errors instead of returning them. -/// -/// Uses offset-based attachment by resolving symbols from .symtab. -/// Fails if the symbol is not found. +/// Macro to attach a function with only an entry probe (no return probe) at a +/// resolved file offset. Also generates a `try_attach_*` variant that looks the +/// symbol up in the resolved offset table, skips it if absent, and logs errors +/// instead of returning them. macro_rules! attach_uprobe { ($name:ident, $prog:ident) => { - fn $name(&mut self, lib_path: &Path, symbol: &str) -> Result<()> { - ensure_symbol_exists(lib_path, symbol)?; - + fn $name(&mut self, lib_path: &Path, offset: usize) -> Result<()> { let link = self .skel .progs @@ -127,16 +217,15 @@ macro_rules! attach_uprobe { .attach_uprobe_with_opts( -1, lib_path, - 0, + offset, UprobeOpts { retprobe: false, - func_name: Some(symbol.to_owned()), ..Default::default() }, ) .context(format!( - "Failed to attach {} uprobe in {}", - symbol, + "Failed to attach uprobe at offset {:#x} in {}", + offset, lib_path.display() ))?; self.probes.push(link); @@ -144,8 +233,43 @@ macro_rules! attach_uprobe { } paste! { - fn [](&mut self, lib_path: &Path, symbol: &str) { - let result = self.$name(lib_path, symbol); + fn [<$name _by_name>](&mut self, lib_path: &Path, symbol: &str) -> Result<()> { + let link = self + .skel + .progs + .$prog + .attach_uprobe_with_opts( + -1, + lib_path, + 0, + UprobeOpts { + retprobe: false, + func_name: Some(symbol.to_owned()), + ..Default::default() + }, + ) + .context(format!( + "Failed to attach uprobe to {} in {}", + symbol, + lib_path.display() + ))?; + self.probes.push(link); + Ok(()) + } + + fn []( + &mut self, + lib_path: &Path, + symbol: &str, + symbols: &ResolvedSymbols, + ) { + let result = if let Some(offset) = symbols.offset(symbol) { + self.$name(lib_path, offset) + } else if symbols.is_defined_without_offset(symbol) { + self.[<$name _by_name>](lib_path, symbol) + } else { + return; + }; log::trace!("{} uprobe attach result: {:?}", symbol, result); } } @@ -280,10 +404,30 @@ impl MemtrackBpf { // Attach methods grouped by allocator // ========================================================================= + /// Attach probes for every discovered allocator library. + pub fn attach_allocators(&mut self, libs: &[AllocatorLib]) -> Result<()> { + for lib in libs { + let offsets = resolve_symbol_offsets(&lib.path)?; + self.attach_allocator_probes_with_offsets(lib.kind, &lib.path, &offsets)?; + } + + Ok(()) + } + /// Attach probes for a specific allocator kind. /// This attaches both standard probes (if the allocator exports them) and /// allocator-specific prefixed probes. pub fn attach_allocator_probes(&mut self, kind: AllocatorKind, lib_path: &Path) -> Result<()> { + let offsets = resolve_symbol_offsets(lib_path)?; + self.attach_allocator_probes_with_offsets(kind, lib_path, &offsets) + } + + fn attach_allocator_probes_with_offsets( + &mut self, + kind: AllocatorKind, + lib_path: &Path, + offsets: &ResolvedSymbols, + ) -> Result<()> { debug!( "Attaching {} probes to: {}", kind.name(), @@ -293,29 +437,29 @@ impl MemtrackBpf { match kind { AllocatorKind::Libc => { // Libc only has standard probes, and they must succeed - self.attach_libc_probes(lib_path) + self.attach_libc_probes(lib_path, offsets) } AllocatorKind::LibCpp => { // libc++ exports C++ operator new/delete symbols - self.attach_libcpp_probes(lib_path) + self.attach_libcpp_probes(lib_path, offsets) } AllocatorKind::Jemalloc => { // Jemalloc exposes libc/libcpp compatible allocator functions: - let _ = self.attach_libc_probes(lib_path); - let _ = self.attach_libcpp_probes(lib_path); - self.attach_jemalloc_probes(lib_path) + let _ = self.attach_libc_probes(lib_path, offsets); + let _ = self.attach_libcpp_probes(lib_path, offsets); + self.attach_jemalloc_probes(lib_path, offsets) } AllocatorKind::Mimalloc => { // Mimalloc exposes libc/libcpp compatible allocator functions: - let _ = self.attach_libc_probes(lib_path); - let _ = self.attach_libcpp_probes(lib_path); - self.attach_mimalloc_probes(lib_path) + let _ = self.attach_libc_probes(lib_path, offsets); + let _ = self.attach_libcpp_probes(lib_path, offsets); + self.attach_mimalloc_probes(lib_path, offsets) } AllocatorKind::Tcmalloc => { // Tcmalloc exposes libc/libcpp compatible allocator functions: - let _ = self.attach_libc_probes(lib_path); - let _ = self.attach_libcpp_probes(lib_path); - self.attach_tcmalloc_probes(lib_path) + let _ = self.attach_libc_probes(lib_path, offsets); + let _ = self.attach_libcpp_probes(lib_path, offsets); + self.attach_tcmalloc_probes(lib_path, offsets) } } } @@ -325,6 +469,7 @@ impl MemtrackBpf { lib_path: &Path, prefixes: &[&str], suffixes: &[&str], + offsets: &ResolvedSymbols, ) -> Result<()> { // Always include "" to capture the basic case let prefixes_with_base: Vec<&str> = std::iter::once("") @@ -339,18 +484,30 @@ impl MemtrackBpf { for prefix in &prefixes_with_base { for suffix in &suffixes_with_base { - self.try_attach_malloc(lib_path, &format!("{prefix}malloc{suffix}")); - self.try_attach_malloc(lib_path, &format!("{prefix}valloc{suffix}")); - self.try_attach_malloc(lib_path, &format!("{prefix}pvalloc{suffix}")); - self.try_attach_calloc(lib_path, &format!("{prefix}calloc{suffix}")); - self.try_attach_realloc(lib_path, &format!("{prefix}realloc{suffix}")); - self.try_attach_aligned_alloc(lib_path, &format!("{prefix}aligned_alloc{suffix}")); - self.try_attach_memalign(lib_path, &format!("{prefix}memalign{suffix}")); - self.try_attach_memalign(lib_path, &format!("{prefix}posix_memalign{suffix}")); - self.try_attach_free(lib_path, &format!("{prefix}free{suffix}")); - self.try_attach_free(lib_path, &format!("{prefix}free_sized{suffix}")); - self.try_attach_free(lib_path, &format!("{prefix}free_aligned_sized{suffix}")); - self.try_attach_free(lib_path, &format!("{prefix}cfree{suffix}")); + self.try_attach_malloc(lib_path, &format!("{prefix}malloc{suffix}"), offsets); + self.try_attach_malloc(lib_path, &format!("{prefix}valloc{suffix}"), offsets); + self.try_attach_malloc(lib_path, &format!("{prefix}pvalloc{suffix}"), offsets); + self.try_attach_calloc(lib_path, &format!("{prefix}calloc{suffix}"), offsets); + self.try_attach_realloc(lib_path, &format!("{prefix}realloc{suffix}"), offsets); + self.try_attach_aligned_alloc( + lib_path, + &format!("{prefix}aligned_alloc{suffix}"), + offsets, + ); + self.try_attach_memalign(lib_path, &format!("{prefix}memalign{suffix}"), offsets); + self.try_attach_memalign( + lib_path, + &format!("{prefix}posix_memalign{suffix}"), + offsets, + ); + self.try_attach_free(lib_path, &format!("{prefix}free{suffix}"), offsets); + self.try_attach_free(lib_path, &format!("{prefix}free_sized{suffix}"), offsets); + self.try_attach_free( + lib_path, + &format!("{prefix}free_aligned_sized{suffix}"), + offsets, + ); + self.try_attach_free(lib_path, &format!("{prefix}cfree{suffix}"), offsets); } } @@ -360,32 +517,32 @@ impl MemtrackBpf { /// Attach standard library allocation probes (libc-style: malloc, free, calloc, etc.) /// This works for libc and allocators that export standard symbol names. /// For non-libc allocators, standard names are optional - just try them silently. - fn attach_libc_probes(&mut self, lib_path: &Path) -> Result<()> { - self.attach_standard_probes(lib_path, &[], &[]) + fn attach_libc_probes(&mut self, lib_path: &Path, offsets: &ResolvedSymbols) -> Result<()> { + self.attach_standard_probes(lib_path, &[], &[], offsets) } /// Attach C++ operator new/delete probes. /// These are mangled C++ symbols that wrap the underlying allocator. /// C++ operators have identical signatures to malloc/free, so we reuse those handlers. - fn attach_libcpp_probes(&mut self, lib_path: &Path) -> Result<()> { - self.try_attach_malloc(lib_path, "_Znwm"); // operator new(size_t) - self.try_attach_malloc(lib_path, "_Znam"); // operator new[](size_t) - self.try_attach_malloc(lib_path, "_ZnwmSt11align_val_t"); // operator new(size_t, std::align_val_t) - self.try_attach_malloc(lib_path, "_ZnamSt11align_val_t"); // operator new[](size_t, std::align_val_t) - self.try_attach_free(lib_path, "_ZdlPv"); // operator delete(void*) - self.try_attach_free(lib_path, "_ZdaPv"); // operator delete[](void*) - self.try_attach_free(lib_path, "_ZdlPvm"); // operator delete(void*, size_t) - C++14 sized delete - self.try_attach_free(lib_path, "_ZdaPvm"); // operator delete[](void*, size_t) - C++14 sized delete - self.try_attach_free(lib_path, "_ZdlPvSt11align_val_t"); // operator delete(void*, std::align_val_t) - self.try_attach_free(lib_path, "_ZdaPvSt11align_val_t"); // operator delete[](void*, std::align_val_t) - self.try_attach_free(lib_path, "_ZdlPvmSt11align_val_t"); // operator delete(void*, size_t, std::align_val_t) - self.try_attach_free(lib_path, "_ZdaPvmSt11align_val_t"); // operator delete[](void*, size_t, std::align_val_t) + fn attach_libcpp_probes(&mut self, lib_path: &Path, offsets: &ResolvedSymbols) -> Result<()> { + self.try_attach_malloc(lib_path, "_Znwm", offsets); // operator new(size_t) + self.try_attach_malloc(lib_path, "_Znam", offsets); // operator new[](size_t) + self.try_attach_malloc(lib_path, "_ZnwmSt11align_val_t", offsets); // operator new(size_t, std::align_val_t) + self.try_attach_malloc(lib_path, "_ZnamSt11align_val_t", offsets); // operator new[](size_t, std::align_val_t) + self.try_attach_free(lib_path, "_ZdlPv", offsets); // operator delete(void*) + self.try_attach_free(lib_path, "_ZdaPv", offsets); // operator delete[](void*) + self.try_attach_free(lib_path, "_ZdlPvm", offsets); // operator delete(void*, size_t) - C++14 sized delete + self.try_attach_free(lib_path, "_ZdaPvm", offsets); // operator delete[](void*, size_t) - C++14 sized delete + self.try_attach_free(lib_path, "_ZdlPvSt11align_val_t", offsets); // operator delete(void*, std::align_val_t) + self.try_attach_free(lib_path, "_ZdaPvSt11align_val_t", offsets); // operator delete[](void*, std::align_val_t) + self.try_attach_free(lib_path, "_ZdlPvmSt11align_val_t", offsets); // operator delete(void*, size_t, std::align_val_t) + self.try_attach_free(lib_path, "_ZdaPvmSt11align_val_t", offsets); // operator delete[](void*, size_t, std::align_val_t) Ok(()) } /// Attach jemalloc-specific probes (prefixed and extended API). - fn attach_jemalloc_probes(&mut self, lib_path: &Path) -> Result<()> { + fn attach_jemalloc_probes(&mut self, lib_path: &Path, offsets: &ResolvedSymbols) -> Result<()> { // The following functions are used in Rust when setting a global allocator: // - rust_alloc: _rjem_malloc and _rjem_mallocx // - rust_alloc_zeroed: _rjem_mallocx / _rjem_calloc @@ -397,16 +554,16 @@ impl MemtrackBpf { let prefixes = ["je_", "_rjem_"]; let suffixes = ["", "_default"]; - self.attach_standard_probes(lib_path, &prefixes, &suffixes)?; + self.attach_standard_probes(lib_path, &prefixes, &suffixes, offsets)?; // Non-standard API that has an additional flag parameter // See: https://jemalloc.net/jemalloc.3.html for prefix in prefixes { for suffix in suffixes { - self.try_attach_malloc(lib_path, &format!("{prefix}mallocx{suffix}")); - self.try_attach_realloc(lib_path, &format!("{prefix}rallocx{suffix}")); - self.try_attach_free(lib_path, &format!("{prefix}dallocx{suffix}")); - self.try_attach_free(lib_path, &format!("{prefix}sdallocx{suffix}")); + self.try_attach_malloc(lib_path, &format!("{prefix}mallocx{suffix}"), offsets); + self.try_attach_realloc(lib_path, &format!("{prefix}rallocx{suffix}"), offsets); + self.try_attach_free(lib_path, &format!("{prefix}dallocx{suffix}"), offsets); + self.try_attach_free(lib_path, &format!("{prefix}sdallocx{suffix}"), offsets); } } @@ -414,20 +571,20 @@ impl MemtrackBpf { } /// Attach mimalloc-specific probes (mi_* API). - fn attach_mimalloc_probes(&mut self, lib_path: &Path) -> Result<()> { + fn attach_mimalloc_probes(&mut self, lib_path: &Path, offsets: &ResolvedSymbols) -> Result<()> { // The following functions are used in Rust when setting a global allocator: // - mi_malloc_aligned // - mi_free // - mi_realloc_aligned // - mi_zalloc_aligned - self.attach_standard_probes(lib_path, &["mi_"], &[])?; + self.attach_standard_probes(lib_path, &["mi_"], &[], offsets)?; // Zero-initialized and aligned variants - self.try_attach_malloc(lib_path, "mi_malloc_aligned"); - self.try_attach_calloc(lib_path, "mi_zalloc"); - self.try_attach_calloc(lib_path, "mi_zalloc_aligned"); - self.try_attach_realloc(lib_path, "mi_realloc_aligned"); + self.try_attach_malloc(lib_path, "mi_malloc_aligned", offsets); + self.try_attach_calloc(lib_path, "mi_zalloc", offsets); + self.try_attach_calloc(lib_path, "mi_zalloc_aligned", offsets); + self.try_attach_realloc(lib_path, "mi_realloc_aligned", offsets); Ok(()) } @@ -437,12 +594,12 @@ impl MemtrackBpf { /// See: /// - https://github.com/google/tcmalloc/blob/master/docs/reference.md /// - https://github.com/gperftools/gperftools/blob/a47243150ec41097602730ff8779fafcc172d1fb/src/tcmalloc.cc#L178-L190 - fn attach_tcmalloc_probes(&mut self, lib_path: &Path) -> Result<()> { - self.attach_standard_probes(lib_path, &["tc_"], &[])?; + fn attach_tcmalloc_probes(&mut self, lib_path: &Path, offsets: &ResolvedSymbols) -> Result<()> { + self.attach_standard_probes(lib_path, &["tc_"], &[], offsets)?; - self.try_attach_free(lib_path, "free_sized"); - self.try_attach_free(lib_path, "free_aligned_sized"); - self.try_attach_free(lib_path, "sdallocx"); + self.try_attach_free(lib_path, "free_sized", offsets); + self.try_attach_free(lib_path, "free_aligned_sized", offsets); + self.try_attach_free(lib_path, "sdallocx", offsets); Ok(()) } diff --git a/crates/memtrack/src/ebpf/mod.rs b/crates/memtrack/src/ebpf/mod.rs index 0badf542..cb4716be 100644 --- a/crates/memtrack/src/ebpf/mod.rs +++ b/crates/memtrack/src/ebpf/mod.rs @@ -3,5 +3,5 @@ mod memtrack; mod poller; mod tracker; -pub use memtrack::MemtrackBpf; +pub use memtrack::{MemtrackBpf, ResolvedSymbols, resolve_symbol_offsets}; pub use tracker::Tracker; diff --git a/crates/memtrack/src/ebpf/tracker.rs b/crates/memtrack/src/ebpf/tracker.rs index 9f1af587..8205b561 100644 --- a/crates/memtrack/src/ebpf/tracker.rs +++ b/crates/memtrack/src/ebpf/tracker.rs @@ -36,12 +36,7 @@ impl Tracker { } pub fn attach_allocators(&mut self, libs: &[AllocatorLib]) -> Result<()> { - for allocator in libs { - self.bpf - .attach_allocator_probes(allocator.kind, &allocator.path)?; - } - - Ok(()) + self.bpf.attach_allocators(libs) } pub fn attach_allocator(&mut self, lib: &AllocatorLib) -> Result<()> { From a56a0dce2405395762b19df4c66bb4f48e0ac6b6 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 7 Jul 2026 18:03:58 +0200 Subject: [PATCH 2/4] perf(memtrack): resolve allocator symbol offsets in parallel Symbol resolution parses each allocator ELF independently, so fan it out across cores with rayon before the serial attach step. Cuts offset resolution for all discovered allocators from ~190ms to ~29ms locally. --- Cargo.lock | 1 + crates/memtrack/Cargo.toml | 1 + crates/memtrack/src/ebpf/memtrack.rs | 10 ++++++++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index daf2000e..8e36728b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2389,6 +2389,7 @@ dependencies = [ "log", "object", "paste", + "rayon", "rstest", "runner-shared", "serde", diff --git a/crates/memtrack/Cargo.toml b/crates/memtrack/Cargo.toml index ae481941..ea6bee60 100644 --- a/crates/memtrack/Cargo.toml +++ b/crates/memtrack/Cargo.toml @@ -34,6 +34,7 @@ paste = "1.0.15" libbpf-rs = { version = "0.26", features = ["vendored"], optional = true } glob = "0.3.3" object = { workspace = true } +rayon = "1.12" [build-dependencies] libbpf-cargo = { version = "0.26", optional = true } diff --git a/crates/memtrack/src/ebpf/memtrack.rs b/crates/memtrack/src/ebpf/memtrack.rs index b497b358..d7787b99 100644 --- a/crates/memtrack/src/ebpf/memtrack.rs +++ b/crates/memtrack/src/ebpf/memtrack.rs @@ -406,8 +406,14 @@ impl MemtrackBpf { /// Attach probes for every discovered allocator library. pub fn attach_allocators(&mut self, libs: &[AllocatorLib]) -> Result<()> { - for lib in libs { - let offsets = resolve_symbol_offsets(&lib.path)?; + use rayon::prelude::*; + + let resolved = libs + .par_iter() + .map(|lib| resolve_symbol_offsets(&lib.path).map(|offsets| (lib, offsets))) + .collect::>>()?; + + for (lib, offsets) in resolved { self.attach_allocator_probes_with_offsets(lib.kind, &lib.path, &offsets)?; } From 9a995ca598679c1c547568465bb209aa92f14f37 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 7 Jul 2026 18:04:24 +0200 Subject: [PATCH 3/4] bench(memtrack): add offset resolution benchmark Divan bench comparing serial vs parallel symbol offset resolution over the discovered allocator libraries. Runs without sudo since it only parses ELFs, no eBPF attach. --- Cargo.lock | 1 + crates/memtrack/Cargo.toml | 5 +++++ crates/memtrack/benches/attach.rs | 36 +++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 crates/memtrack/benches/attach.rs diff --git a/Cargo.lock b/Cargo.lock index 8e36728b..151bb010 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2378,6 +2378,7 @@ dependencies = [ "anyhow", "bindgen", "clap", + "codspeed-divan-compat", "env_logger", "glob", "insta", diff --git a/crates/memtrack/Cargo.toml b/crates/memtrack/Cargo.toml index ea6bee60..6757c07e 100644 --- a/crates/memtrack/Cargo.toml +++ b/crates/memtrack/Cargo.toml @@ -47,6 +47,11 @@ rstest = { workspace = true } test-log = { workspace = true } insta = { workspace = true } test-with = { workspace = true } +divan = { package = "codspeed-divan-compat", version = "4" } + +[[bench]] +name = "attach" +harness = false [package.metadata.dist] targets = ["aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu"] diff --git a/crates/memtrack/benches/attach.rs b/crates/memtrack/benches/attach.rs new file mode 100644 index 00000000..c4cde0ce --- /dev/null +++ b/crates/memtrack/benches/attach.rs @@ -0,0 +1,36 @@ +use memtrack::{AllocatorLib, resolve_symbol_offsets}; +use rayon::prelude::*; + +fn main() { + divan::main(); +} + +fn discovered_allocators() -> Vec { + let libs = AllocatorLib::find_all().expect("failed to discover allocators"); + assert!(!libs.is_empty(), "no allocators discovered"); + libs +} + +/// Resolve symbol offsets for every discovered allocator one library at a time. +#[divan::bench] +fn resolve_offsets_serial(bencher: divan::Bencher) { + let libs = discovered_allocators(); + + bencher.bench_local(|| { + libs.iter() + .map(|lib| resolve_symbol_offsets(&lib.path).expect("failed to resolve offsets")) + .collect::>() + }); +} + +/// Resolve symbol offsets for every discovered allocator in parallel. +#[divan::bench] +fn resolve_offsets_parallel(bencher: divan::Bencher) { + let libs = discovered_allocators(); + + bencher.bench_local(|| { + libs.par_iter() + .map(|lib| resolve_symbol_offsets(&lib.path).expect("failed to resolve offsets")) + .collect::>() + }); +} From 9f32601268e51a5bfc0731f671abc1a75c30b26d Mon Sep 17 00:00:00 2001 From: not-matthias Date: Wed, 8 Jul 2026 19:18:36 +0200 Subject: [PATCH 4/4] ci(memtrack): run offset resolution benchmarks --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b76aa609..116df6de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,13 +134,16 @@ jobs: with: tool: cargo-codspeed + - name: Install libbpf build deps + run: sudo apt-get update && sudo apt-get install -y build-essential pkgconf zlib1g-dev libbpf-dev autopoint bison flex + - name: Build benchmarks - run: cargo codspeed build -p runner-shared + run: cargo codspeed build -p runner-shared -p memtrack - name: Run benchmarks uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4 with: mode: simulation - run: cargo codspeed run -p runner-shared + run: cargo codspeed run -p runner-shared -p memtrack check: runs-on: ubuntu-latest