特定于 Fuchsia 的 Ninja 改进

Fuchsia 构建系统使用自定义 Ninja 二进制文件,该文件对开发者体验进行了多项改进。本页介绍了这些改进。

动机

RFC-0153 详细介绍了为 Fuchsia 自定义 Ninja 的动机。 RFC-0153

简而言之,有许多功能对 Fuchsia 开发者来说非常有用,但在上游版本中很难获得。

所有 Fuchsia 特定的更改都在本地 fuchsia-rfc-0153 branch of our local Ninja git mirror 上执行,并 定期进行变基,以便轻松将其作为 GitHub 拉取请求 发送到上游项目,如 Strategy section of the RFC 中所述。

功能:正在运行的命令的状态

在您的环境中将 NINJA_STATUS_MAX_COMMANDS 设置为严格的正整数,以便 Ninja 在智能终端中运行时,在状态行下方打印最长运行命令的表格及其经过的时间。 例如,使用 export NINJA_STATUS_MAX_COMMANDS=4 时,状态可能如下所示:

[0/28477](260) STAMP host_x64/obj/tools/configc/configc_sdk_meta_generated_file.stamp
  0.4s | STAMP obj/sdk/zircon_sysroot_meta_verify.stamp
  0.4s | CXX obj/BUILD_DIR/fidling/gen/sdk/fidl/fuchsia.me...chsia.media/cpp/fuchsia.media_cpp_common.common_types.cc.o
  0.4s | CXX obj/BUILD_DIR/fidling/gen/sdk/fidl/fuchsia.me...fuchsia.media/cpp/fuchsia.media_cpp.natural_messaging.cc.o
  0.4s | CXX obj/BUILD_DIR/fidling/gen/sdk/fidl/fuchsia.me...dia/cpp/fuchsia.media_cpp_natural_types.natural_types.cc.o

以下动画图片展示了此功能在实际应用中的效果:

Ninja 多行状态示例

请注意:

  • 在 Ninja 的试运行或详细调用(即使用 -n--verbose 标志)中,此功能会自动停用。

  • 当 Ninja 未在交互式 / 智能终端中运行时,此功能会自动停用。

  • 运行控制台命令时,此功能也会暂停(在运行 Bazel 操作时,您可以在上面的示例中看到这一点)。

  • 借助此功能,您可以轻松直观地了解构建中的瓶颈,即阻止其他命令并行运行的长时间运行命令。

默认情况下,命令表每秒更新 10 次,这对于了解哪些长时间运行的命令会阻碍构建非常有用。您可以通过在毫秒内将 NINJA_STATUS_REFRESH_MILLIS 设置为十进制值来更改刷新周期(请注意,任何低于 100 的值都将被忽略,因为经过的时间仅打印到一位小数)。

功能:GNU Make Jobserver 支持

借助 GNU Make Jobserver 协议,构建系统可以在任何时间点限制 并发作业(即线程或进程)的总数,即使存在递归构建工具调用也是如此。

它需要一个顶级服务器 来设置一个作业槽 池,该池由参与的客户端(例如编译器、链接器,甚至是构建工具)共享。

Fuchsia 特定的 Ninja 二进制文件既可以充当协议的客户端,也可以充当服务器。

您可以在启动 Ninja 时,通过 --jobserver 命令行标志启用服务器模式,或在环境中设置 NINJA_JOBSERVER=1

当 Ninja 启动时,系统会通过查看 MAKEFLAGS 环境变量的值自动启用客户端模式。当 Ninja 从充当服务器的不同构建调用时,此功能非常有用。

在 Fuchsia 构建中,在 args.gn 中设置 enable_jobserver = true 可让顶级 Ninja 调用以服务器模式启动。

例如,它针对核心 IDK 和核心 SDK 构建器配置进行了设置,从而节省了 6 到 12 分钟的构建时间。这是因为这些配置需要从顶级构建启动 24 个以上的 Ninja 子构建,这些子构建可以利用该协议更好地协调它们各自如何生成多个并行命令。

功能:以 Chrome Trace JSON 数组格式生成构建轨迹

您可以使用 --chrome_trace FILENAME 选项告知 Ninja 在构建完成后(即使构建失败)生成构建事件的轨迹。

建议在输出 FILENAME 中使用 .gz 后缀,以直接生成 gzip 压缩的轨迹文件,因为这些文件通常小 20 倍。

该文件遵循 Chrome Trace JSON 数组格式 ,可以直接加载到任何 基于 Chromium 的浏览器的 chrome://tracing 标签页中,更有趣的是,还可以加载到 https://ui.perfetto.dev 中,后者还支持读取压缩轨迹作为输入。

请注意,生成的轨迹文件还包含有助于直观呈现构建的关键路径的流事件。相应构建事件的 cat 字段的值中包含 critical_path

功能:用户发起的正常关停

当用户按 Ctrl-C 停止构建时,Ninja 会向其子进程发送 SIGINT,然后等待它们完成,这在某些罕见情况下可能需要很长时间。

如果用户在等待时再次按 Ctrl-C,Ninja 现在会打印一条消息,其中包含一个表格,显示仍在运行的命令,并且还会向这些命令发送 SIGTERM 信号,以正常请求它们停止。

如果这还不够,用户第三次按 Ctrl-C 将向所有子进程发送 SIGKILL,以强制终止它们,并将控制权交还给用户。

以下动画图片展示了此功能在实际应用中的效果:

Ninja 安全关停示例

功能:对失败的命令进行结构化日志记录

构建失败后,构建目录中的 .ninja_errors.json 文件将包含有关每个失败操作的详细信息,例如其命令、状态代码和缓冲输出(Bazel 构建操作除外)。

此处介绍了此文件的 JSON 架构 此处, 工具和开发者可以使用该架构报告和重现失败的操作 以进行调试。

