Commit 4290a2d5b93 for nodejs
commit 4290a2d5b939c86fd3f8963efb51580f7af1d329
Author: James M Snell <jasnell@gmail.com>
Date: Thu Sep 17 23:06:48 2026 +0000
perf_hooks: fix PerformanceObserver observer count leaks
Observing the same entry type more than once on a PerformanceObserver,
through repeated `observe({ type })` calls or duplicates in
`entryTypes`, incremented the internal observer count each time, while
`disconnect()` decremented it only once per observed type. The counts
never returned to zero, so GC tracking stayed installed, and entries
for types such as `'http'` and `'dns'` kept being created after all
observers had disconnected.
Count each entry type at most once per observer.
Assisted-by: OpenCode
Signed-off-by: James M Snell <jasnell@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66097
Reviewed-By: Xuguang Mei <meixuguang@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
diff --git a/lib/internal/perf/observe.js b/lib/internal/perf/observe.js
index d284d4a54fb..3ed9fd3fa73 100644
--- a/lib/internal/perf/observe.js
+++ b/lib/internal/perf/observe.js
@@ -291,16 +291,23 @@ class PerformanceObserver {
maybeDecrementObserverCounts(this.#entryTypes);
this.#entryTypes.clear();
for (let n = 0; n < entryTypes.length; n++) {
- if (ArrayPrototypeIncludes(kSupportedEntryTypes, entryTypes[n])) {
- this.#entryTypes.add(entryTypes[n]);
- maybeIncrementObserverCount(entryTypes[n]);
+ const entryType = entryTypes[n];
+ // Count each entry type at most once per observer, as disconnect()
+ // decrements the counts once per observed type.
+ if (ArrayPrototypeIncludes(kSupportedEntryTypes, entryType) &&
+ !this.#entryTypes.has(entryType)) {
+ this.#entryTypes.add(entryType);
+ maybeIncrementObserverCount(entryType);
}
}
} else {
if (!ArrayPrototypeIncludes(kSupportedEntryTypes, type))
return;
- this.#entryTypes.add(type);
- maybeIncrementObserverCount(type);
+ // Observing the same type again only replaces the options.
+ if (!this.#entryTypes.has(type)) {
+ this.#entryTypes.add(type);
+ maybeIncrementObserverCount(type);
+ }
if (buffered) {
const entries = filterBufferMapByNameAndType(undefined, type);
SafeArrayPrototypePushApply(this.#buffer, entries);
diff --git a/test/parallel/test-performanceobserver-observer-counts.js b/test/parallel/test-performanceobserver-observer-counts.js
new file mode 100644
index 00000000000..46dc8b0606a
--- /dev/null
+++ b/test/parallel/test-performanceobserver-observer-counts.js
@@ -0,0 +1,87 @@
+// Flags: --expose-internals
+'use strict';
+
+// Tests that the observer counts, which gate the creation of performance
+// entries, return to zero once observers disconnect, however the entry types
+// were observed.
+
+require('../common');
+const assert = require('node:assert');
+const { PerformanceObserver } = require('node:perf_hooks');
+const { internalBinding } = require('internal/test/binding');
+const { hasObserver } = require('internal/perf/observe');
+
+const {
+ observerCounts,
+ constants: {
+ NODE_PERFORMANCE_ENTRY_TYPE_GC,
+ NODE_PERFORMANCE_ENTRY_TYPE_HTTP,
+ NODE_PERFORMANCE_ENTRY_TYPE_DNS,
+ },
+} = internalBinding('performance');
+
+const kTypes = {
+ gc: NODE_PERFORMANCE_ENTRY_TYPE_GC,
+ http: NODE_PERFORMANCE_ENTRY_TYPE_HTTP,
+ dns: NODE_PERFORMANCE_ENTRY_TYPE_DNS,
+};
+
+function assertCounts(expected) {
+ for (const { 0: type, 1: index } of Object.entries(kTypes)) {
+ const count = expected[type] ?? 0;
+ assert.strictEqual(observerCounts[index], count,
+ `observer count for '${type}'`);
+ assert.strictEqual(hasObserver(type), count > 0, `hasObserver('${type}')`);
+ }
+}
+
+assertCounts({});
+
+{
+ // Observing the same type more than once counts it once.
+ const obs = new PerformanceObserver(() => {});
+ for (const type of ['gc', 'http', 'dns']) {
+ obs.observe({ type });
+ obs.observe({ type });
+ }
+ assertCounts({ gc: 1, http: 1, dns: 1 });
+ obs.disconnect();
+ assertCounts({});
+ // Disconnecting again must not decrement the counts any further.
+ obs.disconnect();
+ assertCounts({});
+}
+
+{
+ // Duplicate entry types are counted once.
+ const obs = new PerformanceObserver(() => {});
+ obs.observe({ entryTypes: ['http', 'http', 'gc', 'gc'] });
+ assertCounts({ gc: 1, http: 1 });
+ obs.disconnect();
+ assertCounts({});
+}
+
+{
+ // Replacing the observed entry types updates the counts.
+ const obs = new PerformanceObserver(() => {});
+ obs.observe({ entryTypes: ['gc', 'http'] });
+ assertCounts({ gc: 1, http: 1 });
+ obs.observe({ entryTypes: ['http'] });
+ assertCounts({ http: 1 });
+ obs.disconnect();
+ assertCounts({});
+}
+
+{
+ // Each observer is counted separately.
+ const obs1 = new PerformanceObserver(() => {});
+ const obs2 = new PerformanceObserver(() => {});
+ obs1.observe({ type: 'gc' });
+ obs2.observe({ type: 'gc' });
+ obs2.observe({ type: 'gc' });
+ assertCounts({ gc: 2 });
+ obs1.disconnect();
+ assertCounts({ gc: 1 });
+ obs2.disconnect();
+ assertCounts({});
+}