Commit a293ec77854 for nodejs
commit a293ec778547bd943fc243d5682d0de452897b45
Author: James M Snell <jasnell@gmail.com>
Date: Thu Sep 17 23:20:30 2026 +0000
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>
diff --git a/lib/internal/perf/observe.js b/lib/internal/perf/observe.js
index a4a962a8537..4a1931dd299 100644
--- a/lib/internal/perf/observe.js
+++ b/lib/internal/perf/observe.js
@@ -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();
}
}
}
diff --git a/test/fixtures/snapshot/perf-hooks-gc-observer.js b/test/fixtures/snapshot/perf-hooks-gc-observer.js
index cf98bf2692a..353f10fc0ee 100644
--- a/test/fixtures/snapshot/perf-hooks-gc-observer.js
+++ b/test/fixtures/snapshot/perf-hooks-gc-observer.js
@@ -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');
+ });
+ }
});
diff --git a/test/parallel/test-snapshot-perf-hooks-gc-observer.js b/test/parallel/test-snapshot-perf-hooks-gc-observer.js
index 13222665fe7..ce43592a958 100644
--- a/test/parallel/test-snapshot-perf-hooks-gc-observer.js
+++ b/test/parallel/test-snapshot-perf-hooks-gc-observer.js
@@ -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,
+ });
+}