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——Servo 从 Rust 生态系统中使用的数百个板条箱之一——来驱动一个工作窃取级联算法。您可以在Lin Clark 的文章中阅读更多相关内容。并行化带来了许多性能改进,包括亚马逊主页的页面加载速度提高了 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 代码中,模糊测试器主要捕获显式的恐慌,这让人耳目一新,与 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