Prioritized Task Scheduling

Draft Community Group Report,

This version:
https://wicg.github.io/scheduling-apis/
Issue Tracking:
GitHub
Inline In Spec
Editor:
(Google)

Abstract

This specification defines APIs for scheduling and controlling prioritized tasks.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

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 delay = 0;
};

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 by postTask() will be rejected with that error.

The task’s priority is determined by the combination of option’s priority and signal:

If option’s signal is specified, then the signal is used by the Scheduler 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 least delay milliseconds.

result = scheduler . yield()

Returns a promise that is fulfilled with undefined or rejected with the AbortSignal'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 an AbortSignal, then that signal is used to determine if the continuation is aborted. The originating task’s priority (a TaskSignal 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() or yield().


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

A scheduler task t1 is older than scheduler task t2 if t1’s enqueue order less than t2’s enqueue order.
To create a scheduler task queue with TaskPriority priority, a boolean isContinuation, and an algorithm removalSteps:
  1. Let queue be a new scheduler task queue.

  2. Set queue’s priority to priority.

  3. Set queue’s is continuation to isContinuation.

  4. Set queue’s tasks to a new empty set.

  5. Set queue’s removal steps to removalSteps.

  6. Return queue.

To create a task handle given a promise result and an AbortSignal or null signal:
  1. Let handle be a new task handle.

  2. Set handle’s task to null.

  3. Set handle’s queue to null.

  4. Set handle’s abort steps to the following steps:

    1. Reject result with signal’s abort reason.

    2. If task is not null, then

      1. Remove task from queue.

      2. If queue is empty, then run queue’s removal steps.

  5. Set handle’s task complete steps to the following steps:

    1. If signal is not null, then remove handle’s abort steps from signal.

    2. If queue is empty, then run queue’s removal steps.

  6. Return handle.

A scheduler task queue queue’s first runnable task is the first scheduler task in queue’s tasks that is runnable.
A scheduler task queue queue’s effective priority is computed as the third column of the row matching the queue’s priority and is continuation:

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

To queue a scheduler task on a scheduler task queue queue, which performs a series of steps steps, given a numeric enqueue order, a task source source, and a document document:
  1. Let task be a new scheduler task.

  2. Set task’s enqueue order to enqueue order.

  3. Set task’s steps to steps.

  4. Set task’s source to source.

  5. Set task’s document to document.

  6. Set task’s script evaluation environment settings object set to a new empty set.

  7. Append task to queue’s tasks.

  8. 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.

To remove a scheduler task task from scheduler task queue queue, remove task from queue’s tasks.
A scheduler task queue queue is empty if queue’s tasks is empty.

2.4.2. Scheduling Tasks and Continuations

To schedule a postTask task for Scheduler scheduler given a SchedulerPostTaskCallback callback and SchedulerPostTaskOptions options:
  1. Let result be a new promise.

  2. Let signal be options["signal"] if it exists, or otherwise null.

  3. If signal is not null and it is aborted, then reject result with signal’s abort reason and return result.

  4. Let state be a new scheduling state.

  5. Set state’s abort source to signal.

  6. If options["priority"] exists, then set state’s priority source to the result of creating a fixed priority unabortable task signal given options["priority"].

  7. Otherwise if signal is not null and implements the TaskSignal interface, then set state’s priority source to signal.

  8. 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".

  9. Let handle be the result of creating a task handle given result and signal.

  10. If signal is not null, then add handle’s abort steps to signal.

  11. Let enqueueSteps be the following steps:

    1. Set handle’s queue to the result of selecting the scheduler task queue for scheduler given state’s priority source and false.

    2. 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 event loop.

      2. Set event loop’s current scheduling state to state.

      3. 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.

      4. Set event loop’s current scheduling state to null.

  12. Let delay be options["delay"].

  13. 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:

    1. If signal is null or signal is not aborted, then run enqueueSteps.

  14. Otherwise, run enqueueSteps.

  15. 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.

To schedule a yield continuation for Scheduler scheduler:
  1. Let result be a new promise.

  2. Let inheritedState be the scheduler’s relevant agent's event loop's current scheduling state.

  3. Let abortSource be inheritedState’s abort source if inheritedState is not null, or otherwise null.

  4. If abortSource is not null and abortSource is aborted, then reject result with abortSource’s abort reason and return result.

  5. Let prioritySource be inheritedState’s priority source if inheritedState is not null, or otherwise null.

  6. If prioritySource is null, then set prioritySource to the result of creating a fixed priority unabortable task signal given "user-visible".

  7. Let handle be the result of creating a task handle given result and abortSource.

  8. If abortSource is not null, then add handle’s abort steps to abortSource.

  9. Set handle’s queue to the result of selecting the scheduler task queue for scheduler given prioritySource and true.

  10. Schedule a task to invoke an algorithm for scheduler given handle and the following steps:

    1. Resolve result.

  11. Return result.

