随着人们在 Facebook、Twitter、YouTube、Netflix 和 Google Docs 等服务上花费的时间越来越多,多标签浏览的使用越来越频繁,使它们成为人们在互联网上进行日常生活和工作的一部分。
Quantum DOM: 调度是 Project Quantum 的一个重要部分,其重点是使 Firefox 更加响应,尤其是在打开大量标签的情况下。本文将介绍我们在多标签浏览中发现的问题、我们想出的解决方案、Quantum DOM 的当前状态以及对项目的贡献机会。
问题 1:不同类别中的任务优先级
自从 多进程 Firefox (e10s) 首次在 Firefox 48 中启用以来,网页内容标签现在运行在单独的 内容进程 中,以减少给定进程中操作系统资源的拥挤。然而,经过进一步研究,我们发现 内容进程中的主线程的任务队列 仍然被多个类别中的任务所拥挤。内容进程中的任务可能来自多个来源:通过 IPC(进程间通信)来自主进程(例如,用于输入事件、网络数据和 vsync),直接来自网页(例如,来自 setTimeout、requestIdleCallback 或 postMessage),或来自内容进程内部(例如,用于垃圾回收或遥测任务)。为了获得更好的响应能力,我们已经学会将用于用户输入和 vsync 的任务优先于用于 requestIdleCallback 和垃圾回收的任务。
问题 2:标签之间缺乏任务优先级
在 Firefox 内部,运行在前景和背景标签中的任务在单个任务队列中按照 先到先服务 顺序执行。为了提高 Firefox 用户体验的响应能力,优先考虑前景任务而不是背景任务是相当合理的。
目标和解决方案
让我们看看我们如何解决这两个调度难题,将它们分解为一系列导致可实现目标的操作
- 在内容进程的主线程上对任务进行分类并按优先级排序,分为两个维度(类别和标签组),以提供更好的响应能力。
- 如果抢占对用户不可察觉,则抢占正在运行背景标签的任务。
- 当由于资源有限而无法使用更多内容进程时,提供 多个内容进程 (e10s multi) 的替代方案。
任务分类
为了解决我们的第一个问题,我们将内容进程中主线程的任务队列划分为 3 个优先级队列:高(用户输入和刷新驱动程序)、正常(DOM 事件、网络、计时器回调、工作线程消息)和低(垃圾回收、空闲回调)。注意:同一优先级的任务顺序保持不变。
任务分组
在描述我们第二个问题的解决方案之前,让我们定义一个 TabGroup,它是一组通过 window.opener
和 window.parent
关联的打开标签。在 HTML 标准中,这被称为 相关的浏览上下文的单元。如果任务属于不同的 TabGroup,则它们将被隔离,并且无法相互影响。任务分组 确保来自同一 TabGroup 的任务按顺序运行,同时允许我们中断来自背景 TabGroup 的任务以运行来自前景 TabGroup 的任务。
在 Firefox 内部,每个窗口/文档都包含对其所属 TabGroup 对象的引用,该对象提供了 一组有用的调度 API。这些 API 使 Firefox 开发人员更容易将任务与特定 TabGroup 关联起来。
如何在 Firefox 内部对任务进行分组
以下是一些示例,展示了我们在 Firefox 内部对各种类别中的任务进行分组的方式
- 在
window.postMessage()
的实现中,一个名为PostMessageEvent
的异步任务将被调度到主线程的任务队列中
void nsGlobalWindow::PostMessageMozOuter(...) {
...
RefPtr<PostMessageEvent> event = new PostMessageEvent(...);
NS_DispatchToCurrentThread(event);
}
借助将 DOM 窗口与它们的 TabGroup 关联的新方法以及 TabGroup 中提供的新的调度 API,我们现在可以将此任务与相应的 TabGroup 关联并指定 TaskCategory
void nsGlobalWindow::PostMessageMozOuter(...) {
...
RefPtr<PostMessageEvent> event = new PostMessageEvent(...);
// nsGlobalWindow::Dispatch() helps to find the TabGroup of this window for dispatching.
Dispatch("PostMessageEvent", TaskCategory::Other, event);
}
- 除了可以与 TabGroup 关联的任务之外,内容进程内部还有几种类型的任务,例如通过 垃圾回收 收集遥测数据和管理资源,这些任务与任何网页内容都没有关系。以下是垃圾回收启动的方式
void GCTimerFired() {
// A timer callback to start the process of Garbage Collection.
}
void nsJSContext::PokeGC(...) {
...
// The callback of GCTimerFired will be invoked asynchronously by enqueuing a task
// into the task queue of the main thread to run GCTimerFired() after timeout.
sGCTimer->InitWithFuncCallback(GCTimerFired, ...);
}
为了对没有 TabGroup 依赖关系的任务进行分组,引入了名为 SystemGroup
的特殊组。然后,可以修改 PokeGC()
方法,如下所示
void nsJSContext::PokeGC(...) {
...
sGCTimer->SetEventTarget(SystemGroup::EventTargetFor(TaskCategory::GC));
sGCTimer->InitWithFuncCallback(GCTimerFired, ...);
}
我们现在将此 GCTimerFired
任务分组到 SystemGroup
中,并指定了 TaskCategory::GC
。这允许调度程序中断任务以运行任何前景标签的任务。
- 在某些情况下,同一任务可以由特定网页内容或由内容进程中具有系统权限的内部 Firefox 脚本请求。当请求与任何窗口/文档无关时,我们将不得不决定
SystemGroup
对其是否合理。例如,在内容进程中 DNSService 的实现中,可以为可选的TabGroup
版本的事件目标提供一个,用于在 DNS 查询解析后调度结果回调。如果未提供可选的事件目标,则会选择SystemGroup
事件目标,其TaskCategory::Network
指定。我们假设该请求来自内部脚本或内部服务,与任何窗口/文档无关。
nsresult ChildDNSService::AsyncResolveExtendedNative(
const nsACString &hostname,
nsIDNSListener *listener,
nsIEventTarget *target_,
nsICancelable **result)
{
...
nsCOMPtr<nsIEventTarget> target = target_;
if (!target) {
target = SystemGroup::EventTargetFor(TaskCategory::Network);
}
RefPtr<DNSRequestChild> childReq =
new DNSRequestChild(hostname, listener, target);
...
childReq->StartRequest();
childReq.forget(result);
return NS_OK;
}
TabGroup 类别
在 调度程序 内部完成任务分组后,我们从池中为每个标签组分配一个协作线程,以使用 TabGroup
内部的任务。每个协作线程都可以通过调度程序在任何安全点通过 JS 中断进行抢占。然后,主线程通过这些协作线程虚拟化。
在这种新的协作线程方法中,我们确保一次只能有一个线程运行一个任务。这将更多 CPU 时间分配给前景 TabGroup
,并确保 Firefox 中内部数据的正确性,其中包括许多服务、管理器和数据,这些数据被有意地设计为单例对象。
任务分组和调度的障碍
很明显,Quantum-DOM 调度的性能高度依赖于任务分组的工作。理想情况下,我们希望每个任务都只与一个 TabGroup 关联。然而,在现实中,有些任务被设计为服务于多个 TabGroup,这需要提前进行重构才能支持分组,并且并非所有任务都可以在调度程序准备好启用之前及时进行分组。因此,为了在所有任务分组完成之前积极地启用调度程序,采用了以下设计,即在到达未分组任务时暂时禁用抢占,因为我们永远不知道该未分组任务属于哪个 TabGroup。
任务分组的当前状态
我们要感谢来自 DOM、Graphic、ImageLib、Media、Layout、Network、Security 等各个子模块的许多工程师,他们帮助我们清除了这些 未分组(未标记)任务,并按照 遥测结果中显示的频率 进行标记。
下表显示了内容进程中运行的任务的遥测记录,为更好地了解 Firefox 实际执行的操作提供了更清晰的画面
好消息是,最近已清除超过 80% 的任务(按频率加权)。然而,仍然存在相当数量的匿名任务需要清除。额外的遥测 将有助于检查 2 个未分组任务到达主线程之间的平均时间。平均时间越长,我们从 Quantum-DOM 调度程序获得的性能提升就越大。
为 Quantum DOM 开发做出贡献
如上所述,任务分组的越多,我们从调度程序中获得的收益就越多。如果您有兴趣为 Quantum-DOM 做出贡献,以下是一些可以提供帮助的方法
- 从 标记元错误 中选择任何未分配的错误,并按照 指南 进行标记。
- 如果您不熟悉这些未标记的错误,但想帮助命名任务以减少 遥测结果 中的匿名任务,以便在未来改进分析,那么 指南 将对您有所帮助。(更新:此错误 中的某些自动化工具将解决命名匿名任务的问题。)
如果您开始修复错误并遇到问题或疑问,通常可以在 Mozilla 的 #content IRC 频道中找到 Quantum DOM 团队。
关于 Bevis Tseng
曾柏维是 Mozilla 的平台工程师,曾负责 Firefox OS(B2G)的电话相关功能。他最近完成了 Gecko 中 IndexedDB 的 Spec v2 增强,现在正在参与 Quantum-DOM 调度程序的任务标记工作。