Skip to content

Latest commit

 

History

History
123 lines (83 loc) · 13.1 KB

20180812_anjia_这款狗狗引擎这么快?一定是考虑了这几个特性.md

File metadata and controls

123 lines (83 loc) · 13.1 KB

Servo 的设计架构

Servo 是一款现代化的高性能浏览器引擎,既支持常规应用,也支持嵌入使用。官网 https://servo.org

对,我就是 Servo 的官方 Logo

1. 设计简介

Servo 是一个新的 Web 浏览器引擎。她的目标是创建一个多层级的高并发架构,同时在架构层面消除与错误的内存管理、数据竞争相关的常见 bug 和安全漏洞。

因为 C++ 不适合处理这类问题,所以 Servo 是用 Rust 语言编写的。Rust 在设计的时候充分考虑了 Servo 的需求,它提供了任务并行的基础架构和强类型系统,从而保证了内存安全、避免了数据竞争。

在设计的时候,Servo 的架构师们会优先考虑现代 Web 平台的以下特性:高性能、动态、富媒体应用,可能会牺牲一些无法优化的特性。他们想知道一个快速响应的 Web 平台是什么样子的,然后再实现它。

Servo 专注于实现一个功能完备的 Web 浏览器引擎和可靠的嵌入式引擎,前者(Web 浏览器引擎)使用了基于 HTML 的用户界面 Browser.html。尽管 Servo 最初只是一个研究型项目,但在开发它的时候就以提供可用于生产环境的代码为目标。目前,Servo 的一些组件已经迁移到了 Firefox 浏览器。

关于集成到 Firefox 中的 Servo 组件,可查看 Jack Moffitt 的演讲视频 Web Engines Hackfest

并行并发的策略

并发是拆分任务以便交叉执行;并行是同时执行多个任务以提高速度。Servo 在以下环节中用到了并行和并发。

  • 基于任务的架构:系统的主要组件应该有独立的堆,以便有明确的失败/恢复的边界。这也让整个系统的耦合度降低,以便可以轻松地替换掉某些组件,供我们实验和研究。
  • 并发渲染:将渲染和合成从布局中分离出来,以保证良好的响应性。渲染和合成都是单独的线程;合成线程手动管理自己的内存,以避免垃圾回收暂停。
  • 瓦片渲染:将屏幕划分成瓦片网格,并行渲染每一个瓦片。暂且忽略由此带来的收益,移动端渲染的时候是需要这种瓦片的。
  • 层渲染:将显示列表分成子树,并行渲染子树,并将其内容保留在 GPU 上。
  • 选择器匹配:这是一个令人尴尬的并行问题。与 Gecko 不同,Servo 在流树结构的单独传递中进行选择器匹配,这样会让并行更容易。
  • 并行布局:通过并行遍历 DOM 来构建流树,这种遍历遵守由元素(比如浮动元素)生成的顺序依赖关系。
  • 文本形状:作为内联布局的关键部分,文本形状的成本非常高,它很有并行的潜力。未实现。
  • 解析:用 Rust 新写了一个 HTML 解析器,专注于安全性和符合规范。尚未在解析中添加预测性和并行性。
  • 图像解码:并行解码多个图像非常简单。
  • 其他资源的解码:这可能不如图像解码重要,但页面加载的所有内容都是可以并行处理的,比如解析整个样式表、解码视频。
  • GC JS 和布局的并发:在大多数具有并发 JS 和布局的设计中,当查询布局的时候,JS 有时需要等待,而且有可能是非常频繁的。这将是运行 GC 的最佳时机。

GC,Garbage Collection,垃圾回收

挑战

  • 库不利于并行:用到的一些第三方库在多线程环境下不能很好地运行;字体尤其困难;即使从技术角度讲库是线程安全的,但是,通常是通过库的互斥锁来实现线程安全的,而这不利于实现并行。
  • 线程太多:如果在各个方面都抛到最大的并行量和并发量,那么最终会因为线程太多而压垮系统。

2. 任务架构

tasks-sup 图1. 任务监管图,源自 servo/wiki/Design

