mod-alloc v1.0.0 — Stable API
Date: 2026-05-18
Compare: v0.9.5...v1.0.0
Headline
The public surface is frozen. Breaking changes after 1.0.0
require a major version bump per Semantic Versioning.
Path to here: name-claim placeholder in v0.1.0 → real
GlobalAlloc impl in v0.9.0 → inline backtrace capture in
v0.9.1 → symbolication in v0.9.2 → DHAT JSON output in
v0.9.3 → dhat-rs drop-in surface in v0.9.4 → Tier 2
perf optimisation (~32x speedup) in v0.9.5 → stable API
freeze in v1.0.0.
One real downstream consumer (dev-bench v0.9.7) has been live
on crates.io for a day with zero friction. Both Tier 1 and
Tier 2 bench numbers clear REPS section 6 targets with
significant headroom. The DHAT-format JSON output round-trips
cleanly through the upstream dh_view.html viewer. docs.rs
build passes on all features. MSRV holds at 1.75.
What changed in v1.0.0
#[non_exhaustive] on data structs likely to grow fields
The public structs and enums most likely to gain new fields in
future minor versions are now #[non_exhaustive]. Reading
fields by name is fully stable; constructing via struct-literal
syntax from outside the crate is no longer supported for these
types. Use the API surfaces that produce them, or
Default::default()-and-mutate.
| Type | Why marked |
|---|---|
AllocStats |
Counters may grow (e.g. latency histograms, size-class buckets) |
CallSiteStats (backtraces) |
Per-bucket metadata may grow (high-water marks, first-seen) |
SymbolicatedFrame (symbolicate) |
Frame metadata may grow (column, symbol kind, crate-of-origin) |
SymbolicatedCallSite (symbolicate) |
Counterpart of CallSiteStats |
dhat_compat::Mode |
New profiler modes may appear (e.g. event-stream output) |
dhat_compat::HeapStats and dhat_compat::AdHocStats are
intentionally left exhaustive. They mirror dhat-rs's
public surface field-for-field; the entire point of
dhat_compat is that
use mod_alloc::dhat_compat as dhat; makes existing dhat-rs
code keep compiling. Adding #[non_exhaustive] here would
break that claim.
Default derived on data structs
AllocStats and CallSiteStats now derive Default so callers
have a non-literal construction path:
// Before (v0.9.x):
let s = AllocStats { alloc_count: 5, total_bytes: 100, ... };
// After (v1.0.0):
let mut s = AllocStats::default();
s.alloc_count = 5;
s.total_bytes = 100;dhat_compat::AdHocStats already derived Default; that stays.
Documentation polish
src/lib.rscrate-level rustdoc now carries the full
v0.9.0 → v1.0.0 path summary instead of a v0.9.x-focused
status line.- Field-level rustdoc on
AllocStatsincludes a "Stability"
block explaining the#[non_exhaustive]choice and the
recommended construction pattern. - Identical "Stability" blocks on
CallSiteStats,
SymbolicatedFrame,SymbolicatedCallSite, and
dhat_compat::Mode.
No new features
v1.0.0 does not add new functionality. The Tier 2 perf pass
(v0.9.5), DHAT JSON output (v0.9.3), and dhat-rs drop-in
surface (v0.9.4) are all already shipped on crates.io.
v1.0.0 is purely a stability commitment + the
#[non_exhaustive] future-proofing changes above.
Frozen public surface
Everything in this list is stable through the 1.x line.
Always available
pub struct ModAllocwith
pub const fn new(),pub fn snapshot(&self) -> AllocStats,
pub fn reset(&self),impl Default,
unsafe impl GlobalAllocpub struct AllocStats(#[non_exhaustive], derives
Default) with public fields:alloc_count: u64total_bytes: u64peak_bytes: u64current_bytes: u64live_count: u64peak_live_count: u64
pub struct Profilerwith
pub fn start() -> Self,
pub fn stop(self) -> AllocStats
backtraces feature
pub struct CallSiteStats(#[non_exhaustive], derives
Default) with public fields:frames: [u64; 8]frame_count: u8count: u64total_bytes: u64
pub fn ModAlloc::call_sites(&self) -> Vec<CallSiteStats>- Environment variable
MOD_ALLOC_BUCKETS(process-start
override, default 4096, clamped to[64, 1_048_576])
symbolicate feature
pub struct SymbolicatedFrame(#[non_exhaustive]) with
public fields:address: u64function: Option<String>file: Option<PathBuf>line: Option<u32>inlined: bool
pub struct SymbolicatedCallSite(#[non_exhaustive]) with
public fields:count: u64total_bytes: u64frames: Vec<SymbolicatedFrame>
pub fn ModAlloc::symbolicated_report(&self) -> Vec<SymbolicatedCallSite>
dhat-compat feature
pub fn ModAlloc::dhat_json_string(&self) -> Stringpub fn ModAlloc::write_dhat_json<P: AsRef<Path>>(&self, p: P) -> io::Result<()>pub mod dhat_compatcontaining:pub struct Alloc(unit struct,
pub const fn new(),impl Default,
unsafe impl GlobalAlloc)pub struct Profilerwithpub fn new_heap(),
pub fn new_ad_hoc(),pub fn builder(),impl Droppub struct ProfilerBuilderwithpub fn ad_hoc(),
pub fn testing(),pub fn file_name<P>(),
pub fn trim_backtraces(),pub fn build()pub enum Mode { Heap, AdHoc }(#[non_exhaustive])pub struct HeapStats(exhaustive, mirrors dhat-rs) with
pub fn get()and six public fieldspub struct AdHocStats(exhaustive, mirrors dhat-rs)
withpub fn get()and two public fieldspub fn ad_hoc_event(weight: usize)
JSON wire format
dhatFileVersion: 2mode: "rust-heap"(heap profile) or"ad-hoc"(ad-hoc
profile)- Output loadable by upstream
dh_view.htmlshipped with
Valgrind
MSRV
1.75, pinned inCargo.tomlviarust-versionand
verified by CI on every push.
Bench (held from v0.9.5)
| Build | Per alloc + dealloc cycle |
|---|---|
Tier 1 only (counters, default) |
45.5 ns |
Tier 1 + Tier 2 (backtraces) |
62.3 ns |
REPS section 6 targets: Tier 1 <50 ns total, Tier 2 <200 ns of
additional overhead. Both clear with significant headroom.
Verification
Full matrix run on Windows host (x86_64):
cargo build(default features): ✓cargo build --no-default-features: ✓cargo build --features counters: ✓cargo build --features backtraces: ✓cargo build --features symbolicate: ✓cargo build --features dhat-compat: ✓cargo build --all-features: ✓cargo +1.75 build --all-features(MSRV): ✓cargo fmt --all -- --check: ✓cargo clippy --all-targets -- -D warnings: ✓cargo clippy --all-targets --all-features -- -D warnings: ✓cargo clippy --all-targets --no-default-features -- -D warnings: ✓cargo doc --no-deps: ✓cargo doc --all-features --no-deps: ✓cargo test --all-features: 96/96 passcargo run --release --features backtraces --example bench_overhead:
62.3 ns per cycle (Tier 2 overhead 16.8 ns, target was <200 ns)
CI matrix: ubuntu-latest, macos-latest, windows-latest plus the
ASAN nightly Linux job.
Migration from 0.9.x
For most callers — no changes required. Every public method
and field name is unchanged. The #[non_exhaustive] additions
only affect code that constructed these types via struct-literal
syntax from outside the crate:
AllocStatsCallSiteStatsSymbolicatedFrameSymbolicatedCallSitedhat_compat::Mode
For each such call site, switch to Default::default() +
named-field assignment, or consume the value from the appropriate
API surface (snapshot, call_sites, symbolicated_report,
etc.). dhat_compat::HeapStats / AdHocStats are explicitly
exhaustive — drop-in code from dhat-rs keeps compiling.
Stability commitment
From 1.0.0 forward:
- Bug fixes ship as patch versions (
1.0.x). - Backwards-compatible additions ship as minor versions
(1.x.0). Adding fields to#[non_exhaustive]types is
considered backwards-compatible; adding methods to public
types is considered backwards-compatible. - Breaking changes wait for
2.0.0. This includes
removing public items, renaming public fields, tightening
trait bounds on public APIs, raising MSRV beyond1.75, and
changing the JSON wire format in non-additive ways.
The MOD_ALLOC_BUCKETS environment variable and its
[64, 1_048_576] clamp range are part of the stable surface;
default behaviour (4096 buckets, ~384 KB) will not change in
the 1.x line.
Acknowledgements
This crate exists because dhat-rs's backtrace → addr2line
dependency chain forces MSRV 1.85+, which was incompatible
with the broader dev-* family target of 1.75. mod-alloc
provides equivalent core profiling at MSRV 1.75 with zero
runtime dependencies on the alloc hot path and a drop-in
dhat_compat surface that lets consumers migrate with a
one-line import change.
The dh_view.html viewer shipped with Valgrind is what makes
the JSON wire format useful; that work belongs to the upstream
DHAT authors and Valgrind contributors.
Release ceremony
Standard pattern:
git tag -a v1.0.0 -m "Release v1.0.0 - Stable API"git push origin maingit push origin v1.0.0cargo publish --dry-run --all-featurescargo publish- Confirm
https://crates.io/crates/mod-alloc/1.0.0is live.
GitHub release title: v1.0.0 — Stable API. Not tagged as
pre-release this time — this is the real one.
Full Changelog: v0.9.5...v1.0.0