3 ms·
This is a bit odd. It's polling WeakRefs to simulate FinalizationRegistry. I don't know what problem the author had with FinalizationRegistry, but WeakRef and F
by sfink 5y ago
This is a bit odd. It's polling WeakRefs to simulate FinalizationRegistry. I don't know what problem the author had with FinalizationRegistry, but WeakRef and FinalizationRegistry use the same logic (and mostly the same code) to determine when the target has been collected. (Source: I reviewed the implementations of WeakRef and FinalizationRegistry in Firefox -- https://bugzilla.mozilla.org/show_bug.cgi?id=1587096 https://bugzilla.mozilla.org/show_bug.cgi?id=1587096 for one of the bugs.)
My guess is that it ran into the restrictions on collection visibility. If you look at the target at all (or a WeakRef to it) while running JS, then it will not do the finalization callback until JS code completes running even if the object is no longer reachable. (It's artificially kept alive until JS finishes running.) The example code handles this with the setTimeout call, which allows the JS code to return before polling all the WeakRefs.
It could have been a testing problem? Or maybe it's simpler: there wasn't enough allocation pressure to trigger a GC. JS objects won't get collected until there's a GC. (I guess that's kind of a tautology...)
Although allowed by the spec, it would be very surprising for an implementation to clear a WeakRef at a different time than it calls the FinalizationRegistry callback. (Or did we end up requiring it to be in the same tick? I can't remember. There's some restriction, maybe because the finalizer could check whether the WeakRef target is still there and you wouldn't want them to disagree?)
But in general, it's true that you can't rely on any particular timing of finalization. It'll happen eventually in all browsers if there is continued allocation. (I think all browsers might eventually do an idle GC as well?) Things can also be kept alive for weird internal reasons -- what if something gets JITted and has an IC (inline cache) that refers to an object you expect to be dead? The JS engine might keep that jit code alive for longer than you'd expect, even if it collects other things.
And of course this won't handle cycles. But it is exactly the thing that FinalizationRegistry made it into the language for, for better or worse.
- brrrrrm 5y ago> ran into the restrictions on collection visibility Is there any more information about this? It seems really subtle!
- pwdisswordfish9 5y agoBasically, looking at the target of a WeakRef makes it ineligible for collection until at least the next await, which the spec mandates by way of the [[KeptAlive]] list. This is meant to prevent TOCTOU bugs by foolish programmers doing this: if (weakref.deref()) { /* garbage collection here */ use(weakref.deref()); }
- josefx 5y agoIsn't that a rather narrow case? What happens on the following, is that protected? if (weakref.deref()) { /* garbage collection here */ use(heavyComputations(), weakref.deref()); }
- pwdisswordfish9 5y agoYes, as long as there is no intervening suspension (await, or the script otherwise exiting to the event loop).
- pwdisswordfish9 5y ago> Although allowed by the spec, it would be very surprising for an implementation to clear a WeakRef at a different time than it calls the FinalizationRegistry callback. (Or did we end up requiring it to be in the same tick? I can't remember. There's some restriction, maybe because the finalizer could check whether the WeakRef target is still there and you wouldn't want them to disagree?) The spec says that both clearing WeakRefs and scheduling finalization callbacks should happen at the same time: <https://tc39.es/ecma262/#sec-weakref-execution https://tc39.es/ecma262/#sec-weakref-execution>. Of course, since that is merely ‘scheduling’, this means the callback can actually execute in some indefinite future, including never; the callback might as well be scheduled for AD 2101, with the runtime shut down long before then. It has to be done that way, because collection may happen in the middle of execution, but finalization callbacks cannot possibly interrupt it. The bigger problem with the spec is that when an object becomes eligible for finalization in the first place is… not at all obvious: <https://github.com/tc39/ecma262/issues/2650 https://github.com/tc39/ecma262/issues/2650>.
- sfink 5y agoWow, that issue is kind of mindblowing. To me, it proves that making GC observable rips a large hole in the semantics. The damage was ameliorated somewhat with [[KeptAlive]], but the destruction is not contained in any understandable or bounded way, and we'll be bumping into it for years to come. It feels like we managed to add something a lot like C and C++'s undefined behavior into JS. I thought it was just an issue of nondeterminism, but that issue proves that it could be a lot more problematic than that. I don't find the suggested optimizations particularly far-fetched, especially in a trace-compiled or otherwise specialized subset of the code. Something like inlining in a way that looks for all of the things provable locally even if they may not be true globally; it could totally do things like build alias sets of values and optimize down `w.deref() === obj` to `w.deref() !== undefined`.