使用 Macrobenchmark 檢查應用程式效能

1. 事前準備

在本程式碼研究室中,您將學習如何使用 macrobenchmark 程式庫。您需要測量應用程式的啟動時間 (使用者參與度的重要指標) 和影格時間 (推測應用程式可能出現卡頓的位置)。

軟硬體需求

執行步驟

  • 在現有的應用程式中加入基準測試模組
  • 測量應用程式啟動和影格時間

課程內容

  • 穩定評估應用程式效能

2. 開始設定

若要開始,請用以下指令從指令列複製 GitHub 存放區:

$ git clone https://github.com/android/codelab-android-performance.git

或者,您也可以下載兩個 ZIP 檔案:

在 Android Studio 中開啟專案

  1. 在「Welcome to Android Studio」視窗中,選取「c01826594f360d94.png Open an Existing Project」。
  2. 選取資料夾 [Download Location]/android-performance/benchmarking (提示:請確定您選取的是內含 build.gradle 的 benchmarking 目錄)
  3. Android Studio 匯入專案後,請確保您可以執行 app 模組,從而建構要進行基準測試的範例應用程式。

3. Jetpack Macrobenchmark 簡介

Jetpack Macrobenchmark 程式庫可以測量大型使用者互動情形的效能,如啟動、UI 互動情形、動畫等。這個程式庫可讓您直接控制想測試的效能環境。透過控制應用程式的編譯、啟動及停止,您可以直接測量應用程式啟動、影格時間,以及所追蹤的程式碼區段。

您可以使用 Jetpack Macrobenchmark 達成以下目的:

  • 使用多種決定性模式和捲動速度多次測量應用程式
  • 藉由多次測試得到的平均結果瞭解平均的效能變化情形
  • 控制應用程式的編譯狀態,這對效能穩定性有很大的影響
  • 在本機重現 Google Play 商店執行的安裝時間最佳化,藉此檢查實際使用時的效能

使用這個程式庫的檢測作業不會直接呼叫應用程式程式碼,而是會像使用者一樣瀏覽應用程式 (輕觸、點擊、滑動等)。測量操作會在這些互動其間,於裝置端進行。如果想直接測量應用程式程式碼內容,請參閱 Jetpack Microbenchmark。

撰寫基準測試和撰寫檢測設備測試的方式很像,不過您不需要確認應用程式的狀態。基準測試使用 JUnit 語法 (@RunWith、@Rule、@Test 等),但在獨立的程序中執行測試,以允許對您的應用程式進行重新啟動或預先編譯。這樣即可像使用者一樣執行您的應用程式,而不干擾其內部狀態。我們會使用 UiAutomator 和目標應用程式互動,以便達到此目的。

範例應用程式

這個程式碼研究室將使用 JetSnack 範例應用程式。這是一款使用 Jetpack Compose 撰寫的虛擬點心訂購應用程式。想要測量應用程式的效能不需要知道應用程式的詳細構成方式。您需要知道的是應用程式有什麼行為和 UI 的架構,以便從基準測試存取 UI 元素。請執行應用程式,並訂購任何點心,藉此熟悉基本畫面內容。

a1f684feb2456079.png

4. 新增 Macrobenchmark 程式庫

Macrobenchmark 必須在專案中新增 Gradle 模組。最方便的新增方式就是使用 Android Studio 模組精靈。

開啟新模組對話方塊 (例如在「Project」面板的專案或模組上按一下滑鼠右鍵,然後點選「New」>「Module」)。

4ce8ef7fa59b41b8.png

點選「Templates」窗格內的「Benchmark」,並確定基準測試的模組類型選擇「Macrobenchmark」,並檢查詳細資料和您預期的內容相同:

24884734761bcfed.png

  • 「Target application」 – 要進行基準測試的應用程式
  • 「Module name」 – 基準測試 Gradle 模組名稱
  • 「Package name」 – 基準測試的套件名稱
  • 「Minimum SDK」 – 須使用 Android 6 (API 級別 23) 或更新版本

按一下「Finish」。

模組精靈變更的內容

模組精靈會變更專案的部分內容。

精靈會新增一個名為 macrobenchmark 的 Gradle 模組 (或是使用您在精靈內指定的名稱)。這個模組會使用 com.android.test 外掛程式,使 Gradle 不會把它納入您的應用程式內,所以裡面只會有測試程式碼 (或基準測試)。

