使用 Gradle-Profiler 对构建进行基准测试

799 阅读3分钟

安装

$ brew install gradle-profiler

基准测试构建

Gradle-profiler 支持两个用例:基准测试和分析。前者产生基本的汇总统计数据(例如平均值、中位数等),而后者产生带有火焰图或冰柱图的详细配置文件。使用前者在较高级别测试构建脚本更改的影响(我的构建变慢还是变快?多少?不确定性是什么?),而后者用于深入调查代码中的热点.

基础

创建基准测试:

$ gradle-profiler --benchmark --project-dir <root-dir-of-build> <task>

理想情况下,这应该从要进行基准测试的项目的父目录中调用,而且可以指定要进行基准测试的任务。虽然这很有用,但我经常发现我需要配置更多的东西。

场景

可以提供一个场景文件(以 Typesafe 配置格式编写)供工具使用,而不是在命令行上提供 。

对一个场景文件运行 gradle-profiler,如下所示:

$ gradle-profiler --benchmark --project-dir <root-dir-of-build> --scenario-file benchmark.scenarios [<scenarios>...]

其中 [...] 表示要运行的命名场景的可选规范。

让我们从一个简单的例子开始:

// benchmark.scenarios
configuration {
  tasks = ["help"]
}

这将创建一个名为“configuration”的场景,它只是调用 ./gradlew help。这是一个有用的场景,用于查看配置时间与任务执行的改进。

让我们看另一个简单的例子:

noop {
  tasks = [":app:assembleDebug"]
}

这个场景被命名为 noop,它运行 :app:assembleDebug 任务。在这种情况下没有文件更改,因此第一次之后的每个组合都应该是“无操作”。此场景测试任务是否配置良好:任务及其所有依赖项都应该是 UP-TO-DATE;这个构建应该非常快。

增加复杂性:测试构建缓存

构建缓存有什么好处吗?让我们来了解一下:

clean_build_with_cache {
  tasks = ["clean", ":app:assembleDebug"]
  gradle-args = ["--build-cache"]
}
clean_build_without_cache {
  tasks = ["clean", ":app:assembleDebug"]
  gradle-args = ["--no-build-cache"]
}

这是我们在场景文件中有多个场景的第一种情况。当我们有了这个,gradle-profiler 将依次运行每个场景,在所有完成后生成一个总结报告。这两个场景与 noop 场景非常相似,没有源代码更改,但文件系统确实发生了变化:我们删除每个构建的构建目录。在第一种情况下,我们使用构建缓存,而在第二种情况下我们不使用。如果构建缓存实现了它的承诺,那么第一种情况应该会更快。

增加复杂性:测试增量更改

对于那些必须实际更改代码的场景(呃),我们可以迭代我们的基准测试技术。

incremental_app {
  tasks = [":app:assembleDebug"]
  apply-abi-change-to = "app/src/main/java/com/my/project/manager/AccountManager.java"
}
resource_change {
  tasks = [":app:assembleDebug"]
  apply-android-resource-change-to = "app/src/main/res/values/strings.xml"
}

Gradle-profiler 将 apply-abi-change-to 和 apply-android-resource-change-to 称为突变。这两个场景分别对更改 Java 文件和更改资源文件时的构建时间进行基准测试。

这向我展示了项目中最频繁更改的文件:

$ git log --pretty=format: --name-only | sort | uniq -c | sort -rg | head -10

示例用例

我最近能够从我的项目中删除 Jetifier(感谢 John Rodriguez 将 Picasso 更新为不再依赖于支持库!)。我想知道这会对构建时间产生什么影响。所以,我做了以下事情:

# gradle.properties
android.enableJetifier=true
$ gradle-profiler --benchmark --project-dir my-project/ --scenario-file benchmark.scenarios

完成后,我更新了我的 gradle.properties

android.enableJetifier=false

该项目很小,大约有 35k LOC。有七个 Gradle 模块,Android 应用程序、Android 库、Java 库和 Kotlin 库的异构组合;但是,大部分代码都在原始应用程序模块中。我在具有 16 GB 内存的 2020 Macbook Pro 上运行它。在场景运行时,我打开了 Firefox、Slack 和 Android Studio。最后,需要明确的是,这个项目不需要 Jetifier。所以下面的结果只是为了启用它而不是禁用它。

ScenarioJetifiedNon-JetifiedDifference
configuration
...mean1401.51188.6-212.9
...median14081128.0-280
...stddev164.94175.77
noop
...mean2772.32743.60-28.7
...median27332705.50-27.5
...stddev110.69171.99
clean_build_with_cache
...mean40811.234614.70-6196.5
...median4068634004.0-6682.0
...stddev2274.381764.33
clean_build_without_cache
...mean85286.2073584.30-11701.9
...median86250.0073684.0-12566
...stddev7664.003266.77
incremental_app
...mean32120.229754.1-2366.1
...median3316027812.5-5347.5
...stddev4194.693698.54
resource_change
...mean4853.84103.00-750.8
...median4822.04084.50-737.5
...stddev321.5795.75

工作原理

为什么要使用 gradle-profiler,以及如何使用它。现在我们将讨论它的作用,这使得它比仅使用 bash time 命令运行临时构建更可靠。

当谈到 Gradle 时,我遇到的最常见的混淆来源之一是守护进程。从文档:

守护进程是一个长期存在的进程,因此我们不仅能够避免每次构建的 JVM 启动成本,而且能够在内存中缓存有关项目结构、文件、任务等的信息。

Gradle-profiler 通过确保每个场景运行相同的次数,使用相同的“warn”守护进程来帮助提供标准化结果。它通过运行几个 warn-up 构建(默认为六个),然后是一些“measured builds”(默认为十个)来做到这一点。然后,它提供measured builds的摘要。在每个场景之间,它会杀死守护进程,以便每个场景都从一个干净的状态开始。

下面是运行 clean_build_without_cache 场景的控制台输出示例:

* Running scenario `clean_build_without_cache` using Gradle 6.5.1

* Stopping daemons

* Running warm-up build #1
Execution time 128227 ms

* Running warm-up build #2
Execution time 87976 ms

* Running warm-up build #3
Execution time 81087 ms

* Running warm-up build #4
Execution time 85749 ms

* Running warm-up build #5
Execution time 77478 ms

* Running warm-up build #6
Execution time 76109 ms

* Running measured build #1
Execution time 77715 ms

* Running measured build #2
Execution time 74513 ms

* Running measured build #3
Execution time 81199 ms

* Running measured build #4
Execution time 86559 ms

* Running measured build #5
Execution time 78003 ms

* Running measured build #6
Execution time 90885 ms

* Running measured build #7
Execution time 89237 ms

* Running measured build #8
Execution time 85941 ms

* Running measured build #9
Execution time 88481 ms

* Running measured build #10
Execution time 100329 ms

* Stopping daemons

可以看到,第一个构建,使用所谓的“cold”守护进程,耗时超过 128 秒!第二次构建只用了 88 秒,提升了 31%。