Skip to content

Commit 400c4e6

Browse files
committed
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. Assisted-by: OpenCode
1 parent 3c2439a commit 400c4e6

3 files changed

Lines changed: 61 additions & 0 deletions

File tree

‎src/histogram.cc‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1913,6 +1913,7 @@ HistogramBase::HistogramBase(Environment* env,
19131913
HistogramImpl::InternalFields::kImplField,
19141914
static_cast<HistogramImpl*>(this),
19151915
EmbedderDataTag::kDefault);
1916+
ReportExternalMemory();
19161917
}
19171918

19181919
HistogramBase::HistogramBase(Environment* env,
@@ -1924,6 +1925,21 @@ HistogramBase::HistogramBase(Environment* env,
19241925
HistogramImpl::InternalFields::kImplField,
19251926
static_cast<HistogramImpl*>(this),
19261927
EmbedderDataTag::kDefault);
1928+
ReportExternalMemory();
1929+
}
1930+
1931+
HistogramBase::~HistogramBase() {
1932+
env()->external_memory_accounter()->Decrease(env()->isolate(),
1933+
external_memory_);
1934+
}
1935+
1936+
// Reports the size of the native histogram to V8 so that the garbage
1937+
// collector accounts for it. Every object that refers to a native histogram
1938+
// reports its full size, including objects that share one after cloning.
1939+
void HistogramBase::ReportExternalMemory() {
1940+
external_memory_ = histogram()->GetMemorySize();
1941+
env()->external_memory_accounter()->Increase(env()->isolate(),
1942+
external_memory_);
19271943
}
19281944

19291945
void HistogramBase::MemoryInfo(MemoryTracker* tracker) const {

‎src/histogram.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ class HistogramBase final : public BaseObject, public HistogramImpl {
334334
v8::Local<v8::Object> wrap,
335335
std::shared_ptr<Histogram> histogram);
336336

337+
~HistogramBase() override;
338+
337339
BaseObject::TransferMode GetTransferMode() const override {
338340
return TransferMode::kCloneable;
339341
}
@@ -361,6 +363,11 @@ class HistogramBase final : public BaseObject, public HistogramImpl {
361363
};
362364

363365
private:
366+
void ReportExternalMemory();
367+
368+
// The native memory reported to V8 while this object is alive.
369+
size_t external_memory_ = 0;
370+
364371
static v8::CFunction fast_record_;
365372
static v8::CFunction fast_record_delta_;
366373
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
3+
// Tests that histogram objects report the memory of their native histogram to
4+
// V8, so that creating many short-lived histograms triggers garbage
5+
// collection.
6+
7+
const common = require('../common');
8+
const assert = require('assert');
9+
const { setImmediate: setImmediatePromise } = require('timers/promises');
10+
const {
11+
PerformanceObserver,
12+
constants: { NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY },
13+
createHistogram,
14+
} = require('perf_hooks');
15+
16+
let externalMemoryGCs = 0;
17+
const observer = new PerformanceObserver((list) => {
18+
for (const entry of list.getEntries()) {
19+
if (entry.detail.flags & NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY)
20+
externalMemoryGCs++;
21+
}
22+
});
23+
observer.observe({ entryTypes: ['gc'] });
24+
25+
// With the default options, a histogram holds 45,056 64-bit counts, about
26+
// 352 KiB. V8 starts a garbage collection once external memory has grown by
27+
// 64 MiB, so 400 unreferenced snapshots (about 137 MiB) must trigger one.
28+
const histogram = createHistogram();
29+
for (let i = 0; i < 400; i++) histogram.snapshot();
30+
31+
(async () => {
32+
// Performance entries for garbage collections are delivered asynchronously.
33+
for (let i = 0; i < 10 && externalMemoryGCs === 0; i++)
34+
await setImmediatePromise();
35+
observer.disconnect();
36+
assert.ok(externalMemoryGCs > 0,
37+
'Expected a garbage collection caused by external memory');
38+
})().then(common.mustCall());

0 commit comments

Comments
 (0)