调试基准配置文件

本文档介绍了一些最佳实践,可帮助您诊断问题,并确保基准配置文件正确运行,以提供最大优势。

构建问题

如果您已在 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 是否来自包含基准配置文件的 build 变体。若要检查这一点,最简单的方法是在 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 中未找到配置文件。如果您看到此错误,请确保您使用的 build 变体包含基准配置文件,并且该 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
ProfileVerifieris 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 是指应用绘制第一帧的时间。请务必使其尽可能短一些,因为显示内容即向用户表明应用正在运行。您甚至可以显示不确定的进度指示器,表明应用响应迅速。

TTFD 是指可以实际与应用互动的时间。请务必使其尽可能短一些,以免引起用户沮丧。如果您正确发出 TTFD 信号,即在告知系统:在达到 TTFD 的过程中运行的代码是应用启动代码的一部分。因此,系统更有可能将此代码放入配置文件中。

尽可能缩短 TTID 和 TTFD,让用户感觉您的应用响应迅速。

系统能够检测、在 Logcat 中显示以及在启动基准测试过程中报告 TTID。但是,系统无法确定 TTFD,应用应负责报告自己在何时达到完全绘制互动状态。您可以通过调用 reportFullyDrawn()(或 ReportDrawn,如果您使用的是 Jetpack Compose)来实现此目的。如果您有多个后台任务都需要完成,才能使应用被视为完全绘制,则可以使用 FullyDrawnReporter,如提高启动时间准确性中所述。

库配置文件和自定义配置文件

在对配置文件的影响进行基准测试时,可能很难将应用配置文件的优势与库(例如 Jetpack 库)贡献的配置文件区分开来。在您构建 APK 时,Android Gradle 插件会在库依赖项中添加所有配置文件以及您的自定义配置文件。这有利于优化整体性能,建议在发布 build 中使用。不过,这样就很难衡量自定义配置文件能带来多少额外的性能提升。

如需手动查看自定义配置文件提供的其他优化,一种快速方法是移除该优化,然后运行基准。然后替换该 build 并再次运行基准测试。比较这两者,您就会看到单独由库配置文件提供的优化,以及库配置文件和您的自定义配置文件带来的优化。

比较配置文件的一种可自动化方式是创建新的 build 变体,其中仅包含库配置文件,而不包含您的自定义配置文件。将此变体中的基准与同时包含库配置文件和自定义配置文件的发布变体进行比较。以下示例展示了如何设置仅包含库配置文件的变体。将名为 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 限制的虚构实现。

涵盖所有重要的用户体验历程

请务必准确涵盖生成基准配置文件过程中的所有重要的用户体验历程。基准配置文件不会改进未涵盖的任何用户体验历程。最有效的基准配置文件既涵盖所有常见的启动用户体验历程,又涵盖对性能敏感的应用内用户体验历程(例如滚动列表)。