调试基准配置文件

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

构建问题

如果您已在 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) 是否来自包含基准配置文件的 build 变体,请执行以下操作:

  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 命令行工具安装不可调试的 build 时,设备上的编译会自动进行。如果您从 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 中未找到配置文件。如果您看到此错误,请确保您使用的 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) 及更高版本的设备上通过 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
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 触发的。这可能是基准配置文件,也可能是应用使用期间收集的配置文件。

验证启动配置文件对 DEX 和 r8.json 的应用

R8 在构建时使用启动配置文件规则来优化 DEX 文件中类的布局。这种 build 时优化与基准配置文件 (baseline.prof) 的使用方式不同,因为基准配置文件打包在 APK 或 AAB 中,供 ART 执行设备端编译。由于启动配置文件规则是在 build 过程中应用的,因此 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 是指应用绘制第一帧的时间。请务必使其尽可能短一些,因为显示内容即向用户表明应用正在运行。您甚至可以显示不确定的进度指示器,表明应用响应迅速。

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

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

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

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

对配置文件的影响进行基准比较时,很难将应用配置文件的优势与库(例如 Jetpack 库)贡献的配置文件区分开来。构建 APK 时,Android Gradle 插件会添加库依赖项中的所有配置文件以及您的自定义配置文件。这有助于优化整体性能,建议用于发布 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 {}
      ...
    }
  ...
}

从 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 的云配置文件的影响。