跳至主要内容

一篇标记为“rust”的文章

查看所有标签

·阅读时长:12 分钟

我们非常激动地今天将基于 Rust 的新 Relay 编译器预览版开源(版本号为 v13.0.0-rc.1)!这个新编译器速度更快,支持新的运行时功能,并为未来的扩展提供了坚实的基础。

在发布这个版本之前,Meta 的代码库一直在增长,没有停下来的迹象。在我们这个规模下,编译代码库中所有查询所需的时间一直在增加,直接影响着开发人员的效率。虽然我们尝试了多种策略来优化基于 JavaScript 的编译器(在下面讨论),但我们逐步优化性能的能力无法跟上代码库中查询数量的增长。

因此,我们决定用 Rust 重写编译器。我们选择 Rust 是因为它速度快,内存安全,并且可以轻松地跨线程安全地共享大型数据结构。开发工作始于 2020 年初,编译器在当年年底在内部发布。发布过程顺利,没有中断应用程序开发。最初的内部基准测试表明,编译器的平均性能提高了近 5 倍,在 P95 时提高了近 7 倍。我们自那以后进一步提高了编译器的性能。

这篇文章将探讨为什么 Relay 有一个编译器,我们希望用新编译器解锁什么,它的新功能以及我们为什么选择使用 Rust 语言。如果你急于开始使用新编译器,请查看 编译器包自述文件发行说明

为什么 Relay 有一个编译器?

Relay 有一个编译器是为了提供稳定性保证并实现出色的运行时性能。

要理解原因,请考虑使用该框架的工作流程。使用 Relay,开发人员使用一种称为 GraphQL 的声明性语言来指定每个组件需要哪些数据,但不要指定如何获取这些数据。然后,编译器将这些组件的数据依赖关系拼接到查询中,这些查询获取给定页面的所有数据,并预先计算出使 Relay 应用程序具有如此高性能和稳定性的工件。

在这个工作流程中,编译器

  • 允许以隔离的方式对组件进行推理,从而避免了大量错误,并
  • 将尽可能多的工作转移到构建时间,从而显著提高使用 Relay 的应用程序的运行时性能。

让我们依次考察这些内容。

支持本地推理

使用 Relay,组件仅通过使用 GraphQL 片段指定其自己的数据需求。然后,编译器将这些组件的数据依赖关系拼接到查询中,这些查询获取给定页面的所有数据。开发人员可以专注于编写组件,而不必担心其数据依赖关系如何融入更大的查询中。

但是,Relay 更进一步地进行了本地推理。编译器还会生成一些文件,Relay 运行时使用这些文件来读取给定组件片段所选择的数据(我们称之为 数据屏蔽)。因此,组件永远不会访问(实际上,不仅仅是在类型级别!)它没有明确请求的任何数据。

因此,修改一个组件的数据依赖关系不会影响另一个组件看到的数据,这意味着 **开发人员可以以隔离的方式对组件进行推理。** 这为 Relay 应用程序提供了无与伦比的稳定性,并避免了大量错误,这是 Relay 可以扩展到许多开发人员触及同一代码库的关键原因之一。

改进运行时性能

Relay 还利用编译器将尽可能多的工作转移到构建时间,从而提高 Relay 应用程序的性能。

由于 Relay 编译器具有所有组件数据依赖关系的全局知识,因此它能够编写出与手动编写查询一样好(通常甚至更好)的查询。它能够通过以在运行时速度过慢的方式优化查询来做到这一点。例如,它会修剪从生成的查询中永远无法访问的分支,并扁平化查询的相同部分。

由于这些查询是在构建时生成的,因此 Relay 应用程序从不从 GraphQL 片段生成抽象语法树 (AST),也不操纵这些 AST 或在运行时生成查询文本。相反,Relay 编译器用预先计算的优化指令(作为普通的 JavaScript 数据结构)替换应用程序的 GraphQL 片段,这些指令描述了如何将网络数据写入存储库以及如何从存储库中读取数据。

这种安排的一个额外好处是,Relay 应用程序包既不包含模式,也不包含 GraphQL 片段的字符串表示(当使用持久化查询时)。这有助于减小应用程序的大小,从而节省用户的带宽并提高应用程序性能。

事实上,新编译器更进一步,以另一种方式节省了用户的带宽——Relay 可以通知应用程序的服务器在构建时有关每个查询文本的信息,并生成一个唯一的查询 ID,这意味着应用程序不再需要通过用户的慢速网络发送可能很长的查询字符串。当使用这种持久化查询时,唯一需要通过网络发送以进行网络请求的是查询 ID 和查询变量!

新编译器带来了什么?

与动态语言相比,编译语言有时被认为会带来摩擦并减慢开发人员的速度。但是,Relay 利用编译器来减少摩擦并使常见的开发人员任务更容易。例如,Relay 公开了针对常见交互的高级原语,这些交互很容易出现细微的错误,例如分页和使用新变量重新获取查询。

这些交互的共同点是它们需要从旧查询中生成新查询,因此涉及样板代码和重复——这是自动化的理想目标。Relay 利用编译器的全局知识,让开发人员能够通过添加一个指令和更改一个函数调用来启用分页和重新获取。就这样。

**但是,赋予开发人员轻松添加分页的能力只是冰山一角。** 我们对编译器的愿景是,它提供了更多高级工具来发布功能并避免样板代码,为开发人员提供实时帮助和见解,并且由可以被其他工具用于处理 GraphQL 的部分组成。

该项目的主要目标是,重写编译器的架构应该让我们能够在未来几年实现这一愿景。

虽然我们还没有做到,但我们在每个标准上都取得了重大进展。

