性能分析

性能移动应用用户来说相当重要,用户希望应用程序有流畅的滚动和优雅的动画,不愿看到卡顿和掉帧现象。我们如何确保我们的应用程序在各种设备上不会受到卡顿的影响?

When it comes to mobile apps, performance is critical to user experience. Users expect apps to have smooth scrolling and meaningful animations free of stuttering or skipped frames, known as “jank”. How to ensure that your app is free of jank on a wide variety of devices?

以下两种方式可供选择:首先,我们可以在不同的设备对应用程序进行手动测试。这种方式适用于较小的应用程序,但随着应用程序扩展性的提升,它将变得更加繁琐。另外,我们可以运行集成测试,执行特定任务并记录性能时间轴。然后,我们可以检验结果,以确定是否需要对我们应用程序的特定部分进行改善。

There are two options: first, manually test the app on different devices. While that approach might work for a smaller app, it becomes more cumbersome as an app grows in size. Alternatively, run an integration test that performs a specific task and records a performance timeline. Then, examine the results to determine whether a specific section of the app needs to be improved.

在本文中,我们将学习如何在执行特定任务时编写记录性能时间轴的测试,并将结果的摘要保存到本地文件中。

In this recipe, learn how to write a test that records a performance timeline while performing a specific task and saves a summary of the results to a local file.

步骤:

This recipe uses the following steps:

  1. 编写一个滚动列表的测试项目;

    Write a test that scrolls through a list of items.

  2. 记录应用程序的性能;

    Record the performance of the app.

  3. 将结果保存到磁盘;

    Save the results to disk.

  4. 运行测试;

    Run the test.

  5. 检查结果。

    Review the results.

1. 编写一个滚动列表的测试项目

1. Write a test that scrolls through a list of items

在这一小节,我们将记录当滚动列表条目时应用程序的性能。为了专注于性能分析,这一小节在组件测试中 Scrolling in integration tests(列表滚动集成测试) 的基础上进行。

In this recipe, record the performance of an app as it scrolls through a list of items. To focus on performance profiling, this recipe builds on the Scrolling recipe in widget tests.

请按照基础章节的指南新建一个应用程序,编写一个测试程序。最终,确保应用程序按预期运行。

Follow the instructions in that recipe to create an app and write a test to verify that everything works as expected.

2. 记录应用程序的性能

2. Record the performance of the app

然后,我们需要再应用程序的列表滚动的时候记录它的性能。使用 IntegrationTestWidgetsFlutterBinding 类中的 traceAction() 方法实现这项功能。

Next, record the performance of the app as it scrolls through the list. Perform this task using the traceAction() method provided by the IntegrationTestWidgetsFlutterBinding class.

这种方式运行提供的方法,并将应用程序性能的详细信息记录在 Timeline 中。在这个示例中,我们提供一个方法,用以滚动列表的条目并确保指定条目是否被显示出来。当方法执行完成的时候,traceAction() 会返回一个 Timeline

This method runs the provided function and records a Timeline with detailed information about the performance of the app. This example provides a function that scrolls through the list of items, ensuring that a specific item is displayed. When the function completes, the traceAction() creates a report data Map that contains the Timeline.

当运行一个以上的 traceAction 的时候需要指定 reportKey。默认情况下,所有的 Timelines 都会存在 timeline 里,在这个例子中,reportKey 被修改为了 scrolling_timeline:

Specify the reportKey when running more than one traceAction. By default all Timelines are stored with the key timeline, in this example the reportKey is changed to scrolling_timeline.

await binding.traceAction(
  () async {
    // Scroll until the item to be found appears.
    await tester.scrollUntilVisible(
      itemFinder,
      500.0,
      scrollable: listFinder,
    );
  },
  reportKey: 'scrolling_timeline',
);

3. 将结果保存到磁盘

3. Save the results to disk

我们已经获取了一个性能时间轴,我们需要一种方式来对它进行检验, Timeline 对象提供所有已发生事件的相关详细信息,但它不提供快捷方式查看结果。

Now that you’ve captured a performance timeline, you need a way to review it. The Timeline object provides detailed information about all of the events that took place, but it doesn’t provide a convenient way to review the results.

因此,我们可以将 Timeline 转换成 TimelineSummaryTimelineSummary 通过执行两个任务可以使我们更容易的检查结果:

Therefore, convert the Timeline into a TimelineSummary. The TimelineSummary can perform two tasks that make it easier to review the results:

  1. 将一个 json 文件写入磁盘,它包含了 Timeline 中包含的数据的摘要。此摘要包括掉帧数量,最慢构建时间等的信息。

    Writing a json document on disk that summarizes the data contained within the Timeline. This summary includes information about the number of skipped frames, slowest build times, and more.

  2. 它可以将完整的 Timeline 以 json 文件的形式存储在磁盘上,可以使用 Chrome 浏览器的追踪工具打开此文件。追踪工具在这里: chrome://tracing

    Saving the complete Timeline as a json file on disk. This file can be opened with the Chrome browser’s tracing tools found at chrome://tracing.

为了捕获结果内容,需要在 test_driver 文件夹中新建一个 perf_driver.dart 文件,并加入如下代码:

