使用 Macrobenchmark 检查应用性能

1. 准备工作

在此 Codelab 中,您将学习如何使用 Macrobenchmark 库。您将衡量应用启动时间(用户互动度的关键指标)和帧时间(提示应用中可能会出现卡顿的位置)。

所需条件

实践内容

  • 将基准化分析模块添加到现有应用
  • 衡量应用的启动时间和帧时间

学习内容

  • 可靠地衡量应用性能

2. 准备工作

首先,从命令行使用以下命令克隆 GitHub 代码库:

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

或者,您也可以下载两个 ZIP 文件:

在 Android Studio 中打开项目

  1. 在“Welcome to Android Studio”窗口中,选择 c01826594f360d94.png Open an Existing Project
  2. 选择文件夹 [Download Location]/android-performance/benchmarking(提示:务必选择包含 build.gradlebenchmarking 目录)
  3. Android Studio 导入项目后,请确保您可以运行 app 模块,从而构建我们要进行基准测试的示例应用。

3. Jetpack Macrobenchmark 简介

Jetpack Macrobenchmark 库可以衡量大型最终用户互动(例如启动、与界面的互动和动画)的性能。此库可让您直接控制受测试的性能环境。通过控制应用的编译、启动和停止,您可以直接衡量应用启动、帧时间和所跟踪的代码段。

借助 Jetpack Macrobenchmark,您可以执行以下操作:

  • 使用确定的启动模式和滚动速度,对应用进行多次测量
  • 对多次运行测试所得的结果取平均值,从而消除性能差异
  • 控制应用的编译状态(这是保持性能稳定的一项主要因素)
  • 在本地重现 Google Play 商店执行的安装时优化,从而更好地了解实际性能

使用此库的插桩不会直接调用您的应用代码,而是会像用户一样在您的应用中导航(轻触、点击、滑动等)。衡量操作会在这些互动期间在设备上进行。如果您想直接衡量部分应用代码,请改为参阅 Jetpack Microbenchmark

基准的编写方式与插桩测试一样,只不过您不需要验证应用所处的状态。基准使用 JUnit 语法(@RunWith@Rule@Test 等),但测试将在单独的进程中运行,以允许对您的应用进行重启或预编译。这样,我们就可以像用户一样运行您的应用,而不会干扰其内部状态。为此,我们使用 UiAutomator 与目标应用进行互动。

示例应用

在此 Codelab 中,您将使用 JetSnack 示例应用。这是一款使用 Jetpack Compose 的虚拟零食订购应用。您无需了解与此应用的架构方式相关的详细信息,即可衡量其性能。您需要了解此应用的行为和界面结构,以便通过基准访问各个界面元素。请运行此应用,然后通过订购您喜欢的零食来熟悉其中的基本界面。

70978a2eb7296d54.png

4. 添加 Macrobenchmark 库

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

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

54a3ec4a924199d6.png

Templates 窗格中选择 Benchmark,确保已选择 Macrobenchmark 作为基准模块类型,并检查详细信息是否符合预期:

已选择基准模块类型“Macrobenchmark”。

  • Target application - 要进行基准测试的应用
  • Module name - 基准化分析 Gradle 模块的名称
  • Package name - 基准的软件包名称
  • Minimum SDK - 至少需要 Android 6(API 级别 23)或更高版本

点击 Finish

模块向导所做的更改

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

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

向导还会对您选择的目标应用模块进行更改。具体而言,它会将新的 benchmark build 类型添加到 :app 模块 build.gradle,如以下代码段所示:

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> 的用途,请参阅我们的文档

向导的最后一项任务是创建一个 Scaffold,以便对启动时间进行基准测试(我们将在下一步骤中使用)。

现在,您可以开始编写基准了。

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、点击界面元素、滚动、滑动等),并且 Macrobenchmark 会收集在此块中定义的 metrics

如何编写基准操作

Macrobenchmark 将重新安装并重启您的应用。请确保将互动编写为独立于应用状态。Macrobenchmark 提供了多种用于与应用互动的实用函数和形参。

