调试基线配置文件

本文档展示了有助于诊断问题并确保您的基线配置文件正常工作以提供最大益处的最佳实践。

构建问题

如果您已在 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 中创建一个自定义运行配置,以通过选择**运行 > 编辑配置**在模拟器上启用基线配置文件。

Add a custom run configuration to create Baseline Profiles in Now in Android
图 1. 在 Now in Android 中添加自定义运行配置以创建基线配置文件。

安装问题

检查您正在构建的 APK 或 AAB 是否来自包含基线配置文件的构建变体。检查此问题的最简单方法是在 Android Studio 中打开 APK(选择**构建 > 分析 APK**),打开您的 APK,并在/assets/dexopt/baseline.prof文件中查找配置文件。

Check for a Baseline Profile using APK Viewer in Android Studio
图 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正在 Android 的不受支持的 API 版本上运行。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与分析报告库(例如Firebase 的 Google Analytics)结合使用,以生成指示配置文件状态的分析事件。例如,这可以快速提醒您是否发布了不包含基线配置文件的新应用版本。

强制编译基线配置文件

如果基线配置文件的编译状态为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 都尽可能低,以使您的应用感觉响应迅速。

系统能够检测到 TTID,在 Logcat 中显示它,并将其报告为启动基准测试的一部分。但是,系统无法确定 TTFD,应用有责任报告其何时达到完全绘制的交互状态。您可以通过调用reportFullyDrawn()ReportDrawn(如果您使用的是 Jetpack Compose)来执行此操作。如果您有多个后台任务都需要在应用被视为完全绘制之前完成,则可以使用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 绑定实现以进行基准测试。

涵盖所有重要的用户旅程

在生成基线配置文件时,准确涵盖所有重要的用户旅程非常重要。任何未涵盖的用户旅程都不会受到基线配置文件的改进。最有效的基线配置文件包括所有常见启动用户旅程以及性能敏感的应用内用户旅程(例如滚动列表)。