RFC-0153:为 Fuchsia 自定义 Ninja

RFC-0153:针对 Fuchsia 的 Ninja 自定义
状态已接受
区域
  • 构建
说明

提议使用 Fuchsia 的自定义版 Ninja build 工具来加快 build 速度,改进状态报告和易用性。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2022-01-22
审核日期(年-月-日)2022-03-17

摘要

此 RFC 建议使用 Fuchsia 平台 build 所用的开源 Ninja 工具的临时自定义版本,以便集成多个现有的第三方补丁,这些补丁可显著提升该工具的性能和易用性,但上游维护者出于动机部分中说明的各种原因,尚未接受这些补丁。

具体而言,这样做可实现以下目的:

  • 通过优化 Ninja 使用的调度算法,显著缩短总 build 时间。

  • 改进了状态报告和日志记录,并解决了在我们的 CI build 中经常触发的严重易用性问题

  • 在不改变 Ninja 行为的情况下,提高其响应能力和总体性能。例如,将某些 Ninja 操作的速度提高了 22 倍。

  • 上述更改可让您在极短的时间内重新生成 IDE 绑定,从而显著加快构建系统与 Visual Studio Code 和 Vim 等 IDE 之间的集成速度,并提供出色的交互式代码编辑体验。

自定义将通过在 https://fuchsia.googlesource.com/third_party/ninja 上维护自定义 Git 分支来实现,该分支将使用严格的分支策略进行管理,该策略会密切跟踪上游,并使其能够表示为一系列基于最新上游历史记录的小补丁。

此实验性分支的情况将在 2022 年第 3 季度进行修订,因为我们计划帮助上游维护人员接受感兴趣的拉取请求,但这取决于许多超出此 RFC 范围的外部因素。不过,预计到那时,将不再需要自定义分支。

设计初衷

Ninja 构建工具是 Evan Martin 在 Google 工作期间为 Chrome 1 开发的实验性工具。 该实验取得了成功,变成了一个非常有用的工具,并成为官方 Chrome build 的一部分,而且已开源。随后,许多其他广泛部署的 build 配置工具或系统(包括现在的)迅速采用了它:

  • GN build 工具,目前由 Chrome、Fuchsia 和 Pigweed 项目使用。
  • Android 项目使用的 Blueprint 和 Kati 工具。
  • CMake build 生成器,支持 Ninja 作为后端。
  • 许多 Linux 开源项目使用的 Meson 构建系统。
  • LLVM,开发者可以使用 CMake 或 GN 构建,其中包括为 LLVM 做出贡献的 Fuchsia 团队成员。

Ninja 现在已被全球数千名开发者使用,并作为 GitHub 上的开源项目进行维护。Ninja 的维护人员会尽力确保该项目始终符合其最初的目标,即小巧、简单、可靠且极具可移植性。

由于多种原因,上游 Ninja 项目的进展仍然缓慢,大量有趣的拉取请求已经等待审核了数月甚至数年。我们已私下联系了当前维护者,对方承认,遗憾的是,这主要是因为没有时间妥善审核和测试非微不足道的更改,这也是回归测试套件状态的后果,该套件目前非常基础,在许多情况下需要手动测试拉取请求。该维护人员还会首先感谢任何有助于在 GitHub CI 中加快该测试套件的帮助,以便于加快 Ninja 的维护和发展。举例来说,只要能保证代码在旧版发行版(即 Centos 7、Debian 8、Ubuntu 18.04 和 OSX 10.12)上使用其默认工具链正确编译和运行,并且通过适当的持续集成验证来强制执行,他就不会反对将代码库从 C++03 切换到 C++11。

遗憾的是,存在大量阻塞性问题(如之前所述),解决所有这些问题需要大量工作,而 Fuchsia build 团队目前没有能力完成这些工作。

此 RFC 提供了一种临时解决此限制的技术方案,尽管我们强烈希望能够尽快安排适当的人力来与上游维护人员合作解决此问题。这些人员可以来自 Fuchsia 项目,也可以来自 Google 的其他团队(这些团队已表示对 Ninja 的未来发展和改进感兴趣)。不过,为这类工作提供资金支持不在本 RFC 的讨论范围内。

