Skip to content

Commit 659acd9

Browse files
committed
feat(memtrack): support dynamically and statically linked allocators
1 parent 5920fa4 commit 659acd9

9 files changed

Lines changed: 419 additions & 90 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/memtrack/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ itertools = { workspace = true }
3333
paste = "1.0"
3434
libbpf-rs = { version = "0.25.0", features = ["vendored"], optional = true }
3535
glob = "0.3.3"
36+
object = { version = "0.36", default-features = false, features = ["read_core", "elf"] }
3637

3738
[build-dependencies]
3839
libbpf-cargo = { version = "0.25.0", optional = true }
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use std::path::PathBuf;
2+
3+
use crate::{AllocatorKind, AllocatorLib};
4+
5+
/// Returns the glob patterns used to find this allocator's shared libraries.
6+
fn get_allocator_paths(lib: &AllocatorKind) -> &'static [&'static str] {
7+
match lib {
8+
AllocatorKind::Libc => &[
9+
// Debian, Ubuntu: Standard Linux multiarch paths
10+
"/lib/*-linux-gnu/libc.so.6",
11+
"/usr/lib/*-linux-gnu/libc.so.6",
12+
// RHEL, Fedora, CentOS, Arch
13+
"/lib*/libc.so.6",
14+
"/usr/lib*/libc.so.6",
15+
// NixOS: find all glibc versions in the Nix store
16+
"/nix/store/*glibc*/lib/libc.so.6",
17+
],
18+
AllocatorKind::Jemalloc => &[
19+
// Debian, Ubuntu: Standard Linux multiarch paths
20+
"/lib/*-linux-gnu/libjemalloc.so*",
21+
"/usr/lib/*-linux-gnu/libjemalloc.so*",
22+
// RHEL, Fedora, CentOS, Arch
23+
"/lib*/libjemalloc.so*",
24+
"/usr/lib*/libjemalloc.so*",
25+
"/usr/local/lib*/libjemalloc.so*",
26+
// NixOS
27+
"/nix/store/*jemalloc*/lib/libjemalloc.so*",
28+
],
29+
AllocatorKind::Mimalloc => &[
30+
// Debian, Ubuntu: Standard Linux multiarch paths
31+
"/lib/*-linux-gnu/libmimalloc.so*",
32+
"/usr/lib/*-linux-gnu/libmimalloc.so*",
33+
// RHEL, Fedora, CentOS, Arch
34+
"/lib*/libmimalloc.so*",
35+
"/usr/lib*/libmimalloc.so*",
36+
"/usr/local/lib*/libmimalloc.so*",
37+
// NixOS
38+
"/nix/store/*mimalloc*/lib/libmimalloc.so*",
39+
],
40+
}
41+
}
42+
43+
/// Find dynamically linked allocator libraries on the system.
44+
pub fn find_all() -> anyhow::Result<Vec<AllocatorLib>> {
45+
use std::collections::HashSet;
46+
47+
let mut results = Vec::new();
48+
let mut seen_paths: HashSet<PathBuf> = HashSet::new();
49+
50+
for kind in AllocatorKind::all() {
51+
let mut found_any = false;
52+
53+
for pattern in get_allocator_paths(kind) {
54+
let paths = glob::glob(pattern)
55+
.ok()
56+
.into_iter()
57+
.flatten()
58+
.filter_map(|p| p.ok())
59+
.filter_map(|p| p.canonicalize().ok())
60+
.filter(|path| {
61+
std::fs::metadata(path)
62+
.map(|m| m.is_file())
63+
.unwrap_or(false)
64+
})
65+
.collect::<Vec<_>>();
66+
67+
for path in paths {
68+
if seen_paths.insert(path.clone()) {
69+
results.push(AllocatorLib { kind: *kind, path });
70+
found_any = true;
71+
}
72+
}
73+
}
74+
75+
// FIXME: Do we still need this?
76+
if kind.is_required() && !found_any {
77+
anyhow::bail!("Could not find required allocator: {}", kind.name());
78+
}
79+
}
80+
81+
Ok(results)
82+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//! Generic allocator discovery infrastructure.
2+
//!
3+
//! This module provides a framework for discovering and attaching to different
4+
//! memory allocators. It's designed to be easily extensible for adding new allocators.
5+
6+
use std::path::PathBuf;
7+
8+
mod dynamic;
9+
mod static_linked;
10+
11+
/// Represents the different allocator types we support.
12+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13+
pub enum AllocatorKind {
14+
/// Standard C library (glibc, musl, etc.)
15+
Libc,
16+
/// jemalloc - used by FreeBSD, Firefox, many Rust projects
17+
Jemalloc,
18+
/// mimalloc - Microsoft's allocator
19+
Mimalloc,
20+
// Future allocators:
21+
// Tcmalloc,
22+
// Hoard,
23+
// Rpmalloc,
24+
}
25+
26+
impl AllocatorKind {
27+
/// Returns all supported allocator kinds.
28+
pub fn all() -> &'static [AllocatorKind] {
29+
&[
30+
AllocatorKind::Libc,
31+
AllocatorKind::Jemalloc,
32+
AllocatorKind::Mimalloc,
33+
]
34+
}
35+
36+
/// Returns a human-readable name for the allocator.
37+
pub fn name(&self) -> &'static str {
38+
match self {
39+
AllocatorKind::Libc => "libc",
40+
AllocatorKind::Jemalloc => "jemalloc",
41+
AllocatorKind::Mimalloc => "mimalloc",
42+
}
43+
}
44+
45+
/// Returns true if this allocator is required (must be found).
46+
pub fn is_required(&self) -> bool {
47+
matches!(self, AllocatorKind::Libc)
48+
}
49+
}
50+
51+
/// Discovered allocator library with its kind and path.
52+
#[derive(Debug, Clone)]
53+
pub struct AllocatorLib {
54+
pub kind: AllocatorKind,
55+
pub path: PathBuf,
56+
}
57+
58+
impl AllocatorLib {
59+
pub fn find_all() -> anyhow::Result<Vec<AllocatorLib>> {
60+
let mut allocators = static_linked::find_all()?;
61+
allocators.extend(dynamic::find_all()?);
62+
Ok(allocators)
63+
}
64+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use anyhow::Context;
2+
use std::collections::HashSet;
3+
use std::fs;
4+
use std::path::{Path, PathBuf};
5+
6+
use crate::allocators::{AllocatorKind, AllocatorLib};
7+
8+
const JEMALLOC_SYMBOLS: &[&str] = &["_rjem_malloc", "_rjem_free"];
9+
const MIMALLOC_SYMBOLS: &[&str] = &["mi_malloc_aligned", "mi_malloc", "mi_free"];
10+
11+
/// Walk upward from current directory to find `target/codspeed/analysis`.
12+
fn find_analysis_dir() -> Option<PathBuf> {
13+
let mut dir = std::env::current_dir().ok()?;
14+
15+
loop {
16+
let candidate = dir.join("target").join("codspeed").join("analysis");
17+
if candidate.is_dir() {
18+
return Some(candidate);
19+
}
20+
if !dir.pop() {
21+
return None;
22+
}
23+
}
24+
}
25+
26+
fn find_binaries_in_dir(dir: &Path) -> Vec<PathBuf> {
27+
glob::glob(&format!("{}/**/*", dir.display()))
28+
.into_iter()
29+
.flatten()
30+
.filter_map(Result::ok)
31+
.filter(|p| p.is_file())
32+
.collect::<Vec<_>>()
33+
}
34+
35+
fn find_statically_linked_allocator(path: &Path) -> Option<AllocatorKind> {
36+
use object::{Object, ObjectSymbol};
37+
38+
let data = fs::read(path).ok()?;
39+
let file = object::File::parse(&*data).ok()?;
40+
41+
let symbols: HashSet<_> = file
42+
.symbols()
43+
.chain(file.dynamic_symbols())
44+
.filter(|s| s.is_definition())
45+
.filter_map(|s| s.name().ok())
46+
.collect();
47+
48+
// FIXME: We dont support multiple statically linked allocators for now
49+
50+
if JEMALLOC_SYMBOLS.iter().any(|s| symbols.contains(s)) {
51+
Some(AllocatorKind::Jemalloc)
52+
} else if MIMALLOC_SYMBOLS.iter().any(|s| symbols.contains(s)) {
53+
Some(AllocatorKind::Mimalloc)
54+
} else {
55+
None
56+
}
57+
}
58+
59+
pub fn find_all() -> anyhow::Result<Vec<AllocatorLib>> {
60+
// 1. Find all the binaries built by CodSpeed
61+
let bins =
62+
find_binaries_in_dir(&find_analysis_dir().context("Failed to find analysis directory")?);
63+
64+
let mut allocators = Vec::new();
65+
for bin in bins {
66+
let Some(kind) = find_statically_linked_allocator(&bin) else {
67+
continue;
68+
};
69+
70+
allocators.push(AllocatorLib {
71+
kind,
72+
path: bin.to_path_buf(),
73+
});
74+
}
75+
76+
Ok(allocators)
77+
}

0 commit comments

Comments
 (0)