Search code examples
javascriptnode.jsgarbage-collectionv8

Forcing garbage collection in Node to test WeakRef and FinalizationRegistry


I'm playing with WeakRef and FinalizationRegistry in V8, and I'm unable to verify the below code actually works in Node.js.

I'm using Node v15.3.0 and running it like this:

node --expose-gc transient.js:

I expect to see some finalizerCallback called! entries in the console log.

If I run it in a Chromium-based browser (try Run code snippet button), I can get that output if click the Trash icon (Collect garbage) in the Performance section of DevTools (in Chrome v87), while the async script is still running.

It works as expected in Firefox v83, I see all the finalizer callbacks when the script ends.

With Node, I'd expect to see it automatically as I invoke garbage collection explicitly, but the finalizer callbacks don't get called at all.

Is there any problem with the code itself, or is there a more reliable way of forcing GC in Node?

// by @noseratio
// see https://v8.dev/features/weak-references

class Transient {
  constructor(eventTarget) {
    const finalizerCallback = ({ eventTarget, eventListener }) => {
      console.log('finalizerCallback called!');
      eventTarget.removeEventListener('testEvent', eventListener);
    }
    const finalizer = new FinalizationRegistry(finalizerCallback);

    const strongRefs = { finalizer };
    const weakRef = new WeakRef(this);
    const eventListener = () => {
      console.log('eventListener called!');
      strongRefs.finalizer = null;
      weakRef.deref()?.test();
    }

    finalizer.register(this, { eventTarget, eventListener });

    eventTarget.addEventListener('testEvent', eventListener, { once: true });
  }

  test() {
    console.log("test called!");
  }
}

async function main() {
  const gc = globalThis?.global?.gc;
  console.log(`garbage collector func: ${gc}`);

  const eventTarget = new EventTarget();

  for (let i = 10; i > 0; i--) {
    void function () {
      // these instances of Transient really must be getting GC'ed!
      new Transient(eventTarget);
    }();
  
    await new Promise(r => setTimeout(() => r(gc?.(true)), 100));
  }

  console.log("finishing...")
  gc?.(true);
  await new Promise(r => setTimeout(r, 5000));

  eventTarget.dispatchEvent(new Event("testEvent"));
  console.log("done.")
}

main().catch(console.error);

Updated, if I increase the number of the for loop integrations, I'll eventually see some finalizerCallback calls, but they're still sporadic.


Solution

  • I've got the answer directly from @jasnell:

    You could try the natives syntax function %CollectGarbage

    I've tried it, and it worked exactly how I wanted. In real life, it's a chain of AbortController objects, more context here.

    To enable %CollectGarbage:

    node --allow-natives-syntax Transient.js
    

    To hide the % syntax from the static code analyzers, we can use eval:

    eval("%CollectGarbage('all')");