Skip to content

Commit c26cb30

Browse files
author
James Gober
committed
Milestone Update v0.9.1
1 parent 7986afd commit c26cb30

18 files changed

Lines changed: 1891 additions & 8 deletions

.cargo/config.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Build configuration for the `mod-alloc` crate itself.
2+
#
3+
# Enables frame pointers for in-crate builds so the `backtraces`
4+
# feature's inline FP walker can capture real frames during the
5+
# crate's own test suite and examples. This file does NOT affect
6+
# downstream consumers; cargo only honours `.cargo/config.toml`
7+
# inside the crate root being built from.
8+
#
9+
# Downstream callers who want backtrace capture must enable frame
10+
# pointers in their OWN build (the `backtraces` feature's build.rs
11+
# emits a warning if they have not).
12+
13+
[build]
14+
rustflags = ["-C", "force-frame-pointers=yes"]

.github/workflows/ci.yml

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,22 @@ jobs:
3030
- name: Build (no default features)
3131
run: cargo build --no-default-features --verbose
3232

33+
- name: Build (backtraces feature)
34+
env:
35+
RUSTFLAGS: "-C force-frame-pointers=yes"
36+
run: cargo build --features backtraces --verbose
37+
- name: Test (backtraces feature)
38+
env:
39+
RUSTFLAGS: "-C force-frame-pointers=yes"
40+
run: cargo test --features backtraces --verbose
41+
3342
- name: Build (all features)
43+
env:
44+
RUSTFLAGS: "-C force-frame-pointers=yes"
3445
run: cargo build --all-features --verbose
3546
- name: Test (all features)
47+
env:
48+
RUSTFLAGS: "-C force-frame-pointers=yes"
3649
run: cargo test --all-features --verbose
3750

3851
clippy:
@@ -73,8 +86,30 @@ jobs:
7386
msrv:
7487
name: MSRV (Rust 1.75)
7588
runs-on: ubuntu-latest
89+
env:
90+
RUSTFLAGS: "-C force-frame-pointers=yes"
7691
steps:
7792
- uses: actions/checkout@v5
7893
- uses: dtolnay/rust-toolchain@1.75
7994
- uses: Swatinem/rust-cache@v2
80-
- run: cargo build --all-features --verbose
95+
- run: cargo build --all-features --verbose
96+
97+
asan:
98+
name: AddressSanitizer (nightly Linux)
99+
runs-on: ubuntu-latest
100+
env:
101+
# ASAN insurance for the unsafe backtrace path. Catches
102+
# out-of-bounds reads in the FP walker that pass the
103+
# mandatory range checks but would still be observable to
104+
# ASAN. Nightly is required because -Zsanitizer is unstable.
105+
RUSTFLAGS: "-Zsanitizer=address -C force-frame-pointers=yes"
106+
RUSTDOCFLAGS: "-Zsanitizer=address"
107+
steps:
108+
- uses: actions/checkout@v5
109+
- uses: dtolnay/rust-toolchain@nightly
110+
with:
111+
components: rust-src
112+
- uses: Swatinem/rust-cache@v2
113+
- name: Test under ASAN
114+
run: |
115+
cargo test --features backtraces --target x86_64-unknown-linux-gnu -Zbuild-std --verbose

