调试基线配置文件

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

构建问题

如果您已在 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]

status 值指示配置文件编译状态,并且是以下值之一

编译状态 含义
speed‑profile 存在已编译的配置文件,并且正在使用。
verify 不存在已编译的配置文件。

verify状态并不意味着 APK 或 AAB 不包含配置文件,因为它可能会由下一个后台 DEX 优化任务排队进行编译。

reason 值指示触发配置文件编译的原因,并且是以下值之一

原因 含义
install‑dm 安装应用时,由 Google Play 或手动编译基线配置文件。
bg‑dexopt 设备空闲时编译了配置文件。这可能是基线配置文件,也可能是应用使用期间收集的配置文件。
cmdline 使用 adb 触发了编译。这可能是基线配置文件,也可能是应用使用期间收集的配置文件。

性能问题

本节显示了一些正确定义和对基线配置文件进行基准测试的最佳实践,以便最大程度地利用它们。

正确对启动指标进行基准测试

如果您的启动指标定义良好,则您的基线配置文件将更有效。两个关键指标是初始显示时间 (TTID)完全显示时间 (TTFD)

TTID 是应用绘制其第一帧的时间。保持此时间尽可能短非常重要,因为显示某些内容会向用户表明应用正在运行。您甚至可以显示一个不确定的进度指示器,以表明应用正在响应。

TTFD 是应用实际上可以交互的时间。保持此时间尽可能短非常重要,以避免用户感到沮丧。如果您正确地发出 TTFD 信号,则是在告诉系统通往 TTFD 的过程中运行的代码是应用启动的一部分。结果,系统更有可能将此代码放入配置文件中。

保持 TTID 和 TTFD 尽可能低,以使您的应用感觉响应迅速。

系统能够检测 TTID、在 Logcat 中显示它并在启动基准测试中将其报告为一部分。但是,系统无法确定 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 绑定实现以进行基准测试。

涵盖所有重要的用户旅程

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