使用 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. 选择文件夹 [下载位置]/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 模块向导将其添加到项目中。

打开新的模块对话框(例如,在项目面板中右键单击您的项目或模块,然后选择新建 > 模块)。

65fb489132d02a87.png

模板窗格中选择基准测试,确保选中Macrobenchmark 作为基准测试模块类型,并检查详细信息是否符合您的预期

Benchmark module type Macrobenchmark is selected.

  • 目标应用 - 将被基准测试的应用
  • 模块名称 - 基准测试 Gradle 模块的名称
  • 包名称 - 基准测试的包名称
  • 最低 SDK - 至少需要 Android 6(API 级别 23)或更高版本

单击完成

模块向导所做的更改

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

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

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

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

此构建类型应尽可能模拟您的 release 构建类型。与 release 构建类型的区别在于 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 元素、滚动、滑动等),并且宏基准测试将在此块期间收集定义的 metrics

如何编写基准测试操作

宏基准测试会重新安装并重新启动您的应用程序。确保您编写与应用程序状态无关的交互。宏基准测试提供了一些有用的函数和参数来与您的应用程序进行交互。

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

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

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

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

6. 运行基准测试

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

  1. 选择 **构建变体** 面板
  2. 将 **活动构建变体** 更改为 **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).

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

现在,您可以运行基准测试 - 与运行已测测试的方式相同。您可以使用它旁边的代码行图标运行测试函数或整个类。

a1d075775c06d621.png

确保您选择了物理设备,因为在 Android 模拟器上运行基准测试会在运行时失败,并显示一条警告消息,指出这会产生不正确的结果。虽然从技术上讲您可以在模拟器上运行它,但实际上您是在测量主机性能 - 如果主机负载过重,您的基准测试将执行得更慢,反之亦然。

803c3cbd5b288612.png

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

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

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

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

  1. 从运行菜单中选择“编辑配置...”: 55cb46ef01ca5358.png
  2. 从打开的窗口中,选择“选项”图标 1bd68a321477e4fc.png位于“仪器参数” 36c04579f01b1e44.png旁边
  3. 通过点击➕ 并键入详细信息来添加仪器附加参数 35148c9ac7a414fd.png
  4. 点击 **确定** 确认选择。您应该在“仪器参数”行中看到该参数 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() 调用。

从 **项目** 面板中打开 Feed.kt 文件,该文件位于 .ui.home 包中。

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 中定义),我们将启动默认活动,而在已测量交互中(在 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.8ms**,低于帧时间限制 6.4ms。但是,在第 99 个百分位数(**P99**)以上可能出现了一些跳帧,因为帧生成时间为 **35.7ms**,超出限制 33.2ms。

类似于应用程序启动结果,你可以单击 iteration 以打开基准测试期间记录的系统跟踪,并 调查导致这些计时结果的原因

11. 恭喜

恭喜,你已经成功完成了使用 Jetpack Macrobenchmark 测量性能的此代码实验室!

接下来做什么?

查看 使用基准配置文件改进应用程序性能 代码实验室。此外,查看我们包含 Macrobenchmark 和其他性能示例的 性能示例 Github 存储库。

参考文档