使用 Macrobenchmark 检查应用性能

1. 开始之前

在本 Codelab 中,您将学习如何使用 macrobenchmark 库。您将测量应用启动时间(这是用户参与度的关键指标)和帧时间(暗示应用中可能出现卡顿的位置)。

您需要什么

您将做什么

  • 将基准测试模块添加到现有应用程序
  • 测量应用启动和帧时间

您将学到什么

  • 可靠地测量应用程序性能

2. 设置

要开始,请使用以下命令从命令行克隆 Github 存储库

$ git clone https://github.com/android/codelab-android-performance.git

或者,您可以下载两个 zip 文件

在 Android Studio 中打开项目

  1. 在“欢迎使用 Android Studio”窗口中,选择 61d0a4432ef6d396.png 打开现有项目
  2. 选择文件夹 [Download Location]/android-performance/benchmarking(提示:确保您选择包含 build.gradlebenchmarking 目录)
  3. Android Studio 导入项目后,确保您可以运行 app 模块以构建我们将进行基准测试的示例应用程序。

3. Jetpack Macrobenchmark 简介

Jetpack Macrobenchmark 库测量较大最终用户交互的性能,例如启动、与 UI 交互和动画。该库提供对您正在测试的性能环境的直接控制。它允许您控制编译、启动和停止您的应用程序以直接测量应用启动、帧时间和跟踪的代码部分。

使用 Jetpack Macrobenchmark,您可以

  • 使用确定性启动模式和滚动速度多次测量应用
  • 通过对多次测试运行的结果进行平均来平滑性能差异
  • 控制应用的编译状态 - 这是性能稳定性的一个主要因素
  • 通过本地复制 Google Play 商店执行的安装时优化来检查真实世界的性能

使用此库的检测不会直接调用您的应用程序代码,而是像用户一样浏览您的应用程序 - 触摸、点击、滑动等。测量在此类交互期间在设备上进行。如果您想直接测量应用程序代码的部分,请改为参阅 Jetpack Microbenchmark

编写基准测试就像编写检测测试一样,除了您不需要验证应用所处的状态。基准测试使用 JUnit 语法(@RunWith@Rule@Test 等),但测试在单独的进程中运行,以允许重新启动或预编译您的应用。这使我们能够在不干扰其内部状态的情况下运行您的应用,就像用户一样。我们通过使用 UiAutomator 与目标应用程序进行交互来实现这一点。

示例应用

在本 Codelab 中,您将使用 JetSnack 示例 应用程序。这是一个虚拟的零食订购应用,使用 Jetpack Compose。要测量应用程序的性能,您不需要了解应用架构的详细信息。您需要了解的是应用的行为方式和 UI 的结构,以便您可以从基准测试中访问 UI 元素。运行应用并通过订购您选择的零食来熟悉基本屏幕。

23633b02ac7ce1bc.png

4. 添加 Macrobenchmark 库

Macrobenchmark 需要向您的项目添加一个新的 Gradle 模块。将其添加到项目的最简单方法是使用 Android Studio 模块向导。

打开新的模块对话框(例如,右键点击**Project** 面板中的项目或模块,然后选择**New > Module**)。

65fb489132d02a87.png

从**Templates** 面板中选择**Benchmark**,确保选择**Macrobenchmark** 作为 Benchmark 模块类型,并检查详细信息是否符合预期。

Benchmark module type Macrobenchmark is selected.

  • **Target application** – 将要进行基准测试的应用程序
  • **Module name** – 基准测试 Gradle 模块的名称
  • **Package name** – 基准测试的包名
  • **Minimum SDK** – 至少需要 Android 6(API 级别 23)或更高版本。

点击**Finish**。

模块向导所做的更改

模块向导对您的项目进行了一些更改。

它添加了一个名为macrobenchmark(或您在向导中选择的名称)的 Gradle 模块。此模块使用com.android.test 插件,该插件告诉 Gradle 不要将其包含在您的应用程序中,因此它只能包含测试代码(或基准测试)。

向导还会对您选择的目标应用程序模块进行更改。具体来说,它会向:app 模块的build.gradle 中添加新的benchmark build type,如下面的代码片段所示。

