diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 128a9cb4..8f05994b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: with: lfs: true - - name: 'Install rust-toolchain.toml' + - name: "Install rust-toolchain.toml" run: rustup toolchain install # We use Swatinem/rust-cache to cache cargo registry, index and target in this job - uses: Swatinem/rust-cache@v2 @@ -58,16 +58,26 @@ jobs: - uses: moonrepo/setup-rust@v1 - name: Install dependencies required for libbpf-sys (vendored feature) run: sudo apt-get update && sudo apt-get install -y autopoint bison flex + + - name: Install additional allocators + run: sudo apt-get install -y libmimalloc-dev libjemalloc-dev + - name: Run tests - run: sudo -E $(which cargo) test + env: + RUST_LOG: debug + run: sudo -E $(which cargo) test -- --test-threads 1 --nocapture working-directory: crates/memtrack + # Since we ran the tests with sudo, the build artifacts will have root ownership + - name: Clean up + run: sudo chown -R $USER:$USER . + benchmarks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: 'Install rust-toolchain.toml' + - name: "Install rust-toolchain.toml" run: rustup toolchain install - uses: Swatinem/rust-cache@v2 - name: Install cargo codspeed diff --git a/Cargo.lock b/Cargo.lock index 578c22ca..3b7ce927 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1284,9 +1284,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.44.3" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698" +checksum = "248b42847813a1550dafd15296fd9748c651d0c32194559dbc05d804d54b21e8" dependencies = [ "console", "once_cell", @@ -1294,6 +1294,7 @@ dependencies = [ "pest_derive", "serde", "similar", + "tempfile", ] [[package]] @@ -1655,6 +1656,7 @@ dependencies = [ "clap", "env_logger", "glob", + "insta", "ipc-channel", "itertools 0.14.0", "libbpf-cargo", diff --git a/crates/memtrack/Cargo.toml b/crates/memtrack/Cargo.toml index 8e01a28c..c606001d 100644 --- a/crates/memtrack/Cargo.toml +++ b/crates/memtrack/Cargo.toml @@ -44,6 +44,7 @@ bindgen = "0.71" tempfile = { workspace = true } rstest = "0.21" test-log = "0.2" +insta = { version = "1.46.1", default-features = false } [package.metadata.dist] dist = true diff --git a/crates/memtrack/src/allocators/dynamic.rs b/crates/memtrack/src/allocators/dynamic.rs index c35e427c..7bc270ac 100644 --- a/crates/memtrack/src/allocators/dynamic.rs +++ b/crates/memtrack/src/allocators/dynamic.rs @@ -1,6 +1,5 @@ -use std::path::PathBuf; - use crate::{AllocatorKind, AllocatorLib}; +use std::path::PathBuf; /// Returns the glob patterns used to find this allocator's shared libraries. fn get_allocator_paths(lib: &AllocatorKind) -> &'static [&'static str] { @@ -15,6 +14,16 @@ fn get_allocator_paths(lib: &AllocatorKind) -> &'static [&'static str] { // NixOS: find all glibc versions in the Nix store "/nix/store/*glibc*/lib/libc.so.6", ], + AllocatorKind::LibCpp => &[ + // Standard Linux multiarch paths + "/lib/*-linux-gnu/libstdc++.so*", + "/usr/lib/*-linux-gnu/libstdc++.so*", + // RHEL, Fedora, CentOS, Arch + "/lib*/libstdc++.so*", + "/usr/lib*/libstdc++.so*", + // NixOS: find all gcc lib versions in the Nix store + "/nix/store/*gcc*/lib/libstdc++.so*", + ], AllocatorKind::Jemalloc => &[ // Debian, Ubuntu: Standard Linux multiarch paths "/lib/*-linux-gnu/libjemalloc.so*", @@ -62,6 +71,7 @@ pub fn find_all() -> anyhow::Result> { .map(|m| m.is_file()) .unwrap_or(false) }) + .filter(|path| super::is_elf(path)) .collect::>(); for path in paths { diff --git a/crates/memtrack/src/allocators/mod.rs b/crates/memtrack/src/allocators/mod.rs index 70ad34ec..141c2020 100644 --- a/crates/memtrack/src/allocators/mod.rs +++ b/crates/memtrack/src/allocators/mod.rs @@ -13,6 +13,8 @@ mod static_linked; pub enum AllocatorKind { /// Standard C library (glibc, musl, etc.) Libc, + /// C++ standard library (libstdc++, libc++) - provides operator new/delete + LibCpp, /// jemalloc - used by FreeBSD, Firefox, many Rust projects Jemalloc, /// mimalloc - Microsoft's allocator @@ -26,10 +28,13 @@ pub enum AllocatorKind { impl AllocatorKind { /// Returns all supported allocator kinds. pub fn all() -> &'static [AllocatorKind] { + // IMPORTANT: Check non-default allocators first, because they will contain compatibility + // layers for the default allocators. &[ - AllocatorKind::Libc, AllocatorKind::Jemalloc, AllocatorKind::Mimalloc, + AllocatorKind::LibCpp, + AllocatorKind::Libc, ] } @@ -37,6 +42,7 @@ impl AllocatorKind { pub fn name(&self) -> &'static str { match self { AllocatorKind::Libc => "libc", + AllocatorKind::LibCpp => "libc++", AllocatorKind::Jemalloc => "jemalloc", AllocatorKind::Mimalloc => "mimalloc", } @@ -51,7 +57,8 @@ impl AllocatorKind { pub fn symbols(&self) -> &'static [&'static str] { match self { AllocatorKind::Libc => &["malloc", "free"], - AllocatorKind::Jemalloc => &["_rjem_malloc", "_rjem_free"], + AllocatorKind::LibCpp => &["_Znwm", "_Znam", "_ZdlPv", "_ZdaPv"], + AllocatorKind::Jemalloc => &["_rjem_malloc", "je_malloc", "je_malloc_default"], AllocatorKind::Mimalloc => &["mi_malloc_aligned", "mi_malloc", "mi_free"], } } @@ -71,3 +78,22 @@ impl AllocatorLib { Ok(allocators) } } + +/// Check if a file is an ELF binary by reading its magic bytes. +fn is_elf(path: &std::path::Path) -> bool { + use std::fs; + use std::io::Read; + + let mut file = match fs::File::open(path) { + Ok(f) => f, + Err(_) => return false, + }; + + let mut magic = [0u8; 4]; + if file.read_exact(&mut magic).is_err() { + return false; + } + + // ELF magic: 0x7F 'E' 'L' 'F' + magic == [0x7F, b'E', b'L', b'F'] +} diff --git a/crates/memtrack/src/allocators/static_linked.rs b/crates/memtrack/src/allocators/static_linked.rs index 2a0f9bab..45df99d8 100644 --- a/crates/memtrack/src/allocators/static_linked.rs +++ b/crates/memtrack/src/allocators/static_linked.rs @@ -4,23 +4,6 @@ use std::path::{Path, PathBuf}; use crate::allocators::{AllocatorKind, AllocatorLib}; -/// Check if a file is an ELF binary by reading its magic bytes. -fn is_elf(path: &Path) -> bool { - let mut file = match fs::File::open(path) { - Ok(f) => f, - Err(_) => return false, - }; - - let mut magic = [0u8; 4]; - use std::io::Read; - if file.read_exact(&mut magic).is_err() { - return false; - } - - // ELF magic: 0x7F 'E' 'L' 'F' - magic == [0x7F, b'E', b'L', b'F'] -} - /// Walk upward from current directory to find build directories. /// Returns all found build directories in order of preference. fn find_build_dirs() -> Vec { @@ -61,7 +44,7 @@ fn find_binaries_in_dir(dir: &Path) -> Vec { .into_iter() .flatten() .filter_map(Result::ok) - .filter(|p| p.is_file() && is_elf(p)) + .filter(|p| p.is_file() && super::is_elf(p)) .collect::>() } @@ -107,3 +90,13 @@ pub fn find_all() -> anyhow::Result> { Ok(allocators) } + +impl AllocatorLib { + pub fn from_path_static(path: &Path) -> Result> { + let kind = find_statically_linked_allocator(path).ok_or("No allocator found")?; + Ok(Self { + kind, + path: path.to_path_buf(), + }) + } +} diff --git a/crates/memtrack/src/ebpf/memtrack.rs b/crates/memtrack/src/ebpf/memtrack.rs index 7ef10063..8ed30b68 100644 --- a/crates/memtrack/src/ebpf/memtrack.rs +++ b/crates/memtrack/src/ebpf/memtrack.rs @@ -271,10 +271,31 @@ impl MemtrackBpf { self.try_attach_realloc(lib_path, "realloc"); self.try_attach_free(lib_path, "free"); self.try_attach_aligned_alloc(lib_path, "aligned_alloc"); + self.try_attach_memalign(lib_path, "posix_memalign"); self.try_attach_memalign(lib_path, "memalign"); Ok(()) } + /// 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. + pub 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) + + Ok(()) + } + /// Attach probes for a specific allocator kind. /// This attaches both standard probes (if the allocator exports them) and /// allocator-specific prefixed probes. @@ -290,14 +311,22 @@ impl MemtrackBpf { // Libc only has standard probes, and they must succeed self.attach_libc_probes(lib_path) } + AllocatorKind::LibCpp => { + // libc++ exports C++ operator new/delete symbols + self.attach_libcpp_probes(lib_path) + } AllocatorKind::Jemalloc => { // Try standard names (jemalloc may export these as drop-in replacements) let _ = self.attach_libc_probes(lib_path); + // Try C++ operators (jemalloc exports these for C++ programs) + let _ = self.attach_libcpp_probes(lib_path); self.attach_jemalloc_probes(lib_path) } AllocatorKind::Mimalloc => { // Try standard names (mimalloc may export these as drop-in replacements) let _ = self.attach_libc_probes(lib_path); + // Try C++ operators (mimalloc exports these for C++ programs) + let _ = self.attach_libcpp_probes(lib_path); self.attach_mimalloc_probes(lib_path) } } @@ -311,7 +340,27 @@ impl MemtrackBpf { // - rust_dealloc: _rjem_sdallocx // - rust_realloc: _rjem_realloc / _rjem_rallocx - // Prefixed standard API + // je_*_default API (C++ with static linking) + self.try_attach_malloc(lib_path, "je_malloc_default"); + self.try_attach_malloc(lib_path, "je_mallocx_default"); + self.try_attach_free(lib_path, "je_free_default"); + self.try_attach_free(lib_path, "je_sdallocx_default"); + self.try_attach_realloc(lib_path, "je_realloc_default"); + self.try_attach_realloc(lib_path, "je_rallocx_default"); + self.try_attach_calloc(lib_path, "je_calloc_default"); + + // je_* API (internal jemalloc functions, static linking) + self.try_attach_malloc(lib_path, "je_malloc"); + self.try_attach_malloc(lib_path, "je_mallocx"); + self.try_attach_calloc(lib_path, "je_calloc"); + self.try_attach_realloc(lib_path, "je_realloc"); + self.try_attach_realloc(lib_path, "je_rallocx"); + self.try_attach_aligned_alloc(lib_path, "je_aligned_alloc"); + self.try_attach_memalign(lib_path, "je_memalign"); + self.try_attach_free(lib_path, "je_free"); + self.try_attach_free(lib_path, "je_sdallocx"); + + // _rjem_* API (Rust jemalloc crate, dynamic linking) self.try_attach_malloc(lib_path, "_rjem_malloc"); self.try_attach_malloc(lib_path, "_rjem_mallocx"); // Also used for `calloc` self.try_attach_calloc(lib_path, "_rjem_calloc"); diff --git a/crates/memtrack/src/ebpf/tracker.rs b/crates/memtrack/src/ebpf/tracker.rs index 683135a4..950c14a9 100644 --- a/crates/memtrack/src/ebpf/tracker.rs +++ b/crates/memtrack/src/ebpf/tracker.rs @@ -18,22 +18,36 @@ impl Tracker { /// - Attach uprobes to all libc instances /// - Attach tracepoints for fork tracking pub fn new() -> Result { + let mut instance = Self::new_without_allocators()?; + + let allocators = AllocatorLib::find_all()?; + debug!("Found {} allocator instance(s)", allocators.len()); + instance.attach_allocators(&allocators)?; + + Ok(instance) + } + + pub fn new_without_allocators() -> Result { // Bump memlock limits Self::bump_memlock_rlimit()?; let mut bpf = MemtrackBpf::new()?; bpf.attach_tracepoints()?; - // Find and attach to all allocators - let allocators = AllocatorLib::find_all()?; - debug!("Found {} allocator instance(s)", allocators.len()); + Ok(Self { bpf }) + } - for allocator in &allocators { - debug!("Attaching uprobes to: {}", allocator.path.display()); - bpf.attach_allocator_probes(allocator.kind, &allocator.path)?; + 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 }) + Ok(()) + } + + pub fn attach_allocator(&mut self, lib: &AllocatorLib) -> Result<()> { + self.bpf.attach_allocator_probes(lib.kind, &lib.path) } /// Start tracking allocations for a specific PID diff --git a/crates/memtrack/testdata/alloc_cpp/.gitignore b/crates/memtrack/testdata/alloc_cpp/.gitignore new file mode 100644 index 00000000..567609b1 --- /dev/null +++ b/crates/memtrack/testdata/alloc_cpp/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/crates/memtrack/testdata/alloc_cpp/CMakeLists.txt b/crates/memtrack/testdata/alloc_cpp/CMakeLists.txt new file mode 100644 index 00000000..1811a71f --- /dev/null +++ b/crates/memtrack/testdata/alloc_cpp/CMakeLists.txt @@ -0,0 +1,171 @@ +# This file is automatically generated from cmake.toml - DO NOT EDIT +# See https://github.com/build-cpp/cmkr for more information + +cmake_minimum_required(VERSION 3.15) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) + message(FATAL_ERROR "In-tree builds are not supported. Run CMake from a separate directory: cmake -B build") +endif() + +set(CMKR_ROOT_PROJECT OFF) +if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + set(CMKR_ROOT_PROJECT ON) + + # Bootstrap cmkr and automatically regenerate CMakeLists.txt + include(cmkr.cmake OPTIONAL RESULT_VARIABLE CMKR_INCLUDE_RESULT) + if(CMKR_INCLUDE_RESULT) + cmkr() + endif() + + # Enable folder support + set_property(GLOBAL PROPERTY USE_FOLDERS ON) + + # Create a configure-time dependency on cmake.toml to improve IDE support + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS cmake.toml) +endif() + +project(alloc_cpp + LANGUAGES + CXX + VERSION + 0.1.0 +) + +# Find static libraries - CMake searches CMAKE_PREFIX_PATH (NixOS) and system paths (Ubuntu) +find_library(MIMALLOC_STATIC_LIB + NAMES libmimalloc.a mimalloc-static + DOC "Static mimalloc library" +) + +find_library(JEMALLOC_STATIC_LIB + NAMES libjemalloc.a jemalloc_pic + DOC "Static jemalloc library" +) + +if(NOT MIMALLOC_STATIC_LIB) + message(STATUS "Static mimalloc not found, will fall back to dynamic linking") +endif() + +if(NOT JEMALLOC_STATIC_LIB) + message(STATUS "Static jemalloc not found, will fall back to dynamic linking") +endif() + +# Target: alloc_cpp_system +set(alloc_cpp_system_SOURCES + cmake.toml + "src/main.cpp" +) + +add_executable(alloc_cpp_system) + +target_sources(alloc_cpp_system PRIVATE ${alloc_cpp_system_SOURCES}) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${alloc_cpp_system_SOURCES}) + +get_directory_property(CMKR_VS_STARTUP_PROJECT DIRECTORY ${PROJECT_SOURCE_DIR} DEFINITION VS_STARTUP_PROJECT) +if(NOT CMKR_VS_STARTUP_PROJECT) + set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT alloc_cpp_system) +endif() + +# Target: alloc_cpp_jemalloc_static +set(alloc_cpp_jemalloc_static_SOURCES + cmake.toml + "src/main.cpp" +) + +add_executable(alloc_cpp_jemalloc_static) + +target_sources(alloc_cpp_jemalloc_static PRIVATE ${alloc_cpp_jemalloc_static_SOURCES}) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${alloc_cpp_jemalloc_static_SOURCES}) + +target_compile_definitions(alloc_cpp_jemalloc_static PRIVATE + USE_JEMALLOC=1 +) + +get_directory_property(CMKR_VS_STARTUP_PROJECT DIRECTORY ${PROJECT_SOURCE_DIR} DEFINITION VS_STARTUP_PROJECT) +if(NOT CMKR_VS_STARTUP_PROJECT) + set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT alloc_cpp_jemalloc_static) +endif() + +set(CMKR_TARGET alloc_cpp_jemalloc_static) +if(JEMALLOC_STATIC_LIB) + target_link_libraries(alloc_cpp_jemalloc_static PRIVATE ${JEMALLOC_STATIC_LIB}) +else() + # Fallback to dynamic linking + target_link_libraries(alloc_cpp_jemalloc_static PRIVATE jemalloc) +endif() + +# Target: alloc_cpp_jemalloc_dynamic +set(alloc_cpp_jemalloc_dynamic_SOURCES + cmake.toml + "src/main.cpp" +) + +add_executable(alloc_cpp_jemalloc_dynamic) + +target_sources(alloc_cpp_jemalloc_dynamic PRIVATE ${alloc_cpp_jemalloc_dynamic_SOURCES}) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${alloc_cpp_jemalloc_dynamic_SOURCES}) + +target_compile_definitions(alloc_cpp_jemalloc_dynamic PRIVATE + USE_JEMALLOC=1 +) + +target_link_libraries(alloc_cpp_jemalloc_dynamic PRIVATE + jemalloc +) + +get_directory_property(CMKR_VS_STARTUP_PROJECT DIRECTORY ${PROJECT_SOURCE_DIR} DEFINITION VS_STARTUP_PROJECT) +if(NOT CMKR_VS_STARTUP_PROJECT) + set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT alloc_cpp_jemalloc_dynamic) +endif() + +# Target: alloc_cpp_mimalloc_static +set(alloc_cpp_mimalloc_static_SOURCES + cmake.toml + "src/main.cpp" +) + +add_executable(alloc_cpp_mimalloc_static) + +target_sources(alloc_cpp_mimalloc_static PRIVATE ${alloc_cpp_mimalloc_static_SOURCES}) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${alloc_cpp_mimalloc_static_SOURCES}) + +target_compile_definitions(alloc_cpp_mimalloc_static PRIVATE + USE_MIMALLOC=1 +) + +get_directory_property(CMKR_VS_STARTUP_PROJECT DIRECTORY ${PROJECT_SOURCE_DIR} DEFINITION VS_STARTUP_PROJECT) +if(NOT CMKR_VS_STARTUP_PROJECT) + set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT alloc_cpp_mimalloc_static) +endif() + +set(CMKR_TARGET alloc_cpp_mimalloc_static) +if(MIMALLOC_STATIC_LIB) + target_link_libraries(alloc_cpp_mimalloc_static PRIVATE ${MIMALLOC_STATIC_LIB}) +else() + # Fallback to dynamic linking + target_link_libraries(alloc_cpp_mimalloc_static PRIVATE mimalloc) +endif() + +# Target: alloc_cpp_mimalloc_dynamic +set(alloc_cpp_mimalloc_dynamic_SOURCES + cmake.toml + "src/main.cpp" +) + +add_executable(alloc_cpp_mimalloc_dynamic) + +target_sources(alloc_cpp_mimalloc_dynamic PRIVATE ${alloc_cpp_mimalloc_dynamic_SOURCES}) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${alloc_cpp_mimalloc_dynamic_SOURCES}) + +target_compile_definitions(alloc_cpp_mimalloc_dynamic PRIVATE + USE_MIMALLOC=1 +) + +target_link_libraries(alloc_cpp_mimalloc_dynamic PRIVATE + mimalloc +) + +get_directory_property(CMKR_VS_STARTUP_PROJECT DIRECTORY ${PROJECT_SOURCE_DIR} DEFINITION VS_STARTUP_PROJECT) +if(NOT CMKR_VS_STARTUP_PROJECT) + set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT alloc_cpp_mimalloc_dynamic) +endif() diff --git a/crates/memtrack/testdata/alloc_cpp/cmake.toml b/crates/memtrack/testdata/alloc_cpp/cmake.toml new file mode 100644 index 00000000..4b7631aa --- /dev/null +++ b/crates/memtrack/testdata/alloc_cpp/cmake.toml @@ -0,0 +1,71 @@ +[project] +name = "alloc_cpp" +version = "0.1.0" +languages = ["CXX"] +cmake-after = """ +# Find static libraries - CMake searches CMAKE_PREFIX_PATH (NixOS) and system paths (Ubuntu) +find_library(MIMALLOC_STATIC_LIB + NAMES libmimalloc.a mimalloc-static + DOC "Static mimalloc library" +) + +find_library(JEMALLOC_STATIC_LIB + NAMES libjemalloc.a jemalloc_pic + DOC "Static jemalloc library" +) + +if(NOT MIMALLOC_STATIC_LIB) + message(STATUS "Static mimalloc not found, will fall back to dynamic linking") +endif() + +if(NOT JEMALLOC_STATIC_LIB) + message(STATUS "Static jemalloc not found, will fall back to dynamic linking") +endif() +""" + +# System allocator (libc default malloc) +[target.alloc_cpp_system] +type = "executable" +sources = ["src/main.cpp"] + +# Jemalloc - static linking +[target.alloc_cpp_jemalloc_static] +type = "executable" +sources = ["src/main.cpp"] +compile-definitions = ["USE_JEMALLOC=1"] +cmake-after = """ +if(JEMALLOC_STATIC_LIB) + target_link_libraries(alloc_cpp_jemalloc_static PRIVATE ${JEMALLOC_STATIC_LIB}) +else() + # Fallback to dynamic linking + target_link_libraries(alloc_cpp_jemalloc_static PRIVATE jemalloc) +endif() +""" + +# Jemalloc - dynamic linking +[target.alloc_cpp_jemalloc_dynamic] +type = "executable" +sources = ["src/main.cpp"] +compile-definitions = ["USE_JEMALLOC=1"] +link-libraries = ["jemalloc"] + +# Mimalloc - static linking +[target.alloc_cpp_mimalloc_static] +type = "executable" +sources = ["src/main.cpp"] +compile-definitions = ["USE_MIMALLOC=1"] +cmake-after = """ +if(MIMALLOC_STATIC_LIB) + target_link_libraries(alloc_cpp_mimalloc_static PRIVATE ${MIMALLOC_STATIC_LIB}) +else() + # Fallback to dynamic linking + target_link_libraries(alloc_cpp_mimalloc_static PRIVATE mimalloc) +endif() +""" + +# Mimalloc - dynamic linking +[target.alloc_cpp_mimalloc_dynamic] +type = "executable" +sources = ["src/main.cpp"] +compile-definitions = ["USE_MIMALLOC=1"] +link-libraries = ["mimalloc"] diff --git a/crates/memtrack/testdata/alloc_cpp/cmkr.cmake b/crates/memtrack/testdata/alloc_cpp/cmkr.cmake new file mode 100644 index 00000000..6e88a86b --- /dev/null +++ b/crates/memtrack/testdata/alloc_cpp/cmkr.cmake @@ -0,0 +1,260 @@ +include_guard() + +# Change these defaults to point to your infrastructure if desired +set(CMKR_REPO "https://github.com/build-cpp/cmkr" CACHE STRING "cmkr git repository" FORCE) +set(CMKR_TAG "v0.2.45" CACHE STRING "cmkr git tag (this needs to be available forever)" FORCE) +set(CMKR_COMMIT_HASH "" CACHE STRING "cmkr git commit hash (optional)" FORCE) + +# To bootstrap/generate a cmkr project: cmake -P cmkr.cmake +if(CMAKE_SCRIPT_MODE_FILE) + set(CMAKE_BINARY_DIR "${CMAKE_BINARY_DIR}/build") + set(CMAKE_CURRENT_BINARY_DIR "${CMAKE_BINARY_DIR}") + file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}") +endif() + +# Set these from the command line to customize for development/debugging purposes +set(CMKR_EXECUTABLE "" CACHE FILEPATH "cmkr executable") +set(CMKR_SKIP_GENERATION OFF CACHE BOOL "skip automatic cmkr generation") +set(CMKR_BUILD_TYPE "Debug" CACHE STRING "cmkr build configuration") +mark_as_advanced(CMKR_REPO CMKR_TAG CMKR_COMMIT_HASH CMKR_EXECUTABLE CMKR_SKIP_GENERATION CMKR_BUILD_TYPE) + +# Disable cmkr if generation is disabled +if(DEFINED ENV{CI} OR CMKR_SKIP_GENERATION OR CMKR_BUILD_SKIP_GENERATION) + message(STATUS "[cmkr] Skipping automatic cmkr generation") + unset(CMKR_BUILD_SKIP_GENERATION CACHE) + macro(cmkr) + endmacro() + return() +endif() + +# Disable cmkr if no cmake.toml file is found +if(NOT CMAKE_SCRIPT_MODE_FILE AND NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake.toml") + message(AUTHOR_WARNING "[cmkr] Not found: ${CMAKE_CURRENT_SOURCE_DIR}/cmake.toml") + macro(cmkr) + endmacro() + return() +endif() + +# Convert a Windows native path to CMake path +if(CMKR_EXECUTABLE MATCHES "\\\\") + string(REPLACE "\\" "/" CMKR_EXECUTABLE_CMAKE "${CMKR_EXECUTABLE}") + set(CMKR_EXECUTABLE "${CMKR_EXECUTABLE_CMAKE}" CACHE FILEPATH "" FORCE) + unset(CMKR_EXECUTABLE_CMAKE) +endif() + +# Helper macro to execute a process (COMMAND_ERROR_IS_FATAL ANY is 3.19 and higher) +function(cmkr_exec) + execute_process(COMMAND ${ARGV} RESULT_VARIABLE CMKR_EXEC_RESULT) + if(NOT CMKR_EXEC_RESULT EQUAL 0) + message(FATAL_ERROR "cmkr_exec(${ARGV}) failed (exit code ${CMKR_EXEC_RESULT})") + endif() +endfunction() + +# Windows-specific hack (CMAKE_EXECUTABLE_PREFIX is not set at the moment) +if(WIN32) + set(CMKR_EXECUTABLE_NAME "cmkr.exe") +else() + set(CMKR_EXECUTABLE_NAME "cmkr") +endif() + +# Use cached cmkr if found +if(DEFINED ENV{CMKR_CACHE}) + set(CMKR_DIRECTORY_PREFIX "$ENV{CMKR_CACHE}") + string(REPLACE "\\" "/" CMKR_DIRECTORY_PREFIX "${CMKR_DIRECTORY_PREFIX}") + if(CMKR_DIRECTORY_PREFIX MATCHES "^~") + if(WIN32) + string(REGEX REPLACE "^~" "$ENV{USERPROFILE}" CMKR_DIRECTORY_PREFIX "${CMKR_DIRECTORY_PREFIX}") + elseif(UNIX) + string(REGEX REPLACE "^~" "$ENV{HOME}" CMKR_DIRECTORY_PREFIX "${CMKR_DIRECTORY_PREFIX}") + endif() + endif() + if(NOT CMKR_DIRECTORY_PREFIX MATCHES "\\/$") + set(CMKR_DIRECTORY_PREFIX "${CMKR_DIRECTORY_PREFIX}/") + endif() + # Build in release mode for the cache + set(CMKR_BUILD_TYPE "Release") +else() + set(CMKR_DIRECTORY_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/_cmkr_") +endif() +set(CMKR_DIRECTORY "${CMKR_DIRECTORY_PREFIX}${CMKR_TAG}") +set(CMKR_CACHED_EXECUTABLE "${CMKR_DIRECTORY}/bin/${CMKR_EXECUTABLE_NAME}") + +# Helper function to check if a string starts with a prefix +# Cannot use MATCHES, see: https://github.com/build-cpp/cmkr/issues/61 +function(cmkr_startswith str prefix result) + string(LENGTH "${prefix}" prefix_length) + string(LENGTH "${str}" str_length) + if(prefix_length LESS_EQUAL str_length) + string(SUBSTRING "${str}" 0 ${prefix_length} str_prefix) + if(prefix STREQUAL str_prefix) + set("${result}" ON PARENT_SCOPE) + return() + endif() + endif() + set("${result}" OFF PARENT_SCOPE) +endfunction() + +# Handle upgrading logic +if(CMKR_EXECUTABLE AND NOT CMKR_CACHED_EXECUTABLE STREQUAL CMKR_EXECUTABLE) + cmkr_startswith("${CMKR_EXECUTABLE}" "${CMAKE_CURRENT_BINARY_DIR}/_cmkr" CMKR_STARTSWITH_BUILD) + cmkr_startswith("${CMKR_EXECUTABLE}" "${CMKR_DIRECTORY_PREFIX}" CMKR_STARTSWITH_CACHE) + if(CMKR_STARTSWITH_BUILD) + if(DEFINED ENV{CMKR_CACHE}) + message(AUTHOR_WARNING "[cmkr] Switching to cached cmkr: '${CMKR_CACHED_EXECUTABLE}'") + if(EXISTS "${CMKR_CACHED_EXECUTABLE}") + set(CMKR_EXECUTABLE "${CMKR_CACHED_EXECUTABLE}" CACHE FILEPATH "Full path to cmkr executable" FORCE) + else() + unset(CMKR_EXECUTABLE CACHE) + endif() + else() + message(AUTHOR_WARNING "[cmkr] Upgrading '${CMKR_EXECUTABLE}' to '${CMKR_CACHED_EXECUTABLE}'") + unset(CMKR_EXECUTABLE CACHE) + endif() + elseif(DEFINED ENV{CMKR_CACHE} AND CMKR_STARTSWITH_CACHE) + message(AUTHOR_WARNING "[cmkr] Upgrading cached '${CMKR_EXECUTABLE}' to '${CMKR_CACHED_EXECUTABLE}'") + unset(CMKR_EXECUTABLE CACHE) + endif() +endif() + +if(CMKR_EXECUTABLE AND EXISTS "${CMKR_EXECUTABLE}") + message(VERBOSE "[cmkr] Found cmkr: '${CMKR_EXECUTABLE}'") +elseif(CMKR_EXECUTABLE AND NOT CMKR_EXECUTABLE STREQUAL CMKR_CACHED_EXECUTABLE) + message(FATAL_ERROR "[cmkr] '${CMKR_EXECUTABLE}' not found") +elseif(NOT CMKR_EXECUTABLE AND EXISTS "${CMKR_CACHED_EXECUTABLE}") + set(CMKR_EXECUTABLE "${CMKR_CACHED_EXECUTABLE}" CACHE FILEPATH "Full path to cmkr executable" FORCE) + message(STATUS "[cmkr] Found cached cmkr: '${CMKR_EXECUTABLE}'") +else() + set(CMKR_EXECUTABLE "${CMKR_CACHED_EXECUTABLE}" CACHE FILEPATH "Full path to cmkr executable" FORCE) + message(VERBOSE "[cmkr] Bootstrapping '${CMKR_EXECUTABLE}'") + + message(STATUS "[cmkr] Fetching cmkr...") + if(EXISTS "${CMKR_DIRECTORY}") + cmkr_exec("${CMAKE_COMMAND}" -E rm -rf "${CMKR_DIRECTORY}") + endif() + find_package(Git QUIET REQUIRED) + cmkr_exec("${GIT_EXECUTABLE}" + clone + --config advice.detachedHead=false + --branch ${CMKR_TAG} + --depth 1 + ${CMKR_REPO} + "${CMKR_DIRECTORY}" + ) + if(CMKR_COMMIT_HASH) + execute_process( + COMMAND "${GIT_EXECUTABLE}" checkout -q "${CMKR_COMMIT_HASH}" + RESULT_VARIABLE CMKR_EXEC_RESULT + WORKING_DIRECTORY "${CMKR_DIRECTORY}" + ) + if(NOT CMKR_EXEC_RESULT EQUAL 0) + message(FATAL_ERROR "Tag '${CMKR_TAG}' hash is not '${CMKR_COMMIT_HASH}'") + endif() + endif() + message(STATUS "[cmkr] Building cmkr (using system compiler)...") + cmkr_exec("${CMAKE_COMMAND}" + --no-warn-unused-cli + "${CMKR_DIRECTORY}" + "-B${CMKR_DIRECTORY}/build" + "-DCMAKE_BUILD_TYPE=${CMKR_BUILD_TYPE}" + "-DCMAKE_UNITY_BUILD=ON" + "-DCMAKE_INSTALL_PREFIX=${CMKR_DIRECTORY}" + "-DCMKR_GENERATE_DOCUMENTATION=OFF" + ) + cmkr_exec("${CMAKE_COMMAND}" + --build "${CMKR_DIRECTORY}/build" + --config "${CMKR_BUILD_TYPE}" + --parallel + ) + cmkr_exec("${CMAKE_COMMAND}" + --install "${CMKR_DIRECTORY}/build" + --config "${CMKR_BUILD_TYPE}" + --prefix "${CMKR_DIRECTORY}" + --component cmkr + ) + if(NOT EXISTS ${CMKR_EXECUTABLE}) + message(FATAL_ERROR "[cmkr] Failed to bootstrap '${CMKR_EXECUTABLE}'") + endif() + cmkr_exec("${CMKR_EXECUTABLE}" version) + message(STATUS "[cmkr] Bootstrapped ${CMKR_EXECUTABLE}") +endif() +execute_process(COMMAND "${CMKR_EXECUTABLE}" version + RESULT_VARIABLE CMKR_EXEC_RESULT +) +if(NOT CMKR_EXEC_RESULT EQUAL 0) + message(FATAL_ERROR "[cmkr] Failed to get version, try clearing the cache and rebuilding") +endif() + +# Use cmkr.cmake as a script +if(CMAKE_SCRIPT_MODE_FILE) + if(NOT EXISTS "${CMAKE_SOURCE_DIR}/cmake.toml") + execute_process(COMMAND "${CMKR_EXECUTABLE}" init + RESULT_VARIABLE CMKR_EXEC_RESULT + ) + if(NOT CMKR_EXEC_RESULT EQUAL 0) + message(FATAL_ERROR "[cmkr] Failed to bootstrap cmkr project. Please report an issue: https://github.com/build-cpp/cmkr/issues/new") + else() + message(STATUS "[cmkr] Modify cmake.toml and then configure using: cmake -B build") + endif() + else() + execute_process(COMMAND "${CMKR_EXECUTABLE}" gen + RESULT_VARIABLE CMKR_EXEC_RESULT + ) + if(NOT CMKR_EXEC_RESULT EQUAL 0) + message(FATAL_ERROR "[cmkr] Failed to generate project.") + else() + message(STATUS "[cmkr] Configure using: cmake -B build") + endif() + endif() +endif() + +# This is the macro that contains black magic +macro(cmkr) + # When this macro is called from the generated file, fake some internal CMake variables + get_source_file_property(CMKR_CURRENT_LIST_FILE "${CMAKE_CURRENT_LIST_FILE}" CMKR_CURRENT_LIST_FILE) + if(CMKR_CURRENT_LIST_FILE) + set(CMAKE_CURRENT_LIST_FILE "${CMKR_CURRENT_LIST_FILE}") + get_filename_component(CMAKE_CURRENT_LIST_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) + endif() + + # File-based include guard (include_guard is not documented to work) + get_source_file_property(CMKR_INCLUDE_GUARD "${CMAKE_CURRENT_LIST_FILE}" CMKR_INCLUDE_GUARD) + if(NOT CMKR_INCLUDE_GUARD) + set_source_files_properties("${CMAKE_CURRENT_LIST_FILE}" PROPERTIES CMKR_INCLUDE_GUARD TRUE) + + file(SHA256 "${CMAKE_CURRENT_LIST_FILE}" CMKR_LIST_FILE_SHA256_PRE) + + # Generate CMakeLists.txt + cmkr_exec("${CMKR_EXECUTABLE}" gen + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + ) + + file(SHA256 "${CMAKE_CURRENT_LIST_FILE}" CMKR_LIST_FILE_SHA256_POST) + + # Delete the temporary file if it was left for some reason + set(CMKR_TEMP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/CMakerLists.txt") + if(EXISTS "${CMKR_TEMP_FILE}") + file(REMOVE "${CMKR_TEMP_FILE}") + endif() + + if(NOT CMKR_LIST_FILE_SHA256_PRE STREQUAL CMKR_LIST_FILE_SHA256_POST) + # Copy the now-generated CMakeLists.txt to CMakerLists.txt + # This is done because you cannot include() a file you are currently in + configure_file(CMakeLists.txt "${CMKR_TEMP_FILE}" COPYONLY) + + # Add the macro required for the hack at the start of the cmkr macro + set_source_files_properties("${CMKR_TEMP_FILE}" PROPERTIES + CMKR_CURRENT_LIST_FILE "${CMAKE_CURRENT_LIST_FILE}" + ) + + # 'Execute' the newly-generated CMakeLists.txt + include("${CMKR_TEMP_FILE}") + + # Delete the generated file + file(REMOVE "${CMKR_TEMP_FILE}") + + # Do not execute the rest of the original CMakeLists.txt + return() + endif() + # Resume executing the unmodified CMakeLists.txt + endif() +endmacro() diff --git a/crates/memtrack/testdata/alloc_cpp/justfile b/crates/memtrack/testdata/alloc_cpp/justfile new file mode 100644 index 00000000..a0a2f179 --- /dev/null +++ b/crates/memtrack/testdata/alloc_cpp/justfile @@ -0,0 +1,65 @@ +# C++ Allocator Test Fixture Build System + +# Default recipe - show available commands +default: + @just --list + +# Configure the build with CMake +configure: + cmake -B build -DCMAKE_BUILD_TYPE=Release + +# Clean build directory +clean: + rm -rf build + +# Reconfigure from scratch +reconfigure: clean configure + +# Build a specific target +build target: + cmake --build build --target {{target}} -j + +# Build all targets +build-all: configure + cmake --build build -j + +# Build system allocator target +build-system: configure + cmake --build build --target alloc_cpp_system -j + +# Build jemalloc static target +build-jemalloc-static: configure + cmake --build build --target alloc_cpp_jemalloc_static -j + +# Build jemalloc dynamic target +build-jemalloc-dynamic: configure + cmake --build build --target alloc_cpp_jemalloc_dynamic -j + +# Build mimalloc static target +build-mimalloc-static: configure + cmake --build build --target alloc_cpp_mimalloc_static -j + +# Build mimalloc dynamic target +build-mimalloc-dynamic: configure + cmake --build build --target alloc_cpp_mimalloc_dynamic -j + +# Run a specific target binary +run target: (build target) + ./build/{{target}} + +# Run system allocator binary +run-system: build-system + ./build/alloc_cpp_system + +# Run all binaries +run-all: build-all + @echo "Running system allocator..." + ./build/alloc_cpp_system + @echo "\nRunning jemalloc static..." + ./build/alloc_cpp_jemalloc_static + @echo "\nRunning jemalloc dynamic..." + ./build/alloc_cpp_jemalloc_dynamic + @echo "\nRunning mimalloc static..." + ./build/alloc_cpp_mimalloc_static + @echo "\nRunning mimalloc dynamic..." + ./build/alloc_cpp_mimalloc_dynamic diff --git a/crates/memtrack/testdata/alloc_cpp/src/main.cpp b/crates/memtrack/testdata/alloc_cpp/src/main.cpp new file mode 100644 index 00000000..f1edfc77 --- /dev/null +++ b/crates/memtrack/testdata/alloc_cpp/src/main.cpp @@ -0,0 +1,53 @@ +#include +#include +#include +#include +#include + +#ifdef USE_JEMALLOC +#include +#endif + +#ifdef USE_MIMALLOC +#include +#endif + +// Prevent compiler from optimizing away allocations +// Similar to Rust's core::hint::black_box +template +inline void black_box(T* ptr) { + asm volatile("" : : "r,m"(ptr) : "memory"); +} + +int main() { + std::this_thread::sleep_for(std::chrono::seconds(1)); + + auto emit_marker = []() { + uint8_t* ptr = new uint8_t[0xC0D59EED]; + black_box(ptr); + delete[] ptr; + }; + + emit_marker(); + + // array: + uint32_t* allocated = new uint32_t[11111]; + black_box(allocated); + delete[] allocated; + + // single element: + uint64_t* var = new uint64_t; + black_box(var); + delete var; + + // vector: + std::vector vec(22222, 0); + black_box(vec.data()); + + // aligned allocation (64-byte alignment for cache line): + uint8_t* aligned = static_cast(aligned_alloc(64, 64 * 512)); + black_box(aligned); + free(aligned); + + emit_marker(); +} diff --git a/crates/memtrack/testdata/alloc_rust/.gitignore b/crates/memtrack/testdata/alloc_rust/.gitignore new file mode 100644 index 00000000..2f7896d1 --- /dev/null +++ b/crates/memtrack/testdata/alloc_rust/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/crates/memtrack/testdata/alloc_rust/Cargo.lock b/crates/memtrack/testdata/alloc_rust/Cargo.lock new file mode 100644 index 00000000..a221693f --- /dev/null +++ b/crates/memtrack/testdata/alloc_rust/Cargo.lock @@ -0,0 +1,78 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "alloc_rust" +version = "0.1.0" +dependencies = [ + "jemallocator", + "mimalloc", +] + +[[package]] +name = "cc" +version = "1.2.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "jemalloc-sys" +version = "0.5.4+5.3.0-patched" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6c1946e1cea1788cbfde01c993b52a10e2da07f4bac608228d1bed20bfebf2" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "jemallocator" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0de374a9f8e63150e6f5e8a60cc14c668226d7a347d8aee1a45766e3c4dd3bc" +dependencies = [ + "jemalloc-sys", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libmimalloc-sys" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "mimalloc" +version = "0.1.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" +dependencies = [ + "libmimalloc-sys", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" diff --git a/crates/memtrack/testdata/alloc_rust/Cargo.toml b/crates/memtrack/testdata/alloc_rust/Cargo.toml new file mode 100644 index 00000000..e06594c5 --- /dev/null +++ b/crates/memtrack/testdata/alloc_rust/Cargo.toml @@ -0,0 +1,19 @@ +[workspace] + +[package] +name = "alloc_rust" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "alloc_rust" +path = "src/main.rs" + +[dependencies] +jemallocator = { version = "0.5", optional = true } +mimalloc = { version = "0.1", optional = true } + +[features] +default = [] +with-jemalloc = ["jemallocator"] +with-mimalloc = ["mimalloc"] diff --git a/crates/memtrack/testdata/alloc_rust/src/main.rs b/crates/memtrack/testdata/alloc_rust/src/main.rs new file mode 100644 index 00000000..42088656 --- /dev/null +++ b/crates/memtrack/testdata/alloc_rust/src/main.rs @@ -0,0 +1,73 @@ +use std::alloc::GlobalAlloc; + +#[cfg(feature = "with-mimalloc")] +#[global_allocator] +pub static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +#[cfg(feature = "with-jemalloc")] +#[global_allocator] +pub static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; + +#[cfg(not(any(feature = "with-mimalloc", feature = "with-jemalloc")))] +#[global_allocator] +static GLOBAL: std::alloc::System = std::alloc::System; + +fn main() -> Result<(), Box> { + std::thread::sleep(std::time::Duration::from_secs(1)); + + // All the functions exposed by the GlobalAlloc trait (https://doc.rust-lang.org/std/alloc/trait.GlobalAlloc.html) + // IMPORTANT: We need the `black_box` to avoid LLVM from optimizing away our allocations + + let emit_marker = || unsafe { + let layout = std::alloc::Layout::array::(0xC0D59EED).unwrap(); + let ptr = GLOBAL.alloc(layout); + core::hint::black_box(ptr); + GLOBAL.dealloc(ptr, layout); + }; + + emit_marker(); + + // alloc (array) + unsafe { + let layout = std::alloc::Layout::array::(4321)?; + let ptr = GLOBAL.alloc(layout); + core::hint::black_box(ptr); + GLOBAL.dealloc(ptr, layout); + } + + // alloc zeroed (array) + unsafe { + let layout = std::alloc::Layout::array::(1234)?; + let ptr = GLOBAL.alloc_zeroed(layout); + core::hint::black_box(ptr); + GLOBAL.dealloc(ptr, layout); + } + + // alloc (single value) + unsafe { + let layout = std::alloc::Layout::new::(); + let ptr = GLOBAL.alloc(layout); + core::hint::black_box(ptr); + GLOBAL.dealloc(ptr, layout); + } + + // realloc (allocate new size, copy data, deallocate old) + unsafe { + let old_layout = std::alloc::Layout::array::(1111)?; + let old_ptr = GLOBAL.alloc(old_layout); + + // Write some data to the old allocation + std::ptr::write_bytes(old_ptr, 0x42, 1111); + core::hint::black_box(old_ptr); + + // Reallocate to a larger size + let new_ptr = GLOBAL.realloc(old_ptr, old_layout, 2222); + + core::hint::black_box(new_ptr); + GLOBAL.dealloc(new_ptr, std::alloc::Layout::array::(2222)?); + } + + emit_marker(); + + Ok(()) +} diff --git a/crates/memtrack/tests/integration_test.rs b/crates/memtrack/tests/c_tests.rs similarity index 80% rename from crates/memtrack/tests/integration_test.rs rename to crates/memtrack/tests/c_tests.rs index 1834fc81..63b86dc4 100644 --- a/crates/memtrack/tests/integration_test.rs +++ b/crates/memtrack/tests/c_tests.rs @@ -1,36 +1,12 @@ -use libbpf_rs::ErrorExt; -use memtrack::{Event, EventType, Tracker}; +mod shared; + +use memtrack::EventType; use rstest::rstest; use std::fs; use std::path::Path; use std::process::Command; -use std::time::Duration; use tempfile::TempDir; -pub fn track_binary(binary: &Path) -> anyhow::Result<(Vec, std::thread::JoinHandle<()>)> { - let child = Command::new(binary) - .spawn() - .context("Failed to spawn command")?; - let root_pid = child.id() as i32; - - let mut tracker = Tracker::new()?; - tracker.enable()?; - let rx = tracker.track(root_pid)?; - - let mut events = Vec::new(); - while let Ok(event) = rx.recv_timeout(Duration::from_secs(10)) { - events.push(event); - } - - // Drop the tracker in a new thread to not block the test - let thread_handle = std::thread::spawn(move || core::mem::drop(tracker)); - - eprintln!("Tracked {} events", events.len()); - eprintln!("Events: {events:#?}"); - - Ok((events, thread_handle)) -} - /// Compiles C source code and returns the binary path fn compile_c_source( source_code: &str, @@ -55,11 +31,6 @@ fn compile_c_source( Ok(binary_path) } -/// Helper to count events of a specific type -fn count_events_by_type(events: &[Event], event_type: EventType) -> usize { - events.iter().filter(|e| e.event_type == event_type).count() -} - // ============================================================================ // PARAMETERIZED ALLOCATION TESTS // ============================================================================ @@ -129,10 +100,10 @@ fn test_allocation_tracking( let temp_dir = TempDir::new()?; let binary = compile_c_source(test_case.source, test_case.name, temp_dir.path())?; - let (events, thread_handle) = track_binary(&binary)?; + let (events, thread_handle) = shared::track_binary(&binary)?; for (event_type, expected_count) in test_case.assertions { - let actual_count = count_events_by_type(&events, *event_type); + let actual_count = shared::count_events_by_type(&events, *event_type); if test_case.allow_excess { assert!( @@ -163,10 +134,10 @@ fn test_fork_tracking() -> Result<(), Box> { let source = include_str!("../testdata/fork_test.c"); let binary = compile_c_source(source, "fork_test", temp_dir.path())?; - let (events, thread_handle) = track_binary(&binary)?; + let (events, thread_handle) = shared::track_binary(&binary)?; - let malloc_count = count_events_by_type(&events, EventType::Malloc); - let free_count = count_events_by_type(&events, EventType::Free); + let malloc_count = shared::count_events_by_type(&events, EventType::Malloc); + let free_count = shared::count_events_by_type(&events, EventType::Free); // Should have at least 2 mallocs (parent + child) assert!( @@ -198,7 +169,7 @@ fn test_allocation_sizes() -> Result<(), Box> { let source = include_str!("../testdata/alloc_size.c"); let binary = compile_c_source(source, "alloc_size", temp_dir.path())?; - let (events, thread_handle) = track_binary(&binary)?; + let (events, thread_handle) = shared::track_binary(&binary)?; // Filter malloc events and collect their sizes let malloc_events: Vec = events @@ -227,7 +198,7 @@ fn test_allocation_sizes() -> Result<(), Box> { } // Check that we have 4 free events - let free_count = count_events_by_type(&events, EventType::Free); + let free_count = shared::count_events_by_type(&events, EventType::Free); assert_eq!(free_count, 4, "Expected 4 free events, got {free_count}"); thread_handle.join().unwrap(); diff --git a/crates/memtrack/tests/cpp_tests.rs b/crates/memtrack/tests/cpp_tests.rs new file mode 100644 index 00000000..6dd8623d --- /dev/null +++ b/crates/memtrack/tests/cpp_tests.rs @@ -0,0 +1,65 @@ +#[macro_use] +mod shared; + +use memtrack::AllocatorLib; +use rstest::rstest; +use std::path::Path; +use std::process::Command; + +fn compile_cpp_project(project_dir: &Path, target: &str) -> anyhow::Result { + // Configure with cmake -B build + let config = Command::new("cmake") + .current_dir(project_dir) + .args(["-B", "build", "-DCMAKE_BUILD_TYPE=Release"]) + .output()?; + + if !config.status.success() { + eprintln!( + "cmake configure failed: {}", + String::from_utf8_lossy(&config.stderr) + ); + return Err(anyhow::anyhow!("Failed to configure C++ project")); + } + + // Build specific target + let build = Command::new("cmake") + .current_dir(project_dir) + .args(["--build", "build", "--target", target, "-j"]) + .output()?; + + if !build.status.success() { + eprintln!( + "cmake build failed: {}", + String::from_utf8_lossy(&build.stderr) + ); + eprintln!("cmake stdout: {}", String::from_utf8_lossy(&build.stdout)); + return Err(anyhow::anyhow!("Failed to build target: {target}")); + } + + let binary_path = project_dir.join(format!("build/{target}")); + Ok(binary_path) +} + +#[rstest] +#[case("alloc_cpp_system")] +#[case("alloc_cpp_jemalloc_static")] +#[case("alloc_cpp_jemalloc_dynamic")] +#[case("alloc_cpp_mimalloc_static")] +#[case("alloc_cpp_mimalloc_dynamic")] +#[test_log::test] +fn test_cpp_alloc_tracking(#[case] target: &str) -> Result<(), Box> { + let project_path = Path::new("testdata/alloc_cpp"); + let binary = compile_cpp_project(project_path, target)?; + + // Try to find a static allocator in the binary, then attach to it as well + // This is needed because the CWD is different, which breaks the heuristics. + let allocators = AllocatorLib::from_path_static(&binary) + .map(|a| vec![a]) + .unwrap_or_default(); + + let (events, thread_handle) = shared::track_binary_with_opts(&binary, &allocators)?; + assert_events_with_marker!(target, &events); + + thread_handle.join().unwrap(); + Ok(()) +} diff --git a/crates/memtrack/tests/rust_tests.rs b/crates/memtrack/tests/rust_tests.rs new file mode 100644 index 00000000..0710bcf5 --- /dev/null +++ b/crates/memtrack/tests/rust_tests.rs @@ -0,0 +1,56 @@ +#[macro_use] +mod shared; + +use memtrack::AllocatorLib; +use rstest::rstest; +use std::path::Path; +use std::process::Command; + +fn compile_rust_crate( + crate_dir: &Path, + name: &str, + features: &[&str], +) -> anyhow::Result { + let mut cmd = Command::new("cargo"); + cmd.current_dir(crate_dir) + .args(["build", "--release", "--bin", name]); + + if !features.is_empty() { + cmd.arg("--features").arg(features.join(",")); + } + + let output = cmd.output()?; + if !output.status.success() { + eprintln!("cargo stderr: {}", String::from_utf8_lossy(&output.stderr)); + eprintln!("cargo stdout: {}", String::from_utf8_lossy(&output.stdout)); + return Err(anyhow::anyhow!("Failed to compile Rust crate")); + } + + let binary_path = crate_dir.join(format!("target/release/{name}")); + Ok(binary_path) +} + +#[rstest] +#[case("system", &[])] +#[case("jemalloc", &["with-jemalloc"])] +#[case("mimalloc", &["with-mimalloc"])] +#[test_log::test] +fn test_rust_alloc_tracking( + #[case] name: &str, + #[case] features: &[&str], +) -> Result<(), Box> { + let crate_path = Path::new("testdata/alloc_rust"); + let binary = compile_rust_crate(crate_path, "alloc_rust", features)?; + + // Try to find a static allocator in the binary, then attach to it as well + // This is needed because the CWD is different, which breaks the heuristics. + let allocators = AllocatorLib::from_path_static(&binary) + .map(|a| vec![a]) + .unwrap_or_default(); + + let (events, thread_handle) = shared::track_binary_with_opts(&binary, &allocators)?; + assert_events_with_marker!(name, &events); + + thread_handle.join().unwrap(); + Ok(()) +} diff --git a/crates/memtrack/tests/shared.rs b/crates/memtrack/tests/shared.rs new file mode 100644 index 00000000..86eacf81 --- /dev/null +++ b/crates/memtrack/tests/shared.rs @@ -0,0 +1,73 @@ +#![allow(dead_code, unused)] + +use anyhow::Context; +use memtrack::prelude::*; +use memtrack::{AllocatorLib, Event, EventType}; +use std::path::Path; +use std::process::Command; +use std::time::Duration; + +type TrackResult = anyhow::Result<(Vec, std::thread::JoinHandle<()>)>; + +macro_rules! assert_events_with_marker { + ($name:expr, $events:expr) => {{ + use itertools::Itertools; + + // Dedup events by address and type to remove duplicates + let events = $events + .iter() + .sorted_by_key(|e| e.timestamp) + .dedup_by(|a, b| a.addr == b.addr && a.event_type == b.event_type); + + // Remove events outside our 0xC0D59EED marker allocations + let events = events + .sorted_by_key(|e| e.timestamp) + .skip_while(|e| e.size != 0xC0D59EED) + .skip(2) // Skip the marker allocation and free + .take_while(|e| e.size != 0xC0D59EED) + .collect::>(); + + let formatted_events: Vec = events + .iter() + .map(|e| format!("{:?} size={}", e.event_type, e.size)) + .collect(); + insta::assert_debug_snapshot!($name, formatted_events); + }}; +} + +pub fn track_binary_with_opts(binary: &Path, extra_allocators: &[AllocatorLib]) -> TrackResult { + // IMPORTANT: Always initialize the tracker BEFORE spawning the binary, as it can take some time to + // attach to all the allocator libraries (especially when using NixOS). + let mut tracker = memtrack::Tracker::new()?; + tracker.attach_allocators(extra_allocators)?; + + let child = Command::new(binary) + .spawn() + .context("Failed to spawn command")?; + let root_pid = child.id() as i32; + + tracker.enable()?; + let rx = tracker.track(root_pid)?; + + let mut events = Vec::new(); + while let Ok(event) = rx.recv_timeout(Duration::from_secs(10)) { + events.push(event); + } + + // Drop the tracker in a new thread to not block the test + let thread_handle = std::thread::spawn(move || core::mem::drop(tracker)); + + info!("Tracked {} events", events.len()); + trace!("Events: {events:#?}"); + + Ok((events, thread_handle)) +} + +pub fn track_binary(binary: &Path) -> TrackResult { + track_binary_with_opts(binary, &[]) +} + +/// Helper to count events of a specific type +pub fn count_events_by_type(events: &[Event], event_type: EventType) -> usize { + events.iter().filter(|e| e.event_type == event_type).count() +} diff --git a/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_jemalloc_dynamic.snap b/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_jemalloc_dynamic.snap new file mode 100644 index 00000000..62e4d045 --- /dev/null +++ b/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_jemalloc_dynamic.snap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:188695a0294e080392476a233665d19351906aa9fb5df6f5113723b12761fbca +size 243 diff --git a/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_jemalloc_static.snap b/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_jemalloc_static.snap new file mode 100644 index 00000000..62e4d045 --- /dev/null +++ b/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_jemalloc_static.snap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:188695a0294e080392476a233665d19351906aa9fb5df6f5113723b12761fbca +size 243 diff --git a/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_mimalloc_dynamic.snap b/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_mimalloc_dynamic.snap new file mode 100644 index 00000000..43bcb54d --- /dev/null +++ b/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_mimalloc_dynamic.snap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9346ddad21b7acd56da67921bc01de337365bcb1b9f7f40523ed3458e202fe7a +size 268 diff --git a/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_mimalloc_static.snap b/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_mimalloc_static.snap new file mode 100644 index 00000000..43bcb54d --- /dev/null +++ b/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_mimalloc_static.snap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9346ddad21b7acd56da67921bc01de337365bcb1b9f7f40523ed3458e202fe7a +size 268 diff --git a/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_system.snap b/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_system.snap new file mode 100644 index 00000000..62e4d045 --- /dev/null +++ b/crates/memtrack/tests/snapshots/cpp_tests__alloc_cpp_system.snap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:188695a0294e080392476a233665d19351906aa9fb5df6f5113723b12761fbca +size 243 diff --git a/crates/memtrack/tests/snapshots/rust_tests__jemalloc.snap b/crates/memtrack/tests/snapshots/rust_tests__jemalloc.snap new file mode 100644 index 00000000..7348d73a --- /dev/null +++ b/crates/memtrack/tests/snapshots/rust_tests__jemalloc.snap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b54261bc8d7388c428244cdaa10630a1d3402e7d84bcf5b4e4adbbe37ba054de +size 84 diff --git a/crates/memtrack/tests/snapshots/rust_tests__mimalloc.snap b/crates/memtrack/tests/snapshots/rust_tests__mimalloc.snap new file mode 100644 index 00000000..96a82f11 --- /dev/null +++ b/crates/memtrack/tests/snapshots/rust_tests__mimalloc.snap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b8dc5b5a8ea709521031fe7cf40df9688a7e167575b61fcf86af3a35cc2381e +size 298 diff --git a/crates/memtrack/tests/snapshots/rust_tests__system.snap b/crates/memtrack/tests/snapshots/rust_tests__system.snap new file mode 100644 index 00000000..7417acee --- /dev/null +++ b/crates/memtrack/tests/snapshots/rust_tests__system.snap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4bdc6a792d5a476e8c2e03b9deeb2daf2845033db5295a39ee233d79c5e6025a +size 279