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,
+  });
+}