benchmark {
   initWith buildTypes.release
   signingConfig signingConfigs.debug
   matchingFallbacks = ['release']
   debuggable false
}

此 buildType 应尽可能模拟您的release buildType。与release buildType 的区别在于signingConfig 设置为debug,这仅是为了您能够在本地构建应用程序而无需生产密钥库。

但是,由于debuggable 标志被禁用,因此向导会将<profileable> 标签添加到您的AndroidManifest.xml 中,以允许基准测试使用发布版性能分析您的应用程序。

<application>

  <profileable
     android:shell="true"
     tools:targetApi="q" />

</application>

要获取有关<profileable> 功能的更多信息,请查看我们的文档

向导做的最后一件事是创建用于基准测试启动时间的脚手架(我们将在下一步中使用)。

现在您已准备好开始编写基准测试了。

5. 测量应用程序启动时间

应用程序启动时间,或用户开始使用您的应用程序所需的时间,是影响用户参与度的关键指标。模块向导创建了一个ExampleStartupBenchmark 测试类,该类能够测量您的应用程序启动时间,如下所示。

@RunWith(AndroidJUnit4::class)
class ExampleStartupBenchmark {
   @get:Rule
   val benchmarkRule = MacrobenchmarkRule()

   @Test
   fun startup() = benchmarkRule.measureRepeated(
       packageName = "com.example.macrobenchmark_codelab",
       metrics = listOf(StartupTimingMetric()),
       iterations = 5,
       startupMode = StartupMode.COLD,
   ){
        pressHome()
        startActivityAndWait()
   }
}

所有参数的含义是什么?

在编写基准测试时,您的入口点是MacrobenchmarkRulemeasureRepeated 函数。此函数会处理基准测试的所有事项,但您需要指定以下参数。

  • packageName – 基准测试在与被测应用程序不同的进程中运行,因此您需要指定要测量的应用程序。
  • metrics – 您希望在基准测试期间测量哪种类型的信息。在我们的例子中,我们对应用程序启动时间感兴趣。请查看文档以了解其他类型的指标。
  • iterations – 基准测试将重复多少次。更多的迭代意味着更稳定的结果,但代价是执行时间更长。理想的次数将取决于此特定指标对您的应用程序的噪声程度。
  • startupMode – 允许您定义在基准测试开始时您的应用程序如何启动。可用的是COLDWARMHOT。我们使用COLD,因为它代表了应用程序必须执行的最大工作量。
  • measureBlock(最后一个 lambda 参数)– 在此函数中,您定义了希望在基准测试期间测量的操作(启动 Activity、点击 UI 元素、滚动、滑动等),并且 macrobenchmark 将在此块期间收集已定义的metrics

如何编写基准测试操作

Macrobenchmark 将重新安装并重新启动您的应用程序。确保您编写的交互与应用程序的状态无关。Macrobenchmark 提供了一些有用的函数和参数来与您的应用程序交互。

最重要的一个是startActivityAndWait()。此函数将启动您的默认 Activity,并在渲染第一帧之前等待,然后继续执行基准测试中的指令。如果您想启动不同的 Activity 或调整启动 Intent,可以使用可选的intentblock 参数来执行此操作。

另一个有用的函数是pressHome()。这允许您在不终止每次迭代中的应用程序的情况下(例如,当您使用StartupMode.HOT 时)将基准测试重置为基本条件。

对于任何其他交互,您可以使用device 参数,该参数允许您查找 UI 元素、滚动、等待某些内容等。

好的,现在我们已经定义了一个启动基准测试,您将在下一步中运行它。

6. 运行基准测试

在运行基准测试之前,请确保已在 Android Studio 中选择了正确的构建变体。

  1. 选择**Build Variants** 面板。
  2. 将**Active Build Variant** 更改为**benchmark**。
  3. 等待 Android Studio 同步。

b8a622b5a347e9f3.gif

如果您没有这样做,基准测试将在运行时失败,并显示一条错误消息,指出您不应该对debuggable 应用程序进行基准测试。

