优先级任务调度

社区组草案报告,

此版本:
https://wicg.github.io/scheduling-apis/
问题追踪:
GitHub
规范内标注
编辑:
(Google)

摘要

本规范定义了用于调度和控制优先级任务的API。

本文档状态

本规范由Web平台孵化社区组发布。 它不是W3C标准,也不在W3C标准制定流程中。 请注意,根据W3C社区贡献者许可协议(CLA),有有限的选择退出权利并适用其他条件。 了解更多关于W3C社区与业务组的信息。

1. 简介

本节为非规范性内容。

调度可以作为开发者提升网站性能的重要工具。总体而言,调度对两个方面有显著影响:用户感知的延迟和响应性。调度能够优化用户感知的延迟,将较低优先级的工作推迟,以优先处理直接影响体验质量的高优先级工作。例如,在页面加载时推迟部分第三方库脚本的执行,可以让内容更快地呈现于用户。将视口内内容相关的工作优先处理,同样能够改善体验。对于主线程运行的脚本,长任务会阻塞输入和界面更新,影响输入和视觉响应性。将长任务拆分为更小的片段,并调度这些片段或任务续作,是应用和框架开发者提升响应性的重要方法。

用户空间的调度器通常通过提供调度任务的方法及控制任务何时执行来工作。任务通常带有一个优先级,这很大程度上决定了该任务与其他任务的相对执行顺序。调度器常常会执行一段时间(调度时间片)后,将控制权交还浏览器,并通过续作任务(如 setTimeout() 或 postMessage() 等方式)恢复后续执行。

虽然用户空间调度器已被广泛应用,采用集中式浏览器调度器和更完善的调度原语会进一步改善现状。调度器的优先级系统只在其作用范围内生效。对于用户空间调度器而言,UA通常无法获知其任务的优先级,唯一的例外是调度器部分工作使用了 requestIdleCallback() ,但这限于最低优先级的任务。页面上如果有多个调度器(越来越常见,例如React框架内调度、应用自身调度以及内嵌地图等子模块的调度器),浏览器是理想的协调点,因为它掌握全局信息,并负责事件循环和任务管理。

除了优先级,用户空间调度器赖以实现的原语对于现代用例也并不理想。setTimeout(0) 是调度非延迟任务的标准方式,但由于存在最小延迟(如嵌套任务),实际会带来更高延迟和性能问题。一种常用变通方法是利用 postMessage() 或 MessageChannel, 但这些API并非为调度设计,例如,无法将回调加入队列。requestIdleCallback() 适用于部分场景,但仅限空闲任务,且无法根据用户输入(如滚动)动态调整任务优先级,比如对屏幕外内容重新排序。

本规范为开发者引入了新的接口,用于调度和控制优先级任务与续作。这里的任务是指异步运行于自身事件循环 任务的JavaScript回调。续作指的是在让渡控制权给浏览器后,在新的事件循环 任务中恢复JavaScript代码的执行。Scheduler 接口 提供 postTask() 方法用于调度任务,yield() 方法用于调度续作。规范定义了若干TaskPriorities 以控制任务和续作的执行顺序。同时,TaskController 及其相关的 TaskSignal 可用于中止已调度的任务及控制其优先级。

2. 任务与续作的调度

2.1. 任务和续作优先级

本规范正式定义了三种用于支持任务调度的优先级:

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

user-blocking 是最高优先级,专为需要尽快执行的任务准备,若以较低优先级运行会影响用户体验。例如,直接响应用户输入或更新视口内容的(分块)工作。

注意,该优先级调度的任务在事件循环中通常优先于其他任务,但不必然阻塞渲染。需要立即同步完成的工作一般建议同步执行,但如果耗时过长会导致响应性下降。而 "user-blocking" 任务可用于将工作切块,提升输入和渲染的响应性,并尽量加快任务完成速度。

user-visible 是第二高优先级,用于对用户有有用副作用但不一定立刻可见或非关键体验的任务。这类任务的重要性和紧急程度低于 "user-blocking" 任务。此优先级为默认值。

background 是最低优先级,专用于非时间敏感的任务,如日志处理、指标统计或第三方库初始化等后台工作。

注意: 通过同一个 Scheduler 调度的任务以严格优先级顺序运行,即调度器总是优先运行 "user-blocking" 任务,然后是 "user-visible" 任务,最后是 "background" 任务。续作相较同优先级的普通任务有更高有效优先级。

2.2. Scheduler 接口

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();
};

