Commit 3a15c5d1a51 for nodejs
commit 3a15c5d1a5160dc9e2795453e955f08f9decbbc9
Author: Shelley Vohr <shelley.vohr@gmail.com>
Date: Tue Sep 22 14:00:22 2026 +0200
src: free placeholder nodes for cppgc wrappers in MemoryTracker
`MemoryTracker::AddNode(const CppgcMixin*)` allocates a
`MemoryRetainerNode` that only stands in for the wrapper's JS node while
its `MemoryInfo()` runs; unlike the other node kinds it is not handed
to the `EmbedderGraph`, and nothing freed it. Every heap snapshot (or
other `BuildEmbedderGraph` call) leaked one node per live `vm.Script`
or `vm` context.
Keep the placeholders in the tracker and free them with it, and make
the already-seen path in `Track(const CppgcMixin*)` add its edge to the
wrapper's JS node like the first visit does, so the graph never refers
to a placeholder.
Refs: https://github.com/nodejs/node/pull/56534
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65793
Reviewed-By: James M Snell <jasnell@gmail.com>
diff --git a/src/memory_tracker-inl.h b/src/memory_tracker-inl.h
index 3c82983ce01..2e32e113698 100644
--- a/src/memory_tracker-inl.h
+++ b/src/memory_tracker-inl.h
@@ -300,7 +300,7 @@ void MemoryTracker::Track(const CppgcMixin* retainer, const char* edge_name) {
auto it = seen_.find(retainer);
if (it != seen_.end()) {
if (CurrentNode() != nullptr) {
- AddEdge(CurrentNode(), it->second, edge_name);
+ AddEdge(CurrentNode(), it->second->JSWrapperNode(), edge_name);
}
return; // It has already been tracked, no need to call MemoryInfo again
}
@@ -357,6 +357,11 @@ inline void MemoryTracker::TraitTrackInline(const T& retainer,
-(static_cast<int>(MemoryRetainerTraits<T>::SelfSize(retainer))));
}
+MemoryTracker::MemoryTracker(v8::Isolate* isolate, v8::EmbedderGraph* graph)
+ : isolate_(isolate), graph_(graph) {}
+
+MemoryTracker::~MemoryTracker() = default;
+
v8::EmbedderGraph::Node* MemoryTracker::CurrentNode() const {
if (node_stack_.empty()) return nullptr;
MemoryRetainerNode* n = node_stack_.top();
@@ -373,7 +378,8 @@ MemoryRetainerNode* MemoryTracker::AddNode(const CppgcMixin* retainer,
return it->second;
}
- MemoryRetainerNode* n = new MemoryRetainerNode(this, retainer);
+ cppgc_nodes_.push_back(std::make_unique<MemoryRetainerNode>(this, retainer));
+ MemoryRetainerNode* n = cppgc_nodes_.back().get();
seen_[retainer] = n;
if (CurrentNode() != nullptr) {
AddEdge(CurrentNode(), n->JSWrapperNode(), edge_name);
diff --git a/src/memory_tracker.h b/src/memory_tracker.h
index d7893f10b3a..5117cc53271 100644
--- a/src/memory_tracker.h
+++ b/src/memory_tracker.h
@@ -8,10 +8,12 @@
#include <uv.h>
#include <limits>
+#include <memory>
#include <queue>
#include <stack>
#include <string>
#include <unordered_map>
+#include <vector>
namespace v8 {
class BackingStore;
@@ -294,9 +296,8 @@ class MemoryTracker {
inline v8::EmbedderGraph* graph() { return graph_; }
inline v8::Isolate* isolate() { return isolate_; }
- inline explicit MemoryTracker(v8::Isolate* isolate,
- v8::EmbedderGraph* graph)
- : isolate_(isolate), graph_(graph) {}
+ inline explicit MemoryTracker(v8::Isolate* isolate, v8::EmbedderGraph* graph);
+ inline ~MemoryTracker();
// Can be passed to Track() if it is not desirable
// to create a strong edge between nodes, i.e. when
@@ -334,6 +335,8 @@ class MemoryTracker {
v8::EmbedderGraph* graph_;
std::stack<MemoryRetainerNode*> node_stack_;
NodeMap seen_;
+ // Placeholder nodes for cppgc wrappers; the graph only owns their JS nodes.
+ std::vector<std::unique_ptr<MemoryRetainerNode>> cppgc_nodes_;
};
} // namespace node
diff --git a/test/pummel/test-heapdump-vm-script.js b/test/pummel/test-heapdump-vm-script.js
index e7878a78038..73b1b1cd313 100644
--- a/test/pummel/test-heapdump-vm-script.js
+++ b/test/pummel/test-heapdump-vm-script.js
@@ -1,8 +1,10 @@
'use strict';
require('../common');
const { validateByRetainingPath } = require('../common/heap');
+const vm = require('vm');
const source = 'const foo = 123';
-const script = require('vm').createScript(source);
+const script = new vm.Script(source);
+const context = vm.createContext();
validateByRetainingPath('Node / ContextifyScript', [
{ node_name: '(shared function info)' }, // This is the UnboundScript referenced by ContextifyScript.
@@ -10,4 +12,4 @@ validateByRetainingPath('Node / ContextifyScript', [
{ edge_name: 'source', node_type: 'string', node_name: source },
]);
-console.log(script); // Keep the script alive.
+console.log(script, context); // Keep the script and context alive.