例如,新编译器附带对新 @required 指令的支持,该指令将在读取时,如果给定子字段为空,则会使父链接字段为空或引发错误。这听起来可能是一个微不足道的质量改进,但如果你的组件代码中有一半是空检查,那么 @required 就会变得非常有用!

没有 @required 的组件
使用 @required

接下来,编译器为一个内部专用的 VSCode 扩展提供支持,该扩展在您键入时自动完成字段名称,并在悬停时显示类型信息,以及许多其他功能。我们还没有将其公开发布,但我们希望在某个时候发布!我们的经验是,这个 VSCode 扩展使使用 GraphQL 数据变得更加容易和直观。

最后,新编译器被编写为一系列独立的模块,这些模块可以被其他 GraphQL 工具重用。我们称之为 Relay 编译器平台。在内部,这些模块被其他代码生成工具以及不同平台的其他 GraphQL 客户端重用。

编译器性能

到目前为止,我们已经讨论了为什么 Relay 有一个编译器以及我们希望重写它带来的好处。但我们还没有讨论为什么我们决定在 2020 年重写编译器:性能。

在决定重写编译器之前,随着代码库的增长,编译代码库中所有查询所需的时间一直在逐渐但无情地变慢。我们逐步优化性能的能力无法跟上代码库中查询数量的增长,我们看不到任何增量式的解决办法。

到达 JavaScript 的尽头

之前的编译器是用 JavaScript 编写的。这是一种自然的语言选择,原因有几个:它是我们团队最有经验的语言,它是 Relay 运行时使用的语言(使我们能够在编译器和运行时之间共享代码),以及 GraphQL 参考实现和我们的移动 GraphQL 工具使用的语言。

编译器的性能在相当长一段时间内保持合理:Node/V8 附带了一个经过高度优化的 JIT 编译器和垃圾收集器,如果你小心的话,它可以非常快(我们很小心)。但编译时间在增长。

我们尝试了多种策略来跟上

  • 我们已经将编译器改为增量式。对于代码变更,编译器只会重新编译受影响的依赖项。
  • 我们已经识别出哪些转换速度慢(主要是 flatten),并对算法进行了可能的改进(例如添加记忆化)。
  • 官方的 graphql npm 包的 GraphQL 模式表示需要占用多个 GB 的内存来表示我们的模式,因此我们用自定义的 fork 替换了它。
  • 我们在最热门的代码路径中进行了分析器引导的微优化。例如,我们不再使用 ... 运算符来克隆和修改对象,而是更倾向于在复制对象时显式列出对象的属性。这样可以保留对象的隐藏类,并使代码更好地进行 JIT 优化。
  • 我们重构了编译器,使其可以分发到多个 worker,每个 worker 处理一个模式。在 Meta 之外,拥有多个模式的项目并不常见,因此即使这样,大多数用户也会使用单线程编译器。

这些优化不足以跟上 Relay 在内部的快速采用。

最大的挑战是 NodeJS 不支持具有共享内存的多线程程序。最好的办法是启动多个 worker,这些 worker 通过传递消息进行通信。

这在某些情况下效果很好。例如,Jest 使用这种模式,并在运行测试或转换文件时利用了所有核心。这非常适合,因为 Jest 在进程之间不需要共享太多数据或内存。

另一方面,我们的模式过于庞大,无法在内存中拥有多个实例,因此在 JavaScript 中,没有好的方法可以有效地并行化 Relay 编译器,使每个模式使用多于一个线程。

决定使用 Rust

在我们决定重写编译器之后,我们评估了许多语言,以查看哪种语言可以满足我们项目的需求。我们想要一种快速、内存安全并支持并发的语言——最好是在构建时而不是运行时捕获并发错误。同时,我们想要一种在内部得到良好支持的语言。这将选择范围缩小到几个。

  • C++ 满足了大部分标准,但感觉难以学习。而且,编译器在安全方面提供的帮助不如我们希望的那么多。
  • Java 也许是一个不错的选择。它可以很快,并且支持多核,但提供了较少的低级控制。
  • OCaml 是编译器领域的一个经过验证的选择,但多线程具有挑战性。
  • Rust 速度快,内存安全,并支持并发。它可以轻松地跨线程安全共享大型数据结构。由于 Rust 普遍受到关注,我们团队之前有一些使用经验,以及 Facebook 其他团队的使用,因此它成为了我们最明确的选择。

内部推出

事实证明,Rust 非常适合!该团队主要由 JavaScript 开发人员组成,他们发现 Rust 很容易上手。而且,Rust 的高级类型系统在构建时捕获了许多错误,帮助我们保持了高速发展。

我们于 2020 年初开始开发,并在当年年底在内部推出了编译器。最初的内部基准测试表明,编译器的平均性能提高了近 5 倍,在 P95 时提高了近 7 倍。从那时起,我们进一步改进了编译器的性能。

在 OSS 中发布

今天,我们很高兴发布新版编译器,它是 Relay v13 的一部分。新编译器功能包括

您可以在 README发布说明 中找到有关编译器的更多信息!

我们正在继续开发编译器中的功能,例如使开发人员能够访问图上的派生值,添加对更符合人体工程学语法以更新本地数据的支持,以及全面完善我们的 VSCode 扩展,我们希望将所有这些功能发布到开源社区。我们对这次发布感到自豪,但还有很多工作要做!

感谢

感谢 Joe Savona、Lauren Tan、Jason Bonta 和 Jordan Eldredge 对这篇博文的宝贵反馈。感谢 ch1ffa、robrichard、orta 和 sync 提交与编译器错误相关的 issue。感谢 MaartenStaa 添加 TypeScript 支持。感谢 @andrewingram 指出启用 @required 指令的难度,该指令现在默认已启用。还有许多其他贡献者——这确实是一项社区合作成果!