java.lang.AssertionError: ERRORS (not suppressed): DEBUGGABLE
WARNINGS (suppressed):

ERROR: Debuggable Benchmark
Benchmark is running with debuggable=true, which drastically reduces
runtime performance in order to support debugging features. Run
benchmarks with debuggable=false. Debuggable affects execution speed
in ways that mean benchmark improvements might not carry over to a
real user's experience (or even regress release performance).

您可以使用 instrumentation 参数androidx.benchmark.suppressErrors = "DEBUGGABLE" 暂时抑制此错误。您可以按照在 Android 模拟器上运行基准测试步骤中的相同步骤操作。

现在,您可以运行基准测试了——就像运行已测工具测试一样。您可以使用它旁边的代码行图标运行测试函数或整个类。

a1d075775c06d621.png

确保已选择物理设备,因为在 Android 模拟器上运行基准测试将在运行时失败,并显示警告信息,指出这将给出不正确的结果。虽然从技术上讲您可以在模拟器上运行它,但您基本上是在测量主机计算机的性能——如果主机计算机负载过重,您的基准测试将运行得更慢,反之亦然。

803c3cbd5b288612.png

运行基准测试后,您的应用程序将被重新构建,然后它将运行您的基准测试。根据您定义的iterations,基准测试将启动、停止甚至重新安装您的应用程序几次。

7.(可选)在 Android 模拟器上运行基准测试

如果您没有物理设备,但仍想运行基准测试,则可以使用 instrumentation 参数androidx.benchmark.suppressErrors = "EMULATOR" 抑制运行时错误。

要抑制错误,请编辑您的运行配置。

  1. 从运行菜单中选择“Edit Configurations...”: 55cb46ef01ca5358.png
  2. 在打开的窗口中,选择“选项”图标 1bd68a321477e4fc.png,位于“Instrumentation arguments”旁边 36c04579f01b1e44.png
  3. 通过点击➕并输入详细信息来添加 Instrumentation 额外参数 35148c9ac7a414fd.png
  4. 点击**确定**确认选择。您应该在“Instrumentation arguments”行中看到该参数 14a18966bc1deabf.png
  5. 点击**确定**确认运行配置。

或者,如果您需要将其永久保存在您的代码库中,您可以从build.gradle文件中的:macrobenchmark模块中执行此操作。

defaultConfig {
    // ...
    testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = 'EMULATOR'
}

8. 了解启动结果

基准测试运行完成后,它将直接在 Android Studio 中为您提供结果,如下面的屏幕截图所示

1bc4933131e12b4.png

您可以看到,在我们的例子中,Google Pixel 7 上的启动时间,最小值为**294.8ms**,中位数为**301.5ms**,最大值为**314.8ms**。请注意,在您的设备上,运行相同的基准测试时可能会得到不同的结果。结果可能会受到许多因素的影响,例如

  • 设备的性能如何
  • 它使用什么系统版本
  • 后台运行哪些应用

因此,在同一设备上比较结果非常重要,理想情况下应处于相同状态,否则您可能会看到很大的差异。如果您无法保证相同的状态,您可能需要增加iterations的数量以正确处理结果异常值。

为了便于调查,Macrobenchmark 库在基准测试执行期间记录系统跟踪。为了方便起见,Android Studio 将每次迭代和测量的计时标记为指向系统跟踪的链接,以便您可以轻松地打开它进行调查。

9. (可选练习)声明您的应用何时可以使用

Macrobenchmark 可以自动测量应用渲染第一帧的时间(timeToInitialDisplay)。但是,您的应用内容通常在渲染第一帧后才会完成加载,您可能想知道用户必须等待多长时间才能使用该应用。这称为完整显示时间——应用已完全加载内容,用户可以与之交互。Macrobenchmark 库可以自动检测此计时,但您需要调整您的应用以使用Activity.reportFullyDrawn()函数告知何时发生。

示例显示了一个简单的进度条,直到数据加载完成,因此您需要等到数据准备就绪并且零食列表已布局并绘制。让我们调整示例应用程序并添加reportFullyDrawn()调用。