注意: signal 选项既可以是 AbortSignal ,也可以是 TaskSignal, 但此处定义为 AbortSignal ,因为其为 TaskSignal 的超类。如需动态优先级控制,应使用 TaskSignal。仅需取消时,用 AbortSignal 即可,便于与现有使用 AbortSignals 的代码集成。

result = scheduler . postTask( callback, options )

返回一个Promise,成功时为 callback 的返回值,任务被中止时以 AbortSignal 的中止原因 失败。callback 抛错时,Promise以该错误拒绝。

任务的 priority 由 option 的 priority 和 signal 联合确定:

若指定 option 的 signal ,Scheduler用其判断任务是否被中止。

若指定 option 的 delay 且大于0,任务执行会延迟至少 delay 毫秒。

result = scheduler . yield()

返回一个Promise,成功时为 undefined,被中止时以 AbortSignal 的中止原因 失败。

续作的优先级及用于中止的signal均继承自原任务。若源任务以 postTask() 指定了 AbortSignal ,则使用该信号判断续作是否中止;原任务的优先级(TaskSignal 或固定优先级)也决定续作优先级。若原任务无优先级,则默认使用 "user-visible"。

Scheduler 对象拥有一个静态优先级任务队列映射, 即将 (TaskPriority, boolean) 映射为 调度器任务队列。初始化为空map。

Scheduler 对象还有一个动态优先级任务队列映射, 即将 (TaskSignal, boolean) 映射为 调度器任务队列。初始化为空map。

注意: 我们通过将特定 TaskSignal 关联的任务全部加入同一个调度器任务队列,并在prioritychange事件触发时调整队列优先级,以支持动态优先级管理。动态优先级任务队列映射用于存储那些优先级可变的调度器任务队列,其键为对应TaskSignal。

静态优先级任务队列映射存的是优先级不变的队列,包括通过明确 priority 指定或 signal 为 null 或为 AbortSignal 的任务,键为 TaskPriority。

另一种等价实现方式是每 TaskPriority 仅维护一个调度器任务队列,在 TaskSignal priority 变化时将任务在各队列间移动,按入队顺序排序。这简化了从所有调度器选择下一个任务队列的逻辑,但加重了优先级变更处理的复杂度。

postTask(callback, options) 方法:返回调度postTask任务的结果,参数为本对象、callback 和 options。

yield() 方法:返回调度yield续作的结果,参数为本对象。

2.3. 定义

调度器任务是一个任务,并额外包含初始值为0的数值类型入队顺序 条目。

下列任务源 被定义为调度器任务源, 仅可用于调度器任务。

已派发任务源

该任务源用于通过 postTask() 或 yield() 调度的任务。


调度器任务队列是一个结构体,包含如下条目:

优先级

TaskPriority。

是否为续作

布尔值。

任务集

集合,包含调度器任务。

移除算法

算法。


调度状态是一个结构体,包含如下条目:

中止源

AbortSignal 对象或 null,初始为 null。

优先级源

TaskSignal 对象或 null,初始为 null。


续作状态 是一个结构体, 包含如下条目:

状态映射

初始为空map。

注意: 上述状态映射可以用弱映射实现(键为可垃圾回收对象)。


任务句柄为结构体, 包含如下条目:

任务

调度器任务 或 null。

队列

调度器任务队列 或 null。

中止算法

算法。

任务完成算法

算法。

2.4. 处理模型

如果 调度器任务 t1 的 入队顺序 小于 调度器任务 t2 的 入队顺序,则 t1 早于 t2。
要用 TaskPriority priority、布尔值 isContinuation 和算法 removalSteps 创建调度器任务队列:
  1. 令 queue 为一个新的 调度器任务队列。

  2. 将 queue 的 优先级 设为 priority。

  3. 将 queue 的 是否为续作 设为 isContinuation。

  4. 将 queue 的 任务集 设为一个新的空 集合。

  5. 将 queue 的 移除算法 设为 removalSteps。

  6. 返回 queue。

要根据 promise result 和 AbortSignal 或 null signal 创建任务句柄:
  1. 令 handle 为一个新的 任务句柄。

  2. 将 handle 的 任务 设为 null。

  3. 将 handle 的 队列 设为 null。

  4. 将 handle 的 中止算法 设为以下步骤:

    1. 拒绝(reject) result,理由为 signal 的 中止原因。

    2. 如果 task 不为 null:

      1. 从 queue 移除 task。

      2. 如果 queue 为空,则运行 queue 的 移除算法。

  5. 将 handle 的 任务完成算法 设为以下步骤:

    1. 如果 signal 不为 null,则从 signal 移除 handle 的 中止算法。

    2. 如果 queue 为空,则运行 queue 的 移除算法。

  6. 返回 handle。

