v0.9.1 — Inline Backtrace Capture #1
jamesgober
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
mod-alloc v0.9.1 — Inline Backtrace Capture
Date: 2026-05-14
Compare:
v0.9.0...v0.9.1Headline
Tier 2 lands. With
--features backtracesand frame pointersenabled, every tracked allocation captures up to 8 frames of its
call site via inline frame-pointer walking. Per-call-site
aggregation is exposed through a new
ModAlloc::call_sites()API. Zero external dependencies — no
backtracecrate, noaddr2line, nogimli. Symbolication ships inv0.9.2.What's new
Public API additions
unsafe-free hot path additions toGlobalAlloc.alloc,alloc_zeroed, andreallocnow invokebacktrace::record_eventafter the existing counter updatewhen the
backtracesfeature is on.deallocdoes not capture(matches dhat: call sites describe who allocated, not who
freed).
ModAlloc::call_sites() -> Vec<CallSiteStats>drains theper-call-site aggregation table into a vector for inspection
or reporting. Behind
#[cfg(feature = "backtraces")].CallSiteStatspublic type carryingframes: [u64; 8],frame_count: u8,count: u64,total_bytes: u64. Frames areraw return addresses; symbolication lands in
v0.9.2.Capture mechanism
core::arch::asm!reads the FP register (rbponx86_64,
x29on aarch64). Noextern "C"stubs, no build.rscode generation, no
backtracecrate. Stable since Rust 1.59,well under MSRV 1.75.
iteration (null, alignment, in-range, monotonicity, max-frame
cap). The walker is total: for any input bytes it terminates
within 8 iterations without UB. Proven via SplitMix64-driven
random-workload fuzz (10,000 iterations) in
tests/backtrace_fuzz.rs.thread, caches in TLS.
GetCurrentThreadStackLimitsonWindows,
pthread_getattr_npon Linux,pthread_get_stackaddr_npon Darwin / *BSD. All inlineextern "C"/extern "system"; nolibccrate.Storage architecture
VirtualAllocon Windows,mmapon POSIX), 512 events perflush. Flushed synchronously when full, plus on thread exit via
a TLS
Dropimpl.atomic-only hash table. Default 4,096 buckets × 96 bytes ≈
384 KB. Allocated lazily once via raw OS pages on the first
event recorded. Override via
MOD_ALLOC_BUCKETSenv var atprocess start (clamped
[64, 1_048_576], rounded up to nextpower of two).
of input, two ops per word, no memory traffic. Hash value 0 is
remapped to 1 so the bucket's atomic hash field can use 0 as
the "empty" sentinel.
Reentrancy safety
The walker reads memory only inside the cached stack bounds and
only at 16-byte-aligned addresses, which avoids page faults by
construction. The existing
IN_ALLOCreentrancy guard fromv0.9.0covers any pathological allocation triggeredtransitively from inside the backtrace path (e.g. libc lazy-init
during the very first
pthread_getattr_np). No new mechanismintroduced.
Concurrency
Each bucket uses a two-phase publish protocol: CAS on
hashfirst (Release ordering) to claim the slot, then plain stores of
sample_frames, thenframe_count.storewith Release to markthe bucket fully populated. Readers gate on
frame_count > 0after observing a non-zero hash; this prevents torn reads of the
sample frames. Validated by the 32-thread aggregation stress
test (
tests/backtrace_concurrent.rs).Test additions
src/backtrace/*unit tests: hash determinism + clamping +collision floor, walker null / alignment / range / monotonic /
max-frame stops, arena round-trip, table single-site and
multi-site aggregation, raw-mem alloc / free, stack-bounds
discovery sanity.
tests/backtrace_real_chain.rs— captures from a chain of#[inline(never)]functions ending inVec::with_capacity.tests/backtrace_fuzz.rs— SplitMix64 PRNG drives 10,000randomly-sized allocations + occasional drop chains and
format-heavy paths. Verifies the walker is total.
tests/backtrace_concurrent.rs— 32 threads × 1,000 allocationsacross distinct call paths. Verifies the table claim races
resolve without deadlock or drop.
Example
examples/backtraces.rs— installsModAllocas#[global_allocator], runs three known call paths(
alloc_small/alloc_medium/alloc_large), and printsthe top sites by
total_bytes.Tooling
build.rs(one-off approved exception per.dev/DIRECTIVES.mdsection 2.1). Detects whetherRUSTFLAGSincludes
-C force-frame-pointers=yeswhen thebacktracesfeature is on and emits
cargo:warning=if not. Never failsthe build; the walker degrades gracefully.
.cargo/config.tomlin the crate root enables framepointers for the crate's own builds (tests, examples,
doctests). Downstream consumers must set the flag in their own
build.
-Zsanitizer=addresswith-Zbuild-stdon Linux x86_64.Cheap insurance for the unsafe walker path.
Design notes
Per-thread arena is now load-bearing
v0.9.0deferred per-thread buffering because fouru64counters do not benefit from amortising indirection.
v0.9.1captures up to 80 bytes per event (8 × 8-byte frames + size +
frame_count). At that size, going through a global lock-free
queue per allocation would serialise the hot path. The per-
thread arena absorbs 512 events between flushes, amortising the
hash + table-insert cost across the batch.
Why
VirtualAlloc/mmapinstead ofVecThe backtrace path runs inside the alloc hook. Allocating its
own state through
ModAlloc::allocwould recurse (the existingguard catches it, but the storage would silently bypass tracking
and we want clean accounting). Raw OS pages avoid this entirely.
Capture-on-realloc matches dhat
Per maintainer guidance,
realloccaptures all events includingshrinks. This matches dhat's per-event accounting and preserves
v0.9.3's
dhat-compatcompatibility. Documented in the rustdoc.Configuration is runtime, not build-time
MOD_ALLOC_BUCKETSis read at runtime viastd::env::varinside the global table init path. This is intentional: a
build-time constant would force users to recompile to change the
table size, and a
build.rsdoing env-var reads would conflictwith our zero-build-script policy. The runtime read happens once
per process under the reentrancy guard.
Migration
Default builds are unchanged. The
countersTier 1 path isidentical to
v0.9.0. No code edits needed.Users opting in to
backtraces:Without the rustflag,
build.rsemits acargo:warning=and thewalker captures zero or one frame at runtime.
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 dhat-compat(still no-op as documented)cargo build --all-featurescargo +1.75 build --all-features(MSRV)cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo test --all-features: 54/54 pass (32 unit + 4v0.9.1 integration + 4 v0.9.0 integration + 5 smoke + 9
doctests)
comprehensive,robust,seamless,leverage): 0 hits across shipping filesCI matrix unchanged set (ubuntu-latest, macos-latest,
windows-latest) plus new ASAN nightly Linux job.
Measured overhead
cargo run --release --features backtraces --example bench_overhead(1,000,000 × 64-byte alloc / dealloc cycle):
v0.9.0baseline (counters only)v0.9.1withbacktracesNet backtrace overhead: ~1,880 ns per allocation. REPS
section 6 sets the Tier 2 target at <200 ns. We are roughly 10x
over.
Likely contributors (not investigated in this milestone):
read_volatilein the walker prevents reordering — intentionalfor the safety contract, but costly.
thread_local!try_withclosure overhead insideIN_ALLOCand the arena slot access.
AtomicU64stores rather than amemcpy).table::wait_publishedeven when the bucket haslong since been published.
The arithmetic suggests several of these are individually
optimisable; the headline figure is honest end-to-end including
all kernel/OS noise. Treated as a known limitation; an
optimisation pass is filed as a separate milestone (see
"Follow-ups" below) so this release can ship and unblock
downstream work.
Capture quality on stock builds
Default Rust release builds compile
stdwithout frame pointers.The walker correctly stops at the first FP-less frame rather than
crashing, which on a typical workload produces 1-frame traces
(the immediate caller of
GlobalAlloc::allocinsidestd'sallocation infrastructure).
For deep, application-meaningful traces, downstream users need
either:
cargo +nightly -Z build-std=stdwith-C force-frame-pointers=yes(rebuildstdwith FPs), orv0.9.2's symbolication work may explore frame-pointer-lessunwinding (SEH on Windows, DWARF .eh_frame on POSIX) but that is
a separate scoping decision.
Limitations
"Measured overhead" above. Functional correctness verified;
micro-optimisation deferred.
above and in the rustdoc.
with identical 64-bit FxHash values get conflated in the
aggregation table. Statistically rare; not a correctness issue
but a visibility one.
addressed table reaches full load (4,096 distinct sites by
default), new sites are dropped. A
dropped_entriesdiagnosticcounter is planned for
v0.9.2'sDiagnosticsAPI.exit relies on
thread_local!destructor invocation. On abort(
std::process::exit), destructors do not run and the lastN events on each thread remain unflushed. Acceptable; the
global table holds the bulk.
Downstream impact
dev-benchMSRV unblock: still in progress.v0.9.1does notcomplete the dhat replacement (DHAT-format output is
v0.9.3),but it ships the capture mechanism
dev-benchwill eventuallyread from. The overhead figure above means
dev-benchshouldnot enable
backtracesfor every benchmark run; gating itbehind a
bench-with-tracesprofile keeps the default fast.Strategy context:
C:\Dev\libraries\Rust\_strategy\MSRV_AUDIT_DEV_LIBS.md.Follow-ups
listed in "Measured overhead". Suggested as a
v0.9.1.1patchrelease or folded into
v0.9.2's symbolication work.DiagnosticsAPI (Vec<DroppedEntry>, table-full counter,per-bucket fill rate) deferred to
v0.9.2.v0.9.2's entire scope.Release ceremony
Standard pattern:
git tag -a v0.9.1 -m "Release v0.9.1 - inline backtrace capture"git push origin maingit push origin v0.9.1cargo publish --dry-run --all-featurescargo publishhttps://crates.io/crates/mod-alloc/0.9.1is live.GitHub release title:
v0.9.1 — Inline Backtrace Capture.Tag as pre-release;
1.0.0waits forv0.9.4(dev-benchintegration) to ship and bake.
Full Changelog: v0.9.0...v0.9.1
This discussion was created from the release v0.9.1 — Inline Backtrace Capture.
All reactions