transitions_perf_test.dart 7.64 KB
Newer Older
1 2 3 4 5
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:convert' show JsonEncoder;
7

8
import 'package:file/file.dart';
9
import 'package:file/local.dart';
10
import 'package:flutter_driver/flutter_driver.dart';
11
import 'package:path/path.dart' as path;
12 13
import 'package:test/test.dart';

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
class Demo {
  const Demo(this.title, {this.synchronized = true, this.profiled = false});

  /// The title of the demo.
  final String title;

  /// True if frameSync should be enabled for this test.
  final bool synchronized;

  // True if timeline data should be collected for this test.
  //
  // Warning: The number of tests executed with timeline collection enabled
  // significantly impacts heap size of the running app. When run with
  // --trace-startup, as we do in this test, the VM stores trace events in an
  // endless buffer instead of a ring buffer.
  final bool profiled;
}

32 33
// Warning: this list must be kept in sync with the value of
// kAllGalleryItems.map((GalleryItem item) => item.title).toList();
34
const List<Demo> demos = const <Demo>[
35
  // Demos
36 37 38 39
  const Demo('Shrine', profiled: true),
  const Demo('Contact profile', profiled: true),
  const Demo('Animation', profiled: true),

xster's avatar
xster committed
40
  // Material Components
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
  const Demo('Bottom navigation', profiled: true),
  const Demo('Buttons', profiled: true),
  const Demo('Cards', profiled: true),
  const Demo('Chips', profiled: true),
  const Demo('Date and time pickers', profiled: true),
  const Demo('Dialog', profiled: true),
  const Demo('Drawer'),
  const Demo('Expand/collapse list control'),
  const Demo('Expansion panels'),
  const Demo('Floating action button'),
  const Demo('Grid'),
  const Demo('Icons'),
  const Demo('Leave-behind list items'),
  const Demo('List'),
  const Demo('Menus'),
  const Demo('Modal bottom sheet'),
  const Demo('Page selector'),
  const Demo('Persistent bottom sheet'),
  const Demo('Progress indicators', synchronized: false),
  const Demo('Pull to refresh'),
  const Demo('Scrollable tabs'),
  const Demo('Selection controls'),
  const Demo('Sliders'),
  const Demo('Snackbar'),
  const Demo('Tabs'),
  const Demo('Text fields'),
  const Demo('Tooltips'),

xster's avatar
xster committed
69
  // Cupertino Components
70 71 72
  const Demo('Activity Indicator', synchronized: false),
  const Demo('Buttons'),
  const Demo('Dialogs'),
73
  const Demo('Navigation'),
74 75
  const Demo('Sliders'),
  const Demo('Switches'),
76

77
  // Media
78
  const Demo('Animated images'),
79

80 81 82
  // Style
  const Demo('Colors'),
  const Demo('Typography'),
83 84
];

85
final FileSystem _fs = const LocalFileSystem();
86

87 88
const Duration kWaitBetweenActions = const Duration(milliseconds: 250);

