Firefox Quantum 中的无畏并发

2017 年 11 月 14 日 · Manish Goregaokar

如今,Rust 被用于各种各样的项目。但其最初的应用是Servo,一个实验性的浏览器引擎。

现在,经过多年的努力,Servo 的一个主要部分正在投入生产:Mozilla 发布了 Firefox Quantum

Rust 代码去年开始在 Firefox 中发布,最初是一些相对较小的试点项目,例如 MP4 元数据解析器,以替换 libstagefright 的一些用法。这些组件表现良好,并且实际上没有导致崩溃,但是浏览器开发尚未从 Rust 可以提供的全部功能中获得巨大的好处。今天这种情况发生了改变。

Stylo:并行 CSS 引擎

Firefox Quantum 包含了 Stylo,一个纯 Rust CSS 引擎,它充分利用 Rust 的 “无畏并发” 来加速页面样式设置。它是 Servo 第一个与 Firefox 集成的主要组件,对于 Servo、Firefox 和 Rust 来说都是一个重要的里程碑。它用 85,000 行 Rust 代码替换了大约 160,000 行 C++ 代码。

当浏览器加载网页时,它会查看 CSS 并解析规则。然后,它会确定哪些规则适用于哪些元素及其优先级,并将这些规则“层叠”到 DOM 树中,从而计算每个元素的最终样式。样式设置是一个自上而下的过程:您需要知道父元素的样式才能计算其子元素的样式,但是之后可以独立计算其子元素的样式。

这种自上而下的结构非常适合并行处理;但是,由于样式设置是一个复杂的过程,因此很难正确实现。Mozilla 之前尝试使用 C++ 并行化其样式系统两次,但都失败了。但是,Rust 的无畏并发使得并行处理变得切实可行!我们使用 rayon——Rust 生态系统中 Servo 使用的数百个 crates 之一——来驱动工作窃取级联算法。您可以在 Lin Clark 的文章中阅读更多相关信息。并行处理带来了许多性能改进,包括 Amazon 主页的页面加载速度提高了 30%。

无畏并发

Rust 如何防止线程安全错误的示例是 Stylo 中如何共享样式信息。计算出的样式被分组为相关属性的“样式结构”,例如,有一个用于所有字体属性,一个用于所有背景属性等等。现在,其中大多数是共享的;例如,子元素的字体通常与其父元素相同,并且即使兄弟元素与父元素样式不同,它们也经常共享样式。Stylo 使用 Rust 的原子引用计数 Arc<T> 在元素之间共享样式结构。Arc<T> 使其内容不可变,因此它是线程安全的——当其他元素正在使用样式结构时,您不会意外地修改它。

我们使用 Arc::make_mut() 补充这种不可变的访问;例如,这一行 调用 .mutate_font()(对字体样式结构的 Arc::make_mut() 的一个简单封装)来设置字体大小。如果给定元素是唯一引用此特定字体结构的元素,它将仅就地修改它。但是如果不是,make_mut() 会将整个样式结构复制到一个新的、唯一的引用中,然后该引用将被就地修改并最终存储在该元素上。

context.builder.mutate_font().set_font_size(computed);

另一方面,Rust 保证不可能修改元素的样式,因为它保持在不可变引用之后。Rayon 的作用域线程功能确保即使它想获得/存储可变引用,也无法做到。父样式是允许一个线程写入以创建(当正在处理父元素时)的内容,之后每个人都只允许从中读取。您会注意到,该引用是一个零开销的“借用指针”,而不是一个引用计数的指针,因为当 Rust 和 Rayon 可以保证数据至少在线程的生命周期内有效时,它们允许您跨线程共享数据而无需进行引用计数。

就我个人而言,我的 “啊哈,我现在完全理解了 Rust 的强大之处” 的时刻是当 C++ 端出现线程安全问题时。浏览器是复杂的,尽管 Stylo 是 Rust 代码,但它需要多次回调 Firefox 的 C++ 代码。Firefox 每个进程都有一个 “主线程”,虽然它确实使用其他线程,但它们的功能相对有限。Stylo 由于是高度并行的,偶尔会在主线程之外调用 C++ 代码。这通常没有问题,但是当涉及到缓存或全局可变状态时,C++ 代码中会定期出现线程安全错误,这些错误基本上在 Rust 端从来都不是问题。

这些错误不容易被发现,而且通常很难调试。这只是偶尔在主线程之外调用 C++ 代码的情况;感觉如果我们尝试用纯 C++ 完成这个项目,我们将会处理太多这类问题,而无法完成任何有用的事情。实际上,像这样的错误已经阻碍了过去在 Firefox 和其他浏览器中并行化样式的多次尝试。

Rust 的生产力

Firefox 开发人员在学习和使用 Rust 的过程中度过了愉快的时光。人们非常享受能够积极编写代码而无需担心安全性的感觉,许多人提到 Rust 的所有权模型与他们在 Firefox 的大型 C++ 代码库中隐式推理内存的方式非常相似。令人耳目一新的是,模糊测试器主要捕获 Rust 代码中的显式panic,与 C++ 端的分段错误和其他内存安全问题相比,它们更容易调试和修复。

在 Firefox 开发人员中,一个让我印象深刻的对话——包含在 Josh Matthews 在 Rust Belt Rust 的演讲中——是

<heycam> stylo 最好的部分之一是,由于 Rust 的存在,我们更容易实现这些我们需要的样式系统优化

<heycam> 你能想象如果我们需要在我们现有的时间框架内用 C++ 实现所有这些会怎么样吗

<heycam> 是的,真的

<bholley> heycam:我们很少在 Rust 代码中遇到模糊测试错误

<bholley> heycam:考虑到我们正在做的所有复杂的事情

*heycam 回忆起从 gecko 中各种样式系统的东西中获得了很多模糊测试错误

<bholley> heycam:想想如果今天我们遇到的每一个恼人的编译器错误都被明天的模糊测试错误所取代,我们可以节省多少时间 :-)

<heycam> 呵呵

<njn> 你们听起来像在给 Rust 做广告

总结

总的来说,Firefox Quantum 从 Stylo 以及 Rust 中获益匪浅。它不仅加快了页面加载速度,还加快了交互时间,因为可以更快地重新计算样式信息,从而使整个体验更加流畅。

但是,Stylo 仅仅是个开始。还有两个主要的 Rust 集成即将完成。一个是将 Webrender 集成到 Firefox 中;Webrender 大量使用 GPU 来加速渲染。另一个是 Pathfinder,这是一个将字体渲染卸载到 GPU 的项目。此外,还有 Servo 的并行布局和 DOM 工作,它们正在不断发展和改进。Firefox 的未来一片光明。

作为 Rust 团队的成员,我非常高兴看到 Rust 在生产中被如此成功地使用并发挥如此巨大的作用!作为 Servo 和 Stylo 的开发人员,我感谢 Rust 为我们提供的工具,使我们能够完成这项工作,并且我很高兴看到 Servo 的一个大型组件最终进入用户手中!

亲自体验 Rust 的好处——试用 Firefox Quantum