tasks-comm 图2. 任务通信图,源自 servo/wiki/Design

  • 每一个框代表一个 Rust 任务 (注:一个任务就是一个线程)
  • 蓝色框是浏览器管道里的主要任务
  • 灰色框是浏览器管道的辅助任务
  • 白色框是 worker 任务,它表示会有多个任务,具体的任务数要根据工作量来确定
  • 虚线表示主管关系
  • 实线表示通信信道

说明

我们可以把每个 constellation(见“附录.术语”小节)实例看做是浏览器的单个页签或者窗口,它管理着接收输入的任务管道,针对 DOM 运行 JavaScript,执行布局,构建显示列表,将显示列表渲染到瓦片上,最后把最终图像合成到屏幕上。

这个管道由四个主要任务组成:

  • 脚本(Script):创建和拥有 DOM,执行 JavaScript 引擎。它接收来自多个源的事件,包括导航事件。当内容任务(Content)需要查询布局相关的信息时,脚本任务必须向布局任务发送一个请求。每个内容任务都有自己的 JavaScript 运行时。
  • 布局(Layout):获取 DOM 快照,计算样式,构造主要的布局数据结构-流树(flow tree)。流树用于计算节点的布局,从它那可以构建显示列表显示列表会被发送到渲染任务。
  • 渲染(Renderer):接收显示列表,并将可见部分渲染到一个或多个瓦片上,尽可能并行。
  • 合成(Compositor):合成渲染的瓦片,并将它们发送到屏幕上进行显示。作为 UI 线程,合成任务也是 UI 事件的第一个接收器,UI 事件通常会被立即发送到内容任务以供处理(尽管一些事件,比如滚动事件,首先由合成任务处理并响应)。

管道中的多任务通信涉及到两种复杂的数据结构:DOM 和显示列表。DOM 从内容传到布局,显示列表从布局传到渲染。找出一种有效且类型安全的方式来表示、共享和传递这两种数据结构是该项目的诸多挑战之一。

写时复制 DOM

Servo 的 DOM 树节点是有版本控制的,它们可以在单个 writer 和多个 reader 之间共享。DOM 使用写时复制(copy-on-write)的策略允许当有多个 reader 时 writer 也能修改 DOM。writer 总是内容任务,reader 总是布局任务或其子任务。

DOM 节点是 Rust 值(Rust value),而 Rust 值的生命周期由 JavaScript 垃圾收集器管理。JavaScript 直接访问 DOM 节点,而没有依赖 XPCOM 或其它类似的基础设施。

DOM 接口目前不是类型安全的,这可能会导致不正确的节点管理。消除这类不安全是该项目的一个必要的高优先级目标;由于 DOM 节点具有复杂的生命周期,这将会带来一些挑战。

显示列表

Servo 的渲染完全由显示列表驱动,显示列表是由布局任务创建的一系列高级绘图命令。Servo 的显示列表是完全不可变的,因此它可以被同时运行的多个渲染任务所共享。这与 Webkit 和 Gecko 的渲染器不同:WebKit 的渲染器没有使用显示列表;Gecko 的渲染器使用了显示列表,但它在渲染期间还会查询额外的信息。

3. JavaScript 和 DOM 绑定

目前,Servo 使用的脚本引擎是 SpiderMonkey(可插拔引擎是一个长期的、低优先级的目标)。每个内容任务都有自己的 JavaScript 运行时。DOM 绑定使用原生的 JavaScript 引擎 API 而不是 XPCOM。从 WebIDL 自动生成绑定是一个高优任务。

4. 多进程架构

与 Chromium 和 WebKit2 类似,Servo 的架构师们打算做一个可信任的应用程序进程和多个不太可信的引擎进程。高级 API 实际上是基于 IPC 的,非 IPC 实现可能用于测试和单进程用例(虽然预计最糟糕的时候也会用于多进程)。引擎进程将使用操作系统沙箱工具来限制对系统资源的访问。

