细分构建性能

报告问题 查看源代码

Bazel 非常复杂,并且会在构建过程中执行许多不同的操作,其中一些可能会对 build 性能产生影响。本页面会尝试将部分 Bazel 概念映射到它们对构建性能的影响。我们提供了一些示例来说明如何通过提取指标检测构建性能问题,以及您可以采取哪些措施来修复构建性能问题,但并不详尽。我们希望,您在调查构建性能降低问题时可以应用这些概念。

干净构建与增量构建

干净构建是指从头开始构建所有内容,而增量构建会重复使用一些已完成的工作。

我们建议分别查看干净构建和增量构建,尤其是在收集 / 汇总取决于 Bazel 缓存状态的指标时(例如构建请求大小指标)。这些指标也代表两种不同的用户体验。与从头开始启动整洁构建(由于冷缓存而需要更长的时间)相比,随着开发者迭代代码(增量通常更快,因为缓存通常已经很热),增量构建的频率会高得多。

您可以使用 BEP 中的 CumulativeMetrics.num_analyses 字段对 build 进行分类。如果为 num_analyses <= 1,则表示它是干净 build;否则,我们可以将其大致归类为增量 build - 用户可能已切换到不同的标志或不同的目标,导致有效地进行 build。任何更严格的增量定义可能必须采用启发法,例如查看加载的软件包数量 (PackageMetrics.packages_loaded)。

确定性构建指标,以提升构建性能

由于某些指标(例如 Bazel 的 CPU 时间或远程集群上的队列时间)具有不确定性,因此衡量构建性能可能很困难。因此,使用确定性指标作为 Bazel 完成的工作量的代理会很有帮助,后者会影响其性能。

构建请求的大小可能会对构建性能产生重大影响。大型 build 可能意味着分析和构建 build 图方面的工作量更大。随着 build 的自然增多,自然而然地会伴随着开发,因为添加/创建的依赖项会更多,因此复杂性也会增加,构建成本也会更高。

我们可以将此问题划分到各个构建阶段,并使用以下指标作为每个阶段完成的指标:

  1. PackageMetrics.packages_loaded:已成功加载的软件包数量。回归表示在加载阶段读取和解析每个额外的 BUILD 文件需要执行更多工作。

    • 这通常是由于添加了依赖项且必须加载其传递闭包。
    • 使用 query / cquery 查找添加了新依赖项的位置。
  2. TargetMetrics.targets_configured:表示 build 中配置的目标和宽高比的数量。回归表示在构建和遍历已配置的目标图方面所需的工作量更大。

    • 这通常是由于添加了依赖项且必须构造其传递闭包的图。
    • 使用 cquery 查找可能添加了新依赖项的位置。
  3. ActionSummary.actions_created:表示在 build 中创建的操作,回归表示在构建操作图方面执行的工作量更大。请注意,这还包括可能未执行的未使用操作。

  4. ActionSummary.actions_executed:执行的操作数,回归直接表示执行这些操作更多。

    • BEP 会写出操作统计信息 ActionData,其中显示执行最多的操作类型。默认情况下,它会收集前 20 种操作类型,但您可以传入 --experimental_record_metrics_for_all_mnemonics 来收集已执行的所有操作类型的此类数据。
    • 此外,这应该可以帮助您了解执行了哪些操作。
  5. BuildGraphSummary.outputArtifactCount:已执行的操作创建的工件的数量。

    • 如果执行的操作数量没有增加,则可能是规则实现发生了变化。

这些指标都受本地缓存状态的影响,因此您需要确保从中提取指标的 build 是干净 build

我们已经注意到,其中任何一个指标回归都可能伴随着实际用时、CPU 时间和内存用量方面的回归。

使用本地资源

Bazel 会消耗本地机器上的各种资源(包括用于分析构建图和驱动执行情况以及运行本地操作),这可能会影响机器在执行构建时的性能 / 可用性以及其他任务。

所用时间

或许,最容易受噪声影响的指标(可能因构建而异)是时间;尤其是实际用时、CPU 时间和系统时间。您可以使用 bazel-bench 来获取这些指标的基准,只要有足够的 --runs,您就可以提高衡量的统计显著性。

  • 实际用时是指实际经过的时间。

    • 如果仅实际用时回归,我们建议您收集 JSON 跟踪记录配置文件并查找差异。否则,调查其他回归指标可能更高效,因为它们可能会影响实际用时。
  • CPU 时间是指 CPU 执行用户代码所花费的时间。

    • 如果两个项目提交之间的 CPU 时间回归,建议您收集 Starlark CPU 配置文件。您还应该使用 --nobuild 将构建限定到分析阶段,因为这是 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 infoBEP

  • bazel info used-heap-size-after-gc:调用 System.gc() 后已使用的内存量(以字节为单位)。

    • Bazel bench 也为此指标提供基准。
    • 此外,还有 peak-heap-sizemax-heap-sizeused-heap-sizecommitted-heap-size(请参阅文档),但相关性较低。
  • BEPMemoryMetrics.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 会下载因执行操作而构建的工件。因此,您的网络带宽可能会影响构建的性能。

如果您对构建使用远程执行,则可能需要考虑在调用期间使用 BEP 中的 NetworkMetrics.SystemNetworkStats proto 监控网络流量(需要通过 --experimental_collect_system_network_usage)。

此外,JSON 跟踪记录配置文件允许您通过传递 --experimental_collect_system_network_usage 标志(Bazel 6.0 中的新功能)查看整个构建过程中的系统级网络使用情况。

包含系统级网络使用情况的配置文件

图 3. 包含系统级网络使用情况的配置文件。

使用远程执行时网络使用量较高但比较平坦可能表明网络是构建中的瓶颈;如果您尚未使用,请考虑通过传递 --remote_download_minimal 开启没有字节的构建。这样可以避免下载不必要的中间工件,从而加快构建速度。

另一种方法是配置本地磁盘缓存以节省下载带宽。