對基準設定檔進行偵錯

本文說明診斷問題及確保基準設定檔正確運作的最佳做法,幫助您發揮基準設定檔的最大效益。

版本問題

如果您已複製 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 或 AAB 來自包含基準設定檔的建構變化版本。最簡單的確認方法,就是在 Android Studio 中開啟 APK。請依序選取「Build」>「Analyze APK」、開啟 APK,然後找出 /assets/dexopt/baseline.prof 檔案中的設定檔:

使用 Android Studio 中的 APK 檢視器,檢查基準設定檔
圖 2. 使用 Android Studio 中的 APK 檢視器,檢查基準設定檔。

基準設定檔必須在執行應用程式的裝置上編譯。無論是應用程式商店安裝作業,還是透過 PackageInstaller 安裝的應用程式,系統都會在應用程式安裝程序期間執行裝置端編譯作業。但如果應用程式是從 Android Studio 側載,或是使用指令列工具,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 或 AAB 中找不到設定檔。如果看到這項錯誤,但 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) 以上版本的裝置上,如果應用程式是透過應用程式商店或套件管理員安裝,就不會發生這項錯誤,因為系統會在安裝期間執行編譯作業。
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 觸發。該設定檔可能是基準設定檔,也可能是應用程式使用期間收集的設定檔。

效能問題

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

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

如果啟動指標的定義明確,基準設定檔將更有效率。兩項關鍵指標分別是初始顯示時間 (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 {}
      ...
    }
  ...
}

如果只要使用程式庫設定檔進行基準測試,請執行下列指令:

./gradlew :baselineprofile:connectedBenchmarkReleaseWithoutCustomProfileAndroidTest

如要同時使用程式庫設定檔和自訂設定檔進行基準測試,請執行下列指令:

./gradlew :baselineprofile:connectedBenchmarkReleaseAndroidTest

Macrobenchmark 範例應用程式上執行上述程式碼,即表示兩個變化版本之間存在效能差異。如果只使用程式庫設定檔,暖 startupCompose 基準會顯示以下結果:

SmallListStartupBenchmark_startupCompose[mode=COLD]
timeToInitialDisplayMs   min  70.8,   median  79.1,   max 126.0
Traces: Iteration 0 1 2 3 4 5 6 7 8 9

許多 Jetpack Compose 程式庫中都有程式庫設定檔,因此只使用基準設定檔 Gradle 外掛程式即可進行最佳化。不過,使用自訂設定檔時,您必須執行其他最佳化作業:

SmallListStartupBenchmark_startupCompose[mode=COLD]
timeToInitialDisplayMs   min 57.9,   median 73.5,   max 92.3
Traces: Iteration 0 1 2 3 4 5 6 7 8 9

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

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

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

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

涵蓋所有重要使用者歷程

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