精靈也會變更您指定的目標應用程式模組。說得更仔細點,精靈會在 :app 模組 build.gradle.kts 中加入新的 benchmark 建構類型,如以下程式碼片段所示:

create("benchmark") {
   initWith(buildTypes.getByName("release"))
   matchingFallbacks += listOf("release")
   isDebuggable = false
}

這個 buildType 應該可以盡可能模擬您的 release buildType。與 release buildType 的不同之處在於,需要將 signingConfig 設定為 debug,以便在在本機建構應用程式,而無需使用正式版 KeyStore。

不過,因為已經停用了 debuggable 旗標,所以精靈會在 AndroidManifest.xml 裡新增 <profileable> 標記,以便基準測試使用版本效能為應用程式建立設定檔。

<application>

  <profileable
     android:shell="true"
     tools:targetApi="29" />

</application>

如果想進一步瞭解 <profileable> 的功能,請參閱我們的說明文件。

精靈最後會建立鷹架,藉此進行啟動時間的基準測試 (我們會在下一個步驟進行此部分)。

您現在可以開始撰寫基準測試了。

5. 測量應用程式啟動情況

應用程式啟動時間,或使用者開始使用應用程式之前所需花費的時間,是影響使用者參與度的重要指標。模組精靈會建立 ExampleStartupBenchmark 測試類別,可以藉此測量應用程式的啟動時間,如下所示:

@RunWith(AndroidJUnit4::class)
class ExampleStartupBenchmark {
   @get:Rule
   val benchmarkRule = MacrobenchmarkRule()

   @Test
   fun startup() = benchmarkRule.measureRepeated(
       packageName = "com.example.macrobenchmark_codelab",
       metrics = listOf(StartupTimingMetric()),
       iterations = 5,
       startupMode = StartupMode.COLD,
   ){
        pressHome()
        startActivityAndWait()
   }
}

這些參數有什麼意義?

撰寫基準測試時,進入點是 MacrobenchmarkRule 的 measureRepeated 函式。這個函式可以處理基準測試的所有內容,但是您需要指定以下參數:

  • packageName - 基準測試獨立於要測試的應用程式執行,因此您需要指定待測量的應用程式。
  • metrics – 您想藉由基準測試測量的資訊類型。在這個例子裡,我們想調查的是應用程式啟動時間。其他指標類型請參閱說明文件。
  • iterations – 重複執行基準測試的次數。疊代越多,結果就越穩定,但是執行時間也會隨之拉長。理想的次數必須視這個指標對您的應用程式有多重要而定。
  • startupMode – 讓您可以定義在基準測試開始時應用程式的啟動方式。提供 COLD、WARM 和 HOT 等選項。我們在此使用 COLD,這可以代表應用程式必須處理的最高工作數量。
  • measureBlock (最後的 lambda 參數) – 您需要用這個函式定義基準測試要測量的操作 (開始 Activity、點按 UI 元素、捲動、滑動等等),macrobenchmark 會在這個區塊收集定義的 metrics。

如何撰寫基準測試操作方式

Macrobenchmark 會重新安裝並重新啟動應用程式,請確定您撰寫的反應和應用程式狀態彼此獨立。Macrobenchmark 可以提供幾種能夠和應用程式互動的實用函式及參數。

其中最重要的就是 startActivityAndWait()。這個函式會啟動預設的 Activity,並等到第一個影格轉譯完畢,然後才會繼續處理基準測試的指示。如果您想啟動其他 Activity 或調整開始意圖,可以選擇使用 intent 或 block 參數。

pressHome() 也是非常實用的函式。您可以藉由這個函式用基礎條件重新啟動基準測試,以免您並未在每次疊代中終止應用程式 (例如使用 StartupMode.HOT 的情況)。

其他的互動可以使用 device 參數,即可找尋 UI 元素、捲動、等待其他內容等等。

好了,既然啟動基準測試已經定義完畢,您在下一個步驟就能實際執行了。

6. 執行基準測試

在執行基準測試之前,請確定 Android Studio 內選擇的是正確的建構變數:

  1. 選取「Build Variants」面板
  2. 將「Active Build Variant」變更為「benchmark」
  3. 等候 Android Studio 進行同步處理

4f47527b618433a.gif

如果不這樣設定,基準測試會在執行階段失敗並顯示錯誤,說明您不應該對 debuggable 應用程式進行基準測試:

java.lang.AssertionError: ERRORS (not suppressed): DEBUGGABLE
WARNINGS (suppressed):

ERROR: Debuggable Benchmark
Benchmark is running with debuggable=true, which drastically reduces
runtime performance in order to support debugging features. Run
benchmarks with debuggable=false. Debuggable affects execution speed
in ways that mean benchmark improvements might not carry over to a
real user's experience (or even regress release performance).

您可以使用檢測引數 androidx.benchmark.suppressErrors = "DEBUGGABLE" 暫時抑制這個錯誤。您可以按照「在 Android Emulator 內執行基準測試」步驟的說明操作。

現在可以執行基準測試了,就跟檢測設備測試的執行方式相同。您可以執行測試函式,或用旁邊的溝槽圖示執行整個類別。

29c3b85f79816f17.png

請確定您已選擇實體裝置,因為在 Android 模擬器執行基準測試會在執行階段失敗並顯示警告,說明測試會得到錯誤結果。雖然嚴格來說依然可以在模擬器執行測試,但是這樣就等於在測量主體機器的效能,一旦主體機器負載較重,基準測試的效能就會比較慢,反之亦然。

335e1009223de4b3.png

執行基準測試後,系統會重構應用程式並執行您的基準測試。根據您定義的 iterations 不同,基準測試會多次啟動、停止,甚至也會重新安裝應用程式。

7. (非必要) 在 Android Emulator 內執行基準測試

如果您沒有實體裝置,但是依然需要執行基準測試,可以用檢測引數 androidx.benchmark.suppressErrors = "EMULATOR" 隱藏執行階段錯誤

如果想隱藏錯誤,請編輯執行設定:

  1. 點選執行選單內的「Edit Configurations...」:5ed4cbeb68ec4c97.png
  2. 在開啟的視窗中點選「Instrumentation arguments」89be2d51bf94c098.png 旁邊的「options」圖示 d628c071dd2bf454.png
  3. 按一下 ➕ 並輸入詳細資料,藉此新增檢測額外參數 a06c7f6359d6b92c.png
  4. 按一下「OK」,確認所選內容。您應該可以在「Instrumentation arguments」該列看到引數 f0a8a7f54d47e5dc.png
  5. 按一下「OK」確定執行設定。

如果您需要永久在程式碼集裡保留這個項目,可以使用 :macrobenchmark 模組的 build.gradle:

defaultConfig {
    // ...
    testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = 'EMULATOR'
}

8. 判讀啟動結果

基準測試執行完畢後便會直接在 Android Studio 顯示結果,如以下螢幕截圖所示:

a9bddcc8dced7879.png

如同示例顯示,在 Google Pixel 7a 上的啟動時間最小值為 312.5 毫秒,中位數是 337.6 毫秒,最大值是 390.1 毫秒。請注意,在您的裝置上執行同樣的基準測試可能會得到不同的結果這個結果可能受到多種要素影響,例如:

  • 裝置效能強大程度
  • 裝置使用的系統版本
  • 背景執行哪些應用程式

因此重點是要比較同一個裝置上的結果,最好還是相同的狀態,否則之間差異會非常大。如果您無法完全保持相同的狀態,建議您增加 iterations 的數字大小,以便正確處理結果的離群值。

Macrobenchmark 程式庫會記下執行基準測試時的系統追蹤記錄,以作調查之用。為了方便,Android Studio 會將每次疊代和測量時間標記為系統追蹤記錄的連結,讓您可以輕鬆開啟並調查內容。

9. (非必要練習) 宣告應用程式可供使用的時機

Macrobenchmark 可以自動測量應用程式轉譯第一個影格之前所需的時間 (timeToInitialDisplay)。但是,應用程式很常有內容在轉譯第一個影格之後還沒載入完成的情況,而您可能也會想得知使用者在可以使用應用程式之前需要等待多久時間。這就是所謂的「完整顯示所需時間」,也就是應用程式完整載入內容,而使用者可以與其互動的時間。Macrobenchmark 程式庫可以自動偵測這個時間,不過您需要調整應用程式,以便使用 Activity.reportFullyDrawn() 函式。

本範例會在資料載入之前顯示一個簡易的進度列,讓您等到資料準備完畢,可以呈現及繪製點心清單為止。讓我們調整範例應用程式,並加入 reportFullyDrawn() 呼叫。