调度器任务队列 queue 的 首个可运行任务 是 queue 的 调度器任务 中第一个 可运行 的任务。
调度器任务队列 queue 的 有效优先级 由 queue 的 优先级 和 是否为续作 匹配的行的第三列计算得出:

优先级 是否为续作 有效优先级
"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. 调度器任务排队与移除

要在 调度器任务队列 queue 上按照数值 enqueue order、任务源 source、文档 document 执行流程步骤 steps 排队调度器任务:
  1. 令 task 为一个新的 调度器任务。

  2. 将 task 的 入队顺序 设为 enqueue order。

  3. 将 task 的 步骤 设为 steps。

  4. 将 task 的 任务源 设为 source。

  5. 将 task 的 文档 设为 document。

  6. 将 task 的 脚本执行环境设置对象集 设为一个新的空 集合。

  7. 追加 task 至 queue 的 任务集。

  8. 返回 task。

我们应考虑重构 HTML 规范,为 任务 添加构造器。 一个问题在于我们需要新任务为 调度器任务 而不是普通 任务。

要从 调度器任务 task 中 调度器任务队列 queue 移除,需 从 queue 的 任务集 移除 task。
调度器任务队列 queue 为空,当且仅当 queue 的 任务集 为空。

2.4.2. 任务与续作的调度

要将 scheduler(一个 Scheduler)的当前调度状态设置为 state(一个调度状态):
  1. 令 eventLoop 为 scheduler 的 相关 agent 的 事件循环。

  2. 以 eventLoop 为参数,为 scheduler 设置续作状态值为 state。

注意: 状态映射可以使用任何 key,只要对同一 Scheduler 唯一即可。

要获取scheduler(一个 Scheduler)的当前调度状态:
  1. 令 eventLoop 为 scheduler 的 关联 agent 的 事件循环(event loop)。

  2. 返回对 scheduler 及 eventLoop 获取延续状态值 的结果。

要为 Scheduler scheduler,给定 SchedulerPostTaskCallback callback 和 SchedulerPostTaskOptions options,调度一个 postTask 任务:
  1. 令 result 为 一个新的 promise。

  2. 令 signal 为 options["signal"] (如果其 存在),否则为 null。

  3. 如果 signal 非空且其为 已中止,则用 signal 的 中止原因 拒绝 result 并返回 result。

  4. 令 state 为一个新的 调度状态。

  5. 设置 state 的 中止源为 signal。

  6. 如果 options["priority"] 存在,则将 state 的 优先级源 设置为 创建固定优先级不可中止任务信号(参数为 options["priority"])的结果。

  7. 否则,如果 signal 非空且 实现了 TaskSignal 接口,则将 state 的 优先级源 设置为 signal。

  8. 如果 state 的 优先级源 为空,则将 state 的 优先级源 设置为 创建固定优先级不可中止任务信号(参数为 "user-visible")的结果。

  9. 令 handle 为创建任务句柄(参数为 result 和 signal)的结果。

  10. 如果 signal 非空,则 添加 handle 的中止步骤到 signal。

  11. 令 enqueueSteps 为如下步骤:

    1. 将 handle 的 队列 设为 选择调度器任务队列(参数为 scheduler、state 的 优先级源 和 false)的结果。

    2. 调度一个执行算法的任务给 scheduler,参数为 handle 以及以下步骤:

      1. 令 eventLoop 为 scheduler 的 关联 agent 的 事件循环。

      2. 设置当前调度状态为 state。

      3. 令 callbackResult 为 调用 callback,传参为 « » 及 "rethrow" 的结果。 如果其抛出异常,则 拒绝 result;否则,解决 result,值为 callbackResult。

      4. 将 eventLoop 的 当前延续状态 设为 null。

  12. 令 delay 为 options["delay"]。

  13. 如果 delay 大于 0,则 在超时后运行步骤,参数为 scheduler 的 相关全局对象、 "scheduler-postTask"、delay 以及以下步骤:

    1. 如果 signal 为空或 signal 未 中止,则执行 enqueueSteps。

  14. 否则,执行 enqueueSteps。

  15. 返回 result。

