對基準設定檔進行偵錯

本文提供最佳做法和疑難排解步驟,協助您診斷問題,並確保基準設定檔正常運作,發揮最大效益。

版本問題

如果您已複製 Now in Android 範例應用程式中的基準設定檔,可能會在基準設定檔工作期間遇到測試失敗情形,指出測試無法在模擬器上執行:

./gradlew assembleDemoRelease
Starting a Gradle Daemon (subsequent builds will be faster)
Calculating task graph as no configuration cache is available for tasks: assembleDemoRelease
Type-safe project accessors is an incubating feature.

> Task :benchmarks:pixel6Api33DemoNonMinifiedReleaseAndroidTest
Starting 14 tests on pixel6Api33

com.google.samples.apps.nowinandroid.foryou.ScrollForYouFeedBenchmark > scrollFeedCompilationNone[pixel6Api33] FAILED
        java.lang.AssertionError: ERRORS (not suppressed): EMULATOR
        WARNINGS (suppressed):
        ...

失敗原因是 Now in Android 採用 Gradle 管理的裝置產生基準設定檔。這種失敗是正常現象,因為您通常不應在模擬器上執行效能基準測試。不過,您不會在產生基準設定檔時收集效能指標,所以為了方便起見,可以在模擬器上執行基準設定檔收集作業。如要搭配使用基準設定檔和模擬器,請透過指令列執行建構和安裝作業,並設定引數來啟用基準設定檔規則:

installDemoRelease -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile

或者,您也可以在 Android Studio 中建立自訂執行設定,然後依序選取「Run」>「Edit Configurations」,在模擬器上啟用基準設定檔:

新增自訂執行設定,在 Now in Android 中建立基準設定檔
圖 1. 新增自訂執行設定,在 Now in Android 中建立基準設定檔。

確認設定檔安裝和應用

如要確認您檢查的 APK 或 Android App Bundle (AAB) 來自包含基準設定檔的建構變化版本,請按照下列步驟操作:

  1. 在 Android Studio 中,依序選取「Build」>「Analyze APK」
  2. 開啟 AAB 或 APK。
  3. 確認 baseline.prof 檔案存在:

    • 如果您要檢查 AAB,設定檔位於 /BUNDLE-METADATA/com.android.tools.build.profiles/baseline.prof
    • 如果您要檢查 APK,設定檔位於 /assets/dexopt/baseline.prof

      這個檔案存在與否,是建構設定是否正確的第一個指標。如果缺少這項資訊,表示 Android Runtime 在安裝時不會收到任何預先編譯指令。

      使用 Android Studio 中的 APK 分析工具,檢查基準設定檔
      圖 2. 使用 Android Studio 中的 APK 分析工具,檢查基準設定檔。

基準設定檔必須在執行應用程式的裝置上編譯。使用 Android Studio 或 Gradle Wrapper 指令列工具安裝無法偵錯的建構版本時,系統會自動執行裝置端編譯作業。如果從 Google Play 商店安裝應用程式,系統會在背景更新裝置時編譯基準設定檔,而不是在安裝時編譯。如果應用程式是使用其他工具安裝,Jetpack ProfileInstaller 程式庫會負責在下次背景 DEX 最佳化程序中,將設定檔排入編譯佇列。

在這種情況下,如要確保使用基準設定檔,可能需要強制編譯基準設定檔。您可以使用 ProfileVerifier 查詢設定檔的安裝和編譯狀態,如以下範例所示:

Kotlin

private const val TAG = "MainActivity"

class MainActivity : ComponentActivity() {
  ...
  override fun onResume() {
    super.onResume()
    lifecycleScope.launch {
      logCompilationStatus()
    }
  }