与此同时,Ninja 的自定义分支将允许挑选最具影响力的拉取请求,从而为 Fuchsia 开发者带来最大益处。为确保此分支尽可能接近上游,我们将强制执行严格的分支策略,以确保 Fuchsia 分支始终可以表示为一系列基于最新上游版本的补丁。

请注意,这种情况与自定义 Android Ninja 分叉有很大不同,后者自创建以来已严重偏离上游,并且缺少 Fuchsia build 所需的功能。

以下是一些最有趣的 pull 请求,它们促成了此 RFC:

使用更出色的调度算法更快地进行构建

有 3 个未完成的拉取请求可能会以重要方式缩短 build 时间。它们都会修改 Ninja 根据不同条件选择要启动的新命令的方式:

  • PR 2019:使用 build 日志估计要启动的命令的持续时间,并为其分配优先级。作者表示,这可将大型 build 项目的 build 时间从 20 分钟缩短为 15 分钟!

  • PR 1949:限制生成新命令,以避免达到负载限制。这样可以避免在生成过多争用相同 CPU 资源的进程时,导致 build 机器过载。作者声称,在特定条件下,这可将测试 build 时间从 22 分钟缩短到 15 分钟。

  • PR 1140:为 Ninja 添加了对 GNU Make jobserver 的支持,这是一种让 Ninja 及其执行的命令协调其 CPU 进程分配的方式。 这来自 Ninja 的 Kitware 分支。请注意,此 PR 与之前的 PR 有相似之处。作者未发布任何时间信息,并且不太可能有用,因为这需要调用的程序明确支持此功能。

修复了 Ninja 的构建输出,以支持长命令。

在构建期间,Ninja 仅打印已完成命令的状态行。在实践中,当运行会使 build 停滞的长时间运行命令时,会发生以下情况:

  • 如果命令超时(就像我们在 CI 机器人上运行非常长的 Rust 链接命令时发生的那样),输出或 Ninja 日志文件中绝对没有任何内容可以说明哪个目标/命令实际上已过期!

  • 在终端上,单行状态似乎处于冻结状态,并且仍显示上次完成的目标的名称,这令人困惑(许多开发者认为这是延迟的根本原因)。查看实际情况的唯一方法是在另一个终端中使用“ps”,但很少有人知道这一点。

第一点是 Fuchsia build 的问题,不修改 Ninja 就无法解决。第二个是困扰用户多年的易用性问题,以至于有多个拉取请求以不同方式修复此问题:

这是因为 Ninja 的输出是基于文本的,格式非常有限,并且在出现错误时还包含任意命令输出。这会造成各种细微问题,并导致难以可靠地解析输出

事实上,当前行为是 2015 年有意引入的,目的是解决 Ninja 的 build 输出无法进行机器解析的问题。因此,上游认为输出格式的任何更改都非常危险。

不过,在 Fuchsia 的背景下,解析 Ninja 输出的脚本由 Fuchsia 基础架构团队控制,如果对输出格式进行小幅更改就能完全解决问题,那么这种做法是有意义的。

序列化状态更新,以便更好地过滤日志

Android 自定义功能的另一个有趣之处在于,它提供了一种解决输出限制的变通方法,即以结构化二进制数据流的形式将状态更新发送到外部“前端”程序。

这使得 Android 团队能够存储具有不同日志记录级别的多个输出流,将错误消息收集到单独的文件中,并生成可使用 chrome://tracing 加载的准确 build 轨迹。

对于 Fuchsia build,应该可以从 Android 分支中提取该功能,以获得相同的优势。

请注意,此功能曾以拉取请求的形式提出,但在经过一些长时间讨论后被拒绝,因为上游认为解决输出解析问题的更好方法是将 Ninja 变成库,但鉴于代码库的状态,这需要付出相当大的努力(有人曾尝试通过需要 120 次提交的实验性 PR 来实现此目的)。

改善开发者体验

Android 分支通过使用线程来解析输入的分块,在适当的令牌边界处确定并在最终传递中合并,实现了 Ninja 输入文件的解析速度大幅提升(最高可达 22 倍)。

