安装
$ 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。所以下面的结果只是为了启用它而不是禁用它。
| Scenario | Jetified | Non-Jetified | Difference |
|---|---|---|---|
| configuration | |||
| ...mean | 1401.5 | 1188.6 | -212.9 |
| ...median | 1408 | 1128.0 | -280 |
| ...stddev | 164.94 | 175.77 | |
| noop | |||
| ...mean | 2772.3 | 2743.60 | -28.7 |
| ...median | 2733 | 2705.50 | -27.5 |
| ...stddev | 110.69 | 171.99 | |
| clean_build_with_cache | |||
| ...mean | 40811.2 | 34614.70 | -6196.5 |
| ...median | 40686 | 34004.0 | -6682.0 |
| ...stddev | 2274.38 | 1764.33 | |
| clean_build_without_cache | |||
| ...mean | 85286.20 | 73584.30 | -11701.9 |
| ...median | 86250.00 | 73684.0 | -12566 |
| ...stddev | 7664.00 | 3266.77 | |
| incremental_app | |||
| ...mean | 32120.2 | 29754.1 | -2366.1 |
| ...median | 33160 | 27812.5 | -5347.5 |
| ...stddev | 4194.69 | 3698.54 | |
| resource_change | |||
| ...mean | 4853.8 | 4103.00 | -750.8 |
| ...median | 4822.0 | 4084.50 | -737.5 |
| ...stddev | 321.57 | 95.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%。