  private suspend fun logCompilationStatus() {
     withContext(Dispatchers.IO) {
        val status = ProfileVerifier.getCompilationStatusAsync().await()
        when (status.profileInstallResultCode) {
            RESULT_CODE_NO_PROFILE ->
                Log.d(TAG, "ProfileInstaller: Baseline Profile not found")
            RESULT_CODE_COMPILED_WITH_PROFILE ->
                Log.d(TAG, "ProfileInstaller: Compiled with profile")
            RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION ->
                Log.d(TAG, "ProfileInstaller: Enqueued for compilation")
            RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
                Log.d(TAG, "ProfileInstaller: App was installed through Play store")
            RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST ->
                Log.d(TAG, "ProfileInstaller: PackageName not found")
            RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ ->
                Log.d(TAG, "ProfileInstaller: Cache file exists but cannot be read")
            RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE ->
                Log.d(TAG, "ProfileInstaller: Can't write cache file")
            RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION ->
                Log.d(TAG, "ProfileInstaller: Enqueued for compilation")
            else ->
                Log.d(TAG, "ProfileInstaller: Profile not compiled or enqueued")
        }
    }
}

Java

public class MainActivity extends ComponentActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onResume() {
        super.onResume();

        logCompilationStatus();
    }

    private void logCompilationStatus() {
         ListeningExecutorService service = MoreExecutors.listeningDecorator(
                Executors.newSingleThreadExecutor());
        ListenableFuture<ProfileVerifier.CompilationStatus> future =
                ProfileVerifier.getCompilationStatusAsync();
        Futures.addCallback(future, new FutureCallback<>() {
            @Override
            public void onSuccess(CompilationStatus result) {
                int resultCode = result.getProfileInstallResultCode();
                if (resultCode == RESULT_CODE_NO_PROFILE) {
                    Log.d(TAG, "ProfileInstaller: Baseline Profile not found");
                } else if (resultCode == RESULT_CODE_COMPILED_WITH_PROFILE) {
                    Log.d(TAG, "ProfileInstaller: Compiled with profile");
                } else if (resultCode == RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION) {
                    Log.d(TAG, "ProfileInstaller: Enqueued for compilation");
                } else if (resultCode == RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING) {
                    Log.d(TAG, "ProfileInstaller: App was installed through Play store");
                } else if (resultCode == RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST) {
                    Log.d(TAG, "ProfileInstaller: PackageName not found");
                } else if (resultCode == RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ) {
                    Log.d(TAG, "ProfileInstaller: Cache file exists but cannot be read");
                } else if (resultCode
                        == RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE) {
                    Log.d(TAG, "ProfileInstaller: Can't write cache file");
                } else if (resultCode == RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION) {
                    Log.d(TAG, "ProfileInstaller: Enqueued for compilation");
                } else {
                    Log.d(TAG, "ProfileInstaller: Profile not compiled or enqueued");
                }
            }

            @Override
            public void onFailure(Throwable t) {
                Log.d(TAG,
                        "ProfileInstaller: Error getting installation status: " + t.getMessage());
            }
        }, service);
    }
}

下列結果代碼可提示部分問題的原因:

RESULT_CODE_COMPILED_WITH_PROFILE
每當應用程式執行時,系統都會安裝、編譯並使用設定檔。這是您樂見的結果。
RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED
在執行的 APK 中找不到設定檔。如果看到這項錯誤,但 APK 內含設定檔,請確認您使用的是包含基準設定檔的建構變化版本。
RESULT_CODE_NO_PROFILE
透過應用程式商店或套件管理員安裝應用程式時,未安裝此應用程式的設定檔。出現此錯誤代碼的主因是 ProfileInstallerInitializer 已停用,因此設定檔安裝程式並未執行。請注意,系統回報此錯誤時,仍會在應用程式 APK 中找到嵌入的設定檔。如果找不到嵌入的設定檔,則傳回的錯誤代碼為 RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED
RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
設定檔位於 APK 或 AAB 中,且已排入編譯佇列。若是由 ProfileInstaller 安裝設定檔,系統會在下次執行背景 DEX 最佳化工作時,將設定檔排入編譯佇列。編譯完成後,設定檔才會生效。在編譯完成前,切勿嘗試對基準設定檔執行基準測試。您可能需要強制編譯基準設定檔。在搭載 Android 9 (API 28) 以上版本的裝置上,如果應用程式是透過 Play 商店或套件管理員安裝,就不會發生這項錯誤,因為系統會在安裝期間執行編譯作業。
RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING
安裝的設定檔不相符,而系統已使用該設定檔編譯應用程式。這是透過 Google Play 商店或套件管理員完成安裝的結果。請注意,這項結果與 RESULT_CODE_COMPILED_WITH_PROFILE 不同,因為不相符的設定檔只會編譯設定檔和應用程式之間仍共用的方法。實際上,設定檔會小於預期,編譯的方法也會比基準設定檔更少。
RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE
ProfileVerifier 無法寫入驗證結果快取檔案。這可能是因為應用程式資料夾權限有誤,或是裝置可用磁碟空間不足。
RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION
ProfileVerifier is running on an unsupported API version of Android. ProfileVerifier 只支援 Android 9 (API 級別 28) 以上版本。
RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST
查詢應用程式套件的 PackageManager 時,系統擲回 PackageManager.NameNotFoundException。這種情況應該很少發生。請嘗試解除安裝應用程式,並重新安裝所有項目。
RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ
已有先前的驗證結果快取檔案,但無法讀取。這種情況應該很少發生。請嘗試解除安裝應用程式,並重新安裝所有項目。