最重要的函数是 startActivityAndWait()。此函数将启动您的默认 activity,并等待其呈现第一帧,然后再继续按照基准中的说明进行测试。如果您要启动其他 activity 或调整启动 intent,可以使用可选的 intentblock 形参来实现此目的。

另一个实用函数是 pressHome()。如果您在每次迭代时未终止应用(例如,当您使用 StartupMode.HOT 时),可以使用此函数将基准重置为基本条件。

对于任何其他互动,您可以使用 device 形参,通过此形参,您可以查找界面元素、滚动、等待某些内容或执行其他操作。

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

6. 运行基准

在运行基准测试之前,请确保您在 Android Studio 中选择了正确的 build 变体:

  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).

您可以使用插桩实参 androidx.benchmark.suppressErrors = "DEBUGGABLE" 暂时抑制此错误。您可以遵循与在 Android 模拟器上运行基准相同的步骤。

现在,您可以运行基准了,方法与运行插桩测试相同。您可以运行测试函数或整个类(使用其旁边的边线图标)。

e72cc74b6fecffdb.png

请确保您已选择实体设备,因为在 Android 模拟器上运行基准会在运行时失败并显示一条警告,提示说测试会显示错误结果。虽然从技术层面来说,您可以在模拟器上运行,但从根本上说,这时您衡量的是宿主机的性能。如果宿主机负载较高,基准测试就会进展缓慢,反之亦然。

e28a1ff21e9b45b4.png

一旦您运行基准,您的应用就会重新构建,然后运行基准。这些基准将根据您定义的 iterations 多次启动、停止甚至重新安装您的应用。

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

如果您没有实体设备,并且仍想运行基准,可以使用插桩实参 androidx.benchmark.suppressErrors = "EMULATOR" 来抑制运行时错误

如需抑制错误,请修改运行配置:

  1. 从运行菜单中选择“Edit Configurations…”:354500cd155dec5b.png
  2. 在打开的窗口中,选择“Instrumentation arguments”旁边的“选项”图标 d628c071dd2bf454.png a4c3519e48f4ac55.png
  3. 点击 ➕ 并输入详细信息,以添加插桩额外形参 a06c7f6359d6b92c.png
  4. 点击 OK 确认选择。您应该会在“Instrumentation arguments”行中看到相应实参 c30baf54c420ed79.png
  5. 点击 Ok 确认运行配置。

或者,如果您需要将它永久保留在代码库中,可以在 :macrobenchmark 模块的 build.gradle 中执行相关操作:

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

8. 了解启动测试结果

基准运行完毕后,它将直接在 Android Studio 中提供测试结果,如以下屏幕截图所示:

f934731d29dcd25c.png

您可以看到,在本例中,对于 Google Pixel 7 上的启动时间,最小值为 294.8 毫秒,中位数为 301.5 毫秒,最大值为 314.8 毫秒。请注意,在您的设备上运行相同的基准时,结果可能会有所不同。结果可能会受许多因素的影响,例如:

  • 设备性能如何
  • 设备使用的是哪个系统版本
  • 哪些应用在后台运行

因此,请务必比较在同一设备上测得的结果,最好是在相同状态下测得的结果,否则您可能会发现测试结果存在巨大差异。如果您无法保证状态相同,可能需要增加 iterations 的数量,以便正确处理结果离群值。

为便于调查,Macrobenchmark 库会在执行基准测试期间记录系统跟踪数据。为方便起见,Android Studio 会将每次迭代和衡量的时间标记为指向相应系统跟踪记录的链接,以便您轻松打开进行研究。

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

Macrobenchmark 可以自动衡量您的应用呈现第一帧所需的时间 (timeToInitialDisplay)。不过,直到第一帧呈现之后,应用内容仍未完成加载的情况也很常见,而您可能想要了解用户需要等待多长时间才能使用应用。此等待时间称为完全显示所用时间,即应用内容已全部加载,且用户可以与应用互动所需的时间。Macrobenchmark 库可以自动检测此时间,但您需要使用 Activity.reportFullyDrawn() 函数来调整应用,使其向 Macrobenchmark 库传达完全显示发生的时间。

