细分构建性能

报告问题 查看源代码

Bazel 很复杂,会在构建过程中执行很多不同的操作,其中一些操作可能会影响构建性能。本页尝试将其中一些 Bazel 概念与其对构建性能的影响对应起来。我们提供了一些示例来说明如何通过提取指标来检测 build 性能问题,以及您可以采取哪些措施来解决这些问题。因此,我们希望您可以在调查 build 性能衰退时运用这些概念。

干净 build 与增量 build

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

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

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

确定性 build 指标(可作为 build 性能的代理)

由于某些指标的不确定性(例如 Bazel 的 CPU 时间或远程集群上的队列时间),衡量构建性能可能会很困难。因此,使用确定性指标来代表 Bazel 完成的工作量非常有用,而这些工作量又会影响其性能。

构建请求的大小会对构建性能产生重大影响。构建越大,分析和构建构建图的工作就越多。开发过程中自然会出现 build 的自然增长,因为系统会添加/创建更多依赖项,复杂性也随之增加,构建费用也会随之增加。

我们可以将此问题分解为各个构建阶段,并将以下指标用作每个阶段所完成工作的代理指标:

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

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

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

  4. ActionSummary.actions_executed:所执行操作的数量,回归直接表示执行这些操作需要完成更多工作。

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

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

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

我们已经注意到,上述任何指标的衰退都可能伴随着实际用时、CPU 时间和内存用量的衰退。

本地资源的使用

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

所用时间

最容易受噪声影响的指标(可能因 build 的不同而有很大差异)是时间;尤其是实际用时、CPU 时间和系统时间。您可以使用 bazel-bench 获取这些指标的基准,并且获得足够的 --runs 后,您可以提高测量结果的统计显著性。

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

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

    • 如果两次项目提交期间的 CPU 时间回归,我们建议收集 Starlark CPU 配置文件。您可能还需要使用 --nobuild 将构建限制在分析阶段,因为大部分 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 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 会下载因执行操作而构建的工件。因此,网络带宽可能会影响 build 的性能。

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

此外,借助 JSON 轨迹配置文件,您还可以通过传递 --experimental_collect_system_network_usage 标志(Bazel 6.0 中的新功能)查看整个构建过程中的系统级网络使用情况。

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

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

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

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