在正式環境中使用 ProfileVerifier

在實際工作環境中,您可以搭配使用 ProfileVerifier 與數據分析報表程式庫 (例如 Google Analytics for Firebase),產生指出設定檔狀態的數據分析事件。舉例來說,當新推出的應用程式版本不含基準設定檔時,您就能迅速收到快訊。

強制編譯基準設定檔

如果基準設定檔的編譯狀態為 RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION,您可以使用 adb 強制立即編譯:

adb shell cmd package compile -r bg-dexopt PACKAGE_NAME

不使用 ProfileVerifier 檢查基準設定檔編譯狀態

如果不使用 ProfileVerifier,可以運用 adb 檢查編譯狀態,但這無法像使用 ProfileVerifier 一樣取得深入分析資料:

adb shell dumpsys package dexopt | grep -A 2 PACKAGE_NAME

使用 adb 產生的內容會類似如下:

  [com.google.samples.apps.nowinandroid.demo]
    path: /data/app/~~dzJiGMKvp22vi2SsvfjkrQ==/com.google.samples.apps.nowinandroid.demo-7FR1sdJ8ZTy7eCLwAnn0Vg==/base.apk
      arm64: [status=speed-profile] [reason=bg-dexopt] [primary-abi]
        [location is /data/app/~~dzJiGMKvp22vi2SsvfjkrQ==/com.google.samples.apps.nowinandroid.demo-7FR1sdJ8ZTy7eCLwAnn0Vg==/oat/arm64/base.odex]

狀態值代表設定檔編譯狀態,且會是下列其中一個值:

編譯狀態 意義
speed‑profile 含有已編譯的設定檔,且正在使用該設定檔。
verify 沒有已編譯的設定檔。

verify 狀態並非代表 APK 或 AAB 不包含設定檔,因為下一個背景 DEX 最佳化工作可能會將設定檔排入編譯佇列。

原因值可以是下列其中一個值,可指出觸發設定檔編譯作業的原因:

原因 意義
install‑dm 基準設定檔是在應用程式安裝時手動編譯,或由 Google Play 編譯。
bg‑dexopt 系統是在裝置處於閒置狀態時編譯設定檔。該設定檔可能是基準設定檔,也可能是應用程式使用期間收集的設定檔。
cmdline 編譯作業是由 ADB 觸發。該設定檔可能是基準設定檔,也可能是應用程式使用期間收集的設定檔。

將啟動設定檔應用程式驗證至 DEX 和 r8.json

R8 會在建構期間使用啟動設定檔規則,最佳化 DEX 檔案中的類別版面配置。這種建構時間最佳化與基準設定檔 (baseline.prof) 的使用方式不同,因為基準設定檔會封裝在 APK 或 AAB 中,供 ART 執行裝置端編譯。由於啟動設定檔規則是在建構程序本身套用,因此 APK 或 AAB 中沒有可供檢查的獨立 startup.prof 檔案。啟動設定檔的效果會顯示在 DEX 檔案版面配置中。

使用 r8.json 檢查 DEX 配置 (建議使用 AGP 8.8 以上版本)

