RFC-0153:为 Fuchsia 自定义 Ninja

RFC-0153:Ninja 自定义 Fuchsia
状态已接受
领域
  • Build
说明

提议使用面向 Fuchsia 的 Ninja 构建工具的自定义版本,以加快构建速度、改进状态报告和易用性。

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

总结

此 RFC 提议使用 Fuchsia 平台 build 使用的开源 Ninja 工具的临时自定义版本,以集成几个现有的第三方补丁,这些补丁可显著提高其性能和易用性,但上游维护人员由于动机部分所述的各种原因而尚无法接受这些补丁。

具体而言,这样做可以:

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

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

  • 提高 Ninja 的响应速度和一般性能,而不改变 Ninja 的行为。例如,通过将某些 Ninja 操作速度提高 22 倍。

  • 基于上述成果,可以在很短的时间内重新生成 IDE 绑定,并提供出色的交互式代码修改体验,从而显著加快构建系统与 IDE(如 Visual Studio Code 和 Vim)之间的集成。

自定义将通过在 https://fuchsia.googlesource.com/third_party/ninja 上维护一个自定义 git 分支来实现,该分支将使用规范分支策略进行管理,该策略可密切跟踪上游,并使其可表达为在最近的上游历史记录之上的一系列小补丁。

此实验性分支的情况将于 2022 年第 3 季度修改,因为我们计划帮助上游维护人员接受相关拉取请求,但这取决于许多外部因素,而这些外部因素不在本 RFC 的讨论范围内。不过,到那时应该不再需要该自定义分支。

设计初衷

Ninja 构建工具是由 Google 的 Evan Martin 在开发 Chrome 1 期间作为一项实验开发的。实验成功了,它开发出了一款非常实用的工具,成为了 Chrome 正式版本的一部分,并开源了。后来,许多其他广泛部署的 build 配置工具或系统迅速采用该技术,其中包括:

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

Ninja 现在拥有全球数千名开发者在使用,且在 GitHub 上作为开源项目进行维护。Ninja 的维护人员尽力确保项目保持其最初目标,即小巧、简单、可靠且极易可移植。

由于多种原因,上游 Ninja 项目的进度仍然很慢,大量有趣的拉取请求已等待数月(甚至可能数年)的审核。我们已私下联系当前维护者,他们认识到,很遗憾,这主要是因为没有时间来正确审核和测试重要的更改,而且这也是由于回归测试套件的状态导致的,该状态目前非常基本,在许多情况下需要手动测试拉取请求。该维护者也首先希望能提供帮助,以便在 GitHub CI 中提升该测试套件,以便加快和加快 Ninja 的维护和发展。举个例子,他并不反对将代码库从 C++03 切换到 C++11,只要保证代码可以在旧版发行版(即 Centos 7、Debian 8、Ubuntu 18.04 和 OSX 10.12)上使用默认工具链(由适当的持续集成验证强制执行)正确编译和运行。

遗憾的是,存在大量像前面描述的阻塞问题,解决所有这些问题需要 Fuchsia 构建团队目前无力完成的工作量大。

此 RFC 为暂时解决此限制提供了一项技术主张,尽管我们强烈希望尽快安排适当的人员计数来与上游维护人员合作解决此问题。此类员工人数可能来自 Fuchsia 项目,也可能来自对 Ninja 的未来进化和改进感兴趣的 Google 其他团队。但是,为此类工作提供资金并不在本 RFC 的范围内。

同时,Ninja 的自定义分支允许择优挑选影响最大的拉取请求,对 Fuchsia 开发者带来最大好处。为确保此分支尽可能靠近上游,系统将强制执行一个有序的分支策略,以确保 Fuchsia 分支始终可表达为对最新上游版本的一系列补丁。

请注意,这种情况与自定义 Android Ninja 分支大相径庭,后者自创建以来就明显偏离了上游,并且缺少 Fuchsia build 所需的功能。

触发此 RFC 的最有趣的拉取请求如下:

使用更好的调度算法加快构建速度