功能:持久模式,可缩短启动时间

通过在环境中设置 NINJA_PERSISTENT_MODE=1,加快后续 Ninja 调用。借助此功能,Ninja 可以启动后台服务器进程来读取构建清单一次,然后在连续构建之间将构建图保留在内存中。

请注意:

  • 此功能应该是完全透明的,并且不应以其他方式影响 Ninja 的行为。如果您发现问题或差异,请通过 fuchsia-build-team@google.com 告知我们!

  • 系统会自动检测输入 .ninja 文件中的任何更改。在这种情况下,现有服务器将被关闭,并且系统会自动启动新服务器。更改 GN 构建文件或执行 jiri update 后,无需进行额外的用户互动。

  • 服务器进程在空闲 5 分钟后将正常关闭。 在您的环境中设置 NINJA_PERSISTENT_TIMEOUT_SECONDS=<count> 以更改此延迟。

  • 使用 fx build -t server status 检索当前构建目录的服务器状态。

  • 使用 fx build -t server stop 显式停止服务器的任何正在运行的实例。

  • 服务器将日志消息写入构建目录中的 .ninja_persistent_log 文件。不过,您可以在启动 服务器之前,通过在环境中设置 NINJA_PERSISTENT_LOG_FILE=<path>来更改其位置。

  • 对于 core.x64 build 配置,每个服务器进程目前占用大约 1 GiB 的 RAM。确切数字取决于 Ninja 图的大小,而 Ninja 图的大小取决于您的 args.gn 配置。

  • 每个 Ninja 构建目录最多只能由一个服务器进程提供服务。 但是,如果使用多个构建目录,则可以有多个进程。

  • Ninja 工具(例如 ninja -C <dir> -t commands <target>)尚未在 服务器上运行,因此仍将使用缓慢的启动。此问题将在未来得到修复,以加快查询速度。

已知 bug / 注意事项,我们将努力解决:

  • 目前,在同一目录中混合使用持久构建和非持久构建可能会使服务器感到困惑,因为系统无法正确检测对 Ninja 构建和依赖项日志的独立更改。此问题将得到修复。

    解决方法:在环境中取消设置 NINJA_PERSISTENT_MODE 之前,使用 -t server stop 停止服务器。

  • “快速启动”需要几秒钟的时间。目前,每个增量构建仍需要为构建图中的所有文件调用 stat(),这目前需要几秒钟的时间。此问题将在未来得到修复,方法是使用基于 inotify / kqueue 的主机操作系统文件系统监控功能,以便在仅修改少量文件时立即启动。

  • (目前)不适用于 Windows。这是因为 Win32 技术限制阻止将控制台句柄复制到其他进程。这主要是上游 Ninja 团队的问题,因为 Fuchsia 开发不是在 Windows 上进行的。

功能:Bazel 操作的延迟命令

借助此功能,Ninja 可以延迟(“延迟”)准备就绪的 Bazel 构建操作,以便将它们批量处理到单个 bazel build 调用中。系统会并行构建更多 Bazel 目标,从而减少构建期间的 Ninja/Bazel 转换次数。实际上,根据 build 配置,这可以为基础架构构建器节省多达 10 分钟的构建时间。

对操作安排的影响

假设有一个构建计划,其中混合了原生 GN 操作和 Bazel 操作:

   bazel1   gn1
     |       |
     +-------+
         |
      [mixed]   bazel2     gn2
         |         |        |
         +---------+--------+
                |
             [test1]

在此图中:

  • gn1gn2 是原生 Ninja / GN 操作(例如 C++ 编译)。

  • bazel1bazel2 是 Bazel 操作。

  • [mixed][test1] 是虚假目标。

如果没有延迟命令,Ninja 会在每个操作的依赖项满足后立即启动 Bazel 命令,假设 Ninja 仅使用两个并行作业槽 (-j2) 以简化操作,则如下所示:

Time -------------------------------------------------------------------->
Ninja: [gn1      ] [gn2      ]
Bazel: [bazel1        ]        [bazel2        ]
       (startup overhead)      (startup overhead)

由于每个 Bazel 操作都作为独立的子进程运行:

  • Ninja 启动 2 个单独的 bazel build 命令。

  • 每次调用都会支付 Bazel 启动和分析的固定费用。

  • bazel1bazel2 无法在 Bazel 中并行构建。

启用延迟命令后,Ninja 会优先处理非延迟操作(GN/C++ 任务),同时将准备就绪的 Bazel 操作收集到批次中。当没有其他非延迟工作可以取得进展时,Ninja 会在单个调用中构建所有累积的 Bazel 操作:

Time -------------------------------------------------------------------->
Ninja: [gn1    ]
       [gn2     ]
Bazel:           [bazel1 + bazel2]
  • Ninja 首先并行执行 gn1gn2。当 bazel1bazel2 准备就绪时,Ninja 会拦截它们并将其排队,而不是立即启动子进程。

  • 所有正在运行的 GN 任务完成后,Ninja 会调用 Bazel 一次,以一起构建 {bazel1, bazel2}。Bazel 现在可以使用其自己的内部依赖项图和工作器池同时构建这两个目标。

  • 最后,系统会启动 test1 的最后一个 GN 操作。

对正确性的影响

此功能仅更改 Ninja 安排构建操作的时间,而不更改 Ninja 构建的内容。所有输出都是相同的,并且任务类型之间的依赖关系仍然受到尊重。例如,如果 GN 目标依赖于 Bazel 构建的主机工具,则 Ninja 仅在主机工具可用后才构建该目标。

对开发者来说,唯一可见的区别是:

  • 在调用 Bazel 任务之前,系统会在完整构建中预先构建更多 GN 任务。

  • Bazel 的进度输出反映了系统会同时构建多个目标。