CHANGELOG.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,103 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **Tier 2: inline backtrace capture (`backtraces` feature).** Each
13+
tracked allocation, zero-init allocation, and reallocation
14+
captures up to 8 frames of its call site via inline
15+
frame-pointer walking. Available on `x86_64` and `aarch64`.
16+
Other architectures compile but capture is a no-op.
17+
- **`ModAlloc::call_sites()`** drains the per-call-site
18+
aggregation table into a `Vec<CallSiteStats>`. Each row carries
19+
raw return addresses (top of stack first), the number of
20+
allocations attributed to the site, and the total bytes.
21+
Symbolication ships in v0.9.2.
22+
- **`CallSiteStats`** public type behind the `backtraces` feature.
23+
- **Per-thread arena** (64 KB OS-page region per thread, 512
24+
events per flush) and **global aggregation table** (4,096
25+
buckets by default, ~384 KB) allocated through raw
26+
`mmap` / `VirtualAlloc` so the backtrace path never recurses
27+
into `ModAlloc::alloc` for its own state.
28+
- **`MOD_ALLOC_BUCKETS` env var** to override the
29+
aggregation-table size at process start. Value is rounded up to
30+
the next power of two and clamped to `[64, 1_048_576]`.
31+
- **`build.rs`** (one-off approved exception): warns at compile
32+
time when `RUSTFLAGS` is missing `-C force-frame-pointers=yes`
33+
and the `backtraces` feature is on. See
34+
`.dev/DIRECTIVES.md` section 2.1 for the documented exception.
35+
- **`.cargo/config.toml`** in the crate root enables frame
36+
pointers for the crate's own builds so the test suite and
37+
examples produce useful traces. Downstream consumers must
38+
enable the flag in their own builds.
39+
- New tests:
40+
- `tests/backtrace_real_chain.rs`: captures from a deeply
41+
nested `#[inline(never)]` call chain.
42+
- `tests/backtrace_fuzz.rs`: SplitMix64-driven random workload
43+
proving the walker is total under varied allocation patterns
44+
(10,000 iterations).
45+
- `tests/backtrace_concurrent.rs`: 32-thread aggregation
46+
stress test.
47+
- `src/backtrace/*` unit tests cover hash determinism, walker
48+
safety checks (null, alignment, out-of-range, non-monotonic,
49+
max-frame cap), arena round-trip, table claim races, and
50+
stack-bounds discovery.
51+
- **`examples/backtraces.rs`** demonstrates installing
52+
`ModAlloc`, exercising a few distinct call paths, and printing
53+
the top sites by total bytes.
54+
- **CI: AddressSanitizer nightly job.** A dedicated job in
55+
`.github/workflows/ci.yml` runs the test suite under
56+
`-Zsanitizer=address` on Linux x86_64 to catch any UB in the
57+
unsafe FP-walker path that survives the in-walker safety
58+
checks.
59+
60+
### Changed
61+
62+
- `GlobalAlloc::alloc`, `alloc_zeroed`, and `realloc` invoke
63+
`backtrace::record_event` after the existing counter update
64+
when the `backtraces` feature is on. `dealloc` does not capture
65+
(matches dhat: call sites describe who allocated, not who
66+
freed).
67+
- Per maintainer guidance, realloc captures all events including
68+
shrinks, matching dhat's per-event accounting. Documented in
69+
the rustdoc.
70+
- CI workflow runs the `backtraces` test suite with
71+
`RUSTFLAGS="-C force-frame-pointers=yes"` so traces are
72+
meaningful on hosted runners.
73+
74+
### Design notes
75+
76+
- **Reentrancy on the backtrace path.** The walker reads memory
77+
inside the cached stack bounds (which are queried via
78+
`GetCurrentThreadStackLimits` on Windows,
79+
`pthread_getattr_np` on Linux, `pthread_get_stackaddr_np` on
80+
Darwin / BSD). All reads are pointer-aligned and in-range; no
81+
page faults are possible. The existing `IN_ALLOC` reentrancy
82+
guard from v0.9.0 catches any pathological allocation
83+
triggered transitively from inside the backtrace path (e.g.
84+
libc lazy-init during the first `pthread_getattr_np`).
85+
- **No `HashMap` in the hot path.** The global aggregation table
86+
is a fixed-size open-addressed array allocated once via raw OS
87+
pages, with atomic per-bucket CAS for claim and linear probing
88+
for index collisions. Hash collisions on the 64-bit FxHash are
89+
a documented limitation (different sites with identical hashes
90+
get conflated).
91+
- **Bucket publish protocol.** Each bucket uses a two-phase
92+
claim: CAS on `hash` first (Release), then write
93+
`sample_frames`, then store `frame_count` with Release. Readers
94+
gate on `frame_count > 0` after observing a non-zero hash;
95+
this prevents torn reads of the sample frames.
96+
97+
### Migration
98+
99+
The default build (Tier 1 only) is unchanged. Existing callers
100+
need no edits.
101+
102+
Users opting in to the `backtraces` feature must add
103+
`-C force-frame-pointers=yes` to their build configuration. The
104+
included `build.rs` emits a `cargo:warning=` at compile time if
105+
this is missing.
106+
10107
## [0.9.0] - 2026-05-13
11108

12109
### Added

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ edition = "2021"
55
rust-version = "1.75"
66
readme = "README.md"
77
license = "Apache-2.0"
8+
build = "build.rs"
89