Note: The fixed priority unabortable signal created here can be cached and reused to avoid extra memory allocations.

To select the scheduler task queue for a Scheduler scheduler given a TaskSignal object signal and a boolean isContinuation:
  1. If signal does not have fixed priority, then

    1. If scheduler’s dynamic priority task queue map does not contain (signal, isContinuation), then

      1. Let queue be the result of creating a scheduler task queue given signal’s priority, isContinuation, and the following steps:

        1. Remove dynamic priority task queue map[(signal, isContinuation)].

      2. Set dynamic priority task queue map[(signal, isContinuation)] to queue.

      3. Add a priority change algorithm to signal that runs the following steps:

        1. Set queue’s priority to signal’s priority.

    2. Return dynamic priority task queue map[(signal, isContinuation)].

  2. Otherwise

    1. Let priority be signal’s priority.

    2. If scheduler’s static priority task queue map does not contain (priority, isContinuation) , then

      1. Let queue be the result of creating a scheduler task queue given priority, isContinuation, and the following steps:

        1. Remove static priority task queue map[(priority, isContinuation)].

      2. Set static priority task queue map[(priority, isContinuation)] to queue.

    3. Return static priority task queue map[(priority, isContinuation)].

To schedule a task to invoke an algorithm for Scheduler scheduler given a task handle handle and an algorithm steps:
  1. Let global be the relevant global object for scheduler.

  2. Let document be global’s associated Document if global is a Window object; otherwise null.

  3. Let event loop be the scheduler’s relevant agent's event loop.

  4. Let enqueue order be event loop’s next enqueue order.

  5. Increment event loop’s next enqueue order by 1.

  6. 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:

    1. Run steps.

    2. 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

A Scheduler scheduler has a runnable task if the result of getting the runnable task queues for scheduler is non-empty.
To get the runnable task queues for a Scheduler scheduler:
  1. Let queues be the result of getting the values of scheduler’s static priority task queue map.

  2. Extend queues with the result of getting the values of scheduler’s dynamic priority task queue map.

  3. Remove from queues any queue such that queue’s tasks do not contain a runnable scheduler task.

  4. Return queues.

To select the next scheduler task queue from all schedulers given an event loop event loop, perform the following steps. They return a scheduler task queue or null if no Scheduler associated with the event loop has a runnable task.
  1. Let queues be an empty set.

  2. Let schedulers be the set of all Scheduler objects whose relevant agent’s event loop is event loop and that have a runnable task.

  3. For each scheduler in schedulers, extend queues with the result of getting the runnable task queues for scheduler.

  4. If queues is empty return null.

  5. Remove from queues any queue such that queue’s effective priority is less than any other item of queues.

  6. 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.

  7. Return queue.

Note: The next task to run is the oldest, highest priority runnable scheduler task from all Schedulers 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 corresponding TaskSignal prior to this prioritychange event.

The new TaskPriority can be read with event.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 priority = "user-visible";
};

[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 whose signal is set to a newly created TaskSignal with its priority initialized to init’s priority.

controller . setPriority( priority )

Invoking this method will change the associated TaskSignal's priority, signal the priority change to any observers, and cause prioritychange events to be dispatched.

The new TaskController(init) constructor steps are:
  1. Let signal be a new TaskSignal object.

  2. Set signal’s priority to init["priority"].

  3. Set this’s signal to signal.

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) priority = "user-visible";
};

