RenderingNG 架构

Chris Harrelson
Chris Harrelson

下面介绍了 RenderingNG 的组件 以及渲染管道是如何流经这些组件的。

从最高级别开始,渲染任务如下:

  1. 将内容渲染为屏幕上的像素。
  2. 为内容从一种状态到另一种状态添加动画视觉效果
  3. 滚动以响应输入。
  4. 高效地将输入路由到正确的位置,以便开发者脚本和其他子系统能够做出响应。

要呈现的内容是每个浏览器标签页的帧树,加上 。还有来自触摸屏的原始输入事件流, 鼠标、键盘和其他硬件设备。

每个帧都包含:

  • DOM 状态
  • CSS
  • 画布
  • 外部资源,例如图片、视频、字体和 SVG

框架是指 HTML 文档及其网址。 在浏览器标签页中加载的网页有一个顶级框架, 顶级文档中包含的每个 iframe 的子框架, 及其递归 iframe 后代。

视觉效果是应用于位图的图形操作, 例如滚动、转换、裁剪、滤镜、不透明度或混合。

架构组件

在 RenderingNG 中,这些任务在逻辑上被划分到多个阶段和代码中 组件。这些组件最终会进入各种 CPU 进程、线程和进程, 子组件。每个区域都发挥着 可靠性可扩缩的性能 以及适用于所有 Web 内容的可扩展性

渲染流水线结构

<ph type="x-smartling-placeholder">
</ph> 渲染流水线的示意图。
箭头表示每个阶段的输入和输出。阶段 用颜色进行标记,用于表明它们执行的线程或进程。在 某些情况下,阶段可能在多个位置执行,具体取决于 这种情况,因此有些图片就会有两种颜色。 绿色阶段:渲染进程主线程;黄色表示渲染进程合成器 橙色阶段是可视化过程

在创建了许多阶段和工件的流水线中继续渲染 一路走来。每个阶段都代表一些代码,这些代码会在 呈现。工件是数据结构 是阶段的输入或输出。

这些阶段包括:

  1. 动画:更改计算的样式,并根据声明式时间轴随时间改变属性树
  2. 样式:将 CSS 应用于 DOM,并创建计算出的样式
  3. 布局:确定 DOM 元素在屏幕上的大小和位置, 并创建不可变 fragment 树
  4. 预绘制:计算属性树和 失效 任何现有的显示列表和 GPU 纹理图块(如适用)。
  5. 滚动:通过更改属性树来更新文档和可滚动 DOM 元素的滚动偏移量。
  6. Paint:计算显示列表,描述如何从 DOM 中光栅 GPU 纹理图块。
  7. 提交:将属性树和显示列表复制到合成器线程。
  8. 分层:将显示列表拆分为一个合成图层列表,以便分别实现光栅化和动画。
  9. 光栅、解码和绘制 Worklet:将显示列表、编码图像和绘制 Worklet 代码分别转换为 GPU 纹理图块
  10. Activate:创建一个合成器帧,表示如何绘制 GPU 图块并将其放置到屏幕上,以及任何视觉效果。
  11. 汇总:将来自所有可见的合成器帧的合成器帧合并成一个全局合成器帧。
  12. 绘制:在 GPU 上执行聚合的合成器帧,在屏幕上创建像素。

可以跳过渲染流水线的阶段(如果不需要)。 例如,视觉效果和滚动的动画可以跳过布局、预绘制和绘制。 正因如此,此图中动画和滚动操作都标有黄点和绿点。 如果对于视觉效果可以跳过布局、预绘制和绘制 它们可以完全在合成器线程上运行,并跳过主线程。

此处并未直接描绘浏览器界面呈现 但可以将其视为同一流水线的简化版本 (实际上,其实现方案共享了大部分代码)。 视频(亦非直接描绘) 通常使用将帧解码为 GPU 纹理块的独立代码进行渲染 然后插入合成器框架和绘制步骤中

进程和线程结构

CPU 进程

使用多个 CPU 进程可实现性能和安全隔离 和浏览器状态之间 与 GPU 硬件隔离开来。

CPU 进程的各个部分的示意图

  • “渲染过程”是渲染、添加动画、滚动和路由输入, 单个网站和标签页的组合。存在多个渲染进程。
  • 浏览器进程为浏览器界面渲染、添加动画和路由输入 (包括地址栏、标签页标题和图标), 提供给相应的渲染进程。有一个浏览器进程
  • Viz 进程可汇总来自多个渲染进程的合成 以及浏览器进程使用 GPU 进行光栅和绘制。还有 一个 Viz 进程

