使用 Macrobenchmark 检查应用性能

使用 Macrobenchmark 检查应用性能

מידע על Codelab זה

subjectהעדכון האחרון: נוב׳ 7, 2022
account_circleנכתב על ידי Android 性能团队

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 和其他性能示例。

参考文档