Введение 

В процессе разработки часто возникает потребность протестировать в браузере производительность приложения. Такое тестирование способствует обнаружению потенциальных ошибок, замедляющих его работу. В данной статье вашему вниманию будет предложен способ тестирования производительности в Chrome.

Образец приложения 

В нашем случае используется простое приложение, включающее в себя панель приложения, плавающую кнопку действия и бесконечный список элементов, который также показывает число нажатий кнопки. 

У приложения есть вторая страница, содержащая определенную информацию. 

Скопировать приложение можно здесь: 

Что будем тестировать? 

Наша цель  —  протестировать производительность приложения в следующих сценариях использования:

  1. Пользователь прокручивает бесконечный список. 
  2. Пользователь переключается между двумя страницами. 
  3. Пользователь нажимает плавающую кнопку действия. 

Установка фреймворка 

dependencies:
  flutter:
    sdk: flutter
  web_benchmarks_framework:
    git:
      url: https://github.com/material-components/material-components-flutter-experimental.git
      ref: f6ebb4ed3b6489547d9ae58216df9999112be568
      path: web_benchmarks_framework

К pubspec.yaml добавьте следующее: 

Эта зависимость входит в минимальный пакет web_benchmarks_framework, выполняющий тестирование производительности в Chrome. 

Он сформирован из macrobenchmarks и devicelab  —  двух пакетов, используемых Flutter для веб-тестирования производительности во Flutter Gallery. В настоящее время оба этих пакета применяются для аналогичного тестирования во flutter/flutter, поэтому проще импортировать более общий пакет web_benchmarks_framework

Выполните flutter pub get, чтобы присоединить этот пакет. 

Написание первого теста 

Под lib добавьте директорию benchmarks, а к ней  —  файл .dart с именем runner.dart:

Содержит этот файл следующее: 

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:web_benchmarks_framework/recorder.dart';
import 'package:web_benchmarks_framework/driver.dart';
import 'package:web_benchmarks_example/main.dart';
import 'package:web_benchmarks_example/homepage.dart' show textKey;

///Регистратор, измеряющий длительность формирования кадра.  
abstract class AppRecorder extends WidgetRecorder {
  AppRecorder({@required this.benchmarkName}) : super(name: benchmarkName);

  final String benchmarkName;

  Future<void> automate();

  @override
  Widget createWidget() {
    Future.delayed(Duration(milliseconds: 400), automate);
    return MyApp();
  }

  Future<void> animationStops() async {
    while (WidgetsBinding.instance.hasScheduledFrame) {
      await Future<void>.delayed(Duration(milliseconds: 200));
    }
  }
}

class ScrollRecorder extends AppRecorder {
  ScrollRecorder() : super(benchmarkName: 'scroll');

  Future<void> automate() async {
    final scrollable = Scrollable.of(find.byKey(textKey).evaluate().single);
    await scrollable.position.animateTo(
      30000,
      curve: Curves.linear,
      duration: Duration(seconds: 20),
    );
  }
}

Future<void> main() async {
  await runBenchmarks(
    {
      'scroll': () => ScrollRecorder(),
    },
  );
}

Что делает этот тест? 

  • При запуске этого приложения создается объект ScrollRecorder, который управляет приложением, выполняя автоматические жесты. В нашем случае он начинает прокручивать бесконечный список. 
  • Класс ScrollRecorder расширяет класс AppRecorder, в свою очередь расширяющий класс WidgetRecorder, который по мере управления приложением также записывает показатели производительности. 
  • runBenchmarks  —  это функция, определяемая в package:web_benchmarks_framework/driver.dart, которая позволяет пользователю выбрать, какой тест выполнять, и отображает результаты в браузере. 
  • Метод automate использует пакет flutter_test, предоставляющий способы выполнения жестов или поиска определенных виджетов в приложении. 

Выполнение первого теста 

В корневой директории проекта выполните flutter run -d chrome -t lib/benchmarks/runner.dart. Таким образом вы укажите Flutter использовать в качестве входной точки не main.dart, а runner.dart.

На данный момент у нас есть всего один тест производительности, так что запустите его нажатием “scroll”. 

