From 7d2b7e178d47dae06ca6a6fb30d527018be6a616 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 18 Sep 2026 02:27:36 +0000 Subject: [PATCH] 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 Assisted-by: OpenCode PR-URL: https://github.com/nodejs/node/pull/66099 Reviewed-By: Matteo Collina --- src/histogram.cc | 16 ++++++++ src/histogram.h | 7 ++++ ...st-perf-hooks-histogram-external-memory.js | 38 +++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 test/parallel/test-perf-hooks-histogram-external-memory.js 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(this), EmbedderDataTag::kDefault); + ReportExternalMemory(); } HistogramBase::HistogramBase(Environment* env, @@ -2043,6 +2044,21 @@ HistogramBase::HistogramBase(Environment* env, HistogramImpl::InternalFields::kImplField, static_cast(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 wrap, std::shared_ptr 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());