注:此处创建的固定优先级不可中止信号可以被缓存和复用以避免额外的内存分配。

定时器延后运行 不一定涵盖挂起场景;参见 whatwg/html#5925。

要为 Scheduler scheduler 调度一次 yield continuation:
  1. 令 result 为 一个新的 promise。

  2. 令 inheritedState 为对 scheduler 获取当前调度状态 的结果。

  3. 令 abortSource 为 inheritedState 的 中止源(若 inheritedState 不为 null),否则为 null。

  4. 如果 abortSource 不为 null 且被 中止,则用 abortSource 的 中止原因 拒绝(reject) result 并返回 。

  5. 令 prioritySource 为 inheritedState 的 优先级源(若 inheritedState 不为 null),否则为 null。

  6. 如果 prioritySource 为 null,则设为 创建固定优先级不可中止TaskSignal 的结果,参数为 "user-visible"。

  7. 令 handle 为 创建任务句柄 的结果,参数为 result 和 abortSource。

  8. 如果 abortSource 不为 null,则将 中止算法 添加到 abortSource(add)。

  9. 将 handle 的 队列 设为对 scheduler,以 prioritySource 和 true 为参数,调用 选择调度器任务队列 的结果。

  10. 为 scheduler 用 handle 和以下步骤 调度一个运行算法的任务:

    1. resolve result。

  11. 返回 result。

注意: 此处创建的固定优先级不可中止 signal 可缓存和重用,以避免多余的内存分配。

要为 Scheduler scheduler,给定 TaskSignal 对象 signal 和布尔值 isContinuation,选择调度器任务队列:
  1. 如果 signal 没有 固定优先级,则

    1. 如果 scheduler 的 动态优先级任务队列映射 不 包含 (signal, isContinuation),则

      1. 令 queue 为 创建调度器任务队列 的结果,参数为 signal 的 priority、isContinuation 和以下步骤:

        1. 移除 动态优先级任务队列映射[(signal, isContinuation)]。

      2. 设置 动态优先级任务队列映射[(signal, isContinuation)] 为 queue。

      3. 为 signal 添加优先级变更算法,步骤如下:

        1. 将 queue 的 优先级 设为 signal 的 priority。

    2. 返回 动态优先级任务队列映射[(signal, isContinuation)]。

  2. 否则

    1. 令 priority 为 signal 的 priority。

    2. 如果 scheduler 的 静态优先级任务队列映射 不 包含 (priority, isContinuation),则

      1. 令 queue 为 创建调度器任务队列 的结果,参数为 priority、isContinuation 和以下步骤:

        1. 移除 静态优先级任务队列映射[(priority, isContinuation)]。

      2. 设置 静态优先级任务队列映射[(priority, isContinuation)] 为 queue。

    3. 返回 静态优先级任务队列映射[(priority, isContinuation)]。

要为 Scheduler scheduler,给定 task handle handle 和算法 steps,调度一个运行算法的任务:
  1. 令 global 为 scheduler 的 相关全局对象。

  2. 如果 global 是 Window 对象,令 document 为 global 的 关联 Document;否则为 null。

  3. 令 event loop 为 scheduler 的 相关 agent 的 事件循环。

  4. 令 enqueue order 为 event loop 的 next enqueue order。

  5. 将 event loop 的 next enqueue order 加一。

  6. 将 handle 的 任务 设为在 handle 的 队列 上,按 enqueue order, posted task 任务源 和 document 调用 排队调度器任务 的结果,并包含如下步骤:

    1. 执行 steps。

    2. 执行 handle 的 任务完成算法。

由于该算法可以在并行步骤中被调用,因此本算法以及其它算法的部分实现存在数据争用问题。特别是 next enqueue order 应原子性更新,访问 调度器任务队列 也需原子性,后者也影响事件循环任务队列(见 this issue)。

2.4.3. 选择下一个要运行的任务

一个 调度器(Scheduler) scheduler 有可运行任务,如果对 scheduler 获取可运行任务队列 的结果是非空。
要获取可运行任务队列,针对一个 Scheduler scheduler:
  1. 令 queues 为对 scheduler 的 静态优先级任务队列映射 获取值 的结果。

  2. 用 scheduler 的 动态优先级任务队列映射 获取值 的结果,扩展 queues。

  3. 从 queues 中移除所有其 任务不包含可运行的调度器任务的 queue。

  4. 返回 queues。

