Repository navigation
Rust JSG: heap snapshot memory info - #8145
Draft
bartlomieju wants to merge 3 commits into
Draft
bartlomieju wants to merge 3 commits into
bartlomieju wants to merge 3 commits into
Conversation
MemoryTracker builds a heap snapshot's embedder graph from C++ types: trackField() and JSG_MEMORY_INFO find a type's name, size and fields at compile time. Rust JSG resources have none of that, so they need a way to add nodes and edges at run time. pushRetainer() adds a node for an object with fields of its own and makes it the node its fields are reported to, until popNode(). Retainers are deduplicated by address and type together, so an object reached twice is one node, while a retainer and its first field, which share an address, are two. A null address always gets a new node, for objects with no address of their own. pushContainer() does the same for a container's elements, without deduplication.
bartlomieju
force-pushed
the
biwanczuk/rust-jsg-memory-info
branch
from
November 2, 2026 11:32
091f488 to
e12890a
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #8145 +/- ##
==========================================
- Coverage 68.31% 68.16% -0.15%
==========================================
Files 726 727 +1
Lines 189662 190105 +443
Branches 29672 29683 +11
==========================================
+ Hits 129564 129585 +21
- Misses 42721 43143 +422
Partials 17377 17377 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
A Rust JSG resource showed in a V8 heap snapshot only as its
Wrappable: a node named after the type, the size of the C++ Wrappable
alone, and none of what the resource holds. This ports the C++ heap
snapshot support (jsg::MemoryTracker, visitForMemoryInfo, trackField
and JSG_MEMORY_INFO) to Rust JSG.
Every resource's node is now sized as the Rust struct plus its
Wrappable. A resource reports its fields once it opts in, as a C++
resource does by defining visitForMemoryInfo:
#[jsg_resource(memory_info)]
struct EventRouter { ... }
impl jsg::VisitForMemoryInfo for EventRouter {
fn visit_for_memory_info(&self, tracker: &mut jsg::MemoryTracker<'_>) {
tracker.track_field(c"handlers", &self.handlers);
}
}
The opt-in is a macro flag, so a resource with the flag but without
the impl doesn't compile.
* jsg::MemoryInfo says how a field is reported, as trackField's
overloads do. Strings and collections are nodes of what they
allocate, jsg::Rc is an edge to the resource's node, Global and
TracedGlobal are edges into the V8 heap, jsg::Weak reports nothing.
Collections of plain data are one node.
* jsg::MemoryRetainer makes a helper type a node of its own, as
JSG_MEMORY_INFO does; jsg::jsg_memory_retainer! registers it. It is
reported through MemoryTracker::pushRetainer, keyed by its address
and a hash of its TypeId, so a retainer reached twice is one node
and a cycle through std::rc::Rc ends. A collection of retainers
counts only its spare capacity, leaving the elements' bytes to
their nodes, also when they are inline in an Option, a Cell or
another wrapper (MemoryInfo::self_bytes_reported).
* jsg_cpp::Ref<T> is the C++ resource's node, and C++ RustRef<T> gets
trackField(), an edge to the Rust resource's wrapper.
Names are &'static CStr (c"..." literals), since V8 reads them after
the BuildEmbedderGraph callback returns; a compile test checks it.
Reporting runs in a callback from C++, so it must not panic. A Cell is
reported in place, through Cell::as_ptr() as Traced reads it, so
reporting code must not write into anything it can reach; nothing
enforces that, so MemoryInfo's docs and src/rust/AGENTS.md spell it
out. Handles of another isolate are skipped, and debug builds assert
on them.
API changes: Type::memory_self_size() is removed; nothing used it.
GarbageCollected gains memory_self_size() and memory_info(), with
defaults.
jsg_test::Harness::take_heap_snapshot() takes a heap snapshot as the inspector does, and tests/memory_info.rs parses it to check what resources report: * node names, and sizes exact to the struct plus its Wrappable * resources without memory_info report no fields * a resource shared by two holders is one node, and a cycle is tracked once; so is a retainer shared through an std::rc::Rc, and a cycle of retainers through std::rc::Rc * a retainer and the retainer at its start are two nodes, as are two zero-sized retainers or two retainers in Cells * a collection or Box leaves the bytes of a retainer in an Option to the retainer's node * collections, maps, boxes, Dict, Weak, Lenient, Global, BackingStore and a RefCell that is mutably borrowed during the snapshot * in release builds, a handle of another isolate is not reported cpp_interop.rs checks both directions between Rust JSG and C++ JSG: a Rust resource's edge to a held C++ resource, and a C++ resource's edge through a Rust resource's wrapper to its node.
bartlomieju
force-pushed
the
biwanczuk/rust-jsg-memory-info
branch
from
November 3, 2026 15:22
e12890a to
9581086
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A Rust JSG resource shows in a V8 heap snapshot only as its
Wrappable: a node named after the type, the size of the C++Wrappable, and none of what the resource holds. This ports the C++heap snapshot support (
jsg::MemoryTracker,visitForMemoryInfo,trackField,JSG_MEMORY_INFO) to Rust JSG.Usage
Every resource's node is now sized as the Rust struct plus its
Wrappable. To report fields too, a resource opts in, as a C++resource does by defining
visitForMemoryInfo:C++ counterparts:
visitForMemoryInfo:#[jsg_resource(memory_info)]+jsg::VisitForMemoryInfotrackFieldoverloads:jsg::MemoryInfo, viatracker.track_field(c"name", &value)JSG_MEMORY_INFO:jsg::MemoryRetainer+jsg::jsg_memory_retainer!jsg::Ref<T>field:jsg::Rc<R>,jsg_cpp::Ref<T>jsg::V8Ref<T>field:v8::Global<T>,v8::TracedGlobal<T>Design notes
autoref detection, so a resource with the flag but without the impl
fails to compile.
&'static CStr(c"..."literals): V8 reads node andedge names after the
BuildEmbedderGraphcallback returns. A compiletest enforces it.
aborts the process.
size_of_val(self); stringsand collections count what they allocate. A collection of retainers
counts only its spare capacity, leaving the elements' bytes to their
own nodes. Collections of plain data are a single node.
MemoryTracker::pushRetainerkeys nodes byaddress and type, so a retainer reached twice is one node, while a
retainer and its first field (same address) are two. Zero-sized
retainers always get a new node.
jsg_cpp::Ref<T>reports the C++ resource's node;C++
RustRef<T>::trackFieldadds an edge to the Rust resource'swrapper, which V8 links to its node.
API changes
jsg::Type::memory_self_size()is removed (unused, including in theinternal repo).
jsg::GarbageCollectedgainsmemory_self_size()andmemory_info(), both with defaults.jsg::MemoryTrackergainspushRetainer,pushContainerandpopNode.