Bazel 非常复杂,在构建过程中会执行许多不同的操作,其中一些操作可能会影响构建性能。本页面尝试将其中一些 Bazel 概念与它们对 build 性能的影响相关联。虽然我们并未提供详尽的说明,但我们提供了一些示例,说明如何通过提取指标来检测 build 性能问题,以及如何解决这些问题。希望您在调查 build 性能回归问题时能够应用这些概念。
整洁构建与增量构建
干净的 build 会从头开始构建所有内容,而增量 build 会重用一些已完成的工作。
我们建议您分别查看干净 build 和增量 build,尤其是在收集 / 汇总依赖于 Bazel 缓存状态的指标(例如build 请求大小指标)时。它们还代表着两种不同的用户体验。与从头开始执行干净 build(由于缓存冷启动而需要更长时间)相比,增量 build 的执行频率要高得多,因为开发者会迭代代码(通常速度更快,因为缓存通常已经预热)。
您可以使用 BEP 中的 CumulativeMetrics.num_analyses 字段对 build 进行分类。如果为 num_analyses <= 1,则为干净 build;否则,我们可以大致将其归类为增量 build - 用户可能已切换到不同的标志或不同的目标,从而导致有效的干净 build。任何更严格的增量定义都可能需要采用启发式方法,例如查看加载的软件包数量 (PackageMetrics.packages_loaded)。
将确定性 build 指标作为 build 性能的代理
由于某些指标(例如 Bazel 的 CPU 时间或远程集群上的排队时间)具有不确定性,因此衡量 build 性能可能很困难。因此,使用确定性指标作为 Bazel 完成的工作量的代理变量可能很有用,而 Bazel 完成的工作量会影响其性能。
构建请求的大小会对构建性能产生重大影响。较大的 build 可能意味着在分析和构建 build 图方面需要更多工作。随着开发工作的推进,构建的自然增长是不可避免的,因为会添加/创建更多依赖项,从而导致复杂性增加,构建成本也随之提高。
我们可以将此问题分解为各个 build 阶段,并使用以下指标作为每个阶段完成工作的代理指标:
- PackageMetrics.packages_loaded:成功加载的软件包数量。 此处的回归表示在加载阶段读取和解析每个额外的 BUILD 文件时需要完成更多工作。
- TargetMetrics.targets_configured:表示 build 中配置的目标和方面数量。回归表示在构建和遍历配置的目标图方面需要做更多工作。- 这通常是因为添加了依赖项,并且必须构建其传递闭包的图。
- 使用 cquery 查找可能添加了新依赖项的位置。
 
- ActionSummary.actions_created:表示在 build 中创建的操作,而回归表示在构建操作图时需要做更多工作。请注意,这还包括可能未执行的未使用操作。- 使用 aquery 调试回归问题;我们建议先使用 --output=summary,然后再使用--skyframe_state进一步深入分析。
 
- 使用 aquery 调试回归问题;我们建议先使用 
- ActionSummary.actions_executed:执行的操作数,回归直接表示执行这些操作时的工作量增加。- BEP 会写出操作统计信息 ActionData,其中显示了执行次数最多的操作类型。默认情况下,它会收集前 20 种操作类型的数据,但您可以传入--experimental_record_metrics_for_all_mnemonics,以收集所有已执行操作类型的数据。
- 这应该有助于您了解(额外)执行了哪些类型的操作。
 
- BEP 会写出操作统计信息 
- BuildGraphSummary.outputArtifactCount:执行的操作创建的制品数量。- 如果执行的操作数量没有增加,则可能是规则实现发生了变化。
 
这些指标均会受到本地缓存状态的影响,因此您需要确保从中提取这些指标的 build 是干净的 build。
我们注意到,任何这些指标的回归都可能伴随着实际运行时间、CPU 时间和内存使用量的回归。
本地资源的使用情况
Bazel 会占用本地机器上的各种资源(包括用于分析 build 图和驱动执行的资源、用于运行本地操作的资源),这可能会影响机器在执行 build 和其他任务时的性能 / 可用性。
所用时间
可能最容易受到噪声影响(并且在不同 build 之间差异很大)的指标是时间;特别是实际运行时间、CPU 时间和系统时间。您可以使用 bazel-bench 来获取这些指标的基准,并且通过足够数量的 --runs,您可以提高测量的统计显著性。
- 实际时间是指实际经过的时间。 - 如果只有实际时间出现回归,我们建议收集 JSON 轨迹配置文件并查找差异。否则,调查其他出现回归的指标可能会更有效,因为这些指标可能影响了实际时间。
 
