v0.9.2 — Symbolication for Reports
Pre-releasemod-alloc v0.9.2 — Symbolication for Reports
Date: 2026-05-14
Compare: v0.9.1...v0.9.2
Headline
Tier 2 backtraces are now human-readable. With
--features symbolicate (default OFF), the per-call-site report
resolves each raw return address against the running binary's
own debug info, returning function names and (on Linux/macOS)
source file + line. The allocation hot path is untouched; this is
a pure report-generation upgrade.
What's new
Public API additions
ModAlloc::symbolicated_report() -> Vec<SymbolicatedCallSite>
drains the call-site table and resolves each frame. Cached
per-address across calls. Allocates — call from outside the
allocator hook only.SymbolicatedCallSitecarriescount,total_bytes, and
aVec<SymbolicatedFrame>.SymbolicatedFramecarriesaddress,function: Option<String>,
file: Option<PathBuf>,line: Option<u32>, and
inlined: boolto flag expansions from a single physical
return address.
Resolution backends
| Platform | Backend | Resolves |
|---|---|---|
| Linux | addr2line + object (DWARF) |
function, file, line, inlined frames |
| macOS / *BSD | addr2line + object (DWARF) |
function, file, line, inlined frames |
| Windows | pdb |
function name only (file/line deferred) |
All backends share the same return type. Names are demangled via
rustc-demangle. Self-binary path is discovered via
std::env::current_exe.
Storage architecture
- Unix path reads the binary once and
Box::leaks the bytes
soaddr2line::Context(which borrows from the parsed
object::File, which borrows from the bytes) can sit in a
OnceLock<Option<UnixSymbolicator>>with'staticlifetime.
Memory cost: roughly the binary's on-disk size, one-time. - Windows path opens the PDB beside the .exe, walks every
public symbol once, builds a sortedVec<(rva, name)>index,
then drops the PDB. Per-resolve work is a binary search.
Sidesteps PDB's non-Sendinternal types and self-referential
borrows entirely. - Per-process address cache in
symbolicate::reportkeyed by
u64. Cache miss runs the platform symbolicator once;
subsequent calls reuse the cachedVec<SymbolicatedFrame>.
Mutex-protectedHashMap.
Dependency policy
The zero-runtime-deps promise applies to the allocation hot path
and the default build. Symbolication is an explicitly out-of-band
report-generation activity, so the symbolicate feature is the
approved exception. New optional deps:
| Crate | Version | Why |
|---|---|---|
addr2line |
0.21 |
DWARF address → name/file/line |
object |
0.32 |
ELF / Mach-O parser, addr2line backend |
rustc-demangle |
0.1 |
Rust symbol demangler |
pdb |
0.8 |
Windows PDB reader |
uuid |
=1.10.0 |
Pinned to hold MSRV 1.75 (transitive) |
All are pure-Rust, MSRV 1.75-compatible. No FFI. Documented in
.dev/DIRECTIVES.md section 2.2.
The uuid pin is load-bearing: pdb 0.8 pulls uuid in
transitively, and the latest uuid 1.x requires Rust 1.85. The
pinned 1.10.0 supports MSRV 1.63.
Test additions
tests/symbolicate_self.rs— symbolicates the test binary's
own allocations and looks for a known function name in the
resolved frames. Downgrades to a shape-only check when the
test binary was built without debug info.tests/symbolicate_concurrent.rs— 8 threads call
symbolicated_report()simultaneously after a primer call
flushes the main thread's arena. Asserts no deadlock and
consistent row counts across reports.src/symbolicate/self_binary.rsunit tests cover path
resolution and caching.
Example
examples/symbolicate.rs— installsModAlloc, exercises a
few#[inline(never)]call paths, prints the top 10 sites
sorted by total bytes with resolved function names plus any
inlined-frame expansions.
CI
- New CI steps in
.github/workflows/ci.yml:
cargo build --features symbolicate --verboseand
cargo test --features symbolicate --verbose, run on
ubuntu / macos / windows with the FP flag set.
Design notes
Why Box::leak on Unix instead of a self-referential struct
addr2line::Context<R> holds references into object::File,
which holds references into the binary bytes. Storing all three
in a single struct requires self-referential lifetimes that don't
work without ouroboros / yoke or unsafe Pin machinery. For
a profiler that opens its own binary once per process, leaking
the bytes is the simplest correct approach and incurs a one-time
megabyte-scale allocation rather than perpetual transitive crate
deps.
Why a sorted snapshot on Windows instead of holding the PDB
pdb::PDB and its derived AddressMap / SymbolTable /
DebugInformation borrow from each other through trait objects
that aren't auto-marked Send. Storing them together in a static
hits cannot be sent between threads errors. The clean fix is to
do the parse once at first call, copy out the data we need
(Vec<(u32, String)> of (rva, demangled_name)), drop the PDB,
and binary-search on every resolve. PDB's source-line decoding
and S_INLINESITE expansion are deferred to a later release; the
current shipping output is function name plus the raw address.
Why current_exe instead of platform-specific syscalls
std::env::current_exe already wraps readlink /proc/self/exe
on Linux, _NSGetExecutablePath on macOS, and
GetModuleFileNameW on Windows. Reimplementing these adds code
to maintain without any practical benefit; using std directly
keeps the symbolicator module short.
Address-to-RVA approximation on Windows
The captured addresses are absolute virtual addresses. PDB
resolves via RVA (relative virtual address). Without the
module's load base we approximate by masking address to 32
bits and binary-searching the sorted RVA index. For non-ASLR
builds this is exact; for ASLR builds the result is usable for
relative comparison inside the same process run but addresses
across runs will differ.
Migration
Default builds are unchanged. The counters and backtraces
features behave identically. No code edits required for callers
on those paths.
Opting in to symbolicate:
[dependencies]
mod-alloc = { version = "0.9", features = ["symbolicate"] }let report = GLOBAL.symbolicated_report();
for site in &report {
let top = &site.frames[0];
println!("{} allocs at {}",
site.count,
top.function.as_deref().unwrap_or("<unresolved>"));
}backtraces is implied by symbolicate; activating
symbolicate alone is sufficient.
Verification
Full matrix run on Windows host (x86_64):
cargo build(default features)cargo build --no-default-featurescargo build --features counterscargo build --features backtracescargo build --features symbolicatecargo build --features dhat-compatcargo build --all-featurescargo +1.75 build --all-features(MSRV)cargo fmt --all -- --checkcargo clippy --all-targets -- -D warningscargo clippy --all-targets --all-features -- -D warningscargo doc --no-depscargo doc --all-features --no-depscargo test --all-features: 60/60 pass (34 unit + 2 new
v0.9.2 integration + 4 v0.9.1 integration + 4 v0.9.0
integration + 5 smoke + 11 doctests)- Banned-word scan: 0 hits across shipping files
- Em-dash scan: 0 hits across shipping files
CI matrix: ubuntu-latest, macos-latest, windows-latest plus the
ASAN nightly Linux job. New steps for the symbolicate feature.
Limitations
- Windows produces function names only. Source file and line
resolution from PDB is deferred. Inlined-frame expansion from
S_INLINESITEis deferred. - Address-to-RVA approximation on Windows. Best-effort
masking; exact for non-ASLR builds, approximate but
intra-run-consistent for ASLR builds. - Shallow traces on stock builds. When
stdis compiled
without frame pointers, the walker captures only the immediate
caller ofGlobalAlloc::allocinsidestd's allocation
infrastructure. Documented in v0.9.1; symbolication exposes
the limitation more visibly (the resolved frame is often a CRT
helper like__dyn_tls_init_callbackrather than the user's
function). For meaningful traces, build with
cargo +nightly -Z build-std=stdand
RUSTFLAGS="-C force-frame-pointers=yes". - C++ frames remain mangled. Only Rust mangling is decoded
viarustc-demangle. Addingcpp_demangleis a follow-up if
there's demand. fsysintegration deferred. The ROADMAP's mention of
fsysfor reading the binary was not taken:fsys's public
API does not currently support generic-path reads (precedent
set bymod-tempdir's v0.9.3 audit).std::fs::readis used
directly.
Follow-ups
- Windows source-file / line resolution. Adds the PDB
module.line_program()plumbing. Material work but
achievable. - Windows inlined-frame expansion. Decode
S_INLINESITE
records inside per-module symbol streams. - C++ demangling behind the
symbolicatefeature if a real
user needs it. - v0.9.1.1 Tier 2 perf pass (still open from v0.9.1) —
separate milestone, unaffected by symbolication work.
Release ceremony
Standard pattern:
git tag -a v0.9.2 -m "Release v0.9.2 - symbolication for reports"git push origin maingit push origin v0.9.2cargo publish --dry-run --all-featurescargo publish- Confirm
https://crates.io/crates/mod-alloc/0.9.2is live.
GitHub release title: v0.9.2 — Symbolication for Reports.
Tag as pre-release; 1.0.0 waits for v0.9.4 (dev-bench
integration) to ship and bake.
Full Changelog: v0.9.1...v0.9.2