This document summarizes AbortSignal
lifetime requirements, memory management
challenges that arise AbortSignal.any()
and AbortSignal.timeout()
, as well
as strategies for managing signal lifetimes.
An AbortSignal
can signal abort when it is not already aborted and one of the
following is true:
-
The signal was created by
AbortSignal.timeout()
. These signals will inevitably signal abort when the timer expires. -
The signal is the
signal
property for anAbortController
that has not been GCed. Once the controller is out of scope, it can no longer abort. -
The signal was created by
AbortSignal.any(signals)
and one ofsignals
can signal abort. These derived or composite signals depend on the source signals for abort, and the source signals are fixed at creation time. If any one of the sources can signal abort, so can the derived signal. But if none of the sources can signal abort, then neither can the derived signal since it cannot be aborted in any other way.
When an AbortSignal
signals abort,
it can be observed in two ways:
- By 'abort' event listeners attached to the signal
- By algorithms added by other
specs. Often these algorithms have no effect after the underlying
operation completes successfully, e.g.
fetch()
, in which case removing the algorithm would have no effect on correctness.
An AbortSignal
needs to be kept alive as long it can signal abort and
signaling abort can be
observed.
Timeout and derived signals need to be kept alive by the browser since they can still abort and be observed even when no references to them exist in userland. For timeout signals, this can be solved by having the timer (or timer task) hold a strong reference to the signal. For derived signals, this can be solved by having each source signal hold a strong reference.
But this approach holds on to resources for potentially much longer than is
necessary, e.g. when using AbortSignal.any()
with long-lived top-level
controllers or long timeouts associated with an already successful operation.
Can we free up resources when they are no longer needed?
This summarizes the approach we're taking in the Chromium prototype for
AbortSignal.any()
and cleanup of AbortSignal.timeout()
. The strategy is to
detect when a signal can abort and has observers, and to only hold a strong
reference to it when both are true. This only applies to timeout and derived
signals (signals associated with a controller are kept alive by their
controllers, and signals returned by AbortSignal.abort()
are already
aborted).
-
Ensure objects that add abort algorithms keep the associated signal alive while the abort algorithm can still have an effect. Also remove algorithms when signaling abort will no longer have an effect (this is somewhat orthogonal, but is another opportunity to free resources).
-
Simplify source/dependent relationships in
AbortSignal.any()
. Described in detail here, we cut out intermediate signals by linking derived signals directly to their "abort sources", which are signals associated with a timer or controller. This is allowed since new sources cannot be added after creation, and will be specced to provide an expected event dispatch order. -
Keep a strong reference to the signal from its global if the signal has an 'abort' event listener, the signal's relevant global is fully active (if a window), and the signal can still abort (either a timeout signal or it has a source that can still abort).
-
Add a pre-finalizer for
AbortController
that indicates its signal can no longer abort. This removes the source from any dependent (derived) signals. If the signal has no more sources that can abort, remove strong references held because of observers. -
Store references to source signals as strong pointers in derived signals. This ensures timeouts remain alive. Sources associated with controllers are removed in the previous step. Timeout sources will always abort, so they aren't removed, but a derived signal can be GCed when nothing is observing it (if no userland references), which will release the strong reference to the timeout signal.
-
Store references to dependent/derived signals as weak pointers. Source signals don't need to keep dependent signals alive since they are already kept alive if there are observers.
-
Clear all references when a signal is aborted.
As discussed in this issue pending timers associated with timeout signals where the abort cannot be observed can be problematic on some platforms and can waste resources, e.g. wasted CPU cycles and excessive wake-ups. This can be mitigated by:
- Implementing the behavior described above, which allows a timeout signal to be GCed as long as it doesn't have observers, and
- Adding a pre-finalizer on timeout signals that removes the timer