После запуска теста список начинает автоматически прокручиваться вниз. 

Спустя несколько секунд тест заканчиваетсяи выводит на экран следующий результат: 

Этот график показывает время, затраченное приложением на отрисовку каждого (зарегистрированного) кадра. Горизонтальная ось отображает ход времени, а вертикальная  —  продолжительность каждого кадра. 

Первые 2/3 графика окрашены в серый фон  —  эти кадры считаются “подготовительными” и в статистику не включаются. Такие кадры обычно обеспечивают JIT-компилятору время для компиляции кода и заполняют различные кэши, чтобы показатели измеренных кадров отражали “итоговую” производительность приложения, а не только первых секунд его работы. Тем не менее подготовительным этапом не всегда следует пренебрегать, поскольку он может предоставить ценную информацию о производительности приложения в течение первых секунд его выполнения, благодаря чему уже можно будет делать выводы о его работоспособности. 

Красные кадры  —  это “выбросы”, отрисовка которых требует гораздо больше времени. Некоторые из них могут быть почти незаметными. Например, до определенного момента не будет видно подвисаний в начале или конце анимации. А вот заторможенный кадр в середине анимации будет сложно не заметить. 

Выбросы служат хорошим индикатором имеющихся неполадок. Дорабатывая приложение, вы можете уменьшить значения выбросов или сократить их число, что будет свидетельствовать о повышении его плавности.

Сбор данных из Chrome DevTools 

Данный тест производительности полностью выполняется внутри Chrome. Добавьте следующий файл в качестве test/run_benchmarks.dart:

import 'dart:convert' show JsonEncoder;

import 'package:web_benchmarks_framework/server.dart';

Future<void> main () async {
  final taskResult = await runWebBenchmark(
    macrobenchmarksDirectory: '.',
    entryPoint: 'lib/benchmarks/runner.dart',
    useCanvasKit: false,
  );
  print (JsonEncoder.withIndent('  ').convert(taskResult.toJson()));
}

Затем запустите dart test/run_benchmarks.dart

Спустя минуту вы увидите следующие результаты: 

Received profile data
{
  "success": true,
  "data": {
    "scroll.html.preroll_frame.average": 93.88659793814433,
    "scroll.html.preroll_frame.outlierAverage": 1061.3333333333333,
    "scroll.html.preroll_frame.outlierRatio": 11.304417847077339,
    "scroll.html.preroll_frame.noise": 0.3103013467989926,
    "scroll.html.apply_frame.average": 391.1914893617021,
    "scroll.html.apply_frame.outlierAverage": 1462.3333333333333,
    "scroll.html.apply_frame.outlierRatio": 3.738152217266761,
    "scroll.html.apply_frame.noise": 0.24804233283684318,
    "scroll.html.drawFrameDuration.average": 1496.8690476190477,
    "scroll.html.drawFrameDuration.outlierAverage": 3622.8125,
    "scroll.html.drawFrameDuration.outlierRatio": 2.4202601461781335,
    "scroll.html.drawFrameDuration.noise": 0.38481902033678567,
    "scroll.html.totalUiFrame.average": 3441
  },
  "benchmarkScoreKeys": [
    "scroll.html.drawFrameDuration.average",
    "scroll.html.drawFrameDuration.outlierRatio",
    "scroll.html.totalUiFrame.average"
  ]
}

Точные значения теста производительности могут варьироваться в зависимости от компьютера. 

Что делает этот тест? 

  • При выполнении test/run_benchmarks.dart собирается веб-приложение, затем запускается экземпляр Chrome, в котором это приложение выполняется.
  • После этого test/run_benchmarks.dart соединяется с портом Chrome DevTools, собирая и извлекая из него соответствующие показатели производительности.