[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’s priority, which can either be a fixed TaskPriority or a TaskSignal, 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.

To create a dependent task signal from a list of AbortSignal objects signals, a TaskSignalAnyInit init, and a realm:
  1. Let resultSignal be the result of creating a dependent signal from signals using the TaskSignal interface and realm.

  2. Set resultSignal’s dependent to true.

  3. If init["priority"] is a TaskPriority, then:

    1. Set resultSignal’s priority to init["priority"].

  4. Otherwise:

    1. Let sourceSignal be init["priority"].

    2. Set resultSignal’s priority to sourceSignal’s priority.

    3. If sourceSignal does not have fixed priority, then:

      1. If sourceSignal’s dependent is true, then set sourceSignal to sourceSignal’s source signal.

      2. Assert: sourceSignal is not dependent.

      3. Set resultSignal’s source signal to a weak reference to sourceSignal.

      4. Append resultSignal to sourceSignal’s dependent signals.

  5. Return resultSignal.

To signal priority change on a TaskSignal object signal, given a TaskPriority priority:
  1. If signal’s priority changing is true, then throw a "NotAllowedError" DOMException.

  2. If signal’s priority equals priority then return.

  3. Set signal’s priority changing to true.

  4. Let previousPriority be signal’s priority.

  5. Set signal’s priority to priority.

  6. For each algorithm of signal’s priority change algorithms, run algorithm.

  7. Fire an event named prioritychange at signal using TaskPriorityChangeEvent, with its previousPriority attribute initialized to previousPriority.

  8. For each dependentSignal of signal’s dependent signals, signal priority change on dependentSignal with priority.

  9. Set signal’s priority changing to false.

To create a fixed priority unabortable task signal given TaskPriority priority and a realm realm.
  1. Let init be a new TaskSignalAnyInit.

  2. Set init["priority"] to priority.

  3. 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 Schedulers 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:

  1. Let queues be the set of the event loop's task queues that contain at least one runnable task.

  2. Let schedulerQueue be the result of selecting the next scheduler task queue from all schedulers.

Modify step 2 to read:

  1. If schedulerQueue is not null or queues is not empty:

Modify step 2.1 to read:

  1. Let taskQueue be one of the following, chosen in an implementation-defined manner:

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:

  1. Let event loop be incumbent settings's realm's agent's event loop.

  2. Let state be event loop’s current scheduling state.

Modify step 5 to read:

  1. 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:

  1. Let event loop be incumbent settings's realm's agent's event loop.

  2. Set event loop’s current scheduling state to callback.[[HostDefined]].[[SchedulingState]].

Add the following after step 7:

  1. 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:

  1. Let realm be the relevant realm for window.

  2. Let state be a new scheduling state.

  3. Set state’s priority source to the result of creating a fixed priority unabortable task signal given "background" and realm.

  4. Let event loop be realm’s agent's event loop.

  5. Set event loop’s current scheduling state to state.

Add the following after step 3.3:

  1. Set event loop’s current scheduling state to null.

5. Security Considerations

This section is non-normative.

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

This API cannot be used as a high-resolution timing source. Like 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

The second consideration is whether 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:

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.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ECMASCRIPT]
ECMAScript Language Specification. URL: https://tc39.es/ecma262/multipage/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

Informative References

[REQUESTIDLECALLBACK]
Ross McIlroy; Ilya Grigorik. requestIdleCallback() Cooperative Scheduling of Background Tasks. URL: https://w3c.github.io/requestidlecallback/

IDL Index

enum TaskPriority {
  "user-blocking",
  "user-visible",
  "background"
};

dictionary SchedulerPostTaskOptions {
  AbortSignal signal;
  TaskPriority priority;
  [EnforceRange] unsigned long long delay = 0;
};

callback SchedulerPostTaskCallback = any ();

[Exposed=(Window, Worker)]
interface Scheduler {
  Promise<any> postTask(SchedulerPostTaskCallback callback,
                        optional SchedulerPostTaskOptions options = {});
  Promise<undefined> yield();
};

[Exposed=(Window, Worker)]
interface TaskPriorityChangeEvent : Event {
  constructor(DOMString type, TaskPriorityChangeEventInit priorityChangeEventInitDict);

  readonly attribute TaskPriority previousPriority;
};

dictionary TaskPriorityChangeEventInit : EventInit {
  required TaskPriority previousPriority;
};

dictionary TaskControllerInit {
  TaskPriority priority = "user-visible";
};

[Exposed=(Window,Worker)]
interface TaskController : AbortController {
  constructor(optional TaskControllerInit init = {});

  undefined setPriority(TaskPriority priority);
};

dictionary TaskSignalAnyInit {
  (TaskPriority or TaskSignal) priority = "user-visible";
};

[Exposed=(Window, Worker)]
interface TaskSignal : AbortSignal {
  [NewObject] static TaskSignal _any(sequence<AbortSignal> signals, optional TaskSignalAnyInit init = {});

  readonly attribute TaskPriority priority;

  attribute EventHandler onprioritychange;
};

partial interface mixin WindowOrWorkerGlobalScope {
  [Replaceable] readonly attribute Scheduler scheduler;
};

Issues Index

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.
Run steps after a timeout doesn’t necessarily account for suspension; see whatwg/html#5925.
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).
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.
MDN

Scheduler/postTask

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

Scheduler

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskController/TaskController

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskController/setPriority

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskController

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskPriorityChangeEvent/TaskPriorityChangeEvent

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskPriorityChangeEvent/previousPriority

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskPriorityChangeEvent

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskSignal/priority

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskSignal/prioritychange_event

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskSignal/prioritychange_event

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskSignal

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

Window/scheduler

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?