此示例会显示一个简单的进度条,直到数据加载完成为止。因此,您需要等待,直到数据准备就绪且系统列出并绘制零食列表。我们来调整示例应用并添加 reportFullyDrawn() 调用。

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

800f7390ca53998d.png

在此文件中,找到负责编写零食列表的 SnackCollectionList 可组合项。

您需要检查数据是否准备就绪。您知道,在内容准备就绪之前,​​snackCollections 形参会为您提供一个空列表,因此您可以使用 ReportDrawnWhen 可组合项,以便在谓词为 true 时处理报告。

ReportDrawnWhen { snackCollections.isNotEmpty() }

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

或者,您也可以使用接受 suspend 函数的 ReportDrawnAfter{} 可组合项,并等待此函数完成运行。这样一来,您就可以等待一些数据异步加载,或等待动画播放完毕。

完成后,您需要调整 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 函数在界面对象中等待此条件,并将超时时间设置为 5 秒。

现在,您可以再次运行基准,此时该库会自动衡量 timeToInitialDisplaytimeToFullDisplay,如以下屏幕截图所示:

3655d7199e4f678b.png

您可以看到,在本例中,TTID 与 TTFD 的差值为 413 毫秒。这意味着,即使第一帧只需 319.4 毫秒就能呈现给用户,但用户还需再等待 413 毫秒才能滚动列表。

10. 对帧时间进行基准测试

在用户进入您的应用后,他们遇到的第二个指标就是应用的流畅度。用我们的术语说,就是应用是否会丢帧。为了衡量这一指标,我们将使用 FrameTimingMetric

假设您想要衡量项列表的滚动行为,并且不想衡量滚动情景发生前的任何对象。您需要将基准测试拆分为衡量的互动和不衡量的互动。为此,我们将使用 setupBlock lambda 形参。

在不衡量的互动(在 setupBlock 中定义)中,我们将启动默认 activity;在衡量的互动(在 measureBlock 中定义)中,我们将找到界面列表元素、滚动列表并等待屏幕呈现内容。如果您未将互动分为两部分,则将无法区分在应用启动期间生成的帧和列表滚动期间生成的帧。

创建帧时间基准

为了实现上述流程,我们来创建一个新的 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 形参和 setupBlock 除外。FrameTimingMetric 会收集应用生成的帧的时间。

现在,我们来填写 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(),具体取决于您想要滚动的幅度和速度)来实际滚动列表,并等待界面变为空闲状态。

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()
}

该库将在执行定义的操作时衡量应用生成的帧的时间。

现在,您的基准可以运行了。

运行基准

运行基准的方式与启动基准相同。点击测试旁边的边线图标,然后选择 Run 'scroll()'

30043f8d11fec372.png

如需详细了解如何运行基准,请参阅运行基准步骤。

了解测试结果

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

2e02ba58e1b882bc.png

从结果中可以看出,在 Google Pixel 3 上创建帧的中间值 (P50) 为 3.8 毫秒,低于帧时间限制 6.4 毫秒。此外,在百分位数高于 99 (P99) 的帧中可能存在一些跳过的帧,这是因为生成帧需要 35.7 毫秒,超出了上限 33.2 毫秒。

与应用启动测试结果一样,您可以点击 iteration 以打开在基准测试期间记录的系统跟踪数据,并调查所得时间是在哪些因素的作用下产生的

11. 恭喜

恭喜!您已成功完成此 Codelab,了解了如何使用 Jetpack Macrobenchmark 来衡量性能。

后续操作

查看使用基准配置文件提升应用性能 Codelab。此外,请查看性能示例 GitHub 代码库,其中包含 Macrobenchmark 和其他性能示例。

参考文档