Commit 7d2b7e178d4 for nodejs

commit 7d2b7e178d47dae06ca6a6fb30d527018be6a616
Author: James M Snell <jasnell@gmail.com>
Date:   Fri Sep 18 02:27:36 2026 +0000

    perf_hooks: report histogram memory to V8

    The native object behind histograms created by `createHistogram()`,
    `importHistogram()`, and `snapshot()` did not report the memory of its
    HDR histogram to V8. That memory is about 352 KiB with the default
    options, and V8 did not take it into account when scheduling garbage
    collection. Short-lived histograms could therefore hold on to a large
    amount of native memory until an unrelated garbage collection: in a
    loop taking 2,000 snapshots, RSS grew by 477 MiB.

    Report the size of the native histogram while the object is alive, as
    `SlidingWindowHistogram` already does for its chunks. Objects that
    share a native histogram after cloning each report its full size.
    With this change, RSS grows by 8 MiB in the same loop.

    Signed-off-by: James M Snell <jasnell@gmail.com>
    Assisted-by: OpenCode
    PR-URL: https://github.com/nodejs/node/pull/66099
    Reviewed-By: Matteo Collina <matteo.collina@gmail.com>

diff --git a/src/histogram.cc b/src/histogram.cc
index 765397be2e9..a49f94f12dd 100644
--- a/src/histogram.cc
+++ b/src/histogram.cc
@@ -2032,6 +2032,7 @@ HistogramBase::HistogramBase(Environment* env,
       HistogramImpl::InternalFields::kImplField,
       static_cast<HistogramImpl*>(this),
       EmbedderDataTag::kDefault);
+  ReportExternalMemory();
 }

 HistogramBase::HistogramBase(Environment* env,
@@ -2043,6 +2044,21 @@ HistogramBase::HistogramBase(Environment* env,
       HistogramImpl::InternalFields::kImplField,
       static_cast<HistogramImpl*>(this),
       EmbedderDataTag::kDefault);
+  ReportExternalMemory();
+}
+
+HistogramBase::~HistogramBase() {
+  env()->external_memory_accounter()->Decrease(env()->isolate(),
+                                               external_memory_);
+}
+
+// Reports the size of the native histogram to V8 so that the garbage
+// collector accounts for it. Every object that refers to a native histogram
+// reports its full size, including objects that share one after cloning.
+void HistogramBase::ReportExternalMemory() {
+  external_memory_ = histogram()->GetMemorySize();
+  env()->external_memory_accounter()->Increase(env()->isolate(),
+                                               external_memory_);
 }

 void HistogramBase::MemoryInfo(MemoryTracker* tracker) const {
diff --git a/src/histogram.h b/src/histogram.h
index f526e9cb1cc..473ebf97136 100644
--- a/src/histogram.h
+++ b/src/histogram.h
@@ -334,6 +334,8 @@ class HistogramBase final : public BaseObject, public HistogramImpl {
       v8::Local<v8::Object> wrap,
       std::shared_ptr<Histogram> histogram);

+  ~HistogramBase() override;
+
   BaseObject::TransferMode GetTransferMode() const override {
     return TransferMode::kCloneable;
   }
@@ -361,6 +363,11 @@ class HistogramBase final : public BaseObject, public HistogramImpl {
   };

  private:
+  void ReportExternalMemory();
+
+  // The native memory reported to V8 while this object is alive.
+  size_t external_memory_ = 0;
+
   static v8::CFunction fast_record_;
   static v8::CFunction fast_record_delta_;
 };
diff --git a/test/parallel/test-perf-hooks-histogram-external-memory.js b/test/parallel/test-perf-hooks-histogram-external-memory.js
new file mode 100644
index 00000000000..0cca4a7a803
--- /dev/null
+++ b/test/parallel/test-perf-hooks-histogram-external-memory.js
@@ -0,0 +1,38 @@
+'use strict';
+
+// Tests that histogram objects report the memory of their native histogram to
+// V8, so that creating many short-lived histograms triggers garbage
+// collection.
+
+const common = require('../common');
+const assert = require('assert');
+const { setImmediate: setImmediatePromise } = require('timers/promises');
+const {
+  PerformanceObserver,
+  constants: { NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY },
+  createHistogram,
+} = require('perf_hooks');
+
+let externalMemoryGCs = 0;
+const observer = new PerformanceObserver((list) => {
+  for (const entry of list.getEntries()) {
+    if (entry.detail.flags & NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY)
+      externalMemoryGCs++;
+  }
+});
+observer.observe({ entryTypes: ['gc'] });
+
+// With the default options, a histogram holds 45,056 64-bit counts, about
+// 352 KiB. V8 starts a garbage collection once external memory has grown by
+// 64 MiB, so 400 unreferenced snapshots (about 137 MiB) must trigger one.
+const histogram = createHistogram();
+for (let i = 0; i < 400; i++) histogram.snapshot();
+
+(async () => {
+  // Performance entries for garbage collections are delivered asynchronously.
+  for (let i = 0; i < 10 && externalMemoryGCs === 0; i++)
+    await setImmediatePromise();
+  observer.disconnect();
+  assert.ok(externalMemoryGCs > 0,
+            'Expected a garbage collection caused by external memory');
+})().then(common.mustCall());