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.
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')");