- CPU 时间是 CPU 执行用户代码所花费的时间。 - 如果 CPU 时间在两个项目提交之间出现回归,我们建议收集 Starlark CPU 性能剖析文件。您可能还应使用 --nobuild将 build 限制为分析阶段,因为大部分 CPU 密集型工作都是在该阶段完成的。
 
- 如果 CPU 时间在两个项目提交之间出现回归,我们建议收集 Starlark CPU 性能剖析文件。您可能还应使用 
- 系统时间是指 CPU 在内核中花费的时间。 - 如果系统时间出现倒退,则大多与 Bazel 从文件系统读取文件时的 I/O 相关。
 
系统级负载分析
使用 Bazel 6.0 中引入的 --experimental_collect_load_average_in_profiler 标志,JSON 跟踪分析器会在调用期间收集系统平均负载。

图 1. 包含系统平均负载的配置文件。
如果 Bazel 调用期间负载过高,可能表明 Bazel 为您的机器并行安排了过多的本地操作。您可能需要考虑调整 --local_cpu_resources 和 --local_ram_resources,尤其是在容器环境中(至少在 #16512 合并之前)。
监控 Bazel 内存用量
获取 Bazel 内存用量主要有两种来源:Bazel info 和 BEP。
- bazel info used-heap-size-after-gc:调用- System.gc()后的已用内存量(以字节为单位)。- Bazel bench 也可为此指标提供基准。
- 此外,还有 peak-heap-size、max-heap-size、used-heap-size和committed-heap-size(请参阅文档),但相关性较低。
 
- BEP 的 - MemoryMetrics.peak_post_gc_heap_size:垃圾回收后 JVM 堆大小的峰值(以字节为单位)(需要设置- --memory_profile以尝试强制执行完整垃圾回收)。
内存使用量出现回归通常是由于build 请求大小指标出现回归所致,而这往往是因为添加了依赖项或规则实现发生了变化。
如需更精细地分析 Bazel 的内存占用情况,我们建议您使用规则的内置内存性能分析器。
持久工作器的内存分析
虽然持久工作器可以显著加快 build 速度(尤其是对于解释型语言),但其内存占用量可能会带来问题。Bazel 会收集有关其工作器的指标,尤其是 WorkerMetrics.WorkerStats.worker_memory_in_kb 字段,该字段会显示工作器使用的内存量(按助记符)。
JSON 跟踪分析器还会在调用期间通过传入 --experimental_collect_system_network_usage 标志(Bazel 6.0 中的新标志)来收集持久性工作器内存使用情况。

图 2. 包含工作器内存用量的配置文件。
降低 --worker_max_instances 的值(默认值为 4)可能有助于减少持久性工作器使用的内存量。我们正在积极努力,让 Bazel 的资源管理器和调度器变得更智能,以便将来减少此类精细调整的需求。
监控远程 build 的网络流量
在远程执行中,Bazel 会下载因执行操作而构建的制品。因此,网络带宽会影响 build 的性能。
如果您在构建中使用远程执行,不妨考虑使用 BEP 中的 NetworkMetrics.SystemNetworkStats proto 在调用期间监控网络流量(需要传递 --experimental_collect_system_network_usage)。
此外,借助 JSON 跟踪配置文件,您可以通过传递 --experimental_collect_system_network_usage 标志(Bazel 6.0 中的新标志)来查看整个 build 过程中的系统级网络使用情况。

图 3. 包含系统级网络使用情况的配置文件。
如果使用远程执行时网络使用率较高但相对平稳,可能表明网络是构建过程中的瓶颈;如果您尚未启用“不传输字节的构建”,请考虑通过传递 --remote_download_minimal 来启用该功能。这样可以避免下载不必要的中间制品,从而加快 build 速度。
另一种方法是配置本地磁盘缓存,以节省下载带宽。