perf_hooks: restore GC tracking after snapshot deserialization

V8 GC callbacks do not survive a snapshot, so a `'gc'`
PerformanceObserver that was active while building a user-land
snapshot received no entries after deserialization, unless another
`'gc'` observer was created.

When `'gc'` is observed while building a snapshot, add a deserialize
callback that registers the GC callbacks again if `'gc'` observers are
still active.

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>
This commit is contained in:
James M Snell authored and Node.js GitHub Bot committed 2026-09-20 19:14:12 +00:00
1 parent 10559877f4
commit a293ec7785
3 files changed
+59 -23

No files matched your search

+18
View File
@@ -149,6 +149,23 @@ function maybeDecrementObserverCounts(entryTypes) {
}
}
let gcTrackingDeserializeCallbackAdded = false;
// V8 GC callbacks do not survive a snapshot. When building one with active
// 'gc' observers, register the callbacks again after deserialization.
function maybeRestoreGarbageCollectionTrackingOnDeserialize() {
if (gcTrackingDeserializeCallbackAdded) return;
const {
namespace: {
addDeserializeCallback,
isBuildingSnapshot,
},
} = require('internal/v8/startup_snapshot');
if (!isBuildingSnapshot()) return;
gcTrackingDeserializeCallbackAdded = true;
addDeserializeCallback(updateGarbageCollectionTracking);
}
function maybeIncrementObserverCount(type) {
const observerType = getObserverType(type);
@@ -158,6 +175,7 @@ function maybeIncrementObserverCount(type) {
// idempotent, so it is called whenever the 'gc' observer count changes.
if (observerType === NODE_PERFORMANCE_ENTRY_TYPE_GC) {
updateGarbageCollectionTracking();
maybeRestoreGarbageCollectionTrackingOnDeserialize();
}
}
}
+25 -13
View File
@@ -4,7 +4,10 @@ const { PerformanceObserver } = require('node:perf_hooks');
const { setDeserializeMainFunction } = require('node:v8').startupSnapshot;
// Observe 'gc' entries while building the snapshot.
const observer = new PerformanceObserver(() => {});
let received = 0;
const observer = new PerformanceObserver((list) => {
received += list.getEntries().length;
});
observer.observe({ type: 'gc' });
// Performance entries are dispatched asynchronously, so trigger GCs until the
@@ -24,17 +27,26 @@ function waitForEntries(getCount, callback, attempts = 10) {
setDeserializeMainFunction(() => {
// The GC callbacks registered while building the snapshot do not survive
// it. Observing 'gc' after deserialization must register them again.
let received = 0;
const newObserver = new PerformanceObserver((list) => {
received += list.getEntries().length;
});
newObserver.observe({ type: 'gc' });
// it, so they must be registered again after deserialization.
if (process.env.TEST_NEW_OBSERVER) {
// Observing 'gc' again after deserialization.
let newReceived = 0;
const newObserver = new PerformanceObserver((list) => {
newReceived += list.getEntries().length;
});
newObserver.observe({ type: 'gc' });
waitForEntries(() => received, () => {
// Disconnecting must only remove GC callbacks that are registered.
newObserver.disconnect();
observer.disconnect();
console.log('ok');
});
waitForEntries(() => newReceived, () => {
// Disconnecting must only remove GC callbacks that are registered.
newObserver.disconnect();
observer.disconnect();
console.log('ok');
});
} else {
// The observer that was active while building the snapshot.
waitForEntries(() => received, () => {
observer.disconnect();
console.log('ok');
});
}
});
@@ -26,13 +26,19 @@ spawnSyncAndExitWithoutError(process.execPath, [
cwd: tmpdir.path,
});
spawnSyncAndAssert(process.execPath, [
'--expose-gc',
'--snapshot-blob',
blobPath,
], {
cwd: tmpdir.path,
}, {
stdout: 'ok',
trim: true,
});
// The observer that was active while building the snapshot receives entries
// after deserialization. With TEST_NEW_OBSERVER, a new observer is created
// after deserialization instead.
for (const env of [{}, { TEST_NEW_OBSERVER: '1' }]) {
spawnSyncAndAssert(process.execPath, [
'--expose-gc',
'--snapshot-blob',
blobPath,
], {
cwd: tmpdir.path,
env: { ...process.env, ...env },
}, {
stdout: 'ok',
trim: true,
});
}