v0.9.4 — dhat-rs Drop-in Compat Surface #5
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.4 — dhat-rs Drop-in Compat Surface
Date: 2026-05-18
Compare:
v0.9.3...v0.9.4Headline
mod-alloc now exposes a
dhat_compatmodule that mirrorsdhat-rs's public surface method-for-method. Consumers canswap dhat for mod-alloc by changing exactly one line — the
import — and keep their existing global-allocator declaration,
Profiler::new_heap()call, andHeapStats::get()readsworking unchanged. This is the unblock for
dev-bench(andany downstream project pinned to MSRV ≤ 1.84 that today must
choose between dhat's profiling and an older Rust toolchain).
The allocation hot path is unchanged from v0.9.3; the new code
is a thin forwarding wrapper plus two extra atomic counters
to back dhat's
curr_blocks/max_blocksfields.What's new
Public API additions
All gated on the existing
dhat-compatcargo feature:dhat_compat::Alloc— unit-struct global allocatormatching
dhat::Alloc's usage pattern. Forwards everyGlobalAlloccall to a process-wide staticModAlloc.dhat_compat::Profiler— RAII handle that writes aDHAT JSON report on drop. Methods:
new_heap(),new_ad_hoc(),builder().dhat_compat::ProfilerBuilder— fluent builder. Methods:ad_hoc(),testing(),file_name(P),trim_backtraces(Option<usize>),build().dhat_compat::HeapStats— six-field stats struct mirroringdhat::HeapStatsexactly (including the deliberateu64 total_*/usize curr_* max_*asymmetry). Constructedvia
HeapStats::get().dhat_compat::AdHocStats+dhat_compat::ad_hoc_event(weight: usize)— ad-hoc-mode event counters with two-atomic-op tracking.
Public API extensions to existing types
AllocStatsgained two fields:live_count: u64andpeak_live_count: u64. They backHeapStats::curr_blocksandmax_blocks. Wired intorecord_alloc(increment + max) andrecord_dealloc(decrement).record_reallocdoes not touchthem — a realloc is one block from a count perspective, which
matches dhat's accounting.
Storage architecture
static INNER: ModAllocinsrc/dhat_compat/mod.rsis theshared backing store. Every
Allocinstance forwards itsGlobalAlloccalls toINNER, which registers itself intoGLOBAL_HANDLEon first use. TheProfilerandHeapStats::get()read fromGLOBAL_HANDLE, so the dataflows transparently.
Allocforwarder methods are#[inline(always)]. Thestatic-dispatch wrapper folds into
__rust_alloc's call site,matching the call-chain depth of using
ModAllocdirectly.PROFILER_ACTIVE: AtomicBoolas a best-effort single-Profilerguard. dhat-rs panics on double-construction; we treat it as a
no-op ("last writer wins" on the JSON file) since the panic
surprise hurts more than it helps in downstream test harnesses.
Ad-hoc mode JSON
src/dhat_compat/ad_hoc_writer.rsships a minimal ad-hoc-modeJSON writer alongside the v0.9.3 heap-mode writer. Schema:
dhatFileVersion: 2,mode: "ad-hoc", single program pointcarrying
tb = total_units,tbk = total_events. Loads indh_view.htmlthe same way dhat's ad-hoc output does.Test additions
tests/dhat_compat_surface.rs— 7 end-to-end tests:total_bytes.curr_blocksrises and falls correctly across a workload.Profiler::builder().file_name(p).build()Drop writes theDHAT JSON file with all required fields.
.testing()suppresses the drop-time write.ad_hoc_event(w)accumulates both events and units.trim_backtraces(Some(100))accepts oversize values withoutpanicking.
Profiler::new_heap-style construction works cleanly.src/dhat_compat/{mod,profiler,stats,ad_hoc_writer}.rscover unit-struct size, builder defaults, ad-hoc counter math,
JSON rendering, and the empty-state shape.
src/lib.rstests gainedlive_counters_track_alive_blocksandrecord_realloc_does_not_touch_live_countlocking the newcounter semantics.
Example
examples/dhat_drop_in.rs— drop-in pattern showing theone-line import change. Builds against dhat-compat and writes
dhat-heap.jsonto CWD via the Profiler drop.Migration guide
MIGRATING_FROM_DHAT.md(project root) — side-by-side codeexample, full API surface mapping, behavioural-difference
table, rollback steps, and build-flag requirements.
CI
The existing
--all-featuresbuild/test steps cover the newmodule. No new CI step required.
Design notes
Why
Allocis a unit struct, not a type aliasdhat-rs ships
pub struct Alloc;and users writestatic ALLOC: dhat::Alloc = dhat::Alloc;. For a true drop-in,we must accept the same syntax — a value-form initializer of
a unit type. A type alias like
pub type Alloc = ModAllocwouldn't allow the
= dhat::Allocinitializer sinceModAllocisn't a unit struct.
The trade-off:
Alloccarries no state, so all counters liveon a separately-stored
static INNER: ModAlloc. Every methodforwards through inlined trait dispatch. Net cost: zero;
the inliner folds it away.
Why two new counters in the hot path
HeapStats::curr_blocksandHeapStats::max_blockswere notderivable from the v0.9.3 counter set. We need to know how many
allocations are currently alive — not just bytes. Adding two
atomics adjacent to the existing
current_bytes/peak_bytespair costs roughly 3 ns on the alloc hot path(the new ops dual-issue alongside the existing ones).
Acceptable; preserves drop-in parity.
Why double-Profiler is a no-op (not a panic)
dhat-rs panics on
Profiler::new_heap()if one is alreadyalive. We chose no-op + "last writer wins" because:
Dropchains can mask the original error.inadvertently; an aborting panic there is hostile.
recoverable.
Documented in the rustdoc and in
MIGRATING_FROM_DHAT.md.Why
trim_backtracesis silently clampedOur walker captures at most 8 frames per allocation (see
src/backtrace/walk.rs). dhat-rs supports configurablebacktrace depth via its libunwind path. Rather than reject
trim_backtraces(Some(100))with an error, we accept it andclamp internally. This keeps the swap mechanical for projects
that ask for 20 / 50 / 100 frames.
Migration
One-line import swap
Cargo.toml swap
For symbolicated frames in the JSON output (matches dhat-rs's
default richer traces), add
symbolicate:See
MIGRATING_FROM_DHAT.mdfor the full surface mapping.AllocStatsfield additions (breaking for struct-literal callers)AllocStatsgainedlive_countandpeak_live_count. Codeconstructing
AllocStatsvia struct literal needs the newfields. Code consuming
AllocStatsviasnapshot()orProfiler::stop()is unaffected.In-tree update example (from
tests/smoke.rs):This is a deliberate 0.x-window break. v1.0 will freeze the
surface; the right time to add fields is now.
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 clippy --all-targets --no-default-features -- -D warningscargo doc --no-depscargo doc --all-features --no-depscargo test --all-features: 97/97 pass (53 unit + 7 newv0.9.4 integration + 4 v0.9.3 integration + 2 v0.9.2 integration
doctests)
cargo run --release --features dhat-compat --example dhat_drop_in:writes well-formed
dhat-heap.json,HeapStatsreportscorrect counters
CI matrix: ubuntu-latest, macos-latest, windows-latest plus the
ASAN nightly Linux job. The
--all-featuresstep coversdhat-compat; no new CI step required.Limitations
dhat::assert!,dhat::assert_eq!,dhat::assert_ne!require a storedsnapshot-comparator state machine. Use
HeapStats::get()directly in test assertions until they ship (likely v0.9.5
if
dev-benchneeds them).trim_backtraces(Some(n > 8))silently clamps.v0.9.1-class limitation as previous milestones:
ppsmay beempty in
releasebuilds on Windows whenstdis not builtwith frame pointers. Counters always work; per-call-site
tracing requires
cargo +nightly -Z build-std=stdwithRUSTFLAGS="-C force-frame-pointers=yes".Follow-ups
dev-benchswap (consumer-side). The actualswap lives in
dev-bench's repo. With v0.9.4 shipped, theswap is mechanical: change one import in dev-bench, run its
test suite, validate report shape, deprecate the dhat path.
dhat::assert!family. Port if dev-bench's existingtest suite uses them.
Release ceremony
Standard pattern:
git tag -a v0.9.4 -m "Release v0.9.4 - dhat-rs drop-in compat surface"git push origin maingit push origin v0.9.4cargo publish --dry-run --all-featurescargo publishhttps://crates.io/crates/mod-alloc/0.9.4is live.GitHub release title:
v0.9.4 — dhat-rs Drop-in Compat Surface.Tag as pre-release;
1.0.0waits for dev-bench's actual swapto bake (the v0.9.5 follow-up).
Full Changelog: v0.9.3...v0.9.4
This discussion was created from the release v0.9.4 — dhat-rs Drop-in Compat Surface.
All reactions