目前,Servo 并不打算像 Chromium 那样采用极端沙箱(extreme sandboxing),主要是因为锁定沙箱会导致大量的开发工作(特别是在 Windows XP 和旧版 Linux 等低优先级的平台上),并且该项目的其它方面的优先级更高一点。Rust 的类型系统还为内存安全漏洞增加了一层重要的防御功能,虽然仅凭这一点并不能使沙箱在防御不安全代码、类型系统中的错误以及第三方/主机库等方面变得不那么紧迫,但相对于其他浏览器引擎它确实能显著减少 Servo 的攻击面。此外,Servo 的架构师们对某些沙箱技术有性能方面的顾虑(例如,将所有 OpenGL 调用代理到单独的进程)。

5. I/O和资源管理

网页依赖于各种各样的外部资源,而这些资源具有很多的检索和解码机制。这些资源会被缓存在多处,比如磁盘、内存。在并行浏览器的设置中,这些资源一定会在并发的多个 worker 之间调度。

通常,浏览器是单线程的,会在“主线程”上执行 I/O,而“主线程”同时又担负着大部分的计算任务,这就会导致延迟问题。而 Servo 中没有“主线程”,所有外部资源的加载都由一个资源管理任务来处理。

浏览器有很多缓存,而 Servo 的基于任务的架构意味着它可能会拥有比现有浏览器引擎还多的缓存(例如,我们在拥有全局任务缓存的同时,也拥有着一个本地任务缓存,它存储着来自全局缓存的结果,以通过调度程序来保存往返记录)。Servo 应该有一个统一的缓存机制,以便在低内存的环境中也运行良好。

附录. 术语

  • constellation:该线程控制相关网页内容。在支持多页签的浏览器中,可以把它当做单个页签的拥有者;它封装了会话历史记录,知道 frame 树中的所有 frame,是每个 frame 管道的拥有者。
  • 管道(pipeline):为特定文档封装了脚本线程、布局线程和渲染线程之间的通信。每个管道都有一个全局唯一的 id,可以从 constellation 里访问到它。
  • 脚本线程/脚本任务(script thread/script task):这个线程执行 JavaScript,并存储同下所有文档的 DOM 表示。它可以把从 constellation 接收到的输入事件转换为规范里定义的 DOM 事件,也可以在收到新页面的时候调用 HTML 解析,也可以为事件评估 JS。
  • 布局线程(layout thread):这个线程负责将 DOM 树布局到特定文档的层(layer)上。它会收到来自脚本线程的命令,要么是为渲染线程生成一个新的显示列表,要么是为脚本线程返回页面的布局结果。
  • 显示列表(display list):一个具体的渲染说明(高级绘图命令)列表。显示列表是发生在布局之后的,因此所有的项都有相对堆叠上下文的像素位置,并且已经应用了 z-index,所以后加入显示列表的项将始终在其它项的上面。
  • 渲染线程/绘制线程(renderer thread/paint thread):这个线程负责将显示列表转换成一系列的绘图命令。该绘图命令会将关联文档的内容渲染在一个缓冲区里,之后会被发送到合成器。
  • 合成/合成器(Compositor):负责 Web 内容的合成渲染,并将它们尽可能快地显示在屏幕上。也负责从操作系统接收输入事件,并将它们转发到 constellation 线程。

小结

本文主要介绍了 Servo 的设计概况,重点介绍了它基于任务的整体架构及其四个主要任务(也称“线程”,在 Servo 的这个上下文里),即脚本任务、布局任务、渲染任务、合成任务。下图便是对上述内容的一个总结,希望对大家有所帮助和启发。

servo 图3. Servo 概况

下一步

目前,已出炉两篇文章。后续,我会继续探索更多详细内容,敬请期待。

  • Quantum 初探:介绍了 Quantum 项目的由来和概况,也顺便介绍了 Servo 的小历史
  • Servo 的设计架构:介绍了 Servo 的基于任务的设计架构,重点介绍了它的并行并发策略

关于 Quantum 和 Servo,如果您有其它更想知道的,欢迎留言。

致谢

感谢 @cncuckoo(李松峰老师)和 @liuguanyu(二哥)对本文提出的指导意见和建议。手动送花花。🌹🌹

参考