RFC-0153:Fuchsia 的 Ninja 自定义 | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | 提议使用自定义版本的 Ninja build 工具来构建 Fuchsia,以加快构建速度、改进状态报告和易用性。 |
问题 | |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2022-01-22 |
审核日期(年-月-日) | 2022-03-17 |
摘要
此 RFC 提议使用 Fuchsia 平台 build 使用的开源 Ninja 工具的临时自定义版本,以集成几个现有的第三方补丁,这些补丁可显著提升其性能和易用性,但由于动机部分中所述的各种原因,上游维护者尚无法接受。
具体而言,这样做有助于:
通过优化 Ninja 使用的调度算法,显著缩短总构建时间。
改进了状态报告和日志记录,并解决了 CI build 中经常触发的严重易用性问题。
在不更改其行为的情况下,提高 Ninja 的响应能力和整体性能。例如,将某些 Ninja 操作的速度提高了 22 倍。
上述改进的结果是,通过使 IDE 绑定能够在很短的时间内重新生成,并提供出色的交互式代码编辑体验,从而显著加快了构建系统与 Visual Studio Code 和 Vim 等 IDE 之间的集成速度。
自定义方式是通过在 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,其中包括为 LLVM 做出贡献的 Fuchsia 团队成员。
Ninja 目前已被全球数千名开发者使用,并作为 GitHub 上的开源项目进行维护。Ninja 的维护者会努力确保该项目始终坚持其最初的目标:小巧、简单、可靠且极其可移植。
由于多种原因,上游 Ninja 项目的进展仍然缓慢,大量有趣的拉取请求已经等待审核数月甚至数年。我们已私下与当前维护者联系,对方表示,很遗憾,这主要是因为没有时间对非琐碎的更改进行适当的审核和测试,这也是回归测试套件状态所致,该套件目前非常基本,在许多情况下都需要手动测试拉取请求。该维护人员还希望您能帮助在 GitHub CI 中扩展上述测试套件,以便简化和加快 Ninja 的维护和演变。例如,他不反对将代码库从 C++03 切换到 C++11,前提是可以保证代码在使用其默认工具链的旧版发行版(即 Centos 7、Debian 8、Ubuntu 18.04 和 OSX 10.12)上正确编译和运行,并且通过适当的持续集成验证来强制执行。
遗憾的是,存在大量类似于前面所述问题的阻塞问题,解决所有这些问题需要进行大量工作,而 Fuchsia build 团队目前还没有能力做到这一点。
此 RFC 提供了一项技术提案,以暂时解决此限制,但我们强烈希望能够尽快专门配备足够的人员,与上游维护者合作解决此问题。这些人员可以来自 Fuchsia 项目,也可以是与 Google 中对 Ninja 的未来演变和改进感兴趣的其他团队联合发起的。不过,资助此类工作不在本 RFC 的讨论范围之内。
与此同时,自定义的 Ninja 分支可用于精选影响力最大的拉取请求,从而为 Fuchsia 开发者带来最大利益。为了确保此分支尽可能接近上游,我们将强制执行严格的分支策略,以确保 Fuchsia 分支始终可表示为最新上游版本之上的一系列补丁。
请注意,这种情况与自定义 Android Ninja 分支截然不同,自创建以来,该分支已与上游分支发生明显偏差,并且缺少 Fuchsia build 所需的功能。
促使我们制定此 RFC 的最有趣的拉取请求如下:
使用更出色的调度算法更快地构建
三个处于打开状态的拉取请求有可能从重要方面缩短构建时间。它们都会修改 Ninja 根据不同条件选择要启动的新命令的方式:
PR 2019:使用 build 日志估算要启动的命令的持续时间,并为这些命令分配优先级。作者表示,这将大型构建项目的构建时间从 20 分钟缩短到了 15 分钟!
PR 1949:限制生成新命令,以免使负载限制饱和。这样可以避免在争夺相同 CPU 资源的进程过多时使构建机器过载。作者声称,在某些情况下,测试 build 的构建时间可从 22 分钟缩短至 15 分钟。
PR 1140:为 Ninja 添加了对 GNU Make 作业服务器的支持,这是一种让 Ninja 及其执行的命令协调 CPU 进程分配的方式。此版本来自 Ninja 的 Kitware 分支。请注意与上一个 PR 的相似之处。作者未发布任何时间信息,而且该方法不太实用,因为它需要调用的程序明确支持此功能。
修复了针对长命令的 Ninja 构建输出。
在构建期间,Ninja 只会为已完成的命令输出状态行。在实践中,如果运行会导致构建暂停的长时间运行命令,则:
如果命令超时(我们的 CI 机器人会在运行非常长的 Rust 链接命令时发生这种情况),输出或 Ninja 日志文件中绝对不会显示哪个目标/命令实际超时!
在终端上,单行状态似乎冻结,并且仍显示上次完成的目标的名称,这会造成困惑(许多开发者认为这是延迟的根本原因)。要想了解实际发生的情况,唯一的方法是在另一个终端中使用“ps”,但很少有人知道这一点。
第一个问题是 Fuchsia build 的问题,如果不修改 Ninja,就无法解决。第二个问题是用户多年来一直抱怨的易用性问题,以至于有多个拉取请求以不同的方式来解决此问题:
- 针对长时间运行的命令输出“仍在等待”消息 #678。
- 已提议/仍在运行 #629。
- 输出中断边缘的完成状态 #1026。
- 在命令启动时输出信息 #1158。
- 添加了
NINJA_STATUS_STARTED
和_FINISHED
#1191。 - 仅等待一个边缘时,输出该边缘的状态。
这是因为 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 中更改代码和查看编译错误的屏幕截图。
开发者反馈延迟几乎完全归因于 Ninja 花在解析文件上的时间。请参阅下面的演示,了解使用修补的 Ninja 可获得的 IDE 体验。
同样,fx setup-go
之所以被创建,就是为了在本地开发期间专门使用 Go 构建系统,正是因为使用 Ninja 进行增量构建速度太慢。
请注意,Android 团队在 2019 年提出了这个功能,但当时上游维护者拒绝了该功能,因为它需要使用 C++11,而当时还不符合要求。自那时起,时间已经过去,我们可以考虑进行这样的切换,前提是强制执行动机部分中所述的保证。
改进的界面
我们提交了以下 PR,以便在交互式终端中实现表格状状态输出,此做法受到了 Buck 构建系统的启发(请参阅动画示例)。
该 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(构建)。 haowei@google.com(Fuchsia 的 LLVM)。 jayzhuang@google.com(构建)。 olivernewman@google.com(Fuchsia CI)。 phosek@google.com(Fuchsia 工具链)
已咨询:
fangism@google.com、Buildjayzhuang@google.com、Buildtmandry@google.com、Rust on Fuchsiarudymathu@google.com、Fuchsia EngProd
社交:
该想法最初由 Fuchsia 团队的 Rust 开发者 tmandry 提出,然后在 Google 内部电子邮件会话中分享,现在在此公开论坛中展示,以供进一步讨论和审批。
设计
一个由 Fuchsia 控制的 Git 代码库将用于分叉上游 Ninja master
分支,以便管理包含 Fuchsia 专用自定义项的分支,同时跟踪上游 origin/master
。分支的 OWNERS 会审核补丁,并应根据以下各项酌情考虑:
- 提供的好处
- 补丁的可维护性
- 与上游代码分歧,这会影响可维护性。
以及所有者认为适当的其他标准。
根据标准做法,系统会在 FUCHSIA.readme
文件中跟踪本地修改列表。
该分支的维护者将是 Fuchsia Build 团队,该团队负责 Fuchsia build 系统及其正确性、可维护性和性能,并对此负责。
本 RFC 旨在确保此分支不会偏离到上游更改难以集成的程度。为此,我们将采用以下策略:
让 Fuchsia 分支与上游保持接近的策略
Fuchsia Ninja 分支应遵循严格的管理,这意味着,为了使其尽可能接近上游,我们会定期创建“上游同步”分支,以便在近期上游版本的基础上将我们的更改表示为一系列干净的补丁。下面的几个图表应该有助于您理解这里所说的意思。
创建自定义 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
此时,fuchsia
和 upstream-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 分支的维护者应尽可能创建上游同步分支。
实现
该分支将在 Git-on-Borg 实例 (https://fuchsia.googlesource.com/third_party/ninja,已存在) 上进行维护,其公开 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 的标准源代码工具套件,我们可以减少可能发生的任何源代码供应链问题。
隐私注意事项
此提案不应以任何方式影响隐私。
测试
目前,系统会针对提交到 Fuchsia 的 Ninja 分支的任何更改(包括上游同步合并)运行 Ninja 测试套件。更改必须遵循 Ninja 的标准测试做法,包括单元测试。
文档
系统会在 Fuchsia git 分支的顶部添加一个标准的 README.fuchsia
文件,说明分支的当前状态与上游之间的差异。
该文档将包含指向此 RFC 的链接,以说明此处所述的分支管理策略。
缺点、替代方案和未知情况
此方案的主要缺点是,需要通过尽可能频繁地执行上游同步分支,才能让分支与上游保持接近。每项此类操作都可能会触发一个或多个重基冲突,需要 Fuchsia 分支的维护者手动解决。
请注意,若要显著简化这些问题的修复,一个好方法是将 Fuchsia 分支中的更改分解为尽可能多的小补丁(每个补丁都会生成一个完全可测试的源代码树)。
我们曾考虑过尝试使用 Android Ninja 分支作为起点,但其差异很大。仅将近期的上游更改合并到其中就需要解决大量的冲突(例如,他们将所有内容都切换到了 C++17,并完全移除了 C++03 支持代码),并且将其重新定位到上游的一组干净补丁会非常困难。
因此,从上游开始,并在此基础上重新定位一些特定于 Android 的功能,似乎是一个更好的解决方案,可以在几周内取得成效。
此外,我们还尝试了几种其他方法,希望在不更改上游 Ninja 的情况下获得与某些上游补丁相同的好处,但都远远不够理想。最值得注意的是,得益于多年的调查和对 GN 的一系列更改,我们能够缩短 Ninja 无操作 build 时间。不过,我们很失望地发现,我们只取得了微不足道的改进。
请注意,Ninja 目前由 Google 的多个不同团队使用,有些人表示希望以协调的方式管理公司范围内的 Ninja 演变,或者想找到帮助上游维护者的途径。此 RFC 不会阻止任何这些情况,但其首要任务是先为 Fuchsia 平台快速带来好处。
最后,Fuchsia 现在有了一个 Bazel SDK,并演示了适用于 Fuchsia 组件的 Bazel build(请参阅 RFC-0139)。从长远来看,Fuchsia 应考虑将 Ninja 作为平台构建后端的替代方案,并探索可能的多年迁移。在本 RFC 中,我们将此类迁移视为不在讨论范围之内。