Repository navigation
v0.9.3 — DHAT-Compatible JSON Output #4
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.3 — DHAT-Compatible JSON Output
Date: 2026-05-18
Compare:
v0.9.2...v0.9.3Headline
The per-call-site report now serialises to the JSON shape the
upstream
dh_view.htmlviewer (shipped with Valgrind) loadsdirectly. With
--features dhat-compat(default OFF), one methodcall drops a
dhat-heap.jsonfile beside your binary that anyexisting DHAT tooling will consume. No new external dependencies;
the JSON writer is hand-rolled. The allocation hot path is
untouched.
What's new
Public API additions
ModAlloc::dhat_json_string() -> String— renders thereport as a DHAT-format JSON string. Allocates; call from
outside the allocator hook.
ModAlloc::write_dhat_json<P: AsRef<Path>>(path) -> io::Result<()>— writes the rendered JSON to
path. Mirrorsdhat-rs'sdhat-heap.jsonconvention so existing dev tooling that alreadyexpects that filename works unchanged.
Both methods are behind
#[cfg(feature = "dhat-compat")](whichalready implied
backtraces). The feature has shipped as a no-opsince v0.9.0; v0.9.3 wires it up.
Output schema
The emitter writes a
dhatFileVersion: 2,mode: "rust-heap"document. Top-level fields:
dhatFileVersion2mode"rust-heap"verb"Allocated"bkltfalse(we do not track allocation lifetimes)bkaccfalse(we do not track per-block accesses)bu/bsu/bksu"byte"/"bytes"/"blocks"tu/Mtu/tuth"instrs"/"Minstr"/0(decorative underbklt: false)cmdstd::env::args_ospidstd::process::id()tg/te0(no time tracking)ppsftbl[root]Each program point in
ppscarriestb(total bytes),tbk(count),
eb/ebk(zero — we don't snapshot at-end state), andfs(frame-table indices, top of stack first).Frame-string format
cfg-split at compile time:
symbolicate:"0x{addr:x}: <unresolved>"symbolicate:"0x{addr:x}: {function} ({file}:{line})"with
[inlined]flagging inlined expansions and gracefulfallbacks when individual fields are unresolved (e.g.
({file}:?)when line info is missing).Identical strings reused across call sites share one
ftblentryvia a
HashMap<String, u32>interner.Storage architecture
serdeandserde_jsonare notpulled in. The hand-rolled writer in
src/dhat_json/writer.rsis ~30 lines and handles the smallsubset of JSON escaping (
\\,\",\n,\r,\t, lowcontrol bytes as
\u00XX).src/dhat_json/frames.rsbuilds theftbland thepps[*].fsindex arrays in one pass over thereport.
src/dhat_json/frames.rs: when thesymbolicatefeature is on, the JSON emitter pulls fromsymbolicate::symbolicated_report()automatically; otherwiseit pulls from
backtrace::call_sites_report()directly.Test additions
tests/dhat_json_shape.rs— installsModAlloc, runs a smallworkload, asserts the JSON starts and ends with object braces,
contains all required top-level keys, has at least one
ppsentry after the workload, and that
write_dhat_jsonproducesthe same bytes as
dhat_json_string.src/dhat_json/writer.rsunit tests cover JSON escaping forquote, backslash, control bytes, multibyte UTF-8 pass-through,
and empty strings.
src/dhat_json/frames.rsunit tests cover the root entry atindex 0, frame-table dedup correctness, and the raw frame
string format.
Example
examples/dhat_json.rs— installsModAlloc, exercises three#[inline(never)]workloads of different sizes, prints theprocess-wide snapshot, then writes
dhat-heap.jsonandreports the path so the user can open it in
dh_view.html.CI
The existing
--all-featuresbuild/test steps already cover thedhat-compatfeature, including under the Linux ASAN nightlyjob. No new CI step required.
Design notes
Why no
serde_jsonThe schema is fixed (15 top-level keys, 5 keys per program
point) and small. A typical report is 10–100 KB. A hand-rolled
writer is ~50 lines of total code; pulling in
serde+serde_jsonwould add ~30 transitive deps for output we alreadyfully control. Keeping the dep graph small matters here because
dhat-compatis one of the most likely features for downstreamconsumers to enable.
Why
bklt: falseWe do not track per-allocation lifetimes today. With
bklt: false, the viewer hides the lifetime-related columns(
tl,mb,mbk) and renders only the totals we actuallypopulate. Setting the flag true while reporting zeros would
mislead the viewer into showing meaningless "every block freed
immediately" rows.
Why
eb/ebkare zeroThe JSON can be written at any point during execution — there is
no canonical "end" from our perspective. Populating end-state
fields with the current counters would conflate "all time"
totals with "at end" snapshots; emitting zeros matches the
viewer's interpretation of an in-flight profile.
Why frame strings are deduplicated, not the call-site fs path
dh_view.htmlbuilds its tree from theftblstrings, sodeduplicating strings (rather than full stacks) lets the viewer
group call sites that share inner frames without us doing the
tree work ourselves.
Migration
Default builds are unchanged. The
counters,backtraces, andsymbolicatefeatures behave identically. No code edits requiredfor callers on those paths.
Opting in to
dhat-compat:To combine with symbolication for resolved function names in the
output:
backtracesis implied bydhat-compat; activatingdhat-compatalone is sufficient for raw-address output, andadding
symbolicateupgrades the frame strings.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-compatcargo build --features symbolicatecargo 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: 74/74 pass (43 unit + 4 newv0.9.3 integration + 2 v0.9.2 integration + 4 v0.9.1
integration + 4 v0.9.0 integration + 5 smoke + 13 doctests)
cargo run --release --features dhat-compat --example dhat_json:produces a syntactically valid
dhat-heap.jsonwith requiredfields present
cargo run --release --features symbolicate,dhat-compat --example dhat_json:symbolicated path emits the same shape with resolved frame
strings
CI matrix: ubuntu-latest, macos-latest, windows-latest plus the
ASAN nightly Linux job. The
--all-featuresstep transitivelyexercises
dhat-compat; no new CI step required.Manual viewer verification
A one-time check (not a CI gate): drop
dhat-heap.jsonproducedby the example into the
dh_view.htmlshipped with Valgrind. Theviewer renders the call-site tree using the
pps/ftblfields. Lifetime columns are hidden as expected under
bklt: false.Limitations
v0.9.2: when
stdis compiled without frame pointers, thewalker captures only the immediate caller of
GlobalAlloc::allocinside
std's allocation infrastructure. For meaningful tracesin the JSON, build with
cargo +nightly -Z build-std=stdandRUSTFLAGS="-C force-frame-pointers=yes".bklt: falsetheviewer hides them. Adding lifetime tracking is a v1.x topic — it
requires every
deallocto look up the originating call site,which is real overhead on the hot path.
eb/ebkare always zero. See design note above.tg/teare zero. We do not measure instruction countor wall time. The viewer treats these as decorative when
bkltis false.Follow-ups
v0.9.4(dev-benchintegration / drop dhat). Nowunblocked. With v0.9.3,
dev-benchcan consumemod-allocoutput through the same code path it uses for dhat today.
bklt: true). Open question forv1.x: do we add a
dealloc-side lookup of originating callsite? Real per-alloc overhead, but unlocks the lifetime
columns in the viewer.
symbolicate).Only Rust mangling is decoded today via
rustc-demangle.v0.9.1.1Tier 2 perf pass (still open) — separatemilestone, unaffected by this work.
Release ceremony
Standard pattern:
git tag -a v0.9.3 -m "Release v0.9.3 - DHAT-compatible JSON output"git push origin maingit push origin v0.9.3cargo publish --dry-run --all-featurescargo publishhttps://crates.io/crates/mod-alloc/0.9.3is live.GitHub release title:
v0.9.3 — DHAT-Compatible JSON Output.Tag as pre-release;
1.0.0waits forv0.9.4(dev-benchintegration) to ship and bake.
Full Changelog: v0.9.2...v0.9.3
This discussion was created from the release v0.9.3 — DHAT-Compatible JSON Output.
All reactions