测试应用启动性能
用于测试启动的 Shell 命令
本文的编写目的,更多的在于介绍性能、启动测试以及我进行启动测试背后的原因。但如果您只是希望能够快速获得结论,可以直接参考下面的内容:
尽可能锁定 CPU 主频 (请参阅下文);
在命令行运行如下命令 (保证您的设备处于连接状态)。
$ for i in `seq 1 100`
> do
> adb shell am force-stop com.android.samples.mytest
> sleep 1
> adb shell am start-activity -W -n com.android.samples.mytest/.MainActivity | grep "TotalTime" | cut -d ' ' -f 2
> done
想把启动性能测试 "测" 好并非易事
我最近需要测试一款应用的启动性能 (同时摆弄了一下 Startup 库来了解它是如何影响启动性能的,未来的文章中会有更多相关内容)。我发现,就像我以往做这类事情时一样,启动性能并不容易明确地被测试出来。
与以往做这类事情时一样
http://graphics-geek.blogspot.com/
AndroidX benchmark
https://developer.android.google.cn/jetpack/androidx/releases/benchmark
ActivityTaskManager
启动日志
正如我在早些时间的一篇博客 (不幸的是该博客已经过时而且并不正确) 中所写的那样,在 KitKat 发布后,有一个十分方便的日志一直在记录系统信息。无论何时,当一个 Activity 启动时,您都能看到日志中工具输出了以下信息:
博客 http://graphics-geek.blogspot.com/
ActivityTaskManager: Displayed com.android.samples.mytest/.MainActivity: +1s380ms
Activity.reportFullyDrawn()
https://developer.android.google.cn/reference/android/app/Activity#reportFullyDrawn()
2020-11-18 15:44:02.171 1279-1336/system_process
I/ActivityTaskManager: Fully drawn
com.android.samples.mytest/.MainActivity: +2s384ms
自动化启动
性能测试总是应当多次去运行测试用例,以排除结果中的可变因素。进行的运行次数越多,平均结果就越可靠。我至少会尝试运行测试十次,但是做的次数更多效果会更好。根据结果的变化程度以及时间的长短 (因为变量的存在会对持续时间更短的测试产生更大的影响),可能需要运行更多次才行。
疯狂就是重复做相同的事情,却期待不同的结果。 ——阿尔伯特 爱因斯坦
性能测试推论:
"疯了" 就是同一件事只做一次,却希望得到最佳结果。
——不是爱因斯坦说的
通过点击图标来连续多次启动应用是一件非常繁琐的事情。而且这种操作不具备一致性,且有许多难以预测的因素,因为很容易就会引入变量——如您偶然间错误地启动了另一个应用,或者使系统做了额外的工作而无法获得计时结果。
因此,我真正想要的是某种从命令行启动应用的方式。有了它,我就可以反复运行该命令来执行相同的操作,从而避免手动启动应用带来的可变性 (和乏味)。
adb
https://developer.android.google.cn/studio/command-line/adb
$ adb shell am start-activity -W -n
com.android.samples.mytest/.MainActivity
最后一个参数是应用的包名与组件信息。您可以看到它们与上一部分中 ActivityTaskManager 输出的日志相同。
运行此命令将启动应用 (除非该应用已经在前台,但这种情况并不是理想的状态,我们将在下一步对这种情况进行处理),并输出以下信息:
Starting: Intent { cmp=com.android.samples.mytest/.MainActivity }
Status: ok
LaunchState: COLD
Activity: com.android.samples.mytest/.MainActivity
TotalTime: 1380
WaitTime: 1381
Complete
ActivityTaskManager: Displayed
com.android.samples.mytest/.MainActivity: +1s380ms
这意味着我们无需翻看 logcat,而是可以直接从运行命令的控制台中便可获取这些信息。更棒的是,我们可以剥离多余的文本并仅保留启动结果,从而更轻松地提取此数据以供其他地方使用。
为了将上面的输出转换为启动持续时间,我使用 grep 和 cut shell 命令来输出内容 (有多种方法可以执行此操作,我只是随机选择了其中一个):
adb shell am start-activity -W -n
com.android.samples.mytest/.MainActivity | grep "TotalTime" | cut -d ' ' -f 2
$ [start-activity command as above...]
1380
冷启动是性能测试的最佳起点
一致性: 冷启动可以确保您的应用每次启动时都经历相同的操作。应用被热启动时,我们没法明确知道哪些步骤被跳过,而哪些步骤被执行,因而也无从得知您到底在对什么进行计时 (也无法保证重复测试时所测试的内容是否一致); 最坏情况: 按照定义,冷启动是最坏的情况——这是您的用户经历启动过程时间最长的场景。您需要专注于最坏情况的统计数据,而不是状况最好的热启动。如果您忽略最坏情况,许多重大问题将无法被解决。
adb shell am force-stop com.android.samples.mytest
我喜欢循环,让我们来循环它
现在,您已经有了可以启动应用、输出启动持续时间数据,以及退出应用并使其可以再次启动的一系列命令。您可以一遍又一遍地在控制台中输入这些内容,但是在 shell 中,我们可以将这些命令放在循环里,然后只用一个命令就可以重复运行它。
在执行此操作时,为了避免应用被终止而产生副作用 (例如,当应用程序被终止时,系统会将启动器拉到前台),您可能会想要在终止应用后延缓下一次的启动。为此,我增加了一秒钟的 sleep 以在两次操作之间插入一个小的缓冲时间。
下面是我所使用的命令的最终版本,其中包括了终止应用、等待一秒钟,然后重启应用。我将这一过程循环执行了 100 次,从而可以提供一个合理的样本量:
$ for i in `seq 1 100`
> do
> adb shell am force-stop com.android.samples.mytest
> sleep 1
> adb shell am start-activity -W -n com.android.samples.mytest/.MainActivity | grep "TotalTime" | cut -d ' ' -f 2
> done
在运行此命令时,每当启动完成,我都可以获得输出到控制台的启动持续时间,而这正是我要跟踪和分析的数据。
注意: 以上操作其实有更简单的方式,您可以使用 -S (用于首先停止 Activity) 和 -R COUNT (用于执行 start-activity 命令 COUNT 次) 来循环启动 Activity,所以我也可以用下面的命令完成以上操作:
$ adb shell am start-activity -S -W -R 100-n
com.android.samples.mytest/.MainActivity | grep "TotalTime" | cut -d ' ' -f 2
但是,为了在应用的终止和启动之间加入缓冲时间,以确保其处于非活动的状态,我希望能使用 sleep 1 命令,因此我采用了更为冗长的方式进行循环。此外,shell 脚本的代码非常优雅,不是吗?
尽可能地锁住主频
手动锁定 CPU 频率可能很棘手,但幸运的是,AndroidX benchmark 帮您简化了这一操作。实际上,您甚至不需要为 benchmark API 编写代码——您可以通过使用其提供的 lockClocks 与 unlockClocks 工具来使用该库。
AndroidX benchmark
https://developer.android.google.cn/jetpack/androidx/releases/benchmark
// 查看 Benchmark 库的最新版本号
// https://developer.android.google.cn/jetpack/androidx/releases/benchmark
def benchmark_version = "1.0.0"
classpath "androidx.benchmark:benchmark-gradle-plugin:$benchmark_version"
apply plugin: androidx.benchmark
现在,您可以同步您的工程 (Android Studio 可能已经在强迫您执行此操作),同步完成后便可以从 gradlew 中使用锁定任务。
现在,您可以通过在命令行上运行命令来锁定主频了 (我是通过 Android Studio 内部的 "终端" 工具运行它的,但是您也可以在 IDE 外部运行它):
$ ./gradlew lockClocks
Locked CPUs 4,5,6,7 to 1267200 / 2457600 KHz
Disabled CPUs 0,1,2,3
这段输出表明 benchmark 可以在我的 Pixel 2 上正常工作。更好的消息是,我的启动测试现在花费的时间比以前要长得多。您也许会好奇,为什么主频变慢了?
为了让测试结果足够逼真,您甚至可能会期望更差的性能,就像许多用户在现实中所遇到的情况一样。您不会想要只看到最佳情况下的性能,因为那并不是人们通常会在现实中遇到的; CPU 在高频率下运行太长时间会导致过热。我不知道系统在过热时将如何响应 (希望它会降低主频或在出现严重问题之前自动关闭系统),但是我也不想知道答案。
请注意,完成测试后,您需要将主频解锁。设备会在重新启动时进行解锁,但是您也可以通过运行相反的 gradle 任务来解锁主频:
$ ./gradlew unlockClocks
其实这一命令只是重新启动设备以执行重置操作。(如果您想了解 benchmark 锁定功能的更多信息,请查阅用户指南)。
用户指南
https://developer.android.google.cn/studio/profile/benchmark#clock-stability
这样就完成了!
锁定时钟后,我准备好了一切: 能够可靠重现启动状况的系统、一个执行后可以返回结果流的简单命令行。我可以复制结果并粘贴到电子表格中并进行分析 (通过将启动时间平均值与我想尝试的各种情况进行比较)。
理想情况下,我不需要撰写文章来说明如何完成所有这些操作。老实说,您并不需要上文中的全部说明。(但是知道事情的工作原理和原因总是更有趣,不是吗?) 您真正需要的只是 for() 循环 shell 命令,以及可选的锁定主频的方法。
$ for i in `seq 1 100`
> do
> adb shell am force-stop com.android.samples.mytest
> sleep 1
> adb shell am start-activity -W -n com.android.samples.mytest/.MainActivity | grep "TotalTime" | cut -d ' ' -f 2
> done
推荐阅读