Repository: nodejs/node
Pull request: src: add memory retainer traits for external types

Add `MemoryRetainerTraits` to reveal external type memory info without
forcing them to inherit from `MemoryRetainer`.

Refs: https://github.com/nodejs/node/pull/56871#discussion_r1938503993

Changed files:
filename: src/memory_tracker-inl.h
patch: @@ -297,6 +297,27 @@ void MemoryTracker::TrackInlineField(const MemoryRetainer* retainer,
   CurrentNode()->size_ -= retainer->SelfSize();
 }
 
+template <typename T>
+inline void MemoryTracker::TraitTrack(const T& retainer,
+                                      const char* edge_name) {
+  MemoryRetainerNode* n =
+      PushNode(MemoryRetainerTraits<T>::MemoryInfoName(retainer),
+               MemoryRetainerTraits<T>::SelfSize(retainer),
+               edge_name);
+  MemoryRetainerTraits<T>::MemoryInfo(this, retainer);
+  CHECK_EQ(CurrentNode(), n);
+  CHECK_NE(n->size_, 0);
+  PopNode();
+}
+
+template <typename T>
+inline void MemoryTracker::TraitTrackInline(const T& retainer,
+                                            const char* edge_name) {
+  TraitTrack(retainer, edge_name);
+  CHECK(CurrentNode());
+  CurrentNode()->size_ -= MemoryRetainerTraits<T>::SelfSize(retainer);
+}
+
 MemoryRetainerNode* MemoryTracker::CurrentNode() const {
   if (node_stack_.empty()) return nullptr;
   return node_stack_.top();

filename: src/memory_tracker.h
patch: @@ -138,6 +138,33 @@ class MemoryRetainer {
   }
 };
 
+/**
+ * MemoryRetainerTraits allows defining a custom memory info for a
+ * class that can not be modified to implement the MemoryRetainer interface.
+ *
+ * Example:
+ *
+ * template <>
+ * struct MemoryRetainerTraits<ExampleRetainer> {
+ *   static void MemoryInfo(MemoryTracker* tracker,
+ *                         const ExampleRetainer& value) {
+ *     tracker->TrackField("another_retainer", value.another_retainer_);
+ *   }
+ *   static const char* MemoryInfoName(const ExampleRetainer& value) {
+ *     return "ExampleRetainer";
+ *   }
+ *   static size_t SelfSize(const ExampleRetainer& value) {
+ *     return sizeof(value);
+ *   }
+ * };
+ *
+ * This creates the following graph:
+ *   Node / ExampleRetainer
+ *    |> another_retainer :: Node / AnotherRetainerClass
+ */
+template <typename T, typename = void>
+struct MemoryRetainerTraits {};
+
 class MemoryTracker {
  public:
   // Used to specify node name and size explicitly
@@ -254,6 +281,13 @@ class MemoryTracker {
   inline void TrackInlineField(const MemoryRetainer* retainer,
                                const char* edge_name = nullptr);
 
+  // MemoryRetainerTraits implementation helpers.
+  template <typename T>
+  inline void TraitTrack(const T& retainer, const char* edge_name = nullptr);
+  template <typename T>
+  inline void TraitTrackInline(const T& retainer,
+                               const char* edge_name = nullptr);
+
   inline v8::EmbedderGraph* graph() { return graph_; }
   inline v8::Isolate* isolate() { return isolate_; }
 

filename: src/node_url_pattern.cc
patch: @@ -8,6 +8,48 @@
 #include "path.h"
 #include "util-inl.h"
 
+namespace node {
+using node::url_pattern::URLPatternRegexProvider;
+
+template <>
+struct MemoryRetainerTraits<ada::url_pattern<URLPatternRegexProvider>> {
+  using Type = ada::url_pattern<URLPatternRegexProvider>;
+  static void MemoryInfo(MemoryTracker* tracker, const Type& value) {
+    tracker->TraitTrackInline(value.protocol_component, "protocol_component");
+    tracker->TraitTrackInline(value.username_component, "username_component");
+    tracker->TraitTrackInline(value.password_component, "password_component");
+    tracker->TraitTrackInline(value.hostname_component, "hostname_component");
+    tracker->TraitTrackInline(value.port_component, "port_component");
+    tracker->TraitTrackInline(value.pathname_component, "pathname_component");
+    tracker->TraitTrackInline(value.search_component, "search_component");
+    tracker->TraitTrackInline(value.hash_component, "hash_component");
+  }
+
+  static const char* MemoryInfoName(const Type& value) {
+    return "ada::url_pattern";
+  }
+
+  static size_t SelfSize(const Type& value) { return sizeof(value); }
+};
+
+template <>
+struct MemoryRetainerTraits<
+    ada::url_pattern_component<URLPatternRegexProvider>> {
+  using Type = ada::url_pattern_component<URLPatternRegexProvider>;
+  static void MemoryInfo(MemoryTracker* tracker, const Type& value) {
+    tracker->TrackField("pattern", value.pattern);
+    tracker->TrackField("group_name_list", value.group_name_list);
+  }
+
+  static const char* MemoryInfoName(const Type& value) {
+    return "ada::url_pattern_component";
+  }
+
+  static size_t SelfSize(const Type& value) { return sizeof(value); }
+};
+
+}  // namespace node
+
 namespace node::url_pattern {
 
 using v8::Array;
@@ -125,13 +167,7 @@ URLPattern::URLPattern(Environment* env,
 }
 
 void URLPattern::MemoryInfo(MemoryTracker* tracker) const {
-  tracker->TrackFieldWithSize("protocol", url_pattern_.get_protocol().size());
-  tracker->TrackFieldWithSize("username", url_pattern_.get_username().size());
-  tracker->TrackFieldWithSize("password", url_pattern_.get_password().size());
-  tracker->TrackFieldWithSize("hostname", url_pattern_.get_hostname().size());
-  tracker->TrackFieldWithSize("pathname", url_pattern_.get_pathname().size());
-  tracker->TrackFieldWithSize("search", url_pattern_.get_search().size());
-  tracker->TrackFieldWithSize("hash", url_pattern_.get_hash().size());
+  tracker->TraitTrackInline(url_pattern_, "url_pattern");
 }
 
 void URLPattern::New(const FunctionCallbackInfo<Value>& args) {

filename: test/pummel/test-heapdump-urlpattern.js
patch: @@ -0,0 +1,25 @@
+// Flags: --expose-internals
+'use strict';
+require('../common');
+const { validateSnapshotNodes } = require('../common/heap');
+const { URLPattern } = require('node:url');
+
+validateSnapshotNodes('Node / URLPattern', []);
+const urlPattern = new URLPattern('https://example.com/:id');
+validateSnapshotNodes('Node / URLPattern', [
+  {
+    children: [
+      { node_name: 'Node / ada::url_pattern', edge_name: 'url_pattern' },
+    ],
+  },
+]);
+validateSnapshotNodes('Node / ada::url_pattern', [
+  {
+    children: [
+      { node_name: 'Node / ada::url_pattern_component', edge_name: 'protocol_component' },
+    ],
+  },
+]);
+
+// Use `urlPattern`.
+console.log(urlPattern);