乐趣区

关于android:测试应用启动性能

用于测试启动的 Shell 命令

本文的编写目标,更多的在于介绍性能、启动测试以及我进行启动测试背地的起因。但如果您只是心愿可能疾速取得论断,能够间接参考上面的内容:

  1. 尽可能锁定 CPU 主频 (请参阅下文);
  2. 在命令行运行如下命令 (保障您的设施处于连贯状态)。
$ 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

下面的命令会循环 100 次: 启动利用、输入启动过程耗时,而后终止过程以筹备好下一次循环。

想把启动性能测试 “ 测 ” 好并非易事

我最近须要测试一款利用的启动性能 (同时摆弄了一下 Startup 库来理解它是如何影响启动性能的,将来的文章中会有更多相干内容)。我发现,就像我 以往做这类事件时一样,启动性能并不容易明确地被测试进去。

如果您正在测试一段运行时代码,那么有许多解决方案供您抉择。从 “ 编写严密的循环并应用 System.currentTimeMillis() 计算工夫增量 ” 这种琐碎的办法,到更简单和有用的解决方案,如应用 AndroidX benchmark 库所提供的性能。

然而依照定义,利用启动时的许多操作运行在零碎调用您的代码之前。那么您要如何确定整个启动过程所须要的工夫呢?

我浏览了一些日志信息、查看了一些底层 API,并询问了一些平台团队的工程师,终于取得了一些有用的信息。更棒的是,我当初能够应用 adb shell 工具齐全自动化我的测试并输入信息,从而能够轻松地将后果导入到电子表格中进行剖析。

我会在上面的文字中解释上述命令所应用的一些代码片段,并向您展现一到两个启动测试的简略步骤。

ActivityTaskManager 启动日志

正如我在早些工夫的一篇 博客 (可怜的是该博客曾经过期而且并不正确) 中所写的那样,在 KitKat 公布后,有一个非常不便的日志始终在记录零碎信息。无论何时,当一个 Activity 启动时,您都能看到日志中工具输入了以下信息:

ActivityTaskManager: Displayed com.android.samples.mytest/.MainActivity: +1s380ms

这个持续时间 (本例中为 1,380ms) 示意了从启动利用到零碎认为其 “ 已启动 ” 所破费的工夫,其中包含绘制第一帧 (所以是 “ 已显示 ” 的状态)。

达到 “ 已显示 ” (Displayed) 状态的过程并不需要蕴含您利用就绪之前所做的事件的破费工夫。只有您的利用确定已实现加载和初始化,就能够通过调用 Activity.reportFullyDrawn()) 向零碎提供这些额定的信息。当您调用了该可选办法时,零碎会记录另一个带有工夫戳和持续时间的日志:

2020-11-18 15:44:02.171 1279-1336/system_process 
I/ActivityTaskManager: Fully drawn 
com.android.samples.mytest/.MainActivity: +2s384ms

我只想要到 “ 已显示 ” 时所继续的工夫,所以内建的日志对我来讲曾经足够好了。

自动化启动

性能测试总是该当屡次去运行测试用例,以排除后果中的可变因素。进行的运行次数越多,均匀后果就越牢靠。我至多会尝试运行测试十次,然而做的次数更多成果会更好。依据后果的变动水平以及工夫的长短 (因为变量的存在会对持续时间更短的测试产生更大的影响),可能须要运行更屡次才行。

疯狂就是反复做雷同的事件,却期待不同的后果。

——阿尔伯特 爱因斯坦

性能测试推论:

“ 疯了 ” 就是同一件事只做一次,却心愿失去最佳后果。

——不是爱因斯坦说的

通过点击图标来间断屡次启动利用是一件十分繁琐的事件。而且这种操作不具备一致性,且有许多难以预测的因素,因为很容易就会引入变量——如您偶然间谬误地启动了另一个利用,或者使零碎做了额定的工作而无奈取得计时后果。

因而,我真正想要的是某种从命令行启动利用的形式。有了它,我就能够重复运行该命令来执行雷同的操作,从而防止手动启动利用带来的可变性 (和乏味)。

