hot_mode_tests.dart 12 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:async';
import 'dart:convert';
7 8 9
import 'dart:io';

import 'package:path/path.dart' as path;
10
import 'package:process/process.dart';
11

12
import '../framework/devices.dart';
13
import '../framework/framework.dart';
14
import '../framework/running_processes.dart';
15
import '../framework/task_result.dart';
16 17
import '../framework/utils.dart';

18
final Directory _editedFlutterGalleryDir = dir(path.join(Directory.systemTemp.path, 'edited_flutter_gallery'));
19
final Directory flutterGalleryDir = dir(path.join(flutterDirectory.path, 'dev/integration_tests/flutter_gallery'));
20 21
const String kSourceLine = 'fontSize: (orientation == Orientation.portrait) ? 32.0 : 24.0';
const String kReplacementLine = 'fontSize: (orientation == Orientation.portrait) ? 34.0 : 24.0';
22

23 24 25 26 27
TaskFunction createHotModeTest({
  String? deviceIdOverride,
  Map<String, String>? environment,
  bool checkAppRunningOnLocalDevice = false,
}) {
28 29 30 31 32
  // This file is modified during the test and needs to be restored at the end.
  final File flutterFrameworkSource = file(path.join(
    flutterDirectory.path, 'packages/flutter/lib/src/widgets/framework.dart',
  ));
  final String oldContents = flutterFrameworkSource.readAsStringSync();
33
  return () async {
34 35 36 37 38
    if (deviceIdOverride == null) {
      final Device device = await devices.workingDevice;
      await device.unlock();
      deviceIdOverride = device.deviceId;
    }
39
    final File benchmarkFile = file(path.join(_editedFlutterGalleryDir.path, 'hot_benchmark.json'));
40 41
    rm(benchmarkFile);
    final List<String> options = <String>[
42 43 44 45 46 47 48 49 50
      '--hot',
      '-d',
      deviceIdOverride!,
      '--benchmark',
      '--resident',
      '--no-android-gradle-daemon',
      '--no-publish-port',
      '--verbose',
      '--uninstall-first',
51
    ];
52
    int hotReloadCount = 0;
53 54 55 56
    late Map<String, dynamic> smallReloadData;
    late Map<String, dynamic> mediumReloadData;
    late Map<String, dynamic> largeReloadData;
    late Map<String, dynamic> freshRestartReloadsData;
57 58


59
    await inDirectory<void>(flutterDirectory, () async {
60 61 62
      rmTree(_editedFlutterGalleryDir);
      mkdirs(_editedFlutterGalleryDir);
      recursiveCopy(flutterGalleryDir, _editedFlutterGalleryDir);
63

64 65 66 67 68 69 70 71 72 73
      try {
        await inDirectory<void>(_editedFlutterGalleryDir, () async {
          smallReloadData = await captureReloadData(options, environment, benchmarkFile, (String line, Process process) {
            if (!line.contains('Reloaded ')) {
              return;
            }
            if (hotReloadCount == 0) {
              // Update a file for 2 library invalidation.
              final File appDartSource = file(path.join(
                _editedFlutterGalleryDir.path, 'lib/gallery/app.dart',
74
              ));
75 76 77 78 79 80 81 82 83 84
              appDartSource.writeAsStringSync(
                appDartSource.readAsStringSync().replaceFirst(
                  "'Flutter Gallery'", "'Updated Flutter Gallery'",
                ));
              process.stdin.writeln('r');
              hotReloadCount += 1;
            } else {
              process.stdin.writeln('q');
            }
          });
85

86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
          mediumReloadData = await captureReloadData(options, environment, benchmarkFile, (String line, Process process) {
            if (!line.contains('Reloaded ')) {
              return;
            }
            if (hotReloadCount == 1) {
              // Update a file for ~50 library invalidation.
              final File appDartSource = file(path.join(
                _editedFlutterGalleryDir.path, 'lib/demo/calculator/home.dart',
              ));
              appDartSource.writeAsStringSync(
                appDartSource.readAsStringSync().replaceFirst(kSourceLine, kReplacementLine)
              );
              process.stdin.writeln('r');
              hotReloadCount += 1;
            } else {
101 102
              process.stdin.writeln('q');
            }
103
          });
104

105
          largeReloadData = await captureReloadData(options, environment, benchmarkFile, (String line, Process process) async {
106 107 108 109 110 111
            if (!line.contains('Reloaded ')) {
              return;
            }
            if (hotReloadCount == 2) {
              // Trigger a framework invalidation (370 libraries) without modifying the source
              flutterFrameworkSource.writeAsStringSync(
112
                '${flutterFrameworkSource.readAsStringSync()}\n'
113 114 115 116
              );
              process.stdin.writeln('r');
              hotReloadCount += 1;
            } else {
117 118 119
              if (checkAppRunningOnLocalDevice) {
                await _checkAppRunning(true);
              }
120 121
              process.stdin.writeln('q');
            }
122 123
          });

124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
          // Start `flutter run` again to make sure it loads from the previous
          // state. Frontend loads up from previously generated kernel files.
          {
            final Process process = await startProcess(
                path.join(flutterDirectory.path, 'bin', 'flutter'),
                flutterCommandArgs('run', options),
                environment: environment,
            );
            final Completer<void> stdoutDone = Completer<void>();
            final Completer<void> stderrDone = Completer<void>();
            process.stdout
                .transform<String>(utf8.decoder)
                .transform<String>(const LineSplitter())
                .listen((String line) {
              if (line.contains('Reloaded ')) {
                process.stdin.writeln('q');
              }
              print('stdout: $line');
            }, onDone: () {
              stdoutDone.complete();
            });
            process.stderr
                .transform<String>(utf8.decoder)
                .transform<String>(const LineSplitter())
                .listen((String line) {
              print('stderr: $line');
            }, onDone: () {
              stderrDone.complete();
            });

            await Future.wait<void>(
                <Future<void>>[stdoutDone.future, stderrDone.future]);
            await process.exitCode;
157

158 159 160 161
            freshRestartReloadsData =
                json.decode(benchmarkFile.readAsStringSync()) as Map<String, dynamic>;
          }
        });
162 163 164
        if (checkAppRunningOnLocalDevice) {
          await _checkAppRunning(false);
        }
165 166 167
      } finally {
        flutterFrameworkSource.writeAsStringSync(oldContents);
      }
168
    });
169

170
    return TaskResult.success(
171
      <String, dynamic> {
172
        // ignore: avoid_dynamic_calls
173
        'hotReloadInitialDevFSSyncMilliseconds': smallReloadData['hotReloadInitialDevFSSyncMilliseconds'][0],
174
        // ignore: avoid_dynamic_calls
175
        'hotRestartMillisecondsToFrame': smallReloadData['hotRestartMillisecondsToFrame'][0],
176
        // ignore: avoid_dynamic_calls
177
        'hotReloadMillisecondsToFrame' : smallReloadData['hotReloadMillisecondsToFrame'][0],
178
        // ignore: avoid_dynamic_calls
179
        'hotReloadDevFSSyncMilliseconds': smallReloadData['hotReloadDevFSSyncMilliseconds'][0],
180
        // ignore: avoid_dynamic_calls
181
        'hotReloadFlutterReassembleMilliseconds': smallReloadData['hotReloadFlutterReassembleMilliseconds'][0],
182
        // ignore: avoid_dynamic_calls
183
        'hotReloadVMReloadMilliseconds': smallReloadData['hotReloadVMReloadMilliseconds'][0],
184
        // ignore: avoid_dynamic_calls
185
        'hotReloadMillisecondsToFrameAfterChange' : smallReloadData['hotReloadMillisecondsToFrame'][1],
186
        // ignore: avoid_dynamic_calls
187
        'hotReloadDevFSSyncMillisecondsAfterChange': smallReloadData['hotReloadDevFSSyncMilliseconds'][1],
188
        // ignore: avoid_dynamic_calls
189
        'hotReloadFlutterReassembleMillisecondsAfterChange': smallReloadData['hotReloadFlutterReassembleMilliseconds'][1],
190
        // ignore: avoid_dynamic_calls
191
        'hotReloadVMReloadMillisecondsAfterChange': smallReloadData['hotReloadVMReloadMilliseconds'][1],
192
        // ignore: avoid_dynamic_calls
193
        'hotReloadInitialDevFSSyncAfterRelaunchMilliseconds' : freshRestartReloadsData['hotReloadInitialDevFSSyncMilliseconds'][0],
194
        // ignore: avoid_dynamic_calls
195
        'hotReloadMillisecondsToFrameAfterMediumChange' : mediumReloadData['hotReloadMillisecondsToFrame'][1],
196
        // ignore: avoid_dynamic_calls
197
        'hotReloadDevFSSyncMillisecondsAfterMediumChange': mediumReloadData['hotReloadDevFSSyncMilliseconds'][1],
198
        // ignore: avoid_dynamic_calls
199
        'hotReloadFlutterReassembleMillisecondsAfterMediumChange': mediumReloadData['hotReloadFlutterReassembleMilliseconds'][1],
200
        // ignore: avoid_dynamic_calls
201
        'hotReloadVMReloadMillisecondsAfterMediumChange': mediumReloadData['hotReloadVMReloadMilliseconds'][1],
202
        // ignore: avoid_dynamic_calls
203
        'hotReloadMillisecondsToFrameAfterLargeChange' : largeReloadData['hotReloadMillisecondsToFrame'][1],
204
        // ignore: avoid_dynamic_calls
205
        'hotReloadDevFSSyncMillisecondsAfterLargeChange': largeReloadData['hotReloadDevFSSyncMilliseconds'][1],
206
        // ignore: avoid_dynamic_calls
207
        'hotReloadFlutterReassembleMillisecondsAfterLargeChange': largeReloadData['hotReloadFlutterReassembleMilliseconds'][1],
208
        // ignore: avoid_dynamic_calls
209
        'hotReloadVMReloadMillisecondsAfterLargeChange': largeReloadData['hotReloadVMReloadMilliseconds'][1],
210 211 212 213 214 215 216 217
      },
      benchmarkScoreKeys: <String>[
        'hotReloadInitialDevFSSyncMilliseconds',
        'hotRestartMillisecondsToFrame',
        'hotReloadMillisecondsToFrame',
        'hotReloadDevFSSyncMilliseconds',
        'hotReloadFlutterReassembleMilliseconds',
        'hotReloadVMReloadMilliseconds',
218
        'hotReloadMillisecondsToFrameAfterChange',
219 220 221
        'hotReloadDevFSSyncMillisecondsAfterChange',
        'hotReloadFlutterReassembleMillisecondsAfterChange',
        'hotReloadVMReloadMillisecondsAfterChange',
222
        'hotReloadInitialDevFSSyncAfterRelaunchMilliseconds',
223 224 225 226 227 228 229 230
        'hotReloadMillisecondsToFrameAfterMediumChange',
        'hotReloadDevFSSyncMillisecondsAfterMediumChange',
        'hotReloadFlutterReassembleMillisecondsAfterMediumChange',
        'hotReloadVMReloadMillisecondsAfterMediumChange',
        'hotReloadMillisecondsToFrameAfterLargeChange',
        'hotReloadDevFSSyncMillisecondsAfterLargeChange',
        'hotReloadFlutterReassembleMillisecondsAfterLargeChange',
        'hotReloadVMReloadMillisecondsAfterLargeChange',
231
      ],
232
    );
233 234
  };
}
235