要从所有调度器中选择下一个调度器任务队列,给定一个 事件循环 event loop,执行以下步骤。它们返回一个 调度器任务队列,若没有和 event loop 关联的 Scheduler 有可运行任务,则返回 null。
  1. 令 queues 为一个空有序集合。

  2. 令 schedulers 为所有 Scheduler 对象的有序集合,其 相关代理的 事件循环为 event loop,且有可运行任务。

  3. 对 schedulers 中每个 scheduler,用 获取可运行任务队列 的结果 扩展 queues。

  4. 如果 queues 为空,返回 null。

  5. 移除 从 queues 中的任何 queue,使得 queue 的 有效优先级 小于 queues 的任何其他 项。

  6. 令 queue 为 queues 中 调度器任务队列,其 首个可运行任务是最老的。
    两个任务不会有相同的 age,因为 入队顺序是唯一的。

  7. 返回 queue。

注意:下一个将要运行的任务是所有与 事件循环 相关联的 Scheduler 中,优先级最高且最老的可运行 调度器任务。

2.5. 示例

TODO(shaseley):添加示例。

3. 控制任务

通过 Scheduler 接口调度的任务可以通过 TaskController 控制, 只需在调用 postTask() 时传入 TaskSignal, 该信号由 controller.signal 提供, 作为 option 传递。 TaskController 接口支持终止任务或任务组、变更优先级。

3.1. TaskPriorityChangeEvent 接口

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

  readonly attribute TaskPriority previousPriority;
};

dictionary TaskPriorityChangeEventInit : EventInit {
  required TaskPriority previousPriority;
};
event . previousPriority

返回对应 TaskPriority 的 TaskSignal 在本次 prioritychange 事件发生前的值。

新的 TaskPriority 可通过 event.target.priority 读取。

previousPriority 的 getter 步骤是返回该属性在初始化时设置的值。

3.2. TaskController 接口

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

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

  undefined setPriority(TaskPriority priority);
};

注意:TaskController 的 signal getter(继承自 AbortController), 返回一个 TaskSignal 对象。

controller = new TaskController( init )

返回一个新的 TaskController, 其 signal 被设置为新创建的 TaskSignal, 并用 init 的 priority 初始化其 priority。

controller . setPriority( priority )

调用此方法会更改关联的 TaskSignal 的 priority, 向所有观察者发出优先级变更信号,并触发 prioritychange 事件。

new TaskController(init) 构造步骤如下:
  1. 令 signal 为一个新的 TaskSignal 对象。

  2. 将 signal 的 priority 设置为 init["priority"]。

  3. 将 this 的 signal 设置为 signal。

setPriority(priority) 方法步骤为:在 signal priority change 上,对 this 的 signal,传入 priority。

3.3. TaskSignal 接口

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;
};

注意:TaskSignal 继承自 AbortSignal 可用于所有接受 AbortSignal 的 API。 此外,postTask() 也接受 AbortSignal, 如果不需要动态优先级,也非常有用。

TaskSignal . any(signals, init)
返回一个 TaskSignal 实例,当 signals 中任意一个被中止时也会被中止。它的 中止原因 将设置为使其中止的 signals 中的某一个。 此 signal 的 priority 将由 init 的 priority 决定, 可以为固定 TaskPriority ,也可以是 TaskSignal , 若为后者,新信号的 priority 会随该信号改变。
signal . priority

返回该信号的 TaskPriority。

TaskSignal 对象有一个关联的 priority(TaskPriority)。

TaskSignal 对象有一个关联的 priority changing(布尔),初始为 false。

TaskSignal 对象有一个关联的 priority change algorithms, (算法集合,当 priority changing 为 true 时执行),最初为空。

TaskSignal 对象有一个关联的 source signal(对 TaskSignal 的弱引用, 该对象依赖于其 priority),初始为 null。

TaskSignal 对象有一个关联的 dependent signals(对 TaskSignal 的弱集合,这些对象依赖于自身的 priority),初始为空。

TaskSignal 对象有一个关联的 dependent(布尔值),初始为 false。


priority getter 步骤为返回 this 的 priority。

onprioritychange 属性是 事件处理 IDL 属性, 用于 onprioritychange 事件处理函数,其 事件处理类型为 prioritychange。

静态 any(signals, init) 方法步骤为返回创建依赖任务信号 的结果,参数为 signals、init 及当前 realm。


TaskSignal 有固定优先级,如果它是一个 dependent signal 且其 source signal 为 null。