在**Project**窗格中打开.ui.home包中的Feed.kt文件。

7e0c57954d8c343c.png

在该文件中,找到负责组合零食列表的SnackCollectionList可组合函数。

您需要检查数据是否已准备就绪。您知道,在内容准备就绪之前,您会从​​snackCollections参数中获得一个空列表,因此您可以使用ReportDrawnWhen可组合函数,该函数将在谓词为真时处理报告。

ReportDrawnWhen { snackCollections.isNotEmpty() }

Box(modifier) {
   LazyColumn {
   // ...
}

或者,您也可以使用ReportDrawnAfter{}可组合函数,该函数接受suspend函数并等待此函数完成。这样,您可以等待一些数据异步加载或某些动画完成。

完成此操作后,您需要调整ExampleStartupBenchmark以等待内容,否则基准测试将以第一个渲染的帧结束,并且可能会跳过指标。

当前的启动基准测试仅等待第一个渲染的帧。等待本身包含在startActivityAndWait()函数中。

@Test
fun startup() = benchmarkRule.measureRepeated(
   packageName = "com.example.macrobenchmark_codelab",
   metrics = listOf(StartupTimingMetric()),
   iterations = 5,
   startupMode = StartupMode.COLD,
) {
   pressHome()
   startActivityAndWait()

   // TODO wait until content is ready
}

在我们的例子中,您可以等到内容列表有一些子项,因此请添加wait(),如下面的代码片段所示

@Test
fun startup() = benchmarkRule.measureRepeated(
   //...
) {
   pressHome()
   startActivityAndWait()

   val contentList = device.findObject(By.res("snack_list"))
   val searchCondition = Until.hasObject(By.res("snack_collection"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(searchCondition, 5_000)
}

解释代码片段中发生的事情

  1. 我们使用Modifier.testTag("snack_list")找到零食列表。
  2. 我们定义了使用snack_collection作为要等待的元素的搜索条件。
  3. 我们使用UiObject2.wait函数在 UI 对象中等待条件,超时时间为 5 秒。

现在,您可以再次运行基准测试,库将自动测量timeToInitialDisplaytimeToFullDisplay,如下面的屏幕截图所示

8be36fd6991951a4.png

您可以看到,在我们的例子中,TTID 和 TTFD 之间的差异为**413ms**。这意味着,即使您的用户在**319.4ms**内看到第一个渲染的帧,他们也需要额外等待**413ms**才能滚动列表。

10. 基准测试帧计时

用户进入您的应用后,他们遇到的第二个指标是应用的流畅度。或者用我们的术语来说,就是应用是否丢帧。为了测量它,我们将使用FrameTimingMetric

假设您想测量项目列表的滚动行为,并且不想测量该场景之前的任何内容。您需要将基准测试拆分为测量的和未测量的交互。为此,我们将使用setupBlock lambda 参数。

在未测量的交互(在setupBlock中定义)中,我们将启动默认 Activity,在测量的交互(在measureBlock中定义)中,我们将找到 UI 列表元素,滚动列表并等待屏幕渲染内容。如果您没有将交互拆分为这两个部分,您将无法区分应用启动期间产生的帧和列表滚动期间产生的帧。

创建帧计时基准测试

为了实现上述流程,让我们创建一个新的名为ScrollBenchmarks的类,其中包含一个名为scroll()的测试,该测试将包含滚动帧时间基准测试。首先,您需要创建包含基准规则和空测试方法的测试类。

@RunWith(AndroidJUnit4::class)
class ScrollBenchmarks {
   @get:Rule
   val benchmarkRule = MacrobenchmarkRule()

   @Test
   fun scroll() {
       // TODO implement scrolling benchmark
   }
}

然后,添加包含所需参数的基准框架。

@Test
fun scroll() {
   benchmarkRule.measureRepeated(
       packageName = "com.example.macrobenchmark_codelab",
       iterations = 5,
       metrics = listOf(FrameTimingMetric()),
       startupMode = StartupMode.COLD,
       setupBlock = {
           // TODO Add not measured interactions.
       }
   ) {
       // TODO Add interactions to measure list scrolling.
   }
}

该基准测试使用与startup基准测试相同的参数,除了metrics参数和setupBlockFrameTimingMetric收集应用程序生成的帧的时间。

现在,让我们填充setupBlock。如前所述,在该 lambda 表达式中,交互不会由基准测试测量。您可以使用此块仅打开应用程序并等待渲染第一帧。

@Test
fun scroll() {
   benchmarkRule.measureRepeated(
       packageName = "com.example.macrobenchmark_codelab",
       iterations = 5,
       metrics = listOf(FrameTimingMetric()),
       startupMode = StartupMode.COLD,
       setupBlock = {
           // Start the default activity, but don't measure the frames yet
           pressHome()
           startActivityAndWait()
       }
   ) {
       // TODO Add interactions to measure list scrolling.
   }
}

现在,让我们编写measureBlock(最后一个 lambda 参数)。首先,由于向零食列表提交项目是异步操作,因此您应该等到内容准备好。

benchmarkRule.measureRepeated(
   // ...
) {
    val contentList = device.findObject(By.res("snack_list"))

    val searchCondition = Until.hasObject(By.res("snack_collection"))
    // Wait until a snack collection item within the list is rendered
    contentList.wait(searchCondition, 5_000)

   // TODO Scroll the list
}

可选地,如果您不关心测量初始布局设置,可以在setupBlock中等待内容就绪。

接下来,将手势边距设置为零食列表。您需要这样做,因为否则应用程序可能会触发系统导航并退出您的应用程序,而不是滚动内容。

benchmarkRule.measureRepeated(
   // ...
) {
   val contentList = device.findObject(By.res("snack_list"))

   val searchCondition = Until.hasObject(By.res("snack_collection"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(searchCondition, 5_000)

   // Set gesture margin to avoid triggering system gesture navigation
   contentList.setGestureMargin(device.displayWidth / 5)

   // TODO Scroll the list
}

最后,您实际上使用fling()手势滚动列表(您也可以根据需要滚动多少和多快使用scroll()swipe())并等待 UI 变为空闲。

benchmarkRule.measureRepeated(
   // ...
) {
   val contentList = device.findObject(By.res("snack_list"))

   val searchCondition = Until.hasObject(By.res("snack_collection"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(searchCondition, 5_000)

   // Set gesture margin to avoid triggering gesture navigation
   contentList.setGestureMargin(device.displayWidth / 5)

   // Scroll down the list
   contentList.fling(Direction.DOWN)

   // Wait for the scroll to finish
   device.waitForIdle()
}

库将在执行定义的操作时测量我们的应用程序生成的帧的时间。

现在,基准测试已准备好运行。

运行基准测试

您可以像运行启动基准测试一样运行该基准测试。单击测试旁边的装订线图标,然后选择“运行‘scroll()’”。

f6de7ca444ae9136.png

如果您需要有关运行基准测试的更多信息,请查看运行基准测试步骤。

理解结果

FrameTimingMetric以毫秒为单位输出帧的持续时间(frameDurationCpuMs),分别位于第 50、90、95 和 99 个百分位数。在 Android 12(API 级别 31)及更高版本上,它还会返回帧超出限制的时间(frameOverrunMs)。该值可以为负数,这意味着还有额外的时间来生成帧。

81938ed1001973bc.png

您可以从结果中看到,在 Google Pixel 7 上创建帧的中位数(P50)为3.8 毫秒,比帧时间限制低 6.4 毫秒。但是,在超过 99 个百分位数(P99)的百分位数中,可能有一些帧被跳过,因为帧生成需要35.7 毫秒,比限制高 33.2 毫秒。

与应用程序启动结果类似,您可以单击iteration以打开在基准测试期间记录的系统跟踪,并调查导致最终时间的原因

11. 恭喜

恭喜您已成功完成此关于使用 Jetpack Macrobenchmark 测量性能的 codelab!

下一步是什么?

查看使用基线配置文件改进应用程序性能 codelab。此外,请查看我们的性能示例 Github 存储库,其中包含 Macrobenchmark 和其他性能示例。

参考文档