Skip to content

Commit

Permalink
Clean up yield inheritance
Browse files Browse the repository at this point in the history
 1. Key the scheduling state based on the {{Scheduler}} to prevent
    leaking it across (potentially cross-origin) windows. This changes
    the event loop's continuation state to be a small wrapper around a
    map. The continuation state is propagated in the same way, but the
    scheduler state is unique to the scheduler and not shared. In
    practice there will only be one entry in this map (a task or
    microtask can only have originated from one task), but the mechanism
    is generic enough to support other use cases, implementations
    can optimize this, and the key/value mapping hopefully makes the
    isolation clear.

    Alternatively, we could propagate only the state for the current
    scheduler, but we don't always know the current scheduler, e.g. in
    "queue a microtask", and this model is different enough from
    AsyncContext and Chrome's "Task Attribution" that we'd need a
    separate mechanism, which is a performance concern. The main
    behavioral difference is how propagating is handled in the case of
    A --> (B microtask) --> A. With this approach, the context is
    preserved in the second call to A, which matches the synchronous
    behavior of A --> calls B --> calls A.

 2. Propagate the current scheduling state in "queue a microtask",
    unless coming from JavaScript, in which case the propagation is
    handled by the abstract job closure. Previously, the state would be
    inherited only if it wasn't reset by another microtask or after the
    postTask callback ran. This fixes the inconsistency, making directly
    scheduled microtasks match microtasks originating from JavaScript.
  • Loading branch information