要添加优先级更改算法 algorithm 到 TaskSignal 对象 signal,追加 algorithm 到 signal 的 priority change algorithms。

要创建依赖任务信号,根据 list 的 AbortSignal 对象 signals, TaskSignalAnyInit init 及 realm:
  1. 令 resultSignal 为用 TaskSignal 接口 和 realm 从 signals 创建依赖信号 的结果。

  2. 将 resultSignal 的 dependent 设为 true。

  3. 如果 init["priority"] 是 TaskPriority:

    1. 将 resultSignal 的 priority 设为 init["priority"]。

  4. 否则:

    1. 令 sourceSignal 为 init["priority"]。

    2. 将 resultSignal 的 priority 设为 sourceSignal 的 priority。

    3. 如果 sourceSignal 不 有固定优先级,则:

      1. 如果 sourceSignal 的 dependent 为 true,则将 sourceSignal 设为其 source signal。

      2. 断言:sourceSignal 不是 dependent。

      3. 将 resultSignal 的 source signal 设置为 sourceSignal 的弱引用。

      4. 追加 resultSignal 到 sourceSignal 的 dependent signals。

  5. 返回 resultSignal。

要触发优先级变更,针对 TaskSignal 对象 signal,给定 TaskPriority priority:
  1. 如果 signal 的 priority changing 为 true,则 抛出 "NotAllowedError" DOMException。

  2. 如果 signal 的 priority 等于 priority 则返回。

  3. 将 signal 的 priority changing 设为 true。

  4. 令 previousPriority 为 signal 的 priority。

  5. 将 signal 的 priority 设为 priority。

  6. 遍历 signal 的 priority change algorithms,并运行每个 algorithm。

  7. 触发事件,事件名为 prioritychange, 目标为 signal,使用 TaskPriorityChangeEvent, 其 previousPriority 初始化为 previousPriority。

  8. 遍历 signal 的 dependent signals,对每个 dependentSignal 调用 signal priority change ,参数为 priority。

  9. 将 signal 的 priority changing 设为 false。

要创建固定优先级且不可中止任务信号,给定 TaskPriority priority 和 realm realm。
  1. 令 init 为新的 TaskSignalAnyInit。

  2. 将 init["priority"] 设为 priority。

  3. 返回 创建依赖任务信号 的结果,参数为 « »、init 和 realm。

3.3.1. 垃圾回收

dependent TaskSignal 对象在其 source signal 非 null 且为 prioritychange 事件注册过监听器,或其 priority change algorithms 非空时,不可被垃圾回收。

3.4. 示例

TODO(shaseley):添加示例。

4. 对其他标准的修改

4.1. HTML 标准

4.1.1. WindowOrWorkerGlobalScope

每个实现了 WindowOrWorkerGlobalScope mixin 的对象都有一个对应的 scheduler,初始化为新的 Scheduler。

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

scheduler 属性的 getter 步骤为 返回 this 的 scheduler。

4.1.2. 事件循环:定义

替换:对于每个 event loop,每个 task source 必须关联一个具体的 task queue。

改为:对于每个 event loop,每一个不是 scheduler task source 的 task source 必须被关联到一个具体的 task queue。

新增:event loop 有一个数值型 next enqueue order,初始化为 1。

注意:next enqueue order 是严格递增数,用于在所有与同一个 scheduler task queues 相关联的 TaskPriority 下确定任务执行顺序。 随着队列推进,可以使用时间戳,只要它严格递增且唯一。

新增:event loop 有一个 current continuation state(continuation state 或 null),初始为 null。

添加如下算法:

要设置 continuation state 值,针对 key 和 value,给定 eventLoop(一个event loop):
  1. 如果 eventLoop 的 current continuation state 为 null, 则将其设为新的 continuation state。

  2. 令 continuationState 为 eventLoop 的 current continuation state。

  3. 断言:continuationState 的 state map[key] 不 存在。

  4. 将 continuationState 的 state map[key] 设为 value。

要获取 continuation state 值,针对 key ,给定 eventLoop(一个 event loop):
  1. 令 continuationState 为 eventLoop 的 current continuation state。

  2. 如果 continuationState 非 null 且 continuationState 的 state map[key] 存在,则返回 continuationState 的 state map[key],否则返回 null。

4.1.3. 事件循环:处理模型

在事件循环处理步骤的第 2 步之前,添加如下步骤:

  1. 令 queues 为 集合,包含 event loop 的 task queues ,且队列包含至少一个 可运行 任务。

  2. 令 schedulerQueue 为 从所有调度器中选择下一个调度器任务队列 的结果。

