From c887dc3749dcb2cff509e4a62d5e596ff5dcc26b Mon Sep 17 00:00:00 2001 From: Scott Haseley Date: Wed, 5 Mar 2025 11:10:02 -0800 Subject: [PATCH] Clean up yield inheritance 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. --- spec/patches.md | 64 +++++++++++++++++++++++++++++++++------- spec/scheduling-tasks.md | 40 +++++++++++++++++++++---- 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/spec/patches.md b/spec/patches.md index 51dfb20..d3be09e 100644 --- a/spec/patches.md +++ b/spec/patches.md @@ -32,8 +32,30 @@ 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 current scheduling state (a [=scheduling -state=] or null), which is initialized to null. +Add: An [=event loop=] has a current continuation state (a +[=continuation state=] or null), which is initially null. + +Add the following algorithms: + +
+ To set the continuation state value for |key| to |value| given an |eventLoop| (an + [=event loop=]): + + 1. If |eventLoop|'s [=event loop/current continuation state=] is null, then set |eventLoop|'s + [=event loop/current continuation state=] to a new [=continuation state=]. + 1. Let |continuationState| be |eventLoop|'s [=event loop/current continuation state=]. + 1. Assert: |continuationState|'s [=continuation state/state map=][|key|] does not [=map/exist=]. + 1. Set |continuationState|'s [=continuation state/state map=][|key|] to |value|. +
+ +
+ To get the continuation state value 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. +
### Event loop: processing model ### {#sec-patches-html-event-loop-processing} @@ -80,20 +102,36 @@ 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. +### Event Loop: Queuing Tasks ### {#sec-patches-html-queuing-tasks} + +Change the To queue a microtask 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 microtask's steps to the following: + 1. If |ignoreContinuationState| is false, then set |eventLoop|'s + [=event loop/current continuation state=] to |continuationState|. + 1. Run steps. + 1. If |ignoreContinuationState| is false, then set |eventLoop|'s + [=event loop/current continuation state=] to null. + ### HostMakeJobCallback(callable) ### {#sec-patches-html-hostmakejobcallback} Add the following before step 5: 1. Let |event loop| be incumbent settings'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 JobCallback Record { \[[Callback]]: callable, \[[HostDefined]]: { \[[IncumbentSettings]]: incumbent settings, \[[ActiveScriptContext]]: script execution context, - \[[SchedulingState]]: |state| } }. + \[[ContinuationState]]: |state| } }. ### HostCallJobCallback(callback, V, argumentsList) ### {#sec-patches-html-hostcalljobcallback} @@ -101,12 +139,18 @@ Add the following steps before step 5: 1. Let |event loop| be incumbent settings's [=environment settings object/realm=]'s [=realm/agent=]'s [=agent/event loop=]. - 1. Set |event loop|'s [=event loop/current scheduling state=] to - callback.\[[HostDefined]].\[[SchedulingState]]. + 1. Set |event loop|'s [=event loop/current continuation state=] to + callback.\[[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. + +### HostEnqueuePromiseJob(job, realm) ### {#sec-patches-html-hostenqueuepromisejob} + +Change step 2 to: + + 1. Queue a microtask to perform the following steps with |ignoreContinuationState| set to true: ## `requestIdleCallback()` ## {#sec-patches-requestidlecallback} @@ -118,9 +162,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. diff --git a/spec/scheduling-tasks.md b/spec/scheduling-tasks.md index c4874f6..2343010 100644 --- a/spec/scheduling-tasks.md +++ b/spec/scheduling-tasks.md @@ -175,12 +175,22 @@ A scheduler task queue is a [=struct=] with the following [=struct/it A scheduling state is a [=struct=] with the following [=struct/items=]: : abort source -:: An {{AbortSignal}} object or, initially null. +:: An {{AbortSignal}} object or null, initially null. : priority source :: A {{TaskSignal}} object or null, initially null.
+A continuation state is a [=struct=] with the following [=struct/items=]: + +: state map +:: 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. + +
+ A task handle is a [=struct=] with the following [=struct/items=]: : task @@ -289,6 +299,25 @@ A task handle is a [=struct=] with the following [=struct/items=]: ### Scheduling Tasks and Continuations ### {#sec-scheduler-alg-scheduling-tasks-and-continuations} +
+ To set the current scheduling state 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}}. +
+ +
+ To get the current scheduling state 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|. +
+
To schedule a postTask task for {{Scheduler}} |scheduler| given a {{SchedulerPostTaskCallback}} |callback| and {{SchedulerPostTaskOptions}} |options|: @@ -316,12 +345,12 @@ A task handle 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: @@ -340,8 +369,7 @@ Issue: [=Run steps after a timeout=] doesn't necessarily account for suspension; To schedule a yield continuation 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=]