測試應用啓動性能

用於測試啓動的 Shell 命令

本文的編寫目的,更多的在於介紹性能、啓動測試以及我進行啓動測試背後的緣由。但若是您只是但願可以快速得到結論,能夠直接參考下面的內容:android

  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 次: 啓動應用、輸出啓動過程耗時,而後終止進程以準備好下一次循環。shell

想把啓動性能測試 "測" 好並不是易事

我最近須要測試一款應用的啓動性能 (同時擺弄了一下 Startup 庫來了解它是如何影響啓動性能的,將來的文章中會有更多相關內容)。我發現,就像我 以往作這類事情時同樣,啓動性能並不容易明確地被測試出來。架構

若是您正在測試一段運行時代碼,那麼有許多解決方案供您選擇。從 "編寫緊密的循環並使用 System.currentTimeMillis() 計算時間增量" 這種瑣碎的方法,到更復雜和有用的解決方案,如使用 AndroidX benchmark 庫所提供的功能。app

可是按照定義,應用啓動時的許多操做運行在系統調用您的代碼以前。那麼您要如何肯定整個啓動過程所須要的時間呢?工具

我瀏覽了一些日誌信息、檢查了一些底層 API,並詢問了一些平臺團隊的工程師,終於得到了一些有用的信息。更棒的是,我如今可使用 adb shell 工具徹底自動化個人測試並輸出信息,從而能夠輕鬆地將結果導入到電子表格中進行分析。性能

我會在下面的文字中解釋上述命令所使用的一些代碼片斷,並向您展現一到兩個啓動測試的簡單步驟。測試

ActivityTaskManager 啓動日誌

正如我在早些時間的一篇 博客 (不幸的是該博客已通過時並且並不正確) 中所寫的那樣,在 KitKat 發佈後,有一個十分方便的日誌一直在記錄系統信息。不管什麼時候,當一個 Activity 啓動時,您都能看到日誌中工具輸出瞭如下信息:gradle

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

這個持續時間 (本例中爲 1,380ms) 表示了從啓動應用到系統認爲其 "已啓動" 所花費的時間,其中包括繪製第一幀 (因此是 "已顯示" 的狀態)。ui

到達 "已顯示" (Displayed) 狀態的過程並不須要包含您應用就緒以前所作的事情的花費時間。只要您的應用肯定已完成加載和初始化,就能夠經過調用 Activity.reportFullyDrawn()) 向系統提供這些額外的信息。當您調用了該可選方法時,系統會記錄另外一個帶有時間戳和持續時間的日誌:google

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

爲了簡化性能測試和分析,以及整體上提升應用程序性能,咱們的團隊正在研究簡化此過程的方法,請持續關注咱們以得到後續分享的內容。同時,但願以上命令和信息對您的啓動性能測試有所幫助。

相關文章
相關標籤/搜索