89 90
/// Extracts event data from [events] recorded by timeline, validates it, turns
/// it into a histogram, and saves to a JSON file.
91
Future<Null> saveDurationsHistogram(List<Map<String, dynamic>> events, String outputPath) async {
92
  final Map<String, List<int>> durations = <String, List<int>>{};
93 94 95 96 97 98 99 100 101 102
  Map<String, dynamic> startEvent;

  // Save the duration of the first frame after each 'Start Transition' event.
  for (Map<String, dynamic> event in events) {
    final String eventName = event['name'];
    if (eventName == 'Start Transition') {
      assert(startEvent == null);
      startEvent = event;
    } else if (startEvent != null && eventName == 'Frame') {
      final String routeName = startEvent['args']['to'];
103
      durations[routeName] ??= <int>[];
104 105 106 107 108 109 110 111
      durations[routeName].add(event['dur']);
      startEvent = null;
    }
  }

  // Verify that the durations data is valid.
  if (durations.keys.isEmpty)
    throw 'no "Start Transition" timeline events found';
112
  final Map<String, int> unexpectedValueCounts = <String, int>{};
113 114 115 116 117 118 119
  durations.forEach((String routeName, List<int> values) {
    if (values.length != 2) {
      unexpectedValueCounts[routeName] = values.length;
    }
  });

  if (unexpectedValueCounts.isNotEmpty) {
120
    final StringBuffer error = new StringBuffer('Some routes recorded wrong number of values (expected 2 values/route):\n\n');
121 122 123 124
    unexpectedValueCounts.forEach((String routeName, int count) {
      error.writeln(' - $routeName recorded $count values.');
    });
    error.writeln('\nFull event sequence:');
125
    final Iterator<Map<String, dynamic>> eventIter = events.iterator;
126 127
    String lastEventName = '';
    String lastRouteName = '';
128
    while (eventIter.moveNext()) {
129
      final String eventName = eventIter.current['name'];
130 131 132 133

      if (!<String>['Start Transition', 'Frame'].contains(eventName))
        continue;

134
      final String routeName = eventName == 'Start Transition'
135 136 137 138 139 140 141 142 143 144 145 146 147
        ? eventIter.current['args']['to']
        : '';

      if (eventName == lastEventName && routeName == lastRouteName) {
        error.write('.');
      } else {
        error.write('\n - $eventName $routeName .');
      }

      lastEventName = eventName;
      lastRouteName = routeName;
    }
    throw error;
148 149 150
  }

  // Save the durations Map to a file.
151
  final File file = await _fs.file(outputPath).create(recursive: true);
152
  await file.writeAsString(const JsonEncoder.withIndent('  ').convert(durations));
153 154
}

155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
/// Scrolls each demo menu item into view, launches it, then returns to the
/// home screen twice.
Future<Null> runDemos(Iterable<Demo> demos, FlutterDriver driver) async {
  for (Demo demo in demos) {
    print('Testing "${demo.title}" demo');
    final SerializableFinder menuItem = find.text(demo.title);
    await driver.scrollIntoView(menuItem, alignment: 0.5);
    await new Future<Null>.delayed(kWaitBetweenActions);

    for (int i = 0; i < 2; i += 1) {
      await driver.tap(menuItem); // Launch the demo
      await new Future<Null>.delayed(kWaitBetweenActions);
      if (demo.synchronized) {
        await driver.tap(find.byTooltip('Back'));
      } else {
        await driver.runUnsynchronized<Future<Null>>(() async {
          await new Future<Null>.delayed(kWaitBetweenActions);
          await driver.tap(find.byTooltip('Back'));
        });
      }
      await new Future<Null>.delayed(kWaitBetweenActions);
    }
    print('Success');
  }
}

181
void main([List<String> args = const <String>[]]) {
182 183 184 185
  group('flutter gallery transitions', () {
    FlutterDriver driver;
    setUpAll(() async {
      driver = await FlutterDriver.connect();
186 187 188 189
      if (args.contains('--with_semantics')) {
        print('Enabeling semantics...');
        await driver.setSemantics(true);
      }
190 191 192 193
    });

    tearDownAll(() async {
      if (driver != null)
194
        await driver.close();
195 196 197
    });

    test('all demos', () async {
198
      // Collect timeline data for just a limited set of demos to avoid OOMs.
199
      final Timeline timeline = await driver.traceAction(() async {
200 201
        final Iterable<Demo> profiledDemos = demos.where((Demo demo) => demo.profiled);
        await runDemos(profiledDemos, driver);
202
      },
203
      streams: const <TimelineStream>[
204 205
        TimelineStream.dart,
        TimelineStream.embedder,
206
      ]);
207 208 209 210

      // Save the duration (in microseconds) of the first timeline Frame event
      // that follows a 'Start Transition' event. The Gallery app adds a
      // 'Start Transition' event when a demo is launched (see GalleryItem).
211
      final TimelineSummary summary = new TimelineSummary.summarize(timeline);
212
      await summary.writeSummaryToFile('transitions', pretty: true);
213
      final String histogramPath = path.join(testOutputsDirectory, 'transition_durations.timeline.json');
214
      await saveDurationsHistogram(timeline.json['traceEvents'], histogramPath);
215 216 217 218 219

      // Execute the remaining tests.
      final Iterable<Demo> unprofiledDemos = demos.where((Demo demo) => !demo.profiled);
      await runDemos(unprofiledDemos, driver);

220
    }, timeout: const Timeout(const Duration(minutes: 5)));
221 222
  });
}