236
Future<Map<String, dynamic>> captureReloadData(
237
  List<String> options,
238
  Map<String, String>? environment,
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
  File benchmarkFile,
  void Function(String, Process) onLine,
) async {
  final Process process = await startProcess(
    path.join(flutterDirectory.path, 'bin', 'flutter'),
    flutterCommandArgs('run', options),
    environment: environment,
  );

  final Completer<void> stdoutDone = Completer<void>();
  final Completer<void> stderrDone = Completer<void>();
  process.stdout
    .transform<String>(utf8.decoder)
    .transform<String>(const LineSplitter())
    .listen((String line) {
      onLine(line, process);
      print('stdout: $line');
    }, onDone: stdoutDone.complete);

  process.stderr
    .transform<String>(utf8.decoder)
    .transform<String>(const LineSplitter())
    .listen(
      (String line) => print('stderr: $line'),
      onDone: stderrDone.complete,
    );

  await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
  await process.exitCode;
268
  final Map<String, dynamic> result = json.decode(benchmarkFile.readAsStringSync()) as Map<String, dynamic>;
269 270 271
  benchmarkFile.deleteSync();
  return result;
}
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291

Future<void> _checkAppRunning(bool shouldBeRunning) async {
  late Set<RunningProcessInfo> galleryProcesses;
  for (int i = 0; i < 10; i++) {
    final String exe = Platform.isWindows ? '.exe' : '';
    galleryProcesses = await getRunningProcesses(
      processName: 'Flutter Gallery$exe',
      processManager: const LocalProcessManager(),
    );

    if (galleryProcesses.isNotEmpty == shouldBeRunning) {
      return;
    }

    // Give the app time to shut down.
    sleep(const Duration(seconds: 1));
  }
  print(galleryProcesses.join('\n'));
  throw TaskResult.failure('Flutter Gallery app is ${shouldBeRunning ? 'not' : 'still'} running');
}