性能分析

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

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

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

步骤:

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

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

  3. 将结果保存到磁盘;

  4. 运行测试;

  5. 检查结果。

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

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

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

2. 记录应用程序的性能

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

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

当运行一个以上的 traceAction 的时候需要指定 reportKey。默认情况下,所有的 Timelines 都会存在 timeline 里,在这个例子中,reportKey 被修改为了 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. 将结果保存到磁盘

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

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

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

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

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

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

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

        // 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 文件,不过你也可以通过这个例子里的方法重写为生成摘要。

4. 运行测试

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

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

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

5. 检查结果

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

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

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

摘要的示例

{
  "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
  ]
}

完整样例

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

  testWidgets('Counter increments smoke test', (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:flutter_driver/flutter_driver.dart' as driver;
import 'package:integration_test/integration_test_driver.dart';

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

        // 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,
        );
      }
    },
  );
}