shaseley committed Mar 5, 2025
1 parent 83873f3 commit 7680be8
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 16 deletions.
63 changes: 53 additions & 10 deletions spec/patches.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,29 @@ determine task execution order across [=scheduler task queues=] of the same {{Ta
all {{Scheduler}}s associated with the same [=event loop=]. A timestamp would also suffice as long
as it is guaranteed to be strictly increasing and unique.

Add: An [=event loop=] has a <dfn for="event loop">current scheduling state</dfn> (a [=scheduling
state=] or null), which is initialized to null.
Add: An [=event loop=] has a <dfn for="event loop">current continuation state</dfn> (a
[=continuation state=] or null), which is initially null.

Add the following algorithms:

<div algorithm>
To <dfn>set the continuation state value</dfn> for |key| to |value| given an |eventLoop| (an
[=event loop=]):

1. Let |continuationState| be |eventLoop|'s [=event loop/current continuation state=].
1. If |continuationState| is null, then return.
1. Assert: |continuationState|'s [=continuation state/state map=][|key|] does not [=map/exist=].
1. Set |continuationState|'s [=continuation state/state map=][|key|] to |value|.
</div>

<div algorithm>
To <dfn>get the continuation state value</dfn> for |key| given an |eventLoop| (an [=event loop=]):

1. Let |continuationState| be |eventLoop|'s [=event loop/current continuation state=].
1. If |continuationState| is not null and |continuationState|'s
[=continuation state/state map=][|key|] [=map/exists=], then return |continuationState|'s
[=continuation state/state map=][|key|], otherwise return null.
</div>

### <a href="https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model">Event loop: processing model</a> ### {#sec-patches-html-event-loop-processing}

Expand Down Expand Up @@ -80,33 +101,55 @@ Issue: The |taskQueue| in this step will either be a [=set=] of [=tasks=] or a [
*roughly* compatible. Ideally, there would be a common task queue interface that supports a `pop()`
method that would return a plain [=task=], but that would involve a fair amount of refactoring.

### <a href="https://html.spec.whatwg.org/#queuing-tasks">Event Loop: Queuing Tasks</a> ### {#sec-patches-html-queuing-tasks}

Change the <a href="">To queue a microtask</a> algorithm to accept an optional boolean
|ignoreContinuationState| (default false).

Change Step 5 to the following:

1. Let |continuationState| be |eventLoop|'s [=event loop/current continuation state=] if
|ignoreContinuationState| is false, otherwise null.
1. Set <var ignore=''>microtask</var>'s <a attribute for="task">steps</a> to the following:
1. If |ignoreContinuationState| is false, then set |eventLoop|'s
[=event loop/current continuation state=] to |continuationState|.
1. Run <var ignore=''>steps</var>.
1. If |ignoreContinuationState| is false, then set |eventLoop|'s
[=event loop/current continuation state=] to null.

### <a href="https://html.spec.whatwg.org/multipage/webappapis.html#hostmakejobcallback">HostMakeJobCallback(callable)</a> ### {#sec-patches-html-hostmakejobcallback}

Add the following before step 5:

1. Let |event loop| be <var ignore=''>incumbent settings<var>'s
[=environment settings object/realm=]'s [=realm/agent=]'s [=agent/event loop=].
1. Let |state| be |event loop|'s [=event loop/current scheduling state=].
1. Let |state| be |event loop|'s [=event loop/current continuation state=].

Modify step 5 to read:

1. Return the <span>JobCallback Record</span> { \[[Callback]]: <var ignore=''>callable</var>,
\[[HostDefined]]: { \[[IncumbentSettings]]: <var ignore=''>incumbent settings</var>,
\[[ActiveScriptContext]]: <var ignore=''>script execution context</var>,
\[[SchedulingState]]: |state| } }.
\[[ContinuationState]]: |state| } }.

### <a href="https://html.spec.whatwg.org/multipage/webappapis.html#hostcalljobcallback">HostCallJobCallback(callback, V, argumentsList)</a> ### {#sec-patches-html-hostcalljobcallback}

Add the following steps before step 5:

1. Let |event loop| be <var ignore=''>incumbent settings<var>'s
[=environment settings object/realm=]'s [=realm/agent=]'s [=agent/event loop=].
1. Set |event loop|'s [=event loop/current scheduling state=] to
<var ignore=''>callback</var>.\[[HostDefined]].\[[SchedulingState]].
1. Set |event loop|'s [=event loop/current continuation state=] to
<var ignore=''>callback</var>.\[[HostDefined]].\[[ContinuationState]].

Add the following after step 7:

1. Set |event loop|'s [=event loop/current scheduling state=] to null.
1. Set |event loop|'s [=event loop/current continuation state=] to null.

### <a href="https://html.spec.whatwg.org/multipage/webappapis.html#hostenqueuepromisejob">HostEnqueuePromiseJob(job, realm)</a> ### {#sec-patches-html-hostenqueuepromisejob}

Change step 2 to:

1. Queue a microtask to perform the following steps with |ignoreContinuationState| set to true:

## <a href="https://w3c.github.io/requestidlecallback/">`requestIdleCallback()`</a> ## {#sec-patches-requestidlecallback}

Expand All @@ -118,9 +161,9 @@ Add the following step before step 3.3:
1. Let |state| be a new [=scheduling state=].
1. Set |state|'s [=scheduling state/priority source=] to the result of [=creating a fixed priority
unabortable task signal=] given "{{TaskPriority/background}}" and |realm|.
1. Let |event loop| be |realm|'s [=realm/agent=]'s [=agent/event loop=].
1. Set |event loop|'s [=event loop/current scheduling state=] to |state|.
1. Let |scheduler| be the {{Scheduler}} whose [=relevant realm=] is |realm|.
1. [=Set the current scheduling state=] for |scheduler| to |state|.

Add the following after step 3.3:

1. Set |event loop|'s [=event loop/current scheduling state=] to null.
1. Set |event loop|'s [=event loop/current continuation state=] to null.
40 changes: 34 additions & 6 deletions spec/scheduling-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,22 @@ A <dfn>scheduler task queue</dfn> is a [=struct=] with the following [=struct/it
A <dfn>scheduling state</dfn> is a [=struct=] with the following [=struct/items=]:

: <dfn for="scheduling state">abort source</dfn>
:: An {{AbortSignal}} object or, initially null.
:: An {{AbortSignal}} object or null, initially null.
: <dfn for="scheduling state">priority source</dfn>
:: A {{TaskSignal}} object or null, initially null.

<br/>

A <dfn>continuation state</dfn> is a [=struct=] with the following [=struct/items=]:

: <dfn for="continuation state">state map</dfn>
:: An initially empty [=map=].

Note: The [=continuation state/state map=] can be implemented as weak map if its keys are
implemented as garbage collected objects.

<br/>

A <dfn>task handle</dfn> is a [=struct=] with the following [=struct/items=]:

: <dfn for="task handle">task</dfn>
Expand Down Expand Up @@ -289,6 +299,25 @@ A <dfn>task handle</dfn> is a [=struct=] with the following [=struct/items=]:

### Scheduling Tasks and Continuations ### {#sec-scheduler-alg-scheduling-tasks-and-continuations}

<div algorithm>
To <dfn>set the current scheduling state</dfn> for |scheduler| (a {{Scheduler}}) to |state| (a
[=scheduling state=]):

1. Let |eventLoop| be |scheduler|'s [=relevant agent=]'s [=agent/event loop=].
1. [=Set the continuation state value=] for |scheduler| to |state| given |eventLoop|.

Note: Any key can be used for the [=continuation state/state map=] as long as it is unique to the
{{Scheduler}}.
</div>

<div algorithm>
To <dfn>get the current scheduling state</dfn> for |scheduler| (a {{Scheduler}}):

1. Let |eventLoop| be |scheduler|'s [=relevant agent=]'s [=agent/event loop=].
1. Return the result of [=getting the continuation state value=] for |scheduler| given
|eventLoop|.
</div>

<div algorithm>
To <dfn>schedule a postTask task</dfn> for {{Scheduler}} |scheduler| given a
{{SchedulerPostTaskCallback}} |callback| and {{SchedulerPostTaskOptions}} |options|:
Expand Down Expand Up @@ -316,12 +345,12 @@ A <dfn>task handle</dfn> is a [=struct=] with the following [=struct/items=]:
for |scheduler| given |state|'s [=scheduling state/priority source=] and false.
1. [=Schedule a task to invoke an algorithm=] for |scheduler| given |handle| and the following
steps:
1. Let |event loop| be the |scheduler|'s [=relevant agent=]'s [=agent/event loop=].
1. Set |event loop|'s [=event loop/current scheduling state=] to |state|.
1. Let |eventLoop| be the |scheduler|'s [=relevant agent=]'s [=agent/event loop=].
1. [=Set the current scheduling state=] for |scheduler| to |state|.
1. Let |callbackResult| be the result of [=invoking=] |callback| with « » and "`rethrow`".
If that threw an exception, then [=reject=] |result| with that. Otherwise, [=resolve=]
|result| with |callbackResult|.
1. Set |event loop|'s [=event loop/current scheduling state=] to null.
1. Set |eventLoop|'s [=event loop/current continuation state=] to null.
1. Let |delay| be |options|["{{SchedulerPostTaskOptions/delay}}"].
1. If |delay| is greater than 0, then [=run steps after a timeout=] given |scheduler|'s [=relevant
global object=], "`scheduler-postTask`", |delay|, and the following steps:
Expand All @@ -340,8 +369,7 @@ Issue: [=Run steps after a timeout=] doesn't necessarily account for suspension;
To <dfn>schedule a yield continuation</dfn> for {{Scheduler}} |scheduler|:

1. Let |result| be [=a new promise=].
1. Let |inheritedState| be the |scheduler|'s [=relevant agent=]'s [=agent/event loop=]'s
[=event loop/current scheduling state=].
1. Let |inheritedState| be the result of [=getting the current scheduling state=] for |scheduler|.
1. Let |abortSource| be |inheritedState|'s [=scheduling state/abort source=] if |inheritedState|
is not null, or otherwise null.
1. If |abortSource| is not null and |abortSource| is [=AbortSignal/aborted=], then [=reject=]
Expand Down

0 comments on commit 7680be8

Please sign in to comment.