这是因为 Android 构建工具生成了大约 1.2 GiB 的 Ninja 构建计划,在执行任何操作之前需要 12 秒来解析这些计划(在具有热文件系统缓存的强大工作站上)。

Fuchsia build 也遇到了类似的情况,目前生成的 Ninja build 计划大约为 800 MiB,解析时间长达 10 秒,这使得增量 build 的体验令人烦恼。Fuchsia build 团队认为这些改进非常重要,应集成到 build 所使用的 Ninja 版本中。

在编写代码时,编译器错误消息和警告是向开发者提供的第一行反馈。在静态语言中,这种情况尤为明显;而在 Rust 等旨在在编译时检查各种条件的新语言中,这种情况也越来越明显。因此,及时获得反馈对于提高开发者工作效率至关重要。每等待一秒编译器输出,开发者就会多一秒不确定自己的代码是否正确,或者多一秒可能会被其他事情分心,而快速反馈对于仍在学习某种语言的程序员来说尤为重要。

在最高效的环境中,文件保存后,开发环境会立即提供反馈。这种“流畅”的感觉非常重要,因此 Fuchsia 开发者创建了工具,使他们能够使用完全不同的构建系统 cargo,从而获得快速反馈和测试周期。尽管此工具不受支持且经常出现故障,但仍有一些开发者在使用。

对于 Fuchsia,Ninja 在每次调用开始时最多需要 10 秒来解析其自己的 build 文件(build.ninja 文件及其包含的其他文件)。这对于我们提供反馈的速度来说是一个非常大的下限,至少比我们目前使用 cargo 提供的速度慢 25 倍。 通过将解析时间缩短 25-100 倍,并结合本季度推出的其他工具工作流,我们可以将 Fuchsia 的代码编写体验从“慢如蜗牛”提升到“快速响应”。这样一来,开发周期会缩短,工程师会更满意,软件质量也会更高。

您可以在下方看到在 IDE 中更改代码并查看编译错误的当前体验的屏幕截图。

显示 IDE 反馈缓慢的屏幕截图

开发者反馈的延迟几乎完全归因于 Ninja 解析其文件所花费的时间。请与下面的演示视频进行比较,了解使用已修补的 Ninja 可以实现怎样的 IDE 体验。

显示快速 IDE 反馈的屏幕截图

同样,创建 fx setup-go 的目的是在本地开发期间专门使用 Go 构建系统,正是因为使用 Ninja 进行增量构建太慢了。

请注意,Android 团队曾在 2019 年提出过此确切功能,但当时被上游维护人员拒绝,原因是它需要 C++11,而这在当时被认为尚不可接受。自那时以来,时间已经过去,可以考虑进行此类切换,前提是强制执行动机部分中所述的保证。

改进了界面

我们提交了以下 PR,以在交互式终端中实现类似表格的状态输出,灵感来自 Buck 构建系统(请参阅动画示例)。

该 PR 已被其作者舍弃,但可以重新基于该 PR 来提供非常出色的界面改进,以便在本地构建 Fuchsia 时使用。

利益相关方

辅导员

pascallouis@google.com 已被 FEC 委任为负责引导此 RFC 完成 RFC 流程。

审核者

shayba@google.com,Build maruel@google.com,Fuchsia 平台 EngProd abarth@google.com,平台主管

积极参与过之前有关此主题的对话的其他团队成员:

brettw@google.com(作为 Fuchsia 工具贡献者)。 fangism@google.com,Build haowei@google.com,Fuchsia 的 LLVM jayzhuang@google.com,Build olivernewman@google.com,Fuchsia CI phosek@google.com,Fuchsia 工具链

已咨询

fangism@google.com, Build jayzhuang@google.com, Build<0A>tmandry@google.com, Rust on Fuchsia rudymathu@google.com, Fuchsia EngProd

共同化

该想法最初由 Fuchsia 团队的 Rust 成员 tmandry 提出,然后在 Google 内部电子邮件线程中进行了讨论,现在在公开论坛中展示,以便进一步讨论和批准。

设计