三个待处理的拉取请求有可能以重要方式缩短构建时间。它们都会修改 Ninja 根据不同的条件接收新命令以启动的方式:

  • PR 2019:为要启动的命令分配优先级,并使用构建日志来估算其时长。作者表示,这会将大型构建项目的构建时间从 20 分钟缩短到 15 分钟!

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

  • PR 1140:向 Ninja 添加了对 GNU Make jobserver 的支持,这是 Ninja 及其为了协调 CPU 进程分配而执行的命令的方式。这本书来自 Ninja 的 Kitware 分支。请注意与之前的 PR 具有相似之处。作者未发布任何时间,而且用处不大,因为它需要调用的程序明确支持这一点。

修复了 Ninja 的长命令的 build 输出问题。

在构建期间,Ninja 仅输出已完成的命令的状态行。实际上,如果一个暂停构建且长时间运行的命令正在运行,则:

  • 如果该命令超时(在我们的 CI 聊天机器人上,运行很长的 Rust 链接命令时就会发生这种情况),那么输出或 Ninja 日志文件中绝对没有任何内容可判断哪个目标/命令实际已过期!

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

第一点是 Fuchsia build 的一个问题,如果不修改 Ninja,便无法解决这一问题。第二个是易用性问题,多年来一直困扰着用户,以至于有多个拉取请求需要通过不同的方式进行修复:

这是因为 Ninja 的输出基于文本,格式有限,其中还包括任意命令输出,以防发生错误。这会导致各种细微的问题,并使输出难以可靠地解析

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

但是,在 Fuchsia 环境中,解析 Ninja 输出的脚本由 Fuchsia 基础架构团队控制,如果能够完全解决问题,对输出格式稍作更改也是可行的。

序列化状态更新,改进了日志过滤

Android 自定义的另一个有趣功能是解决输出限制问题,即以结构化二进制数据流的形式,将状态更新发送到外部“前端”程序。

这使得 Android 团队能够存储具有不同日志记录的多个输出流、将错误消息收集到单独的文件中,并生成可通过 chrome://tracing 加载的准确构建跟踪记录。

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

请注意,这是以拉取请求的形式提出的,但在经过一些长时间 讨论后被拒,上游认为解决输出解析问题的更好方法是将 Ninja 转换为库,考虑到代码库的状态,这项工作付出很多(有人试图通过需要 120 项提交的实验性 PR 完成这项工作)。

改善了开发者体验

Android 分支通过使用线程解析输入的拆分(在适当的词元边界确定并最终遍历中确定),大幅提升了 Ninja 输入文件的解析速度(高达 22 倍)。

产生这种现象的原因是,Android 构建工具生成了大约 1.2 GiB 的 Ninja 构建计划,这需要 12 秒才能解析完毕(在具有热文件系统缓存的强大工作站上)。

Fuchsia build 遇到了类似的情况,但遗憾的是,目前的 Ninja build 计划生成了大约 800 MiB 的 Ninja build 计划,解析过程需要长达 10 秒的时间,这让增量构建体验变得令人厌烦。Fuchsia 构建团队认为这些改进意义重大,应集成到 build 使用的 Ninja 版本中。

编写代码时,编译器错误消息和警告是给开发者的第一行反馈。在静态语言中尤其如此,在 Rust 等较新的语言中更是如此,这些语言旨在在编译时检查各种条件。因此,获取反馈的时间对于开发者的工作效率至关重要。等待编译器输出所花费的每一秒钟时间,开发者需要花一秒钟时间来猜测其代码是否正确,或者是否有被其他内容分散注意力的风险,快速反馈对于仍在学习语言的程序员尤为重要。

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

对于 Fuchsia,Ninja 在每次调用开始时最多需要 10 秒来解析自己的 build 文件(build.ninja 文件及其包含的其他文件)。这是一个非常大的下限,反馈的提交速度非常快,而且速度至少是我们目前所能提供的反馈的 25 倍。通过将解析时间缩短 25-100 倍,再加上本季度推出的其他工具工作流,我们可以将 Fuchsia 编写代码的体验从“在黑板上慢慢地“蜗牛”转变为“简洁且响应迅速”。这将加快开发周期,提高工程师的满意度,并开发更高质量的软件。

下图显示了在 IDE 中更改代码和查看编译错误时的当前体验。

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

开发者反馈的延迟几乎完全源于 Ninja 花在解析其文件的时间。以下演示展示了经过修补的 Ninja 可能实现的 IDE 体验。

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

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

