測試應用啓動性能

用於測試啓動的 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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章