如果專案使用 Android Gradle 外掛程式 (AGP) 8.8 以上版本,您可以檢查產生的 r8.json 檔案,確認是否已套用啟動設定檔。這個檔案會封裝在 AAB 中。

  1. 開啟 AAB 封存檔,找出 r8.json 檔案。
  2. 在檔案中搜尋 dexFiles 陣列,其中列出產生的 DEX 檔案。
  3. 找出包含鍵/值組合 "startup": truedexFiles 物件。這明確指出啟動設定檔規則已套用至該特定 DEX 檔案,以最佳化版面配置。

    "dexFiles": [
     {
       "checksum": "...",
       "startup": true // This flag confirms profile application to this DEX file
     },
     // ... other DEX files
    ]
    

檢查所有 AGP 版本的 DEX 配置

如果您使用的 AGP 版本低於 8.8,檢查 DEX 檔案是確認啟動設定檔是否已正確套用的主要方式。如果您使用 AGP 8.8 以上版本,且想手動檢查 DEX 版面配置,也可以使用這個方法。舉例來說,如果成效未如預期提升,如要檢查 DEX 安排,請執行下列操作:

  1. 在 Android Studio 中,使用「Build」>「Analyze APK」開啟 AAB 或 APK。
  2. 前往第一個 DEX 檔案。例如:classes.dex
  3. 檢查這個 DEX 檔案的內容。您應該可以驗證啟動設定檔 (startup-prof.txt) 中定義的重要類別和方法是否位於這個主要 DEX 檔案中。成功套用後,系統會優先載入這些對新創公司至關重要的元件,加快載入速度。

效能問題

本節介紹一些最佳做法,說明如何對基準設定檔設下正確定義、執行基準測試,進而發揮基準設定檔的最大效益。

正確執行啟動指標的基準測試

如果啟動指標的定義明確,基準設定檔將更有效率。兩項關鍵指標分別是初始顯示時間 (TTID)完整顯示時間 (TTFD)

TTID 是指應用程式繪製第一個影格的時間。請務必盡量縮短 TTID,因為使用者看到內容後,就會知道應用程式正在執行。您甚至可以顯示未確定的進度指標,表示應用程式有回應。

TTFD 是指使用者能實際與應用程式互動的時間。請務必盡量縮短 TTFD,以免使用者感到不悅。如果正確發出 TTFD 信號,即表示在 TTFD 前執行的程式碼,皆屬於應用程式啟動程序。如此一來,系統就較可能將這類程式碼放入設定檔。

請盡量降低 TTID 和 TTFD,讓使用者覺得應用程式有所回應。

系統可以偵測 TTID、將 TTID 顯示在 Logcat 中,並回報為啟動基準測試的一部分,但系統無法判定 TTFD。在完整繪製出可供互動的畫面前,須由應用程式負責回報 TTFD。您可以呼叫 reportFullyDrawn() 來回報這項資訊。如果使用 Jetpack Compose,則可呼叫 ReportDrawn。如有許多需要完成的背景工作,而且這些工作必須在應用程式完全繪製前完成,可以使用 FullyDrawnReporter,詳情請參閱「提高啟動時間準確度」。

媒體庫設定檔和自訂設定檔

在評估設定檔的影響時,很難將應用程式設定檔的優點與程式庫 (例如 Jetpack 程式庫) 提供的設定檔分開。建構 APK 時,Android Gradle 外掛程式會加入程式庫依附元件中的任何設定檔,以及您的自訂設定檔。這有助於提升整體效能,建議用於發布版本。不過,這樣就難以評估自訂設定檔帶來的額外效能提升幅度。

如要快速手動查看自訂設定檔提供的額外最佳化項目,請移除設定檔並執行基準測試。然後取代該檔案,再次執行基準測試。比較這兩者,即可查看單獨由程式庫設定檔提供的最佳化項目,以及程式庫設定檔加上自訂設定檔提供的最佳化項目。