请注意,Android 团队在 2019 年提出了这一功能,但当时由于其针对 C++11 的要求而遭到上游维护者的拒绝,而这尚不被接受。时间已过,所以我们已强制执行动机部分中所述的保证,因此可以考虑进行这种转换。

经过改进的界面

受 Buck 构建系统的启发,提交了以下 PR,以便在交互式终端中实现表类状态输出(请参阅动画示例)。

PR 已被其作者放弃,但在本地构建 Fuchsia 时,可以进行重新设置以提供非常出色的界面改进。

利益相关方

教员

FEC 已指定 pascallouis@google.com 通过 RFC 流程照管此 RFC。

审核者

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

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

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

咨询人员

fangism@google.com、Build jayzhuang@google.com、Build tmandry@google.com、Rust on Fuchsia rudyMathu@google.com、Fuchsia EngProd

社交

该想法最初由 Rust 在 Fuchsia 团队中的 tmandry 提出,然后通过 Google 内部电子邮件会话进行社交化处理,现在将其发布到一个可公开访问的论坛中以供进一步讨论和批准。

设计

Fuchsia 控制的 Git 代码库将用于复刻上游 Ninja master 分支,以便管理包含 Fuchsia 专用自定义内容的分支,同时跟踪上游 origin/master。补丁将由分支的所有者进行审核,应根据具体情况,从以下方面考虑:

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

以及所有者看到的其他适用条件。

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

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

此 RFC 旨在确保此分支不会致使上游更改难以集成。为了实现这一点,系统将应用以下策略:

使 Fuchsia 分支靠近上游的策略

Fuchsia Ninja 分支应该严格执行,这意味着为了尽可能使其靠近上游,我们会定期创建“usptream-sync”分支,以在最近的上游版本之上以一系列干净的补丁的形式表示我们的更改。几个图形有助于理解这里的内容

创建自定义 Git Fork 时,开发者通常会在现有上游版本之上创建一个新分支,该分支会添加新的提交。以下图形说明了“上游”历史记录分支为“fuchsia”分支的情况,该分支在其之上添加 3 项提交:

upstream  ___U1__U2___
                      \
fuchsia                \__F1__F2__F3

提交由其维护者添加到上游项目。上游和紫红色的历史记录现在有所不同,如下所示:

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 分支的副本,并在 U5 的基础上对其进行 rebase 操作,从而解决这一过程中出现的任何冲突(可以先使用 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 方案会相应地切换其源 GIT 网址,因为它当前指向上游 GitHub 代码库的镜像。

性能

缩短 Fuchsia 平台构建时间是推出此 RFC 的主要原因,同时通过缩短工具响应时间改善开发者体验。

工效学设计

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

这种缩短的延迟时间还为 Fuchsia 的构建系统开辟了新的用例,例如在 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 的标准源代码工具套件,我们可以缓解可能发生的任何源代码供应链问题。

隐私注意事项

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

测试

和目前的情况一样,Ninja 测试套件将针对提交到 Fuchsia 的 Ninja 分支的任何更改运行,包括上游同步合并。变更必须遵循 Ninja 的标准测试做法,包括单元测试。

文档

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

其中包含指向此 RFC 的链接,用于说明此处所述的分支管理策略。

缺点、替代方案和未知情况

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

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

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

因此,从上游开始,并在其基础上重新构建一些 Android 专用功能,这似乎是更好的解决方案,能够在几周内提供结果。

此外,我们还进行了其他一些尝试,在不更改上游 Ninja 的情况下获得与某些上游补丁相同的优势,但这些都远远不足。最值得注意的是,得益于多年的调查GN 的一系列更改,我们得以缩短 Ninja 的空操作构建时间。但是,我们发现自己只取得了边际改进,这令我们感到失望。

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

最后,Fuchsia 现已拥有 Bazel SDK,并演示了适用于 Fuchsia 组件的 Bazel 构建(请参阅 RFC-0139)。从长远来看,Fuchsia 应考虑使用 Ninja 的替代方案作为平台构建后端,并探索潜在的多年迁移。在此 RFC 中,我们认为此类迁移不在涵盖范围内。