Firefox Quantum 中的无畏并发

2017 年 11 月 14 日 · Manish Goregaokar

如今,Rust 被用于各种事物。但它最初的应用是Servo,一个实验性的浏览器引擎。

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

Rust 代码于去年开始在 Firefox 中发布,最初是相对较小的试点项目,例如用于替换部分 libstagefright 用途的 MP4 元数据解析器。这些组件表现良好,几乎没有导致崩溃,但浏览器开发尚未充分体会到 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 生态系统使用的数百个crate之一——来驱动一个工作窃取级联算法。你可以在Lin Clark 的文章中阅读更多相关信息。并行化带来了许多性能提升,包括将亚马逊首页的页面加载速度提高了 30%。

无畏并发

Rust 预防线程安全 bug 的一个例子是 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++ 代码中会定期暴露出线程安全 bug,而这些问题在 Rust 端基本上从未出现过。

这些 bug 不容易被注意到,而且通常非常难以调试。而这还只是偶尔从主线程之外调用 C++ 代码;感觉如果我们尝试在纯 C++ 中完成这个项目,我们将不得不处理这些问题太多,以至于无法完成任何有用的事情。事实上,类似的 bug 过去曾多次阻碍了在 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 代码中很少出现模糊测试 bug

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

*heycam 记得从 gecko 中各种样式系统相关的东西中收到一堆模糊测试 bug*

<bholley> heycam:想想看,如果今天那些烦人的编译器错误每一个都能换成明天的模糊测试 bug,我们能省下多少时间 :-)

<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