To capture the results, create a file named perf_driver.dart in the test_driver folder and add the following code:

import 'package:integration_test/integration_test_driver.dart';
import 'package:flutter_driver/flutter_driver.dart' as driver;

Future<void> main() {
  return integrationDriver(
    responseDataCallback: (data) async {
      if (data != null) {
        final timeline = driver.Timeline.fromJson(data['scrolling_timeline']);

        // Convert the Timeline into a TimelineSummary that's easier to
        // read and understand.
        final summary = driver.TimelineSummary.summarize(timeline);

        // Then, write the entire timeline to disk in a json format.
        // This file can be opened in the Chrome browser's tracing tools
        // found by navigating to chrome://tracing.
        // Optionally, save the summary to disk by setting includeSummary
        // to true
        await summary.writeTimelineToFile(
          'scrolling_timeline',
          pretty: true,
          includeSummary: true,
        );
      }
    },
  );
}

你可以自定义 integrationDriver 函数的 responseDataCallback 方法,默认情况下,它会将结果写入 integration_response_data.json 文件,不过你也可以通过这个例子里的方法重写为生成摘要。

The integrationDriver function has a responseDataCallback which you can customize. By default, it writes the results to the integration_response_data.json file, but you can customize it to generate a summary like in this example.

4. 运行测试

4. Run the test

在我们为了捕获一个性能 Timeline 配置了测试代码,并且将结果的摘要保存在了磁盘上,我们可以使用以下命令运行测试代码:

After configuring the test to capture a performance Timeline and save a summary of the results to disk, run the test with the following command:

flutter drive \
  --driver=test_driver/perf_driver.dart \
  --target=integration_test/scrolling_test.dart \
  --profile

--profile 命令行选项代表着应用将以 profile 模式 (性能模式) 运行,这种模式下运行的应用会比 debug 模式更接近最终用户的体验。

The --profile option means to compile the app for the “profile mode” rather than the “debug mode”, so that the benchmark result is closer to what will be experienced by end users.

5. 检查结果

5. Review the results

在测试代码运行成功以后,在项目根目录下的 build 文件夹里包含以下两个文件:

After the test completes successfully, the build directory at the root of the project contains two files:

  1. scrolling_summary.timeline_summary.json 包含摘要。可以使用任何文本编辑器打开它并查看其中包含的信息。通过更高级的设置,我们可以在每次测试时保存摘要并创建一个结果图。

    scrolling_summary.timeline_summary.json contains the summary. Open the file with any text editor to review the information contained within. With a more advanced setup, you could save a summary every time the test runs and create a graph of the results.

  2. scrolling_timeline.timeline.json 包含完整的时间轴数据。使用 Chorme 浏览器的追踪工具打开这个文件。追踪工具在这里:chrome://tracing。追踪工具提供了一个便捷的用户界面,用以检测时间轴数据并发现其中导致性能问题的源头。

    scrolling_timeline.timeline.json contains the complete timeline data. Open the file using the Chrome browser’s tracing tools found at chrome://tracing. The tracing tools provide a convenient interface for inspecting the timeline data to discover the source of a performance issue.

摘要的示例

Summary example

{
  "average_frame_build_time_millis": 4.2592592592592595,
  "worst_frame_build_time_millis": 21.0,
  "missed_frame_build_budget_count": 2,
  "average_frame_rasterizer_time_millis": 5.518518518518518,
  "worst_frame_rasterizer_time_millis": 51.0,
  "missed_frame_rasterizer_budget_count": 10,
  "frame_count": 54,
  "frame_build_times": [
    6874,
    5019,
    3638
  ],
  "frame_rasterizer_times": [
    51955,
    8468,
    3129
  ]
}

完整样例

Complete example

integration_test/scrolling_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import 'package:scrolling/main.dart';

void main() {
  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
      as IntegrationTestWidgetsFlutterBinding;

  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(MyApp(
      items: List<String>.generate(10000, (i) => "Item $i"),
    ));

    final listFinder = find.byType(Scrollable);
    final itemFinder = find.byKey(const ValueKey('item_50_text'));

    await binding.traceAction(
      () async {
        // Scroll until the item to be found appears.
        await tester.scrollUntilVisible(
          itemFinder,
          500.0,
          scrollable: listFinder,
        );
      },
      reportKey: 'scrolling_timeline',
    );
  });
}

test_driver/perf_driver.dart

import 'package:integration_test/integration_test_driver.dart';
import 'package:flutter_driver/flutter_driver.dart' as driver;

Future<void> main() {
  return integrationDriver(
    responseDataCallback: (data) async {
      if (data != null) {
        final timeline = driver.Timeline.fromJson(data['scrolling_timeline']);

        // Convert the Timeline into a TimelineSummary that's easier to
        // read and understand.
        final summary = driver.TimelineSummary.summarize(timeline);

        // Then, write the entire timeline to disk in a json format.
        // This file can be opened in the Chrome browser's tracing tools
        // found by navigating to chrome://tracing.
        // Optionally, save the summary to disk by setting includeSummary
        // to true
        await summary.writeTimelineToFile(
          'scrolling_timeline',
          pretty: true,
          includeSummary: true,
        );
      }
    },
  );
}