從「Project」窗格開啟 .ui.home 套件裡的 Feed.kt 檔案。

fe64e7f9cb75beb4.png

在這個檔案裡找到負責撰寫點心清單的 SnackCollectionList 可組合項。

您需要確定資料已經準備完畢。您知道,在內容準備就緒之前,​​snackCollections 參數都會提供一個空白清單,所以您可以使用 ReportDrawnWhen 可組合函式,以便在述詞為 true 後處理報告。

ReportDrawnWhen { snackCollections.isNotEmpty() }

Box(modifier) {
   LazyColumn {
   // ...
}

或者,您也可以使用 ReportDrawnAfter{} 可組合項接受 suspend 函式,並等候此函式完成。這樣一來,您可以等候系統以非同步方式載入部分資料,或完成某些動畫。

設定完畢後,您需要調整 ExampleStartupBenchmark 使其等到內容出現,否則基準測試會在第一個影格轉譯完畢前結束,並可能會略過指標。

現在的啟動基準測試只會等到第一個影格轉譯完畢為止。這段等待操作位於 startActivityAndWait() 函式內。

@Test
fun startup() = benchmarkRule.measureRepeated(
   packageName = "com.example.macrobenchmark_codelab",
   metrics = listOf(StartupTimingMetric()),
   iterations = 5,
   startupMode = StartupMode.COLD,
) {
   pressHome()
   startActivityAndWait()

   // TODO wait until content is ready
}

在我們的例子裡,您可以等到內容清單出現子項為止,所以可以按照以下程式碼片段加入 wait():

@Test
fun startup() = benchmarkRule.measureRepeated(
   //...
) {
   pressHome()
   startActivityAndWait()

   val contentList = device.findObject(By.res("snack_list"))
   val searchCondition = Until.hasObject(By.res("snack_collection"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(searchCondition, 5_000)
}

說明這個程式碼片段的內容:

  1. 我們藉由 Modifier.testTag("snack_list") 找到點心清單
  2. 定義了搜尋條件,使用 snack_collection 當做我們要等待的元素
  3. 我們使用 UiObject2.wait 函式在 UI 專案裡等待條件,並設定 5 秒的逾時時間

您現在可以再度執行基準測試,程式庫會自動測量 timeToInitialDisplay 和 timeToFullDisplay,如以下螢幕截圖所示:

cbbff5648512369.png

您可以看到 TTID 和 TTFD 之間的差距為 389.8 毫秒。也就是說,雖然使用者可以在 392.9 毫秒後看到第一個影格轉譯完畢,但必須再等 389.8 毫秒才能捲動清單。

10. 基準影格時間

使用者進入應用程式之後,會注意到的第二項指標就是應用程式的順暢程度,如果用我們的術語來說,就是應用程式會不會有降低影格的情況。我們會使用 FrameTimingMetric 進行測量。

假設您想測量項目清單的捲動行為,而不想測量在這之前發生的任何情況。您需要把基準測試拆成測量完畢和尚未測量過的互動。我們將使用 setupBlock lambda 參數達到這個效果。

在尚未測量的互動 (在 setupBlock 內定義) 中,我們會啟動預設的 Activity,而在測量完畢的互動 (在 measureBlock 內定義) 中,我們會找到 UI 清單元素並捲動清單,然後等到畫面轉譯內容完畢。如果您沒有把互動拆成兩個部分,就無法分辨啟動應用程式時和捲動清單時產生的兩種影格。

建立影格時間基準

為了達到以上流程,我們要建立新的 ScrollBenchmarks 類別,並有 scroll() 測試,其中含有捲動影格時間的基準測試。首先,您要用基準測試規則建立測試類別,並清除測試方法:

@RunWith(AndroidJUnit4::class)
class ScrollBenchmarks {
   @get:Rule
   val benchmarkRule = MacrobenchmarkRule()

   @Test
   fun scroll() {
       // TODO implement scrolling benchmark
   }
}

然後再用必要的參數加入基準測試的架構。

@Test
fun scroll() {
   benchmarkRule.measureRepeated(
       packageName = "com.example.macrobenchmark_codelab",
       iterations = 5,
       metrics = listOf(FrameTimingMetric()),
       
       setupBlock = {
           // TODO Add not measured interactions.
       }
   ) {
       // TODO Add interactions to measure list scrolling.
   }
}

這項基準測試使用的參數除了 metrics 參數和 setupBlock 之外,都和 startup 基準測試相同。FrameTimingMetric 會收集應用程式產生的影格時間。

現在,我們可以填入 setupBlock。如同上文說明的一樣,在這個 lambda 內,互動並非用基準測試進行測量。您可以只用這個區塊開啟應用程式,然後等到第一個影格轉譯完畢。

@Test
fun scroll() {
   benchmarkRule.measureRepeated(
       packageName = "com.example.macrobenchmark_codelab",
       iterations = 5,
       metrics = listOf(FrameTimingMetric()),
       startupMode = StartupMode.COLD,
       setupBlock = {
           // Start the default activity, but don't measure the frames yet
           pressHome()
           startActivityAndWait()
       }
   ) {
       // TODO Add interactions to measure list scrolling.
   }
}

現在,讓我們一起撰寫 measureBlock (最後的 lambda 參數)。首先,由於將項目送到點心清單的過程是非同步的操作,因此您應該等到內容準備完畢再開始操作。

benchmarkRule.measureRepeated(
   // ...
) {
    val contentList = device.findObject(By.res("snack_list"))

    val searchCondition = Until.hasObject(By.res("snack_collection"))
    // Wait until a snack collection item within the list is rendered
    contentList.wait(searchCondition, 5_000)

   // TODO Scroll the list
}

如果您不想測量一開始的版面配置設定,也可以選擇在 setupBlock 等待內容準備完畢。

接著,設定點心清單的手勢邊界。這項設定非常重要,否則應用程式可能會觸發系統操作並退出應用程式,而不會捲動內容。

benchmarkRule.measureRepeated(
   // ...
) {
   val contentList = device.findObject(By.res("snack_list"))

   val searchCondition = Until.hasObject(By.res("snack_collection"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(searchCondition, 5_000)

   // Set gesture margin to avoid triggering system gesture navigation
   contentList.setGestureMargin(device.displayWidth / 5)

   // TODO Scroll the list
}

最後,您要實際用 fling() 手勢 (也可以使用 scroll() 或 swipe(),以想捲動的數量和速度決定) 捲動清單,然後等到 UI 成為閒置狀態為止。

benchmarkRule.measureRepeated(
   // ...
) {
   val contentList = device.findObject(By.res("snack_list"))

   val searchCondition = Until.hasObject(By.res("snack_collection"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(searchCondition, 5_000)

   // Set gesture margin to avoid triggering gesture navigation
   contentList.setGestureMargin(device.displayWidth / 5)

   // Scroll down the list
   contentList.fling(Direction.DOWN)

   // Wait for the scroll to finish
   device.waitForIdle()
}

程式庫會在執行定義操作的同時測量應用程式產生的影格時間。

現在您有可以執行的基準測試了。

執行基準測試

執行基準測試的方法和啟動基準測試相同。按一下測試旁邊的空白邊圖示,然後選取「Run ‘scroll()'」。

9be4fb8b07d4ce5b.png

如果想進一步瞭解如何執行基準測試,請參閱執行基準測試步驟。

判讀結果

FrameTimingMetric 會在 50、90、95、99 等百分位數以毫秒為單位輸出影格期間 (frameDurationCpuMs)。如果在 Android 12 (API 級別 31) 以上版本,則也會回傳影格超出限制的時間 (frameOverrunMs)。這個值可以是負數,表示產生影格後還有多餘的時間。

c88387d833116a42.png

您可以在結果中看到,在 Google Pixel 7 上面建立影格的中位數 (P50) 是 3.8 毫秒,比影格時間限制少 6.4 毫秒。但是,也可能在超過 99 (P99) 的百分位數發生略過影格的情形,因為影格產生時間為 35.7 毫秒,超出限制 33.2 毫秒。

跟應用程式啟動結果一樣,您可以按一下 iteration 開啟基準測試期間的系統追蹤記錄,然後調查結果時間的產生原因。

11. 恭喜

恭喜!您已成功完成本程式碼研究室,瞭解如何使用 Jetpack Macrobenchmark 測量效能!

後續步驟

請參閱「使用基準設定檔改善應用程式效能」程式碼研究室。也請參閱我們的效能範例 GitHub 存放區,內有 Macrobenchmark 和其他效能範例。

參考文件