修改步骤 2:

  1. 若 schedulerQueue 不为 null 或 queues 不为空:

修改 2.1 步如下:

  1. 令 taskQueue 为下列之一,以 实现自定义方式选出:

注意:HTML 规范通过让事件循环处理步骤中选择下一个 task source 的优先级可按源调整, 同理,本规范中用于在所有相关 event loop 的 task queues 和 Scheduler 任务之间选择任务也是实现自定义的, 这样为 UA 提供最大调度灵活性。

不过本规范的目的是让 Scheduler 任务的 TaskPriority 能影响事件循环优先级。 其中 "background" 任务和连续体通常比大多数其他事件循环任务更不重要, 而 "user-blocking" 任务和连续体,以及 "user-visible" 连续体(非任务)通常被视为更重要。

一种策略是,让 Scheduler 任务中 effective priority 大于等于 3 的任务优先执行, 比如设置为只低于输入、渲染等紧急工作,但高于大多数其他 task sources。 Scheduler 任务的 effective priority 为 0 或 1 只在事件循环 task queues 无可运行任务时才调度,而为 2 时可视为超时等调度任务源。

这一步中的 taskQueue 既可能是 任务集合,也可能是 任务 的集合,或 scheduler tasks 的集合。后续步骤只是 删除一个 项目,因此大致兼容。理想情况下应有统一的任务队列接口并支持 pop() 方法直接返回普通 task,但这涉及较多重构。

4.1.4. 事件循环:任务排队

将 排队微任务算法修改为接受一个可选布尔值 ignoreContinuationState(默认为 false)。

将第 5 步修改为:

  1. 令 continuationState 为 null。

  2. 如果 ignoreContinuationState 为 false 且 eventLoop 的当前连续体状态不为 null, 则将 continuationState 设为克隆 event loop 的当前连续体状态的结果。

  3. 将 microtask 的 steps 设为如下步骤:

    1. 如果 ignoreContinuationState 为 false,则将 eventLoop 的当前连续体状态设为 continuationState。

    2. 运行 steps。

    3. 如果 ignoreContinuationState 为 false,则将 eventLoop 的当前连续体状态设为 null。

4.1.5. HostMakeJobCallback(callable)

在第 5 步之前添加如下内容:

  1. 令 event loop 为 incumbent settings 的realm 的 agent 的 事件循环。

  2. 令 state 为 克隆 event loop 的当前连续体状态的结果(如 当前连续体状态不为 null),否则为 null。

将第 5 步修改为:

  1. 返回 JobCallback Record { [[Callback]]: callable, [[HostDefined]]: { [[IncumbentSettings]]: incumbent settings, [[ActiveScriptContext]]: script execution context, [[ContinuationState]]: state } }。

4.1.6. HostCallJobCallback(callback, V, argumentsList)

在第 5 步之前添加如下步骤:

  1. 令 event loop 为 incumbent settings 的realm 的 agent 的 事件循环。

  2. 将 event loop 的当前连续体状态设为 callback.[[HostDefined]].[[ContinuationState]]。

第 7 步后添加如下内容:

  1. 将 event loop 的当前连续体状态设为 null。

4.1.7. HostEnqueuePromiseJob(job, realm)

将第 2 步修改为:

  1. 排队一个微任务,ignoreContinuationState 设为 true,执行如下步骤:

4.2. requestIdleCallback()

4.2.1. 调用 idle callback 算法

在第 3.3 步之前添加如下步骤:

  1. 令 realm 为 window 的相关 realm。

  2. 令 state 为一个新的调度状态。

  3. 将 state 的优先级来源设为创建固定优先级不可中止任务信号的返回值,传入 "background" 和 realm。

  4. 令 scheduler 为 Scheduler, 其相关 realm为 realm。

  5. 为 scheduler 设置当前调度状态为 state。

在第 3.3 步后添加:

  1. 将 event loop 的当前连续体状态设为 null。

5. 安全性考虑

本节为非规范内容。

本规范定义的 API 的主要安全性考虑是是否可能通过基于时序的侧信道攻击泄露不同来源间的信息。

5.1. postTask 作为高精度计时源

该 API 不能作为高精度计时源使用。与 setTimeout() 的 timeout 值类似,postTask() 的 delay 单位为毫秒(最小非零延迟为 1 ms),调用者无法指定精度高于 1 ms 的计时。此外,由于任务是在延迟到期后排队而非立即执行,实际可用精度会进一步降低。