不同的网站最终总是会得到展示 不同的渲染进程

同一网站的多个浏览器标签页或窗口通常会以不同的呈现方式呈现 进程,除非标签页具有相关性,例如 打开另一个窗口。 桌面设备上的内存压力较大时,Chromium 可能会放置多个标签页 访问同一个呈现进程,即使这些进程并不相关。

在单个浏览器标签页中 来自不同网站的帧始终处于不同的渲染进程中, 但来自同一网站的帧始终处于同一渲染进程中。 从呈现的角度来看 多个呈现流程的重要优势在于 和标签页实现性能隔离 相互通信。 此外,源站可以选择进一步隔离

所有 Chromium 都只有一个 Viz 进程,因为通常只有 一个 GPU 和一个屏幕来绘制图像

将 Viz 拆分为一个单独的进程有助于在 GPU 驱动程序或硬件。也非常适用于安全隔离 这对于 Vulkan总体安全性

由于浏览器可以包含多个标签页和窗口 它们都有可供绘制的浏览器界面像素 您可能想知道:为什么只有一个浏览器进程? 这是因为每次只聚焦其中一个参数; 事实上,不可见的浏览器标签页多数会被停用,并丢弃所有 GPU 内存。 然而,复杂的浏览器界面渲染功能正在越来越多地实施。 (也称为 WebUI - 并非出于性能隔离方面的原因 但是为了利用 Chromium 的网页呈现引擎的易用性。

在旧版 Android 设备上, 在 WebView 中使用时,会共享渲染和浏览器进程 (这通常不适用于 Android 版 Chromium,仅适用于 WebView)。 在 WebView 上,浏览器进程也会与嵌入应用共享, WebView 只有一个渲染进程。

有时还有一个实用程序进程,用于解码受保护的视频内容。 前面的图表未描述此过程。

Threads

尽管任务运行缓慢,但线程有助于实现性能隔离和响应能力, 流水线并行处理和多缓冲。

渲染过程的示意图。

  • 主线程运行脚本、渲染事件循环、文档生命周期, 命中测试、脚本事件分派,以及 HTML、CSS 和其他数据格式解析。
    • 主线程帮助程序可执行创建图片位图和 blob 等需要编码或解码的任务。
    • Web Worker 运行脚本和 OffscreenCanvas 的渲染事件循环。
  • 合成器线程会处理输入事件, 对网页内容执行滚动和动画 计算 Web 内容的最佳分层, 并协调图像解码、绘制 Worklet 和光栅任务。
    • 合成器线程帮助程序可协调可视化光栅任务, 并执行图像解码任务、绘制 Worklet 和后备光栅。
  • 媒体、多路分配器或音频输出线程解码; 处理和同步视频与音频流。 (请记住,视频与主渲染管道并行执行。)

分离主线程和合成器线程对于 性能隔离 从主线程工作开始的动画和滚动。

每个渲染进程只有一个主线程, 即使来自同一网站的多个标签页或框架最终可能出现在同一个进程中,也是如此。 不过,与各种浏览器 API 中执行的工作之间存在性能隔离。 例如,在 Canvas API 中生成图片位图和 blob 时,会在主线程帮助程序线程中运行。

同样,每个渲染进程只有一个合成器线程。 通常情况下,只有一个代码是没有问题的 因为合成器线程上必须执行的所有开销非常大的操作 被委托给合成器工作器线程或 Viz 进程, 这项工作可以与输入路由、滚动或动画并行完成。 合成器工作器线程用于协调在 Viz 进程中运行的任务, 而是在所有平台中 可能会由于 Chromium 无法控制的原因而失败, 例如驱动程序错误 在这些情况下,工作线程将在 CPU 的回退模式下执行工作。

合成器工作器线程的数量取决于设备的功能。 例如,桌面设备通常会使用更多线程 因为与移动设备相比,此类设备具有更多 CPU 核心,而且耗电量更低。 这是一个 纵向扩容和缩容

渲染进程线程架构是一个应用,由 优化模式:

  • 帮助程序线程:将长时间运行的子任务发送到其他线程以保留 以便于父线程对同时发出的其他请求做出响应。主线程 helper 和 compositor helper 线程是这种技术的好例子。
  • 多次缓冲: 在渲染新内容的同时显示之前渲染的内容,以隐藏 呈现延迟时间合成器线程会使用此方法。
  • 流水线并行化:在多个位置运行渲染流水线 。这就是实现快速滚动和动画效果的原因。即使 主线程渲染更新正在发生时,滚动和动画 并行运行。

浏览器进程

浏览器进程示意图,显示了渲染和合成线程与渲染和合成线程帮助程序之间的关系。

  • 渲染和合成线程响应浏览器界面中的输入, 将其他输入路由到正确的渲染进程;对浏览器界面进行布局和绘制。
  • 渲染和合成线程帮助程序 执行图像解码任务以及后备光栅或解码。

浏览器进程的渲染线程和合成线程类似 呈现进程的代码和功能 只不过,主线程和合成器线程会合并成一个。 在这种情况下,只需要一个线程,因为不需要 实现与长时间运行的主线程任务的性能隔离, 因为我们并没有设计任何此类规则

可视化进程

Viz 进程包含 GPU 主线程和显示合成器线程。

  • GPU 主线程光栅会在 GPU 纹理图块中显示列表和视频帧, 并将合成器帧绘制到屏幕上
  • 显示合成器线程会聚合并优化每个渲染进程的合成, 和浏览器进程合并成单个合成器框架,以便呈现在屏幕上。

光栅和绘制通常在同一线程上进行, 因为它们都依赖于 GPU 资源 并且很难可靠地多线程使用 GPU (更方便的多线程访问 GPU 是开发新的 Vulkan 标准)。 在 Android WebView 上,有一个单独的操作系统级渲染线程用于绘制 这要归功于 WebView 是如何嵌入到原生应用中的。 其他平台未来可能会出现此类讨论帖。

显示合成器位于不同的线程上,因为它需要能够始终响应, 并且不会阻塞 GPU 主线程上任何可能的减速源。 GPU 主线程运行速度减慢的一个原因是调用非 Chromium 代码, 例如供应商专用的 GPU 驱动程序,它们在难以预测的情况下可能运行缓慢。

组件结构

在每个渲染进程的主线程或合成器线程中, 还有一些逻辑软件组件以结构化方式相互交互。

渲染进程主线程组件

Blink 渲染程序的示意图。

在 Blink Renderer 中:

  • 本地框架树 fragment 表示本地框架的树和框架内的 DOM。
  • DOM API 和 Canvas API 组件包含所有这些 API 的实现。
  • 文档生命周期运行程序会执行渲染流水线步骤,一直到提交步骤(包括提交步骤)。
  • 输入事件命中测试和调度组件可执行命中测试, 找出某个事件针对哪个 DOM 元素,并运行输入事件 调度算法和默认行为。

呈现事件循环调度器和运行程序决定对事件运行什么 以及何时循环。它会安排按照与设备匹配的节奏进行渲染 。

帧树的示意图。

本地帧树 fragment 有点复杂。 回想一下,框架树是以递归方式呈现的主页面及其子 iframe。 如果某个帧在某个渲染进程中渲染,该帧是该进程的本地; 否则就会远程工作

您可以想象根据帧的渲染过程着色。 在上图中,绿色圆圈表示一个渲染进程中的所有帧; 橙色的显示在一秒内,蓝色的显示在三分之一处。

本地框架树 fragment 是在框架树中具有相同颜色的连接组件。 图片中有四个本地框架树:两个网站 A、一个网站 B、一个网站 C。 每个本地帧树都有自己的 Blink 渲染程序组件。 本地框架树的 Blink 渲染程序不一定在同一个渲染进程中 与其他局部帧树一样。它取决于渲染进程的选择方式(如前所述)。

渲染进程合成器线程结构

显示渲染进程合成器组件的示意图。

渲染进程合成器组件包括:

  • 数据处理程序,用于维护合成的图层列表、显示列表和属性树。
  • 一个生命周期运行程序,用于运行动画、滚动、合成、光栅 以及解码和激活渲染管道的步骤。 (请注意,动画和滚动都可以在主线程和合成器中进行。)
  • 输入和点击测试处理程序以合成层的分辨率执行输入处理和点击测试, 确定是否可以在合成器线程上运行滚动手势, 以及渲染进程命中测试的目标

实际中的架构示例

本例中有三个标签页:

标签页 1:foo.com

<html>
  <iframe id=one src="foo.com/other-url"></iframe>
  <iframe  id=two src="bar.com"></iframe>
</html>

标签页 2:bar.com

<html>
 …
</html>

标签页 3:baz.com html <html> … </html>

这些标签页的进程、线程和组件结构如下所示:

标签页的过程示意图。

我们来逐一了解渲染的四大任务。 提醒:

  1. 将内容渲染为屏幕上的像素。
  2. 为内容从一种状态到另一种状态添加动画效果
  3. 滚动以响应输入。
  4. 高效地将输入路由到正确的位置,以便开发者脚本和其他子系统能够做出响应。

渲染标签页 1 更改后的 DOM,请执行以下操作:

  1. 开发者脚本会在 foo.com 的渲染进程中更改 DOM。
  2. Blink 渲染程序会告知合成器它需要执行渲染。
  3. 合成器会告知 Viz 需要渲染。
  4. Viz 会向合成器发出“开始渲染”的信号。
  5. 合成器会将启动信号转发到 Blink 渲染程序。
  6. 主线程事件循环运行程序运行文档生命周期。
  7. 主线程将结果发送到合成器线程。
  8. 合成器事件循环运行程序会运行合成生命周期。
  9. 对于光栅任务,系统会将所有光栅任务都发送到 Viz(通常会有多项任务)。
  10. Viz 可在 GPU 上光栅显示内容。
  11. Viz 确认完成光栅任务。 注意:Chromium 通常不会等待光栅完成, 而是使用 同步令牌 必须解决的 。
  12. 合成器帧会发送到 Viz。
  13. Viz 会为 foo.com 渲染进程汇总合成器帧, bar.com iframe 呈现流程和浏览器界面的相关信息。
  14. Viz 安排了平局。
  15. Viz 将聚合的合成器帧绘制到屏幕上。

要为第 2 个标签页上的 CSS 转换过渡添加动画效果,请执行以下操作:

  1. bar.com 渲染进程的合成器线程 tick 动画 合成器事件循环中现有的属性树。 然后,系统会重新运行合成器生命周期。(可能会发生光栅和解码任务,但此处并未说明)。
  2. 合成器帧会发送到 Viz。
  3. Viz 为 foo.com 渲染进程、bar.com 渲染进程和浏览器界面汇总了合成器帧。
  4. Viz 安排了平局。
  5. Viz 将聚合的合成器帧绘制到屏幕上。

要在第 3 个标签页上滚动网页,请执行以下操作:

  1. 一系列 input 事件(鼠标、触摸或键盘)进入浏览器进程。
  2. 每个事件都会路由到 baz.com 的渲染进程合成器线程。
  3. 合成器会确定主线程是否需要了解相应事件。
  4. 如有必要,该事件会发送到主线程。
  5. 主线程触发 input 事件监听器 (pointerdowntouchstarpointermovetouchmovewheel) 以了解监听器是否会对事件调用 preventDefault
  6. 主线程会返回是否向合成器调用了 preventDefault
  7. 否则,系统会将输入事件发送回浏览器进程。
  8. 浏览器进程通过将其与其他近期事件结合,将其转换为滚动手势。
  9. 该滚动手势将再次发送到 baz.com 的渲染进程合成器线程,
  10. 滚动应用在此处,并且 bar.com 的合成器线程 渲染进程在其合成器事件循环中对动画进行 tick 操作。 然后,这将改变属性树中的滚动偏移,并重新运行合成器生命周期。 还会告知主线程触发 scroll 事件(此处未介绍)。
  11. 合成器帧会发送到 Viz。
  12. Viz 会为 foo.com 渲染进程汇总合成器帧, bar.com 呈现进程和浏览器界面之间的差异。
  13. Viz 安排了平局。
  14. Viz 将聚合的合成器帧绘制到屏幕上。

要为标签页 1 的 iframe #two 中的超链接路由 click 事件,请执行以下操作:

  1. 浏览器进程收到 input 事件(鼠标、触摸或键盘)。 它会执行近似命中测试 来确定 bar.com iframe 呈现进程应该接收点击,并将其发送到此处。
  2. bar.com 的合成器线程将 click 事件路由到主线程 并安排渲染事件循环任务来处理它。
  3. 输入事件处理器,用于 bar.com 的主线程点击测试,以确定 用户点击了 iframe 中的 DOM 元素,并触发 click 事件以供脚本观察。 如果听不到 preventDefault,则会转到超链接。
  4. 超链接目标网页加载后,呈现新状态, 其步骤与“渲染已更改的 DOM”上一个示例。 (这些后续更改未在此处介绍。)

外卖

可能需要大量时间才能记住和内化渲染的运作方式。

最重要的一点是,渲染管道经过仔细的 注重细节的模块化,已拆分为多个 独立的组件这些组件随后被并行拆分 以最大化进程和线程的 可扩缩的性能可扩展性方面的机会。

每个组件都发挥着关键作用, 现代 Web 应用。

请继续阅读关键数据结构, 它们对 RenderingNG 与代码组件同样重要。


插图作者:Una Kravets。