Bazel 很复杂,会在构建过程中执行许多不同的操作,其中一些操作可能会影响构建性能。本页尝试将其中一些 Bazel 概念对应到它们对构建性能的影响。我们提供了一些示例,说明如何通过提取指标来检测 build 性能问题,以及如何修复这些问题。因此,我们希望您在调查 build 性能下降问题时能够运用这些概念。
干净 build 与增量 build
整洁 build 是从零开始构建所有内容,而增量构建会重复使用一些已完成的工作。
我们建议您分别查看整洁构建和增量构建,尤其是在收集 / 汇总依赖于 Bazel 缓存状态的指标时(例如构建请求大小指标)。这两种构建也代表两种不同的用户体验。与从头开始启动干净构建(由于冷缓存而需要更长时间)相比,随着开发者对代码进行迭代,增量构建的频率要高得多(因为缓存通常已经很热,所以增量构建的速度通常会更快)。
您可以使用 BEP 中的 CumulativeMetrics.num_analyses
字段对 build 进行分类。如果为 num_analyses <= 1
,则这是一个干净 build;否则,我们可以大致将其归类为增量 build - 用户可能已切换到不同的标志或不同的目标,从而产生有效干净的 build。任何更严格的增量定义可能都必须采用启发法的形式,例如查看加载的软件包数量 (PackageMetrics.packages_loaded
)。
确定性 build 指标,作为 build 性能的代理
由于某些指标(例如 Bazel 的 CPU 时间或远程集群上的排队时间)具有不确定性,因此衡量构建性能可能很困难。因此,使用确定性指标作为 Bazel 完成的工作量的代理很有用,而 Bazel 完成的工作量又会影响其性能。
构建请求的大小会对构建性能产生重大影响。build 越大,可能表示分析和构建 build 图的工作量越大。build 的自然增长与开发一起自然增长,因为系统会添加/创建更多的依赖项,从而导致复杂性增加,构建成本也会提高。
我们可以将此问题划分为不同的构建阶段,并使用以下指标作为每个阶段所完成工作的代理指标:
PackageMetrics.packages_loaded
:成功加载的软件包的数量。这里的回归表示,为了在加载阶段读取和解析每个额外的 BUILD 文件,需要完成更多工作。TargetMetrics.targets_configured
:表示 build 中配置的目标和切面的数量。回归表示构造和遍历配置的目标图所需的工作更多。- 这通常是由于需要添加依赖项并且必须构造其传递闭包的图。
- 使用 cquery 查找可能添加了新依赖项的位置。
ActionSummary.actions_created
:表示在构建中创建的操作,回归表示构建操作图方面的更多工作。请注意,这还包括可能未执行的未使用的操作。- 使用 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 会使用本地机器上的各种资源(用于分析构建图和驱动执行,以及运行本地操作),这可能会影响机器在执行构建以及其他任务时的性能 / 可用性。
所用时间
也许最容易受到噪声影响的指标(可能因构建而异)是时间;尤其是实际用时、CPU 时间和系统时间。您可以使用 bazel-bench 获取这些指标的基准,如果 --runs
的数量足够多,您就可以提高测量结果的统计显著性。
实际用时是指实际经过的时间。
- 如果仅实际存在时间回归,我们建议收集 JSON 轨迹配置文件并查找差异。否则,调查其他回归指标可能更高效,因为它们可能会影响实际用时。
CPU 时间是 CPU 执行用户代码所花的时间。
- 如果两个项目提交中的 CPU 时间回归,我们建议收集 Starlark CPU 配置文件。您还应使用
--nobuild
将构建限制为仅进入分析阶段,因为这是大多数 CPU 密集型工作的完成阶段。
- 如果两个项目提交中的 CPU 时间回归,我们建议收集 Starlark CPU 配置文件。您还应使用
系统时间是内核中 CPU 花费的时间。
- 如果系统时间退化,则当 Bazel 从您的文件系统读取文件时,出现此问题主要与 I/O 相关。
系统级负载分析
JSON 轨迹性能分析器通过使用 Bazel 6.0 中引入的 --experimental_collect_load_average_in_profiler
标志来收集调用期间的系统平均负载。
图 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 基准测试也为此指标提供了基准。
- 此外,还有
peak-heap-size
、max-heap-size
、used-heap-size
和committed-heap-size
(请参阅文档),但它们的相关性较低。
BEP 的
MemoryMetrics.peak_post_gc_heap_size
:GC 之后的 JVM 堆大小上限(以字节为单位),需要设置尝试强制执行完整 GC 的--memory_profile
。
内存用量下降通常是由于构建请求大小指标回归所致,而这通常是由于添加依赖项或规则实现更改所致。
如需更精细地分析 Bazel 的内存占用量,我们建议您使用内置内存分析器规则。
对持久性工作器进行内存分析
虽然持久性工作器有助于显著加快构建速度(尤其是对于解释型语言),但其内存占用量可能存在问题。Bazel 会收集关于其工作器的指标,特别是,WorkerMetrics.WorkerStats.worker_memory_in_kb
字段会(通过助记符)告知工作器使用的内存量。
JSON 轨迹性能分析器还会通过传入 --experimental_collect_system_network_usage
标志(Bazel 6.0 中的新功能)来收集调用期间的持久性工作器内存用量。
图 2. 包含工作器内存用量的配置文件。
降低 --worker_max_instances
的值(默认值为 4)可能有助于减少持久性工作器使用的内存量。我们正积极努力提升 Bazel 的资源管理器和调度器的智能化程度,以便将来需要的微调机会更少。
监控远程构建的网络流量
在远程执行中,Bazel 会下载因执行操作而构建的工件。因此,网络带宽可能会影响 build 的性能。
如果您要对构建使用远程执行,则可能需要考虑使用 BEP 中的 NetworkMetrics.SystemNetworkStats
proto 在调用期间监控网络流量(需要传递 --experimental_collect_system_network_usage
)。
此外,借助 JSON 轨迹配置文件,您还可以通过传递 --experimental_collect_system_network_usage
标志(Bazel 6.0 中的新功能)在构建过程中查看系统级网络使用情况。
图 3. 包含系统级网络使用情况的配置文件。
使用远程执行时,网络使用量较高但相当平坦可能表示网络是构建中的瓶颈;如果您尚未使用它,请考虑传递 --remote_download_minimal
以开启不包含字节的 Build。这样可以避免下载不必要的中间工件,从而加快构建速度。
另一种方法是配置本地磁盘缓存以节省下载带宽。