受 Fuchsia 控制的 Git 代码库将用于派生上游 Ninja master 分支,以便管理包含 Fuchsia 特有自定义项的分支,同时跟踪上游 origin/master。补丁将由分支的所有者审核,并且应根据具体情况考虑以下方面:

  • 提供的福利
  • 补丁的可维护性
  • 与上游的偏离,这会影响可维护性。

以及所有者认为合适的其他条件。

按照标准做法,本地修改的列表将记录在 FUCHSIA.readme 文件中。

该分支的维护者将是 Fuchsia Build 团队,该团队负责 Fuchsia 构建系统及其正确性、可维护性和性能,并对此负责。

此 RFC 的目的是确保此分支不会分叉到难以集成上游更改的程度。为实现此目标,我们将应用以下策略:

使 Fuchsia 分支与上游保持同步的策略

Fuchsia Ninja 分支应受到约束,这意味着为了尽可能接近上游,我们将定期创建“upstream-sync”分支,这些分支将我们的更改表示为一系列干净的补丁,这些补丁位于最新的上游版本之上。以下几张图表有助于您理解此处的含义。

创建自定义 Git 分叉时,通常会在现有上游版本的基础上创建一个新分支,并添加新的提交。下图展示了“上游”历史记录分支为“fuchsia”分支的情况,该分支在“上游”历史记录的基础上添加了 3 个提交:

upstream  ___U1__U2___
                      \
fuchsia                \__F1__F2__F3

提交由上游项目的维护者添加到上游项目中。上游和 Fuchsia 历史记录现在已分叉,如下所示:

upstream  ___U1__U2______U3__U4__U5
                      \
fuchsia                \__F1__F2__F3

将上游更改纳入 Fuchsia 分支的一种简单方法是执行合并操作,这可能会发现提交之间存在冲突,需要手动解决,如下所示:

upstream  ___U1__U2______U3__U4___U5___
                      \                \
fuchsia                \__F1__F2__F3____\F4__

其中,F4 表示合并提交。现在,“fuchsia”分支拥有来自上游的所有改进,但它不再能表示为一系列基于上游最新版本(即 U5)的补丁。

为了实现这一目标,可以避免直接将上游合并到 Fuchsia 分支中。而是创建 fuchsia 分支的副本,并将其重新基于 U5,同时解决过程中出现的任何冲突(这可以先通过 git rebase 操作在本地完成)。我们以 upstream-sync-U5 为例进行说明,如下所示:

upstream  ___U1__U2______U3__U4__U5
                      \            \
upstream-sync-U5       \            \__F1'__F2'__F3'
                        \
fuchsia                  \__F1__F2__F3

此时,fuchsiaupstream-sync-U5 在功能上应等效(通过测试强制执行),除非必须解决一些冲突,否则新提交 F1' 到 F3' 的内容甚至可能与 F1 到 F3 相同。

现在可以将后者合并到前者中(只需接受 upstream-sync-U5 中的更改即可解决任何冲突),如下所示:

upstream  ___U1__U2______U3__U4__U5
                      \            \
upstream-sync-U5       \            \__F1'__F2'__F3'
                        \                           \
fuchsia                  \__F1__F2__F3_______________\F4

在与其他上游更改同步时,可以稍后重复此操作,如:

upstream  ___U1__U2______U3__U4__U5__________________U6__U7
                      \            \                       \
upstream-sync-U5       \            \__F1'__F2'__F3'        \
                        \                           \        \
upstream-sync-U7         \                           \        \__F1"__F2"__F3"__F5"
                          \                           \                            \
fuchsia                    \__F1__F2__F3_______________\F4__F5______________________\F6

每个新的 upstream-sync-XXX 分支都将自定义状态表示为一系列基于最新上游版本的补丁。这使得 Fuchsia 更改更易于理解,并提高了在需要时将它们发送回上游的能力。

Fuchsia 分支的维护者应尽可能创建上游同步分支。

实现

该分支将维护在 https://fuchsia.googlesource.com/third_party/ninja(已存在)的 Git-on-Borg 实例上,并且其公共 Gerrit 实例将用于审核和接受补丁。

