Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

Commit

Permalink
Normative: Replace finalization iterator with multiple callback calls (
Browse files Browse the repository at this point in the history
…#187)

* Normative: Replace finalization iterator with multiple callback calls

* Rename to FinalizationRegistry in the README

* Update explainer for one-off FinalizationRegistry calls

Co-authored-by: Jordan Harband <[email protected]>
Co-authored-by: Shu-yu Guo <[email protected]>
  • Loading branch information
3 people authored Apr 7, 2020
1 parent 582b027 commit df16d7d
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 125 deletions.
36 changes: 15 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Finalizers are tricky business and it is best to avoid them. They can be invoke

The proposed specification allows conforming implementations to skip calling finalization callbacks for any reason or no reason. Some reasons why many JS environments and implementations may omit finalization callbacks:
- If the program shuts down (e.g., process exit, closing a tab, navigating away from a page), finalization callbacks typically don't run on the way out. (Discussion: [#125](https://github.com/tc39/proposal-weakrefs/issues/125))
- If the FinalizationRegistry becomes "dead" (basically, unreachable), then finalization callbacks registered against it might not run. (Discussion: [#66](https://github.com/tc39/proposal-weakrefs/issues/66))
- If the FinalizationRegistry becomes "dead" (approximately, unreachable), then finalization callbacks registered against it might not run. (Discussion: [#66](https://github.com/tc39/proposal-weakrefs/issues/66))

All that said, sometimes finalizers are the right answer to a problem. The following examples show a few important problems that would be difficult to solve without finalizers.

Expand All @@ -90,10 +90,8 @@ The `FinalizationRegistry` class represents a group of objects registered with a

```js
class FileStream {
static #cleanUp(heldValues) {
for (const file of heldValues) {
console.error(`File leaked: ${file}!`);
}
static #cleanUp(heldValue) {
console.error(`File leaked: ${file}!`);
}

static #finalizationGroup = new FinalizationRegistry(this.#cleanUp);
Expand Down Expand Up @@ -132,9 +130,9 @@ This example shows usage of the whole `FinalizationRegistry` API:
- The object whose lifetime we're concerned with. Here, that's `this`, the `FileStream` object.
- A held value, which is used to represent that object when cleaning it up in the finalizer. Here, the held value is the underlying `File` object. (Note: the held value should not have a reference to the weak target, as that would prevent the target from being collected.)
- An unregistration token, which is passed to the `unregister` method when the finalizer is no longer needed. Here we use `this`, the `FileStream` object itself, since `FinalizationRegistry` doesn't hold a strong reference to the unregister token.
- The `FinalizationRegistry` constructor is called with a callback as an argument. This callback is called with an iterator of the held values.
- The `FinalizationRegistry` constructor is called with a callback as an argument. This callback is called with a held value.

The finalizer callback is called *after* the object is garbage collected, a pattern which is sometimes called "post-mortem". For this reason, a separate held value is put in the iterator, rather than the original object--the object's already gone, so it can't be used.
The finalizer callback is called *after* the object is garbage collected, a pattern which is sometimes called "post-mortem". For this reason, the `FinalizerRegistry` callback is called with a separate held value, rather than the original object--the object's already gone, so it can't be used.

In the above code sample, the `fs` object will be unregistered as part of the `close` method, which will mean that the finalizer will not be called, and there will be no error log statement. Unregistration can be useful to avoid other sorts of "double free" scenarios.

Expand All @@ -149,7 +147,7 @@ function makeAllocator(size, length) {
const freeList = Array.from({length}, (v, i) => size * i);
const memory = new ArrayBuffer(size * length);
const finalizationGroup = new FinalizationRegistry(
iterator => freeList.unshift(...iterator));
held => freeList.unshift(held));
return { memory, size, freeList, finalizationGroup };
}

Expand All @@ -167,9 +165,9 @@ This code uses a few features of the `FinalizationRegistry` API:
- An object can have a finalizer referenced by calling the `register` method of `FinalizationRegistry`. In this case, two arguments are passed to the `register` method:
- The object whose lifetime we're concerned with. Here, that's the `Uint8Array`
- A held value, which is used to represent that object when cleaning it up in the finalizer. In this case, the held value is an integer corresponding to the offset within the `WebAssembly.Memory` object.
- The `FinalizationRegistry` constructor is called with a callback as an argument. This callback is called with an iterator of the held values.
- The `FinalizationRegistry` constructor is called with a callback as an argument. This callback is called with a held value.

The `FinalizationRegistry` 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, once 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". The engine is free to batch calls, and a batch of calls only runs after all of the Promises have been processed. How the engine batches callbacks is implementation-dependent, and how those callbacks intersperse with Promise work should not be depended upon.

### Avoid memory leaks for cross-worker proxies

Expand All @@ -179,7 +177,7 @@ In a system with proxies and processes, remote proxies need to keep local object

Note: This kind of setup cannot collect cycles across workers. If in each worker the local object holds a reference to a proxy for the remote object, then the remote descriptor for the local object prevents the collection of the proxy for the remote object. None of the objects can be collected automatically when code outside the proxy library no longer references them. To avoid leaking, cycles across isolated heaps must be explicitly broken.

## Using `WeakRef`s and `FinalizationRegistry`s together
## Using `WeakRef` objects and `FinalizationRegistry` objects together

It sometimes makes sense to use `WeakRef` and `FinalizationRegistry` together. There are several kinds of data structures that want to weakly point to a value, and do some kind of cleanup when that value goes away. Note however that weak refs are cleared when their object is collected, but their associated `FinalizationRegistry` cleanup handler only runs in a later task; programming idioms that use weak refs and finalizers on the same object need to mind the gap.

Expand All @@ -191,12 +189,10 @@ In the initial example from this README, `makeWeakCached` used a `Map` whose val
// Fixed version that doesn't leak memory.
function makeWeakCached(f) {
const cache = new Map();
const cleanup = new FinalizationRegistry(iterator => {
for (const key of iterator) {
// See note below on concurrency considerations.
const ref = cache.get(key);
if (ref && !ref.deref()) cache.delete(key);
}
const cleanup = new FinalizationRegistry(key => {
// See note below on concurrency considerations.
const ref = cache.get(key);
if (ref && !ref.deref()) cache.delete(key);
});

return key => {
Expand Down Expand Up @@ -234,10 +230,8 @@ class IterableWeakMap {
#refSet = new Set();
#finalizationGroup = new FinalizationRegistry(IterableWeakMap.#cleanup);

static #cleanup(iterator) {
for (const { set, ref } of iterator) {
set.delete(ref);
}
static #cleanup({ set, ref }) {
set.delete(ref);
}

constructor(iterable) {
Expand Down
28 changes: 6 additions & 22 deletions spec/abstract-jobs.html
Original file line number Diff line number Diff line change
Expand Up @@ -275,33 +275,17 @@ <h1>AddToKeptObjects ( _object_ )</h1>
</emu-note>
</emu-clause>

<emu-clause id="sec-check-for-empty-cells" aoid="CheckForEmptyCells">
<h1> CheckForEmptyCells ( _finalizationRegistry_ ) </h1>
<p> The following steps are performed: </p>
<emu-alg>
1. Assert: _finalizationRegistry_ has an [[Cells]] internal slot.
1. For each _cell_ in _finalizationRegistry_.[[Cells]], do
1. If _cell_.[[WeakRefTarget]] is ~empty~, then
1. Return *true*.
1. Return *false*.
</emu-alg>
</emu-clause>

<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.
1. If CheckForEmptyCells(_finalizationRegistry_) is *false*, return.
1. Let _iterator_ be ! CreateFinalizationRegistryCleanupIterator(_finalizationRegistry_).
1. Assert: _finalizationRegistry_ has [[Cells]] and [[CleanupCallback]] internal slots.
1. If _callback_ is not present or *undefined*, set _callback_ to _finalizationRegistry_.[[CleanupCallback]].
1. Set _finalizationRegistry_.[[IsFinalizationRegistryCleanupJobActive]] to *true*.
1. Let _result_ be Call(_callback_, *undefined*, &laquo; _iterator_ &raquo;).
1. Set _finalizationRegistry_.[[IsFinalizationRegistryCleanupJobActive]] to *false*.
1. Set _iterator_.[[FinalizationRegistry]] to ~empty~.
1. If _result_ is an abrupt completion, return _result_.
1. Else, return NormalCompletion(*undefined*).
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*, &laquo; _cell_.[[HeldValue]] &raquo;).
1. Return NormalCompletion(*undefined*).
</emu-alg>
<emu-note type=editor>When called from HostCleanupFinalizationRegistry, if calling the callback throws an error, this will be caught within the RunJobs algorithm and reported to the host. HTML does not apply the RunJobs algorithm, but will also <a href="https://html.spec.whatwg.org/#report-the-error">report the error</a>, which may call `window.onerror`.</emu-note>
</emu-clause>
Expand Down
86 changes: 4 additions & 82 deletions spec/finalization-registry.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,12 @@ <h1> FinalizationRegistry ( _cleanupCallback_ ) </h1>
1. If NewTarget is *undefined*, throw a *TypeError* exception.
1. If IsCallable(_cleanupCallback_) is *false*, throw a *TypeError* exception.
1. Let _finalizationRegistry_ be ? OrdinaryCreateFromConstructor(NewTarget,
`"%FinalizationRegistryPrototype%"`, &laquo; [[Realm]], [[CleanupCallback]], [[Cells]],
[[IsFinalizationRegistryCleanupJobActive]] &raquo;).
`"%FinalizationRegistryPrototype%"`, &laquo; [[Realm]], [[CleanupCallback]],
[[Cells]] &raquo;).
1. Let _fn_ be the active function object.
1. Set _finalizationRegistry_.[[Realm]] to _fn_.[[Realm]].
1. Set _finalizationRegistry_.[[CleanupCallback]] to _cleanupCallback_.
1. Set _finalizationRegistry_.[[Cells]] to be an empty List.
1. Set _finalizationRegistry_.[[IsFinalizationRegistryCleanupJobActive]] to *false*.
1. Return _finalizationRegistry_.
</emu-clause>
</emu-clause>
Expand Down Expand Up @@ -90,8 +89,7 @@ <h1>Properties of the FinalizationRegistry Prototype Object</h1>
</li>
<li>is an ordinary object.</li>
<li>
does not have [[Cells]], [[CleanupCallback]], and
[[IsFinalizationRegistryCleanupJobActive]] internal slots.
does not have [[Cells]] and [[CleanupCallback]] internal slots.
</li>
</ul>

Expand Down Expand Up @@ -153,8 +151,6 @@ <h1>FinalizationRegistry.prototype.cleanupSome ( [ _callback_ ] )</h1>
<emu-alg>
1. Let _finalizationRegistry_ be the *this* value.
1. Perform ? RequireInternalSlot(_finalizationRegistry_, [[Cells]]).
1. If _finalizationRegistry_.[[IsFinalizationRegistryCleanupJobActive]] is *true*,
throw a *TypeError* exception.
1. If _callback_ is not *undefined* and IsCallable(_callback_) is
*false*, throw a *TypeError* exception.
1. Perform ? CleanupFinalizationRegistry(_finalizationRegistry_, _callback_).
Expand All @@ -174,81 +170,7 @@ <h1>Properties of FinalizationRegistry Instances</h1>
<p>
FinalizationRegistry instances are ordinary objects that inherit
properties from the FinalizationRegistry prototype. FinalizationRegistry
instances also have [[Cells]], [[CleanupCallback]], and
[[IsFinalizationRegistryCleanupJobActive]] internal slots.
instances also have [[Cells]] and [[CleanupCallback]] internal slots.
</p>
</emu-clause>

<emu-clause id="sec-finalization-registry-cleanup-iterator-objects">
<h1>FinalizationRegistry Cleanup Iterator Objects</h1>
<p>
A FinalizationRegistry Cleanup Iterator is an ordinary object, with
the structure defined below, that represents a specific iteration
over some specific FinalizationRegistry instance object. There is not
a named constructor for FinalizationRegistry Cleanup Iterator
objects. Instead, these iterator objects are created when the
cleanupCallback of the FinalizationRegistry is called.
</p>

<emu-clause id="sec-createfinalizationregistrycleanupiterator" aoid="CreateFinalizationRegistryCleanupIterator">
<h1>CreateFinalizationRegistryCleanupIterator ( _finalizationRegistry_ )</h1>
<p>The following steps are taken:</p>
<emu-alg>
1. Assert: Type(_finalizationRegistry_) is Object.
1. Assert: _finalizationRegistry_ has a [[Cells]] internal slot.
1. Assert: _finalizationRegistry_.[[Realm]].[[Intrinsics]].[[%FinalizationRegistryCleanupIteratorPrototype%]] exists and has been initialized.
1. Let _prototype_ be _finalizationRegistry_.[[Realm]].[[Intrinsics]].[[%FinalizationRegistryCleanupIteratorPrototype%]].
1. Let _iterator_ be ObjectCreate(_prototype_, &laquo; [[FinalizationRegistry]] &raquo;).
1. Set _iterator_.[[FinalizationRegistry]] to _finalizationRegistry_.
1. Return _iterator_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-%finalizationregistrycleanupiterator%-object">
<h1>The %FinalizationRegistryCleanupIteratorPrototype% Object</h1>
<p>The <dfn>%FinalizationRegistryCleanupIteratorPrototype%</dfn> object:</p>
<ul>
<li>has properties that are inherited by all FinalizationRegistry
Cleanup Iterator Objects.</li>
<li>is an ordinary object.</li>
<li>has a [[Prototype]] internal slot whose value is the intrinsic object %IteratorPrototype%.</li>
<li>has the following properties:</li>
</ul>

<emu-clause id="sec-%finalizationregistrycleanupiterator%.next">
<h1>%FinalizationRegistryCleanupIteratorPrototype%.next ( )</h1>
<emu-alg>
1. Let _iterator_ be the *this* value.
1. Perform ? RequireInternalSlot(_iterator_, [[FinalizationRegistry]]).
1. If _iterator_.[[FinalizationRegistry]] is ~empty~,
throw a *TypeError* exception.
1. Let _finalizationRegistry_ be _iterator_.[[FinalizationRegistry]].
1. Assert: Type(_finalizationRegistry_) is Object.
1. Assert: _finalizationRegistry_ has a [[Cells]] internal slot.
1. If _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. Return CreateIterResultObject(_cell_.[[HeldValue]], *false*).
1. Return CreateIterResultObject(*undefined*, *true*).
</emu-alg>
</emu-clause>

<emu-clause id="sec-%finalizationregistrycleanupiteratorprototype%-@@tostringtag">
<h1>%FinalizationRegistryCleanupIteratorPrototype% [ @@toStringTag ]</h1>
<p>The initial value of the @@toStringTag property is the String value `"FinalizationRegistry Cleanup Iterator"`.</p>
<p>This property has the attributes { [[Writable]]: *false*, [[Enumerable]]: *false*, [[Configurable]]: *true* }.</p>
</emu-clause>
</emu-clause>

<emu-clause id="sec-properties-of-finalization-registry-cleanup-iterator-instances">
<h1>Properties of FinalizationRegistry Cleanup Iterator Instances</h1>
<p>FinalizationRegistry Cleanup Iterator instances are ordinary
objects that inherit properties from the
%FinalizationRegistryCleanupIteratorPrototype% intrinsic
object. FinalizationRegistry Cleanup Iterator instances are
initially created with a [[FinalizationRegistry]] internal slot.
</emu-clause>

</emu-clause>
</emu-clause>

0 comments on commit df16d7d

Please sign in to comment.