-
Notifications
You must be signed in to change notification settings - Fork 41
Normative: Replace finalization iterator with multiple callback calls #187
Conversation
README.md
Outdated
|
||
The `FinalizationGroup` callback is passed an iterator of held values to give that callback control over how much work it wants to process. The callback may pull in only part of the iterator, and in this case, the rest of the work would be "saved for later". The callback is not called during execution of other JavaScript code, but rather "in between turns"; it is currently proposed to be restricted to run after all of the `Promise`-related work is done, right before turning control over to the event loop. | ||
The `FinalizationRegistry` callback is called potentially multiple times, for each registered object that becomes dead, with a relevant held value. The callback is not called during execution of other JavaScript code, but rather "in between turns"--it is not interspersed with Promise work, for example, but only runs after all of the Promises have been processed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interestingly the Promise interspersing would be one way for a program to detect that a callback is part of a "batch" of empties cells. A program probably shouldn't rely on non-interspersing since an engine is free to empty cells one at a time (or make it look like it did)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, non-interspersing of the finalization is not a reliable property (maybe I should just not mention it?), but it is reliable that it won't intersperse into Promises which are queued before the finalizers more (at least on the web and environments that queue tasks in a similar way).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably a valid warning to say that Promise work may not be performed before the next callback invocation and that a program should be careful if it does any asynchronous finalization work?
I'm just worried about making it sound like Promise work will never be performed between callback invocations since engines are free to stop iterations whenever they want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for raising this concern. Does @syg's wording resolve the issue for you?
<emu-clause id="sec-cleanup-finalization-registry" aoid="CleanupFinalizationRegistry"> | ||
<h1> CleanupFinalizationRegistry ( _finalizationRegistry_ [ , _callback_ ] ) </h1> | ||
<p> The following steps are performed: </p> | ||
<emu-alg> | ||
1. Assert: _finalizationRegistry_ has [[Cells]], | ||
[[CleanupCallback]], and [[IsFinalizationRegistryCleanupJobActive]] internal slots. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you still need [[IsFinalizationRegistryCleanupJobActive]]
to prevent re-entrancy through cleanupSome
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mhofman I see how this is an observable difference and we could keep that, but what would be the problem if cleanupSome is called in a reentrant way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends on what the original concerns with re-entrancy were.
If it was because the program could end up with 2 active iterators, then yes we don't need this check anymore. But in that case we could have allowed re-entrancy and just correctly tracked the currently "active" iterator instead of throwing on cleanupSome
.
If it was to avoid having 2 internal iterations over the empty cells, then the re-entrancy check is still needed. It feels like it could be complicated for implementers to support right, I'll let them voice their opinion on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the latter was a concern, but it'd be good if implementers told me whether this assumption is valid. I think the concern was really just at the level of, how do the iterators even work if leaked--that you should only be able to iterate on them during the cleanup task, and not before or after. In this sense, [[IsFinalizationRegistryCleanupJobActive]] may have already been somewhat overkill.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see an issue with recursively calling cleanupSome without iterators to worry about. It's an odd way to iterate through holdings but I don't see any problems really.
Co-Authored-By: Jordan Harband <[email protected]>
Co-Authored-By: Jordan Harband <[email protected]>
Co-Authored-By: Jordan Harband <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm with some wording suggestions
Co-Authored-By: Shu-yu Guo <[email protected]>
Co-Authored-By: Shu-yu Guo <[email protected]>
1. While _finalizationRegistry_.[[Cells]] contains a Record _cell_ such that _cell_.[[WeakRefTarget]] is ~empty~, then an implementation may perform the following steps, | ||
1. Choose any such _cell_. | ||
1. Remove _cell_ from _finalizationRegistry_.[[Cells]]. | ||
1. Perform ? Call(_callback_, *undefined*, « _cell_.[[HeldValue]] »). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading this again, this termination behavior isn't quite what I envisioned wrt backpressure on the cleanup tasks.
It's correct for cleanupSome to propagate the error up, but for the cleanup task, @mhofman's right in that letting exceptions interrupt is technically the same as letting the user express backpressure to the engine. The question to the engine is the same: if it has an interrupted iteration in the cleanup task, what should it do with that FR? Should the engine reschedule it? What if the user cleanup function always throws? Should the engine drop the FR after N tries?
I was envisioning something like in the background task, that all [[Cells]] in the FR will be iterated even if individual calls to cleanup throw. window.onerror
will be called once each for the error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I guess according to the current spec an implementation interrupts iteration is indistinguishable from an implementation that doesn't. The implementation that doesn't interrupt is equivalent to immediately running cleanup again after the current iteration is interrupted.
If this is intentional (which I'm happy with if so), then this deserves a note.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah wait, no, it's distinguishable in observing when the microtask checkpoint is performed...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think cleanupSome will just trigger one iteration, so it's even more distinguishable. With the automatically scheduled thing, yeah, we don't give flexibility on the microtask checkpoint around reporting an error that we could decide to give.
One path here would be to collect all the errors and throw an AggregateError if there's more than one (regardless of whether it's from cleanupSome or the background scheduler). What do you think of those semantics?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After thinking about this more and discussing it with @syg offline, my (our?) current thought is:
- AggregateError is unnecessary complexity; just throwing the first exception as the current PR does is a good solution.
- It's OK to exit iteration early due to an exception, as long as we keep making forward progress. Each time, we'll have at least one iteration, so that's guaranteed (both with cleanupSome and with background calls).
- Implementations could maintain the logic for background threads, to keep calling the cleanup callback with all the items they have, with the simple addition of a microtask checkpoint required after any exception thrown (which they are allowed to do anyway even if there's no exception). With this strategy, no requeueing is required in the background case.
Does anyone want to work on a test262 PR for this change? It might be easiest to develop in the context of an actual implementation; I'm not sure if anyone's drafted one yet. |
I have a totally untested V8 implementation on the finalizationregistry-callback branch, for those interested. Don't trust it being correct please! |
Actually I'll do the test262 PR. |
Hi Folks, would it be ok to push this to the last day for discussion? |
I don't mind, but it is very important that we resolve it during the March meeting. |
this achieved consensus and the tests are merged 🎉 |
…ls. r=jonco Implements the spec changes from: tc39/proposal-weakrefs#187 The spec change removes the `FinalizationRegistryCleanupIterator` in favour of calling the clean-up callback for each finalised value. It also allows to call `cleanupSome()` within the callback function. `FinalizationRegistryObject::cleanupQueuedRecords()` has been changed to iterate from back to front, because this allows us to call `GCVector::popCopy()`, which makes it more efficient to remove entries from the `records` vector. Differential Revision: https://phabricator.services.mozilla.com/D70821
…ls. r=jonco Implements the spec changes from: tc39/proposal-weakrefs#187 The spec change removes the `FinalizationRegistryCleanupIterator` in favour of calling the clean-up callback for each finalised value. It also allows to call `cleanupSome()` within the callback function. `FinalizationRegistryObject::cleanupQueuedRecords()` has been changed to iterate from back to front, because this allows us to call `GCVector::popCopy()`, which makes it more efficient to remove entries from the `records` vector. Differential Revision: https://phabricator.services.mozilla.com/D70821
…ls. r=jonco Implements the spec changes from: tc39/proposal-weakrefs#187 The spec change removes the `FinalizationRegistryCleanupIterator` in favour of calling the clean-up callback for each finalised value. It also allows to call `cleanupSome()` within the callback function. `FinalizationRegistryObject::cleanupQueuedRecords()` has been changed to iterate from back to front, because this allows us to call `GCVector::popCopy()`, which makes it more efficient to remove entries from the `records` vector. Differential Revision: https://phabricator.services.mozilla.com/D70821
…ls. r=jonco Implements the spec changes from: tc39/proposal-weakrefs#187 The spec change removes the `FinalizationRegistryCleanupIterator` in favour of calling the clean-up callback for each finalised value. It also allows to call `cleanupSome()` within the callback function. `FinalizationRegistryObject::cleanupQueuedRecords()` has been changed to iterate from back to front, because this allows us to call `GCVector::popCopy()`, which makes it more efficient to remove entries from the `records` vector. Differential Revision: https://phabricator.services.mozilla.com/D70821
Note, there's no microtask checkpoint between all these callbacks--it's like Array.prototype.map
Landing this is blocking on some sort of consensus that it's the right way. Discussion is ongoing in #60.
Closes #155
Closes #60