Skip to content

v1.0.0 — Stable API

Latest

Choose a tag to compare

@jamesgober jamesgober released this 18 May 09:34

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.3dhat-rs drop-in surface in v0.9.4 → Tier 2
perf optimisation (~32x speedup) in v0.9.5stable 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.rs crate-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 AllocStats includes 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 ModAlloc with
    pub const fn new(), pub fn snapshot(&self) -> AllocStats,
    pub fn reset(&self), impl Default,
    unsafe impl GlobalAlloc
  • pub struct AllocStats (#[non_exhaustive], derives
    Default) with public fields:
    • alloc_count: u64
    • total_bytes: u64
    • peak_bytes: u64
    • current_bytes: u64
    • live_count: u64
    • peak_live_count: u64
  • pub struct Profiler with
    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: u8
    • count: u64
    • total_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: u64
    • function: Option<String>
    • file: Option<PathBuf>
    • line: Option<u32>
    • inlined: bool
  • pub struct SymbolicatedCallSite (#[non_exhaustive]) with
    public fields:
    • count: u64
    • total_bytes: u64
    • frames: Vec<SymbolicatedFrame>
  • pub fn ModAlloc::symbolicated_report(&self) -> Vec<SymbolicatedCallSite>

dhat-compat feature

  • pub fn ModAlloc::dhat_json_string(&self) -> String
  • pub fn ModAlloc::write_dhat_json<P: AsRef<Path>>(&self, p: P) -> io::Result<()>
  • pub mod dhat_compat containing:
    • pub struct Alloc (unit struct,
      pub const fn new(), impl Default,
      unsafe impl GlobalAlloc)
    • pub struct Profiler with pub fn new_heap(),
      pub fn new_ad_hoc(), pub fn builder(), impl Drop
    • pub struct ProfilerBuilder with pub 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 fields
    • pub struct AdHocStats (exhaustive, mirrors dhat-rs)
      with pub fn get() and two public fields
    • pub fn ad_hoc_event(weight: usize)

JSON wire format

  • dhatFileVersion: 2
  • mode: "rust-heap" (heap profile) or "ad-hoc" (ad-hoc
    profile)
  • Output loadable by upstream dh_view.html shipped with
    Valgrind

MSRV

  • 1.75, pinned in Cargo.toml via rust-version and
    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 pass
  • cargo 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:

  • AllocStats
  • CallSiteStats
  • SymbolicatedFrame
  • SymbolicatedCallSite
  • dhat_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 beyond 1.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:

  1. git tag -a v1.0.0 -m "Release v1.0.0 - Stable API"
  2. git push origin main
  3. git push origin v1.0.0
  4. cargo publish --dry-run --all-features
  5. cargo publish
  6. Confirm https://crates.io/crates/mod-alloc/1.0.0 is 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