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.