1. Introduction
This section is non-normative.
Scheduling can be an important developer tool for improving website performance. Broadly speaking, there are two areas where scheduling can be impactful: user-percieved latency and responsiveness. Scheduling can improve user-perceived latency to the degree that lower priority work can be pushed off in favor of higher priority work that directly impacts quality of experience. For example, pushing off execution of certain 3P library scripts during page load can benefit the user by getting pixels to the screen faster. The same applies to prioritizing work associated with content within the viewport. For script running on the main thread, long tasks can negatively affect both input and visual responsiveness by blocking input and UI updates from running. Breaking up these tasks into smaller pieces and scheduling the chunks or task continuations is a proven approach that applications and framework developers use to improve responsiveness.
Userspace schedulers typically work by providing methods to schedule tasks and
controlling when those tasks execute. Tasks usually have an associated
priority, which in large part determines when the task will run, in relation to
other tasks the scheduler controls. The scheduler typically operates by
executing tasks for some amount of time (a scheduler quantum) before yielding
control back to the browser. The scheduler resumes by scheduling a continuation
task, e.g. a call to setTimeout()
or postMessage()
.
While userspace schedulers have been successful, the situation could be
improved with a centralized browser scheduler and better scheduling primitives.
The priority system of a scheduler extends only as far as the scheduler’s
reach. A consequence of this for userspace schedulers is that the UA generally
has no knowledge of userspace task priorities. The one exception is if the
scheduler uses requestIdleCallback()
for some of its work, but this
is limited to the lowest priority work. The same holds if there are multiple schedulers on the page, which is increasingly common. For example, an app might
be built with a framework that has a schedueler (e.g. React), do some
scheduling on its own, and even embed a feature that has a scheduler (e.g. an
embedded map). The browser is the ideal coordination point since the browser
has global information, and the event loop is responsible for running tasks.
Prioritization aside, the current primitives that userspace schedulers rely on
are not ideal for modern use cases. setTimeout(0)
is the canonical way to
schedule a non-delayed task, but there are often minimum delay values (e.g. for
nested tasks) which can lead to poor performance due to increased latency. A well-known workaround is to
use postMessage()
or a MessageChannel
, but these APIs were not designed for scheduling, e.g. you
cannot queue callbacks. requestIdleCallback()
can be effective for
some use cases, but this only applies to idle tasks and does not account for
tasks whose priority can change, e.g. re-prioritizing off-screen content in
response to user input, like scrolling.
This specification introduces a new interface for developers to schedule and
control prioritized tasks and continuations. A task in this context is a
JavaScript callback that runs asynchronously in its own event loop task.
A continuation is the resumption of JavaScript code in a new event loop task after yielding control to the browser. The Scheduler
interface
exposes a postTask()
method to schedule tasks and a yield()
method to schedule continuations. The specification
defines a number of TaskPriorities
to control task and
continuation execution order. Additionally, a TaskController
and its
associated TaskSignal
can be used to abort scheduled tasks and control their
priorities.
2. Scheduling Tasks and Continuations
2.1. Task and Continuation Priorities
This spec formalizes three priorities to support scheduling tasks:
enum {
TaskPriority "user-blocking" ,"user-visible" ,"background" };
user-blocking
is the highest priority, and it is meant for
tasks that should run as soon as possible, such that running them at a lower priority would degrade
user experience. This could be (chunked) work that is directly in response to user input, or
updating the in-viewport UI state, for example.
Note that tasks scheduled with this priority will typically have a higher event loop priority
compared to other tasks, but they are not necessarily render-blocking. Work that needs to happen
immediately without interruption should typically be done synchronously — but this can lead to
poor responsiveness if the work takes too long. "user-blocking
" tasks, on the other
hand, can be used to break up work and remain remain responsive to input and rendering, while
increasing the liklihood that the work finishes as soon as possible.
user-visible
is the second highest priority, and it is meant
for tasks that will have useful side effects that are observable to the user, but either which are
not immediately observable or which are not essential to user experience. Tasks with this prioriy
are less important or less urgent than "user-blocking
" tasks. This is the default
priority.
background
is the lowest priority, and it is meant to be used
for tasks that are not time-critical, such as background log or metrics processing or initializing
certain third party libraries.
Note: Tasks scheduled through a given Scheduler
run in strict priority order, meaning the
scheduler will always run "user-blocking
" tasks before
"user-visible
" tasks, which in turn always run before "background
"
tasks. Continuations have a higher effective priority than tasks with the
same TaskPriority
.
2.2. The Scheduler
Interface
dictionary {
SchedulerPostTaskOptions AbortSignal ;
signal TaskPriority ; [
priority EnforceRange ]unsigned long long = 0; };
delay callback =
SchedulerPostTaskCallback any (); [Exposed =(Window ,Worker )]interface {
Scheduler Promise <any >postTask (SchedulerPostTaskCallback ,
callback optional SchedulerPostTaskOptions = {});
options Promise <undefined >yield (); };
Note: The signal
option can be either an AbortSignal
or a TaskSignal
, but is defined as an AbortSignal
since it is a superclass of TaskSignal
. For
cases where the priority might change, a TaskSignal
is needed. But for cases where only
cancellation is needed, an AbortSignal
would suffice, potentially making it easier to integrate
the API into existing code that uses AbortSignals
.
result = scheduler .
postTask
( callback, options )-
Returns a promise that is fulfilled with the return value of callback or rejected with the
AbortSignal
's abort reason, if the task is aborted. If callback throws an error during execution, the promise returned bypostTask()
will be rejected with that error.The task’s
priority
is determined by the combination of option’spriority
andsignal
:-
If option’s
priority
is specified, then thatTaskPriority
will be used to schedule the task, and the task’s priority is immutable. -
Otherwise, if option’s
signal
is specified and is aTaskSignal
object, then the task’s priority is determined by option’ssignal
's priority. In this case the task’s priority is dynamic, and can be changed by callingcontroller.setPriority()
for the associatedTaskController
. -
Otherwise, the task’s priority defaults to "
user-visible
".
If option’s
signal
is specified, then thesignal
is used by theScheduler
to determine if the task is aborted.If option’s
delay
is specified and greater than 0, then the execution of the task will be delayed for at leastdelay
milliseconds. -
result = scheduler .
yield
()-
Returns a promise that is fulfilled with
undefined
or rejected with theAbortSignal
's abort reason, if the continuation is aborted.The priority of the continuation and the signal used to abort it are inherited from the originating task. If the originating task was scheduled with via
postTask()
with anAbortSignal
, then that signal is used to determine if the continuation is aborted. The originating task’s priority (aTaskSignal
or fixed priority) is also used to determine the continuation’s priority. If the originating task did not have a priority, then "user-visible
" is used.
A Scheduler
object has an associated static priority task queue map,
which is a map from (TaskPriority
, boolean) to scheduler task queue. This map is
initialized to a new empty map.
A Scheduler
object has an associated dynamic priority task queue map,
which is a map from (TaskSignal
, boolean) to scheduler task queue. This map is
initialized to a new empty map.
Note: We implement dynamic prioritization by enqueuing tasks associated with a specific TaskSignal
into the same scheduler task queue, and changing that queue’s priority in
response to prioritychange
events. The dynamic priority task queue map holds the scheduler task queues whose priorities can change, and the map key is the TaskSignal
which
all tasks in the queue are associated with.
The values of the static priority task queue map are scheduler task queues whose
priorities do not change. Tasks with static priorities — those that were scheduled with an
explicit priority
option or a signal
option that is null or is an AbortSignal
— are placed in these queues, based on TaskPriority
, which is the key for the map.
An alternative, and logicially equivalent implementation, would be to maintain a single
per-TaskPriority
scheduler task queue, and move tasks between scheduler task queues in
response to a TaskSignal
's priority changing, inserting based on enqueue order. This approach would simplify selecting the next scheduler task queue from
all schedulers, but make priority changes more complex.
The postTask(callback, options)
method steps are to return the result of scheduling a postTask task for this given callback and options.
The yield()
method steps are to
return the result of scheduling a yield continuation for this.
2.3. Definitions
A scheduler task is a task with an additional numeric enqueue order item, initially set to 0.
The following task sources are defined as scheduler task sources, and must only be used for scheduler tasks.
- The posted task task source
-
This task source is used for tasks scheduled through
postTask()
oryield()
.
A scheduler task queue is a struct with the following items:
- priority
-
A
TaskPriority
. - is continuation
-
A boolean.
- tasks
-
A set of scheduler tasks.
- removal steps
-
An algorithm.
A scheduling state is a struct with the following items:
- abort source
-
An
AbortSignal
object or, initially null. - priority source
-
A
TaskSignal
object or null, initially null.
A task handle is a struct with the following items:
- task
-
A scheduler task or null.
- queue
-
A scheduler task queue or null.
- abort steps
-
An algorithm.
- task complete steps
-
An algorithm.
2.4. Processing Model
TaskPriority
priority, a boolean isContinuation, and an algorithm removalSteps:
-
Let queue be a new scheduler task queue.
-
Set queue’s priority to priority.
-
Set queue’s is continuation to isContinuation.
-
Set queue’s removal steps to removalSteps.
-
Return queue.
AbortSignal
or null signal:
-
Let handle be a new task handle.
-
Set handle’s task to null.
-
Set handle’s queue to null.
-
Set handle’s abort steps to the following steps:
-
Reject result with signal’s abort reason.
-
If task is not null, then
-
Remove task from queue.
-
If queue is empty, then run queue’s removal steps.
-
-
-
Set handle’s task complete steps to the following steps:
-
If signal is not null, then remove handle’s abort steps from signal.
-
If queue is empty, then run queue’s removal steps.
-
-
Return handle.
Priority | Is Continuation | Effective Priority |
---|---|---|
"background "
| false
| 0 |
"background "
| true
| 1 |
"user-visible "
| false
| 2 |
"user-visible "
| true
| 3 |
"user-blocking "
| false
| 4 |
"user-blocking "
| true
| 5 |
2.4.1. Queueing and Removing Scheduler Tasks
-
Let task be a new scheduler task.
-
Set task’s enqueue order to enqueue order.
-
Set task’s steps to steps.
-
Set task’s source to source.
-
Set task’s document to document.
-
Set task’s script evaluation environment settings object set to a new empty set.
-
Return task.
We should consider refactoring the HTML spec to add a constructor for task. One problem is we need the new task to be a scheduler task rather than a task.
2.4.2. Scheduling Tasks and Continuations
Scheduler
scheduler given a SchedulerPostTaskCallback
callback and SchedulerPostTaskOptions
options:
-
Let result be a new promise.
-
Let signal be options["
signal
"] if it exists, or otherwise null. -
If signal is not null and it is aborted, then reject result with signal’s abort reason and return result.
-
Let state be a new scheduling state.
-
Set state’s abort source to signal.
-
If options["
priority
"] exists, then set state’s priority source to the result of creating a fixed priority unabortable task signal given options["priority
"]. -
Otherwise if signal is not null and implements the
TaskSignal
interface, then set state’s priority source to signal. -
If state’s priority source is null, then set state’s priority source to the result of creating a fixed priority unabortable task signal given "
user-visible
". -
Let handle be the result of creating a task handle given result and signal.
-
If signal is not null, then add handle’s abort steps to signal.
-
Let enqueueSteps be the following steps:
-
Set handle’s queue to the result of selecting the scheduler task queue for scheduler given state’s priority source and false.
-
Schedule a task to invoke an algorithm for scheduler given handle and the following steps:
-
Let event loop be the scheduler’s relevant agent's event loop.
-
Set event loop’s current scheduling state to state.
-
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. -
Set event loop’s current scheduling state to null.
-
-
-
Let delay be options["
delay
"]. -
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:-
If signal is null or signal is not aborted, then run enqueueSteps.
-
-
Otherwise, run enqueueSteps.
-
Return result.
Note: The fixed priority unabortable signals created here can be cached and reused to avoid extra memory allocations.
Run steps after a timeout doesn’t necessarily account for suspension; see whatwg/html#5925.
Scheduler
scheduler:
-
Let result be a new promise.
-
Let inheritedState be the scheduler’s relevant agent's event loop's current scheduling state.
-
Let abortSource be inheritedState’s abort source if inheritedState is not null, or otherwise null.
-
If abortSource is not null and abortSource is aborted, then reject result with abortSource’s abort reason and return result.
-
Let prioritySource be inheritedState’s priority source if inheritedState is not null, or otherwise null.
-
If prioritySource is null, then set prioritySource to the result of creating a fixed priority unabortable task signal given "
user-visible
". -
Let handle be the result of creating a task handle given result and abortSource.
-
If abortSource is not null, then add handle’s abort steps to abortSource.
-
Set handle’s queue to the result of selecting the scheduler task queue for scheduler given prioritySource and true.
-
Schedule a task to invoke an algorithm for scheduler given handle and the following steps:
-
Resolve result.
-
-
Return result.
Note: The fixed priority unabortable signal created here can be cached and reused to avoid extra memory allocations.
Scheduler
scheduler given a TaskSignal
object signal and a boolean isContinuation:
-
If signal does not have fixed priority, then
-
If scheduler’s dynamic priority task queue map does not contain (signal, isContinuation), then
-
Let queue be the result of creating a scheduler task queue given signal’s priority, isContinuation, and the following steps:
-
Remove dynamic priority task queue map[(signal, isContinuation)].
-
-
Set dynamic priority task queue map[(signal, isContinuation)] to queue.
-
Add a priority change algorithm to signal that runs the following steps:
-
-
Return dynamic priority task queue map[(signal, isContinuation)].
-
-
Otherwise
-
Let priority be signal’s priority.
-
If scheduler’s static priority task queue map does not contain (priority, isContinuation) , then
-
Let queue be the result of creating a scheduler task queue given priority, isContinuation, and the following steps:
-
Remove static priority task queue map[(priority, isContinuation)].
-
-
Set static priority task queue map[(priority, isContinuation)] to queue.
-
-
Return static priority task queue map[(priority, isContinuation)].
-
Scheduler
scheduler given a task handle handle and an algorithm steps:
-
Let global be the relevant global object for scheduler.
-
Let document be global’s associated
Document
if global is aWindow
object; otherwise null. -
Let event loop be the scheduler’s relevant agent's event loop.
-
Let enqueue order be event loop’s next enqueue order.
-
Increment event loop’s next enqueue order by 1.
-
Set handle’s task to the result of queuing a scheduler task on handle’s queue given enqueue order, the posted task task source, and document, and that performs the following steps:
-
Run steps.
-
Run handle’s task complete steps.
-
Because this algorithm can be called from in parallel steps, parts of this and other algorithms are racy. Specifically, the next enqueue order should be updated atomically, and accessing the scheduler task queues should occur atomically. The latter also affects the event loop task queues (see this issue).
2.4.3. Selecting the Next Task to Run
Scheduler
scheduler has a runnable
task if the result of getting the runnable task queues for scheduler is
non-empty. Scheduler
scheduler:
-
Let queues be the result of getting the values of scheduler’s static priority task queue map.
-
Extend queues with the result of getting the values of scheduler’s dynamic priority task queue map.
-
Remove from queues any queue such that queue’s tasks do not contain a runnable scheduler task.
-
Return queues.
Scheduler
associated with the event loop has a runnable task.
-
Let queues be an empty set.
-
Let schedulers be the set of all
Scheduler
objects whose relevant agent’s event loop is event loop and that have a runnable task. -
For each scheduler in schedulers, extend queues with the result of getting the runnable task queues for scheduler.
-
If queues is empty return null.
-
Remove from queues any queue such that queue’s effective priority is less than any other item of queues.
-
Let queue be the scheduler task queue in queues whose first runnable task is the oldest.
Two tasks cannot have the same age since enqueue order is unique. -
Return queue.
Note: The next task to run is the oldest, highest priority runnable scheduler task from all Scheduler
s associated with the event loop.
2.5. Examples
TODO(shaseley): Add examples.
3. Controlling Tasks
Tasks scheduled through the Scheduler
interface can be controlled with a TaskController
by
passing the TaskSignal
provided by controller.signal
as the option
when calling postTask()
. The TaskController
interface supports aborting and changing the priority of a task or group of
tasks.
3.1. The TaskPriorityChangeEvent
Interface
[Exposed =(Window ,Worker )]interface :
TaskPriorityChangeEvent Event {(
constructor DOMString ,
type TaskPriorityChangeEventInit );
priorityChangeEventInitDict readonly attribute TaskPriority previousPriority ; };dictionary :
TaskPriorityChangeEventInit EventInit {required TaskPriority ; };
previousPriority
event .
previousPriority
-
Returns the
TaskPriority
of the correspondingTaskSignal
prior to thisprioritychange
event.The new
TaskPriority
can be read withevent.target.priority
.
The previousPriority
getter steps are to return the
value that the corresponding attribute was initialized to.
3.2. The TaskController
Interface
dictionary {
TaskControllerInit TaskPriority = "user-visible"; }; [
priority Exposed =(Window ,Worker )]interface :
TaskController AbortController {constructor (optional TaskControllerInit = {});
init undefined setPriority (TaskPriority ); };
priority
Note: TaskController
's signal
getter, which is inherited from AbortController
, returns a TaskSignal
object.
controller = new
TaskController
( init )-
Returns a new
TaskController
whosesignal
is set to a newly createdTaskSignal
with itspriority
initialized to init’spriority
. controller .
setPriority
( priority )-
Invoking this method will change the associated
TaskSignal
's priority, signal the priority change to any observers, and causeprioritychange
events to be dispatched.
new TaskController(init)
constructor steps are:
-
Let signal be a new
TaskSignal
object.
The setPriority(priority)
method steps are to signal priority change on this's signal given priority.
3.3. The TaskSignal
Interface
dictionary { (
TaskSignalAnyInit TaskPriority or TaskSignal )= "user-visible"; }; [
priority Exposed =(Window ,Worker )]interface :
TaskSignal AbortSignal { [NewObject ]static TaskSignal _any (sequence <AbortSignal >,
signals optional TaskSignalAnyInit = {});
init readonly attribute TaskPriority priority ;attribute EventHandler onprioritychange ; };
Note: TaskSignal
inherits from AbortSignal
and can be used in APIs that accept an AbortSignal
. Additionally, postTask()
accepts an AbortSignal
, which can be
useful if dynamic prioritization is not needed.
TaskSignal . any(signals, init)
- Returns a
TaskSignal
instance which will be aborted if any of signals is aborted. Its abort reason will be set to whichever one of signals caused it to be aborted. The signal’s priority will be determined by init’spriority
, which can either be a fixedTaskPriority
or aTaskSignal
, in which case the new signal’s priority will change along with this signal. signal .
priority
-
Returns the
TaskPriority
of the signal.
A TaskSignal
object has an associated priority (a TaskPriority
).
A TaskSignal
object has an associated priority changing (a boolean), which is intially set to false.
A TaskSignal
object has associated priority change algorithms,
(a set of algorithms that are to be executed when its priority changing value
is true), which is initially empty.
A TaskSignal
object has an associated source signal (a weak refernece
to a TaskSignal
that the object is dependent on for its priority), which is
initially null.
A TaskSignal
object has associated dependent signals (a weak set of TaskSignal
objects that are dependent on the object for their priority), which
is initially empty.
A TaskSignal
object has an associated dependent (a boolean), which is
initially false.
The priority
getter steps are to return this's priority.
The onprioritychange
attribute is an event
handler IDL attribute for the onprioritychange
event handler, whose event handler event
type is prioritychange
.
The static any(signals, init)
method steps are to
return the result of creating a dependent task signal from signals, init, and the current realm.
A TaskSignal
has fixed priority if it is a dependent signal with a null source signal.
To add a priority change algorithm algorithm to a TaskSignal
object signal, append algorithm to signal’s priority change algorithms.
AbortSignal
objects signals,
a TaskSignalAnyInit
init, and a realm:
-
Let resultSignal be the result of creating a dependent signal from signals using the
TaskSignal
interface and realm. -
Set resultSignal’s dependent to true.
-
If init["
priority
"] is aTaskPriority
, then: -
Otherwise:
-
Let sourceSignal be init["
priority
"]. -
If sourceSignal does not have fixed priority, then:
-
If sourceSignal’s dependent is true, then set sourceSignal to sourceSignal’s source signal.
-
Assert: sourceSignal is not dependent.
-
Set resultSignal’s source signal to a weak reference to sourceSignal.
-
Append resultSignal to sourceSignal’s dependent signals.
-
-
-
Return resultSignal.
TaskSignal
object signal, given a TaskPriority
priority:
-
If signal’s priority changing is true, then throw a "
NotAllowedError
"DOMException
. -
If signal’s priority equals priority then return.
-
Set signal’s priority changing to true.
-
Let previousPriority be signal’s priority.
-
Set signal’s priority to priority.
-
For each algorithm of signal’s priority change algorithms, run algorithm.
-
Fire an event named
prioritychange
at signal usingTaskPriorityChangeEvent
, with itspreviousPriority
attribute initialized to previousPriority. -
For each dependentSignal of signal’s dependent signals, signal priority change on dependentSignal with priority.
-
Set signal’s priority changing to false.
TaskPriority
priority and a realm realm.
-
Let init be a new
TaskSignalAnyInit
. -
Set init["
priority
"] to priority. -
Return the result of creating a dependent task signal from « », init, and realm.
3.3.1. Garbage Collection
A dependent TaskSignal
object must not be garbage collected while its source signal is non-null and it has registered event listeners for its prioritychange
event or its priority change algorithms is non-empty.
3.4. Examples
TODO(shaseley): Add examples.
4. Modifications to Other Standards
4.1. The HTML Standard
4.1.1. WindowOrWorkerGlobalScope
Each object implementing the WindowOrWorkerGlobalScope
mixin has a corresponding scheduler, which is initialized as a new Scheduler
.
partial interface mixin WindowOrWorkerGlobalScope { [Replaceable ]readonly attribute Scheduler scheduler ; };
The scheduler
attribute’s getter steps are to
return this's scheduler.
4.1.2. Event loop: definitions
Replace: For each event loop, every task source must be associated with a specific task queue.
With: For each event loop, every task source that is not a scheduler task source must be associated with a specific task queue.
Add: An event loop has a numeric next enqueue order which is initialized to 1.
Note: The next enqueue order is a strictly increasing number that is used to
determine task execution order across scheduler task queues of the same TaskPriority
across
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.
4.1.3. Event loop: processing model
Add the following steps to the event loop processing steps, before step 2:
-
Let queues be the set of the event loop's task queues that contain at least one runnable task.
-
Let schedulerQueue be the result of selecting the next scheduler task queue from all schedulers.
Modify step 2 to read:
-
If schedulerQueue is not null or queues is not empty:
Modify step 2.1 to read:
-
Let taskQueue be one of the following, chosen in an implementation-defined manner:
-
If queues is not empty, one of the task queues in queues, chosen in an implementation-defined manner.
-
schedulerQueue’s tasks, if schedulerQueue is not null.
-
Note: the HTML specification enables per-task source prioritization by making the selection of
the next task's task queue in the event loop processing steps implementation-defined.
Similarly, this specification makes selecting between the next Scheduler
task and the next task
from an event loop's task queues implementation-defined, which provides UAs with the
most scheduling flexibility.
But the intent of this specification is that the TaskPriority
of Scheduler
tasks would
influence the event loop priority. Specifically, "background
" tasks and
continuations are typically considered less important than most other event loop tasks, while
"user-blocking
" tasks and continuations, as well as "user-visible
"
continuations (but not tasks), are typically considered to be more important.
One strategy is to run Scheduler
tasks with an effective priority of 3
or higher with an elevated priority, e.g. lower than input, rendering, and other urgent work, but higher than most other task sources. Scheduler
tasks with an effective priority of 0 or 1 could be run only when no other tasks in an event loop's task queues are runnable, and Scheduler
tasks with an effective priority of 2 could be scheduled like other scheduling-related task sources,
e.g. the timer task source.
The taskQueue in this step will either be a set of tasks or a set of scheduler tasks. The steps that follow only remove an item, so they are 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.
4.1.4. HostMakeJobCallback(callable)
Add the following before step 5:
-
Let event loop be incumbent settings's realm's agent's event loop.
-
Let state be event loop’s current scheduling state.
Modify step 5 to read:
-
Return the JobCallback Record { [[Callback]]: callable, [[HostDefined]]: { [[IncumbentSettings]]: incumbent settings, [[ActiveScriptContext]]: script execution context, [[SchedulingState]]: state } }.
4.1.5. HostCallJobCallback(callback, V, argumentsList)
Add the following steps before step 5:
-
Let event loop be incumbent settings's realm's agent's event loop.
-
Set event loop’s current scheduling state to callback.[[HostDefined]].[[SchedulingState]].
Add the following after step 7:
-
Set event loop’s current scheduling state to null.
4.2. requestIdleCallback()
4.2.1. Invoke idle callbacks algorithm
Add the following step before step 3.3:
-
Let realm be the relevant realm for window.
-
Let state be a new scheduling state.
-
Set state’s priority source to the result of creating a fixed priority unabortable task signal given "
background
" and realm. -
Let event loop be realm’s agent's event loop.
-
Set event loop’s current scheduling state to state.
Add the following after step 3.3:
-
Set event loop’s current scheduling state to null.
5. Security Considerations
The main security consideration for the APIs defined in this specification is whether or not any information is potentially leaked between origins by timing-based side-channel attacks.
5.1. postTask
as a High-Resolution Timing Source
setTimeout()
's timeout value, postTask()
's delay
is expressed in
whole milliseconds (the minimum non-zero delay being 1 ms), so callers cannot
express any timing more precise than 1 ms. Further, since tasks are queued when
their delay expires and not run instantly, the precision available to callers
is further reduced. 5.2. Monitoring Another Origin’s Tasks
postTask()
or yield()
leak any information about other origins' tasks. We
consider an attacker running on one origin trying to obtain information about
code executing in another origin (and hence in a separate event loop) that is
scheduled in the same thread in a browser.
Because a thread within a UA can only run tasks from one event loop at a time, an attacker might be able to gain information about tasks running in another event loop by monitoring when their tasks run. For example, an attacker could flood the system with tasks and expect them to run consecutively; if there are large gaps in between, then the attacker could infer that another task ran, potentially in a different event loop. The information exposed in such a case would depend on implementation details, and implementations can reduce the amount of information as described below.
What Information Might Be Gained?
Concretely, an attacker would be able to detect when other tasks are executed
by the browser by either flooding the system with tasks or by recursively
scheduling tasks. This is a known attack that can be executed with existing APIs like postMessage()
.
The tasks that run instead of the attacker’s can be tasks in other event loops
as well as other tasks in the attacker’s event loop, including internal UA tasks
(e.g. garbage collection).
Assuming the attacker can determine with a high degree of probability that the task executing is in another event loop, then the question becomes what additional information can the attacker learn? Since inter-event-loop task selection is not specified, this information will be implementation-dependent and depends on how UAs order tasks between event loops. But UAs that use a prioritization scheme that treats event loops sharing a thread as a single event loop are vulnerable to exposing more information.
It is helpful to think about the set of potential tasks that a UA might choose instead of the attacker’s, which corresponds to the information gained. When an attacker floods the system with tasks, the set of possible tasks would be anything the UA deems to be higher priority at that moment. This could be the result of a static prioritization scheme, e.g. input is always highest priority, network is second highest, etc., or this could be more dynamic, e.g. the UA occasionally chooses to run tasks from other task sources depending on how long they’ve been starved. Using a dynamic scheme increases the set of potential task which in turn decreases the fidelity of the information.
postTask()
and yield()
support prioritization for
the tasks and continuations they schedule. How these tasks are interleaved with
other task sources is also implementation-dependent, however it might be
possible for an attacker to further reduce the set of potential tasks that can
run instead of its own by leveraging this priority. For example, if a UA uses a
simple static prioritization scheme spanning all event loops in a thread, then
using "user-blocking
" postTask()
tasks or
"user-visible
" and higher priority yield()
continuations — which are meant to have a higher event loop priority
— instead of postMessage()
tasks
might decrease this set, depending on their relative prioritization and what
runs between.
What Mitigations are Possible?
There are mitigations that implementers can consider to minimize the risk:
-
Where possible, isolate cross-origin event loops by running them in different threads. This type of attack depends on the event loops sharing a thread.
-
Use an inter-event-loop scheduling policy that is not strictly based on priority. For example, an implementation might use round-robin or fair-scheduling between event loops to prevent leaking information about task priority. Another possibility is to ensure lower priority tasks are periodically cycled in to prevent inferring priority information.
6. Privacy Considerations
This section is non-normative.
We have evaluated the APIs defined in this specification from a privacy perspective and do not believe there to be any privacy considerations.