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());