Введение
В процессе разработки часто возникает потребность протестировать в браузере производительность приложения. Такое тестирование способствует обнаружению потенциальных ошибок, замедляющих его работу. В данной статье вашему вниманию будет предложен способ тестирования производительности в Chrome.
Образец приложения
В нашем случае используется простое приложение, включающее в себя панель приложения, плавающую кнопку действия и бесконечный список элементов, который также показывает число нажатий кнопки.
У приложения есть вторая страница, содержащая определенную информацию.
Скопировать приложение можно здесь:
Что будем тестировать?
Наша цель — протестировать производительность приложения в следующих сценариях использования:
- Пользователь прокручивает бесконечный список.
- Пользователь переключается между двумя страницами.
- Пользователь нажимает плавающую кнопку действия.
Установка фреймворка
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, чтобы избежать внесения изменений, сильно влияющих на производительность.
- Вы также можете настроить информационную панель, отслеживающую тенденции тестов производительности.
Читайте также:
- Создаём расширение для Chrome
- Как добавить в проект тестирование скриншотов с Cypress
- Тестирование сервиса ASP.NET Core с помощью xUnit
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Tianguang Zhang: Performance testing on the web