adb (Android 调试桥,浏览至此的读者应该都对它很相熟了吧) 提供了我所须要的货色。更具体地说,adb shell 提供了用于启动利用的命令行界面: adb shell am start-activity。该命令还可能在利用启动实现之前放弃阻塞状态,因而咱们还要应用 -W 参数 (这对下一步来说是必须的。咱们下一步将应用后续命令杀死启动后的利用)。这是残缺的启动命令:

$ 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

检查一下 TotalTime 后果: 后果与咱们在日志中看到的信息完全相同:

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 能够解决这一问题。

有几个不同的 shell 命令可用于终止利用。最不言而喻的是 adb shell am kill…… 但事实上这条命令并不能解决问题。当您启动利用后,利用会处在前台,而 kill 不会终止处在前台的利用。作为代替,您须要应用 force-quit 命令:

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 架构,尤其是 CPU 频率,是影响挪动设施性能的重要因素。具体而言,挪动设施缩小电量耗费及避免出现过热的问题的次要办法之一,便是限度 CPU 速度。

限度 CPU 对于节俭电量很有用,但却对性能测试有负面影响,因为在这类测试中,后果的一致性至关重要。

现实状况下,在运行性能测试时,您应该管制 CPU 频率。然而您是否可能执行这一操作取决于您所领有的设施——您须要领有设施的 root 拜访权限能力管制 CPU 调速器,从而能力管制 CPU 频率,并且不同的设施执行这一行为的形式也可能不同。

接下来的内容仅实用于您的设施容许且您能够获得 root 拜访权限的状况。而在设施方面,我晓得 Pixel 设施能够取得拜访权限,但这不代表其余设施也同样能够。

在任何状况下,如果能够的话,建议您锁定 CPU 主频。对于您特定的测试而言,可能不会有显著的影响 (实际上,零碎通常会在启动利用时使 CPU 运行在较高的频率上,因而可能曾经提供了所需的一致性)。然而,这么做至多能够打消 CPU 主频这一可变因素。

手动锁定 CPU 频率可能很辣手,但侥幸的是,AndroidX benchmark 帮您简化了这一操作。实际上,您甚至不须要为 benchmark API 编写代码——您能够通过应用其提供的 lockClocksunlockClocks 工具来应用该库。

首先,向工程级别的 build.gradle 文件中退出 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"

接下来,在利用级别的 build.gradle 文件中利用 benchmark 插件:

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 上失常工作。更好的音讯是,我的启动测试当初破费的工夫比以前要长得多。您兴许会好奇,为什么主频变慢了?

该 benchmark 工具将主频锁定在便于继续运行的级别,而不是高性能级别。如果将主频设置为尽可能高,则可能会取得更好的性能,然而:

  • 为了让测试后果足够真切,您甚至可能会冀望更差的性能,就像许多用户在事实中所遇到的状况一样。您不会想要只看到最佳状况下的性能,因为那并不是人们通常会在事实中遇到的;
  • CPU 在高频率下运行太长时间会导致过热。我不晓得零碎在过热时将如何响应 (心愿它会升高主频或在呈现重大问题之前主动关闭系统),然而我也不想晓得答案。

请留神,实现测试后,您须要将主频解锁。设施会在重新启动时进行解锁,然而您也能够通过运行相同的 gradle 工作来解锁主频:

$ ./gradlew unlockClocks

其实这一命令只是重新启动设施以执行重置操作。(如果您想理解 benchmark 锁定性能的更多信息,请查阅 用户指南)。

这样就实现了!

锁定时钟后,我筹备好了所有: 可能牢靠重现启动情况的零碎、一个执行后能够返回后果流的简略命令行。我能够复制后果并粘贴到电子表格中并进行剖析 (通过将启动工夫平均值与我想尝试的各种状况进行比拟)。

现实状况下,我不须要撰写文章来阐明如何实现所有这些操作。诚实说,您并不需要上文中的全副阐明。(然而晓得事件的工作原理和起因总是更乏味,不是吗?) 您真正须要的只是 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

为了简化性能测试和剖析,以及总体上进步应用程序性能,咱们的团队正在钻研简化此过程的办法,请继续关注咱们以取得后续分享的内容。同时,心愿以上命令和信息对您的启动性能测试有所帮忙。

退出移动版