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 = 0; };delay 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 的
priority,则使用该TaskPriority,任务优先级为固定不可变。 -
否则,若指定了 option 的
signal且类型为TaskSignal,则任务优先级由 option 的signal的priority 属性动态决定,可通过关联的TaskController的controller.setPriority()动态更改。 -
否则,优先级默认为 "
user-visible"。
若指定 option 的
signal,Scheduler用其判断任务是否被中止。 -
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()调度的任务。
- 优先级
- 是否为续作
-
布尔值。
- 任务集
- 移除算法
-
算法。
- 中止源
-
AbortSignal对象或 null,初始为 null。 - 优先级源
-
TaskSignal对象或 null,初始为 null。
- 状态映射
-
初始为空map。
注意: 上述状态映射可以用弱映射实现(键为可垃圾回收对象)。
2.4. 处理模型
TaskPriority
priority、布尔值 isContinuation 和算法 removalSteps 创建调度器任务队列:
AbortSignal
或 null signal 创建任务句柄:
| 优先级 | 是否为续作 | 有效优先级 |
|---|---|---|
"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. 调度器任务排队与移除
2.4.2. 任务与续作的调度
Scheduler)的当前调度状态:
-
令 eventLoop 为 scheduler 的 关联 agent 的 事件循环(event loop)。
-
返回对 scheduler 及 eventLoop 获取延续状态值 的结果。
Scheduler
scheduler,给定 SchedulerPostTaskCallback
callback 和 SchedulerPostTaskOptions
options,调度一个 postTask 任务:
-
令 result 为 一个新的 promise。
-
令 state 为一个新的 调度状态。
-
设置 state 的 中止源为 signal。
-
如果 options["
priority"] 存在,则将 state 的 优先级源 设置为 创建固定优先级不可中止任务信号(参数为 options["priority"])的结果。 -
否则,如果 signal 非空且 实现了
TaskSignal接口,则将 state 的 优先级源 设置为 signal。 -
如果 state 的 优先级源 为空,则将 state 的 优先级源 设置为 创建固定优先级不可中止任务信号(参数为 "
user-visible")的结果。 -
令 handle 为创建任务句柄(参数为 result 和 signal)的结果。
-
令 enqueueSteps 为如下步骤:
-
将 handle 的 队列 设为 选择调度器任务队列(参数为 scheduler、state 的 优先级源 和 false)的结果。
-
调度一个执行算法的任务给 scheduler,参数为 handle 以及以下步骤:
-
-
令 delay 为 options["
delay"]。 -
如果 delay 大于 0,则 在超时后运行步骤,参数为 scheduler 的 相关全局对象、 "
scheduler-postTask"、delay 以及以下步骤:-
如果 signal 为空或 signal 未 中止,则执行 enqueueSteps。
-
-
否则,执行 enqueueSteps。
-
返回 result。
注:此处创建的固定优先级不可中止信号可以被缓存和复用以避免额外的内存分配。
定时器延后运行 不一定涵盖挂起场景;参见 whatwg/html#5925。
Scheduler
scheduler 调度一次 yield continuation:
-
令 result 为 一个新的 promise。
-
令 inheritedState 为对 scheduler 获取当前调度状态 的结果。
-
令 abortSource 为 inheritedState 的 中止源(若 inheritedState 不为 null),否则为 null。
-
如果 abortSource 不为 null 且被 中止,则用 abortSource 的 中止原因 拒绝(reject) result 并返回 。
-
令 prioritySource 为 inheritedState 的 优先级源(若 inheritedState 不为 null),否则为 null。
-
如果 prioritySource 为 null,则设为 创建固定优先级不可中止TaskSignal 的结果,参数为 "
user-visible"。 -
令 handle 为 创建任务句柄 的结果,参数为 result 和 abortSource。
-
将 handle 的 队列 设为对 scheduler,以 prioritySource 和 true 为参数,调用 选择调度器任务队列 的结果。
-
为 scheduler 用 handle 和以下步骤 调度一个运行算法的任务:
-
resolve result。
-
-
返回 result。
注意: 此处创建的固定优先级不可中止 signal 可缓存和重用,以避免多余的内存分配。
Scheduler
scheduler,给定 TaskSignal
对象 signal 和布尔值 isContinuation,选择调度器任务队列:
-
如果 signal 没有 固定优先级,则
-
如果 scheduler 的 动态优先级任务队列映射 不 包含 (signal, isContinuation),则
-
令 queue 为 创建调度器任务队列 的结果,参数为 signal 的 priority、isContinuation 和以下步骤:
-
移除 动态优先级任务队列映射[(signal, isContinuation)]。
-
-
设置 动态优先级任务队列映射[(signal, isContinuation)] 为 queue。
-
为 signal 添加优先级变更算法,步骤如下:
-
-
返回 动态优先级任务队列映射[(signal, isContinuation)]。
-
-
否则
-
令 priority 为 signal 的 priority。
-
如果 scheduler 的 静态优先级任务队列映射 不 包含 (priority, isContinuation),则
-
令 queue 为 创建调度器任务队列 的结果,参数为 priority、isContinuation 和以下步骤:
-
移除 静态优先级任务队列映射[(priority, isContinuation)]。
-
-
设置 静态优先级任务队列映射[(priority, isContinuation)] 为 queue。
-
-
返回 静态优先级任务队列映射[(priority, isContinuation)]。
-
Scheduler
scheduler,给定 task
handle handle 和算法 steps,调度一个运行算法的任务:
-
令 global 为 scheduler 的 相关全局对象。
-
如果 global 是
Window对象,令 document 为 global 的 关联Document;否则为 null。 -
令 enqueue order 为 event loop 的 next enqueue order。
-
将 event loop 的 next enqueue order 加一。
-
将 handle 的 任务 设为在 handle 的 队列 上,按 enqueue order, posted task 任务源 和 document 调用 排队调度器任务 的结果,并包含如下步骤:
-
执行 steps。
-
执行 handle 的 任务完成算法。
-
由于该算法可以在并行步骤中被调用,因此本算法以及其它算法的部分实现存在数据争用问题。特别是 next enqueue order 应原子性更新,访问 调度器任务队列 也需原子性,后者也影响事件循环任务队列(见 this issue)。
2.4.3. 选择下一个要运行的任务
Scheduler
scheduler:
-
令 queues 为对 scheduler 的 静态优先级任务队列映射 获取值 的结果。
-
用 scheduler 的 动态优先级任务队列映射 获取值 的结果,扩展 queues。
-
返回 queues。
Scheduler 有可运行任务,则返回 null。
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 = "user-visible"; }; [priority Exposed =(Window ,Worker )]interface :TaskController AbortController {constructor (optional TaskControllerInit = {});init undefined setPriority (TaskPriority ); };priority
注意:TaskController
的
signal
getter(继承自 AbortController),
返回一个 TaskSignal
对象。
controller = newTaskController( init )-
返回一个新的
TaskController, 其signal被设置为新创建的TaskSignal, 并用 init 的priority初始化其priority。 controller .setPriority( priority )-
调用此方法会更改关联的
TaskSignal的 priority, 向所有观察者发出优先级变更信号,并触发prioritychange事件。
new TaskController(init)
构造步骤如下:
-
令 signal 为一个新的
TaskSignal对象。
setPriority(priority) 方法步骤为:在 signal priority change 上,对 this 的 signal,传入 priority。
3.3. TaskSignal 接口
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 ; };
注意: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。
AbortSignal
对象 signals,
TaskSignalAnyInit
init 及 realm:
-
令 resultSignal 为用
TaskSignal接口 和 realm 从 signals 创建依赖信号 的结果。 -
将 resultSignal 的 dependent 设为 true。
-
如果 init["
priority"] 是TaskPriority: -
否则:
-
令 sourceSignal 为 init["
priority"]。 -
如果 sourceSignal 不 有固定优先级,则:
-
如果 sourceSignal 的 dependent 为 true,则将 sourceSignal 设为其 source signal。
-
断言:sourceSignal 不是 dependent。
-
将 resultSignal 的 source signal 设置为 sourceSignal 的弱引用。
-
追加 resultSignal 到 sourceSignal 的 dependent signals。
-
-
-
返回 resultSignal。
TaskSignal
对象 signal,给定 TaskPriority
priority:
-
如果 signal 的 priority changing 为 true,则 抛出 "
NotAllowedError"DOMException。 -
如果 signal 的 priority 等于 priority 则返回。
-
将 signal 的 priority changing 设为 true。
-
令 previousPriority 为 signal 的 priority。
-
将 signal 的 priority 设为 priority。
-
遍历 signal 的 priority change algorithms,并运行每个 algorithm。
-
触发事件,事件名为
prioritychange, 目标为 signal,使用TaskPriorityChangeEvent, 其previousPriority初始化为 previousPriority。 -
遍历 signal 的 dependent signals,对每个 dependentSignal 调用 signal priority change ,参数为 priority。
-
将 signal 的 priority changing 设为 false。
TaskPriority
priority 和 realm realm。
-
令 init 为新的
TaskSignalAnyInit。 -
将 init["
priority"] 设为 priority。 -
返回 创建依赖任务信号 的结果,参数为 « »、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。
添加如下算法:
-
如果 eventLoop 的 current continuation state 为 null, 则将其设为新的 continuation state。
-
令 continuationState 为 eventLoop 的 current continuation state。
-
将 continuationState 的 state map[key] 设为 value。
-
令 continuationState 为 eventLoop 的 current continuation state。
-
如果 continuationState 非 null 且 continuationState 的 state map[key] 存在,则返回 continuationState 的 state map[key],否则返回 null。
4.1.3. 事件循环:处理模型
在事件循环处理步骤的第 2 步之前,添加如下步骤:
-
令 queues 为 集合,包含 event loop 的 task queues ,且队列包含至少一个 可运行 任务。
-
令 schedulerQueue 为 从所有调度器中选择下一个调度器任务队列 的结果。
修改步骤 2:
-
若 schedulerQueue 不为 null 或 queues 不为空:
修改 2.1 步如下:
-
令 taskQueue 为下列之一,以 实现自定义方式选出:
-
若 queues 不为空,则在 queues 中以 实现自定义方式选一个 task queues。
-
若 schedulerQueue 不为 null,则为 schedulerQueue 的 tasks。
-
注意: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 步修改为:
-
令 continuationState 为 null。
-
如果 ignoreContinuationState 为 false 且 eventLoop 的当前连续体状态不为 null, 则将 continuationState 设为克隆 event loop 的当前连续体状态的结果。
-
将 microtask 的 steps 设为如下步骤:
4.1.5. HostMakeJobCallback(callable)
在第 5 步之前添加如下内容:
将第 5 步修改为:
-
返回 JobCallback Record { [[Callback]]: callable, [[HostDefined]]: { [[IncumbentSettings]]: incumbent settings, [[ActiveScriptContext]]: script execution context, [[ContinuationState]]: state } }。
4.1.6. HostCallJobCallback(callback, V, argumentsList)
在第 5 步之前添加如下步骤:
-
将 event loop 的当前连续体状态设为 callback.[[HostDefined]].[[ContinuationState]]。
第 7 步后添加如下内容:
-
将 event loop 的当前连续体状态设为 null。
4.1.7. HostEnqueuePromiseJob(job, realm)
将第 2 步修改为:
-
排队一个微任务,ignoreContinuationState 设为 true,执行如下步骤:
4.2.
requestIdleCallback()
4.2.1. 调用 idle callback 算法
在第 3.3 步之前添加如下步骤:
-
令 realm 为 window 的相关 realm。
-
令 state 为一个新的调度状态。
-
将 state 的优先级来源设为创建固定优先级不可中止任务信号的返回值,传入 "
background" 和 realm。 -
为 scheduler 设置当前调度状态为 state。
在第 3.3 步后添加:
-
将 event loop 的当前连续体状态设为 null。
5. 安全性考虑
本规范定义的 API 的主要安全性考虑是是否可能通过基于时序的侧信道攻击泄露不同来源间的信息。
5.1.
postTask 作为高精度计时源
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()
任务时,可能会减小此集合,具体视优先级及插队内容而定。
如何规避风险?
实现者可以通过如下措施减少风险:
-
可能时,将跨源事件循环隔离到不同线程。此类攻击依赖于事件循环共享线程。
-
采用非仅优先级排序的事件循环调度策略。例如可以使用轮转(round-robin)或公平调度(fair-scheduling),以防止优先级信息泄露。也可以确保低优先级任务周期性获得调度,以避免推断优先级信息。
6. 隐私性注意事项
本节为非规范内容。
我们已从隐私角度评估本规范定义的 API,认为不存在隐私性问题。