效能分析

效能移動應用使用者來說相當重要,使用者希望應用程式有流暢的捲動和優雅的動畫,不願看到卡頓和掉幀現象。我們如何確保我們的應用程式在各種裝置上不會受到卡頓的影響?

以下兩種方式可供選擇:首先,我們可以在不同的裝置對應用程式進行手動測試。這種方式適用於較小的應用程式,但隨著應用程式擴充套件性的提升,它將變得更加繁瑣。另外,我們可以執行整合測試,執行特定任務並記錄效能時間軸。然後,我們可以檢驗結果,以確定是否需要對我們應用程式的特定部分進行改善。

在本文中,我們將學習如何在執行特定任務時編寫記錄效能時間軸的測試,並將結果的摘要儲存到本地檔案中。

步驟:

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