用于为 Fuchsia 平台 build 构建 Ninja 预构建二进制文件的 LUCI recipe 将相应地切换其源 GIT 网址,因为它目前指向上游 GitHub 代码库的镜像。

性能

此 RFC 的主要目的是缩短 Fuchsia 平台构建时间,同时通过大幅缩短工具响应时间来改善开发者体验。

工效学设计

通过缩短开发者反馈时间,我们将提高开发者吞吐量并减少上下文切换。

这种更低的延迟也为 Fuchsia 的 build 系统开辟了新的使用场景,例如在 IDE 中报告依赖于生成文件的源代码的编译时错误。这包括使用 FIDL 的代码以及所有 Rust 和 Go 源代码。

向后兼容性

允许树内工具依赖于 Fuchsia 的 Ninja 分支的 CLI 公开的新功能。任何打算在 Fuchsia 树之外使用的工具都不得假定存在 Fuchsia 的 Ninja 分叉。

不过,这些特定于 Fuchsia 的功能应限制在最低限度,因为能够回滚到上游是此 RFC 的一个重要目标。

例如,对于某些用途,一种有效的替代方案是编写可直接解析 Ninja 构建计划并对其执行计算的离线工具(如 https://fuchsia-review.googlesource.com/c/fuchsia/+/644561 中所示)。

安全注意事项

此变更不应对安全性产生任何重大影响。Ninja 已经能够在开发者的系统上运行任意命令。Ninja 中的任何安全漏洞都可能来自上游,而我们的分支将涉及 Fuchsia 工程师审核每一项更改,这比现状有所改进。

通过使用 Google 的标准源代码工具套件,我们可以缓解可能出现的任何源代码供应链问题。

隐私注意事项

此提案不应以任何方式影响隐私权。

测试

与目前的情况一样,对于提交到 Fuchsia 的 Ninja 分支的任何更改(包括上游同步合并),系统都会运行 Ninja 测试套件。更改必须遵循 Ninja 的标准测试实践,包括单元测试。

文档

在 Fuchsia Git 分支的顶部,系统会添加一个标准的 README.fuchsia 文件,用于说明分支的当前状态与上游之间的差异。

它将包含指向此 RFC 的链接,以说明此处描述的分支管理策略。

缺点、替代方案和未知因素

此方案的主要缺点是,需要尽可能频繁地执行上游同步分支,才能使分支与上游保持接近。每次此类操作都可能会触发一个或多个变基冲突,需要由 Fuchsia 分支的维护人员手动解决。

请注意,一种可大幅简化这些问题的修复过程的好方法是将 Fuchsia 分支中的更改分解为尽可能多的小补丁(每个补丁都会生成一个完全可测试的源代码树)。

我们曾考虑使用 Android Ninja 分支作为起点,但其差异非常大。仅将最近的上游更改合并到其中就需要解决大量冲突(例如,他们将所有内容都切换到了 C++17 并完全移除了 C++03 支持代码),并且很难重新基于上游创建一组干净的补丁。

从上游开始,并在此基础上重新设置一些特定于 Android 的功能,这似乎是一个更好的解决方案,能够在几周内提供结果。

此外,我们还尝试了其他几种方法,希望在不更改上游 Ninja 的情况下获得与某些上游补丁相同的优势,但这些方法都远远不够。最值得一提的是,经过多年的调查和对 GN 的一系列更改,我们成功缩短了 Ninja 无操作构建时间。不过,我们很失望地发现,我们只取得了微小的改进

请注意,Google 的多个不同团队都在使用 Ninja,并且有些人表示有兴趣以协调的方式管理整个公司的 Ninja 发展,或者找到一种方法来帮助上游维护人员。此 RFC 不会阻止上述任何情况,但其首要任务是先快速为 Fuchsia 平台带来好处。

最后,Fuchsia 现在拥有 Bazel SDK,并且已展示了针对 Fuchsia 组件的 Bazel build(请参阅 RFC-0139)。从长远来看,Fuchsia 应考虑使用 Ninja 以外的替代方案作为平台构建后端,并探索可能需要数年的迁移。就本 RFC 而言,我们认为此类迁移不在讨论范围内。