Что означают результаты теста? 

  • При отображении кадра происходит двойной обход дерева слоев
  • “Preroll”  —  это первый обход. Он ничего не отображает, но вычисляет значения, которые позже используются для отрисовки. Примеры включают: матрицы преобразований, обратные преобразования и обрезку кадров. 
  • “Apply frame”  —  это второй обход, при котором отображается UI.
  • “Draw frame”  —  это общее время, необходимое фреймворку для отображения кадра. Этот этап включает “Preroll” и “Apply frame”, а также время, затраченное на создание и расположение виджетов.
  • “Total UI frame” включает все составляющие компоненты “Draw frame”, а также скрытую работу, выполняемую браузером: обновления дерева слоев, пересчет стилей и браузерный макет (не путать с собственным макетом Flutter).
  • Когда набор данных (список временных интервалов) собран, алгоритм удаляет выбросы. 
  • Сначала вычисляются среднее и стандартное отклонения данных, и любая точка данных, превышающая эти значения (среднее + 1 стандартное отклонение), считается выбросом. 
  • Затем с помощью среднего и стандартного отклонения данных без выбросов (чистых данных) вычисляются средние значения и шум всего набора, которые впоследствии сообщаются. 
  • Кроме того, сообщается средний показатель всех выбросов, а также отношение этого показателя к чистым данным.
  • Показатели “outlierRatio” и “noise” для каждого набора данных отчетливо указывают, сколько шума содержится в производительности приложения. Наличие чрезмерного шума свидетельствует о проблемах с последовательностью выполнения (например, торможение кадров в результате задержек сборки мусора). Уменьшив шум, вы сможете повысить плавность работы приложения.

Добавляем больше тестов 

Отредактируйте lib/benchmarks/runner.dart, чтобы добавить еще два теста. 

Сначала измените функцию main

Future<void> main() async {
  await runBenchmarks(
    {
      'scroll': () => ScrollRecorder(),
      'page': () => PageRecorder(),
      'tap': () => TapRecorder(),
    },
  );
}

Затем добавьте еще два класса, расширяющих AppRecorder

class PageRecorder extends AppRecorder {
  PageRecorder() : super(benchmarkName: 'page');

  bool _completed = false;

  @override
  bool shouldContinue() => profile.shouldContinue() || !_completed;

  Future<void> automate() async {
    final controller = LiveWidgetController(WidgetsBinding.instance);
    for (int i = 0; i < 10; ++i) {
      print('Testing round $i...');
      await controller.tap(find.byKey(aboutPageKey));
      await animationStops();
      await controller.tap(find.byKey(backKey));
      await animationStops();
    }
    _completed = true;
  }
}

class TapRecorder extends AppRecorder {
  TapRecorder() : super(benchmarkName: 'tap');

  bool _completed = false;

  @override
  bool shouldContinue() => profile.shouldContinue() || !_completed;

  Future<void> automate() async {
    final controller = LiveWidgetController(WidgetsBinding.instance);
    for (int i = 0; i < 10; ++i) {
      print('Testing round $i...');
      await controller.tap(find.byIcon(Icons.add));
      await animationStops();
    }
    _completed = true;
  }
}

Что делают эти тесты? 

  • Мы добавили два оставшихся теста: один проверяет переключение между страницами, другой  —  касание плавающей кнопки действия.
  • animationStops систематически контролирует выполнение анимации и останавливается по мере ее полного прекращения. Это гарантирует, например, успешный переход на страницу “about”.
  • В тестах для проверки “page” и “tap” логический тип _completed отслеживает завершение автоматических жестов. 
  • В указанных тестах переопределение метода shouldContinue приводит к тому, что AppRecorder останавливает запись кадров после завершения всех жестов. 

Как выполнять эти тесты? 

Для запуска этих тестов (и просмотра анимации) в Chrome выполните: 

  • flutter run -d chrome -t lib/benchmarks/runner.dart --profile.

Для запуска данных тестов и сбора данных DevTools выполните:

  • dart test/run_benchmarks.dart.

Что дальше? 

Теперь, владея способом сбора данных производительности, вы можете использовать его по своему усмотрению:

  • Вы можете настроить задачу в CI, запускающую эти тесты производительности каждый раз, как кто-нибудь отправляет PR, чтобы избежать внесения изменений, сильно влияющих на производительность.  
  • Вы также можете настроить информационную панель, отслеживающую тенденции тестов производительности.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Tianguang Zhang: Performance testing on the web

Предыдущая статья5 доказательств силы итерируемых объектов в Python
Следующая статьяТОП-5 признаков качественного продуктового дизайна