5.2. 监控其他来源的任务

第二个考虑是 postTask() 或 yield() 是否会泄露关于其它来源任务的信息。我们考虑攻击者运行在一个来源,试图获取调度在同一浏览器线程但属于另一个事件循环(且为不同来源)的代码信息。

由于 UA 中一个线程一次只能运行一个事件循环的任务,攻击者可以通过监测自己任务的运行时机推断其他事件循环中的任务。例如,攻击者可以用大量任务填充系统并期望连贯地执行;如果中间出现较大间歇,则可以推断有另一个任务(可能来自其他事件循环)运行。此时泄露信息的多少取决于实现细节,实际实现可通过下述措施减少信息泄露。

可能获得哪些信息?
具体来说,攻击者可以通过大量排队任务或递归排队任务检测浏览器何时执行了其它任务。这是一种已知攻击,可通过现有 API(如 postMessage())实现。 被插队的任务可能是其他事件循环的任务,也可能是攻击者自身事件循环的任务,包括 UA 内部任务(如垃圾回收)。

假定攻击者能够高概率判断当前执行的任务属于另一个事件循环,那么接下来问题是还能获知哪些额外信息?因为跨事件循环任务的选择没有标准化,所以这些信息依赖于实现以及 UA 如何排列不同事件循环的任务。但在一些实现中,若将共享线程的事件循环视为一个整体进行优先级调度,则可能泄漏更多信息。

可以把 UA 可能选择而非攻击者自身任务的“潜在任务集合”看作可获得的信息。当攻击者向系统排队大量任务时,可能被调度的任务集合就是当时 UA 认为更高优先级的所有任务。这可能是静态优先级(如输入恒为最高、网络次之等)的结果,也可能更动态,比如 UA 会偶尔调度其他已饿死的任务源。动态调度会扩大潜在任务集合,从而降低泄露信息的精确性。

postTask() 和 yield() 支持给调度的任务和连续体分配优先级。这些任务和其他任务源如何交错也取决于实现。但攻击者可以通过利用优先级降低可能被“插队”的任务集合。例如,如果 UA 使用全局静态优先级调度,则使用 "user-blocking" postTask() 任务或 "user-visible" 及更高优先级 yield() 连续体(这些连续体本应具有更高事件循环优先级), 而不是 postMessage() 任务时,可能会减小此集合,具体视优先级及插队内容而定。

如何规避风险?
实现者可以通过如下措施减少风险:

6. 隐私性注意事项

本节为非规范内容。

我们已从隐私角度评估本规范定义的 API,认为不存在隐私性问题。

一致性

文档约定

一致性要求通过描述性断言与RFC 2119术语结合表达。 规范性部分中的关键字 “MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“MAY” 和 “OPTIONAL” 应按照RFC 2119中的描述进行解释。 但为提高可读性, 本规范中这些词不会全部用大写字母出现。

本规范的所有文本都是规范性的, 除非明确标记为非规范性、示例和注释。[RFC2119]

本规范中的示例以“例如”引入, 或使用 class="example" 与规范性文本区分开, 如下所示:

这是一个说明性示例。

说明性注释以“注”开头, 并用 class="note" 与规范性文本区分, 如下所示:

注,这是一个说明性注释。

索引

本规范定义的术语

引用定义的术语

参考文献

规范性引用

[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/

说明性引用

[REQUESTIDLECALLBACK]
Scott Haseley. requestIdleCallback(). URL: https://w3c.github.io/requestidlecallback/

IDL 索引

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;
};

问题索引

我们应考虑重构 HTML 规范,为 task 添加构造函数。 一个问题是我们需要的新任务是 scheduler task,而不是 task。↵
超时后运行步骤 不一定考虑了挂起;见 whatwg/html#5925。↵
因为此算法可以从 并行 步骤中被调用, 此处及其它算法的部分内容存在竞态情况。特别是,next enqueue order 应该以原子方式更新,并且访问 scheduler task queues 也应原子地进行。后者也影响事件循环任务队列(见 该问题)。↵
此步骤中的 taskQueue 要么是 set 的 tasks,要么是 set 的 scheduler tasks。后续步骤仅 移除 一个 项,所以它们是 大致 兼容的。理想情况下,应该有一个通用的任务队列接口支持 pop() 方法,该方法能返回一个普通的 task,但这会涉及较多重构。↵
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?