910
authors = [
1011
"James Gober <me@jamesgober.com>"

build.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//! mod-alloc build script.
2+
//!
3+
//! Approved exception per `.dev/DIRECTIVES.md` section 2.1. The
4+
//! crate does not generally use `build.rs`. This script exists
5+
//! solely to detect whether the user's toolchain has frame
6+
//! pointers enabled when the `backtraces` feature is on, and to
7+
//! emit a `cargo:warning=` directive if not.
8+
//!
9+
//! Frame pointers are a prerequisite for the inline FP walker
10+
//! shipped in `v0.9.1`. Without them the walker returns empty or
11+
//! shallow traces on release builds; a build-time hint saves
12+
//! users from debugging that downstream.
13+
//!
14+
//! This script never fails the build. The walker degrades
15+
//! gracefully at runtime if FP support is absent.
16+
17+
use std::env;
18+
19+
fn main() {
20+
println!("cargo:rerun-if-env-changed=RUSTFLAGS");
21+
println!("cargo:rerun-if-env-changed=CARGO_ENCODED_RUSTFLAGS");
22+
23+
let backtraces_on = env::var_os("CARGO_FEATURE_BACKTRACES").is_some();
24+
if !backtraces_on {
25+
return;
26+
}
27+
28+
let rustflags = env::var("CARGO_ENCODED_RUSTFLAGS")
29+
.or_else(|_| env::var("RUSTFLAGS"))
30+
.unwrap_or_default();
31+
32+
let has_fp = rustflags.contains("force-frame-pointers=yes")
33+
|| rustflags.contains("force-frame-pointers=y")
34+
|| rustflags.contains("force-frame-pointers=on");
35+
36+
if !has_fp {
37+
println!(
38+
"cargo:warning=mod-alloc: the `backtraces` feature is enabled but \
39+
RUSTFLAGS does not include `-C force-frame-pointers=yes`. The inline \
40+
FP walker requires frame pointers; without them traces will be empty \
41+
or shallow on release builds. Add to .cargo/config.toml: [build] \
42+
rustflags = [\"-C\", \"force-frame-pointers=yes\"]"
43+
);
44+
}
45+
}

examples/backtraces.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//! Per-call-site report demo.
2+
//!
3+
//! Run with:
4+
//! cargo run --release --features backtraces --example backtraces
5+
//!
6+
//! Requires frame pointers for useful output. The in-crate
7+
//! `.cargo/config.toml` enables `-C force-frame-pointers=yes` for
8+
//! this crate's own builds; downstream users opting into the
9+
//! `backtraces` feature must enable the flag in their own build.
10+
11+
#[cfg(feature = "backtraces")]
12+
#[global_allocator]
13+
static GLOBAL: mod_alloc::ModAlloc = mod_alloc::ModAlloc::new();
14+
15+
#[cfg(feature = "backtraces")]
16+
#[inline(never)]
17+
fn alloc_small() {
18+
let v: Vec<u8> = Vec::with_capacity(64);
19+
std::hint::black_box(&v);
20+
}
21+
22+
#[cfg(feature = "backtraces")]
23+
#[inline(never)]
24+
fn alloc_medium() {
25+
let v: Vec<u8> = Vec::with_capacity(1024);
26+
std::hint::black_box(&v);
27+
}
28+
29+
#[cfg(feature = "backtraces")]
30+
#[inline(never)]
31+
fn alloc_large() {
32+
let v: Vec<u8> = Vec::with_capacity(64 * 1024);
33+
std::hint::black_box(&v);
34+
}
35+
36+
#[cfg(feature = "backtraces")]
37+
fn main() {
38+
for _ in 0..1_000 {
39+
alloc_small();
40+
}
41+
for _ in 0..100 {
42+
alloc_medium();
43+
}
44+
for _ in 0..10 {
45+
alloc_large();
46+
}
47+
48+
let snap = GLOBAL.snapshot();
49+
println!("Process-wide snapshot:");
50+
println!(" alloc_count: {}", snap.alloc_count);
51+
println!(" total_bytes: {}", snap.total_bytes);
52+
println!(" current_bytes: {}", snap.current_bytes);
53+
println!(" peak_bytes: {}", snap.peak_bytes);
54+
println!();
55+
56+
let mut sites = GLOBAL.call_sites();
57+
sites.sort_by_key(|s| std::cmp::Reverse(s.total_bytes));
58+
59+
println!("Top 10 call sites by total bytes:");
60+
println!(
61+
"{:>10} {:>14} {:>4} {:>18}",
62+
"count", "total_bytes", "frm", "top frame"
63+
);
64+
for (rank, site) in sites.iter().take(10).enumerate() {
65+
println!(
66+
"{rank:>2}: {count:>6} {bytes:>14} {frm:>4} {top:#018x}",
67+
rank = rank,
68+
count = site.count,
69+
bytes = site.total_bytes,
70+
frm = site.frame_count,
71+
top = site.frames[0],
72+
);
73+
}
74+
}
75+
76+
#[cfg(not(feature = "backtraces"))]
77+
fn main() {
78+
eprintln!(
79+
"this example requires the `backtraces` feature; run with \
80+
`cargo run --features backtraces --example backtraces`"
81+
);
82+
}

0 commit comments

Comments
 (0)