如要以自動化方式比較設定檔,可以建立新的建構變體,其中只包含程式庫設定檔,不含自訂設定檔。將這個變種版本的基準與包含程式庫設定檔和自訂設定檔的發布變種版本進行比較。以下範例說明如何設定只包含程式庫設定檔的變體。在設定檔消耗者模組 (通常是應用程式模組) 中,新增名為 releaseWithoutCustomProfile 的新變數:

Kotlin

android {
  ...
  buildTypes {
    ...
    // Release build with only library profiles.
    create("releaseWithoutCustomProfile") {
      initWith(release)
    }
    ...
  }
  ...
}
...
dependencies {
  ...
  // Remove the baselineProfile dependency.
  // baselineProfile(project(":baselineprofile"))
}

baselineProfile {
  variants {
    create("release") {
      from(project(":baselineprofile"))
    }
  }
}

Groovy

android {
  ...
  buildTypes {
    ...
    // Release build with only library profiles.
    releaseWithoutCustomProfile {
      initWith(release)
    }
    ...
  }
  ...
}
...
dependencies {
  ...
  // Remove the baselineProfile dependency.
  // baselineProfile ':baselineprofile"'
}

baselineProfile {
  variants {
    release {
      from(project(":baselineprofile"))
    }
  }
}

上述程式碼範例會從所有變體中移除 baselineProfile 依附元件,並只選擇性地套用至 release 變體。移除設定檔產生器模組的依附元件後,系統仍會新增程式庫設定檔,這可能違反直覺。不過,這個模組只負責產生自訂設定檔。Android Gradle 外掛程式仍會為所有變體執行,並負責納入程式庫設定檔。

您也需要在設定檔產生器模組中新增變體。在這個範例中,生產端模組名為 :baselineprofile

Kotlin

android {
  ...
    buildTypes {
      ...
      // Release build with only library profiles.
      create("releaseWithoutCustomProfile") {}
      ...
    }
  ...
}

Groovy

android {
  ...
    buildTypes {
      ...
      // Release build with only library profiles.
      releaseWithoutCustomProfile {}
      ...
    }
  ...
}

從 Android Studio 執行基準測試時,請選取 releaseWithoutCustomProfile 變體,只使用程式庫設定檔測量效能,或選取 release 變體,使用程式庫和自訂設定檔測量效能。

避免 I/O 密集型應用程式啟動程序

如果應用程式在啟動期間執行大量 I/O 呼叫或網路呼叫,可能會對應用程式啟動時間和啟動基準測試的準確度造成負面影響。這類高負載呼叫所需的處理時間並不一定,不僅可能隨時間變化,甚至在同一基準測試的疊代間都可能不同。I/O 呼叫通常優於網路呼叫,因為網路呼叫可能會受裝置外部因素和裝置本身的影響。請避免在啟動期間發出網路呼叫。如果必須發出呼叫,請使用 I/O 呼叫。

您採用的應用程式架構應能在不發出網路呼叫或 I/O 呼叫的情況下啟動應用程式,即使只會在基準測試啟動期間使用這項架構也一樣。這樣一來,就能盡量降低不同基準測試疊代間產生差異的可能性。

如果應用程式使用 Hilt,您可以在 Microbenchmark 和 Hilt 中進行基準化時,提供虛假的 I/O 繫結實作。

涵蓋所有重要使用者歷程

請務必準確涵蓋產生基準設定檔時的所有重要使用者歷程。基準設定檔不會改善未涵蓋的使用者歷程。最有效的基準設定檔包含所有常見的啟動使用者歷程,以及易受效能影響的應用程式內使用者歷程,例如捲動清單。

A/B 測試編譯時間設定檔變更

由於啟動和基準設定檔是編譯時間最佳化,因此一般來說,正式版不支援使用 Google Play 商店直接進行不同 APK 的 A/B 測試。如要在類似於實際工作環境的環境中評估影響,請考慮下列方法:

  • 非週期性發布:將非週期性發布內容上傳至一小部分使用者,其中只包含設定檔變更。這有助於收集實際的成效差異指標。

  • 本機基準測試:在本機為應用程式執行基準測試,並套用設定檔。不過請注意,本機基準測試會顯示設定檔的最佳情況,因為其中不包含生產裝置中 ART 的 Cloud Profiles 效果。