run_hot.dart 35.6 KB
Newer Older
1 2 3 4 5 6
// 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';

7 8
import 'package:json_rpc_2/error_code.dart' as rpc_error_code;
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
9
import 'package:meta/meta.dart';
10

11
import 'base/common.dart';
12
import 'base/context.dart';
13
import 'base/file_system.dart';
14
import 'base/logger.dart';
15
import 'base/terminal.dart';
16
import 'base/utils.dart';
17
import 'build_info.dart';
18
import 'compile.dart';
19
import 'convert.dart';
20
import 'dart/dependencies.dart';
21
import 'dart/pub.dart';
22
import 'devfs.dart';
23 24 25
import 'device.dart';
import 'globals.dart';
import 'resident_runner.dart';
26
import 'usage.dart';
27
import 'vmservice.dart';
28

29 30 31 32 33
class HotRunnerConfig {
  /// Should the hot runner compute the minimal Dart dependencies?
  bool computeDartDependencies = true;
  /// Should the hot runner assume that the minimal Dart dependencies do not change?
  bool stableDartDependencies = false;
34 35 36 37 38
  /// A hook for implementations to perform any necessary initialization prior
  /// to a hot restart. Should return true if the hot restart should continue.
  Future<bool> setupHotRestart() async {
    return true;
  }
39 40 41 42 43
  /// A hook for implementations to perform any necessary operations right
  /// before the runner is about to be shut down.
  Future<void> runPreShutdownOperations() async {
    return;
  }
44 45 46 47
}

HotRunnerConfig get hotRunnerConfig => context[HotRunnerConfig];

48 49
const bool kHotReloadDefault = true;

50 51 52 53 54 55 56
class DeviceReloadReport {
  DeviceReloadReport(this.device, this.reports);

  FlutterDevice device;
  List<Map<String, dynamic>> reports; // List has one report per Flutter view.
}

57
// TODO(mklim): Test this, flutter/flutter#23031.
58 59
class HotRunner extends ResidentRunner {
  HotRunner(
60
    List<FlutterDevice> devices, {
61 62
    String target,
    DebuggingOptions debuggingOptions,
63 64
    bool usesTerminalUI = true,
    this.benchmarkMode = false,
65
    this.applicationBinary,
66
    this.hostIsIde = false,
67 68
    String projectRootPath,
    String packagesFilePath,
69
    this.dillOutputPath,
70
    bool saveCompilationTrace = false,
71 72
    bool stayResident = true,
    bool ipv6 = false,
73
  }) : super(devices,
74 75
             target: target,
             debuggingOptions: debuggingOptions,
76 77 78
             usesTerminalUI: usesTerminalUI,
             projectRootPath: projectRootPath,
             packagesFilePath: packagesFilePath,
79
             saveCompilationTrace: saveCompilationTrace,
80 81
             stayResident: stayResident,
             ipv6: ipv6);
82

83
  final bool benchmarkMode;
84
  final File applicationBinary;
85
  final bool hostIsIde;
86
  bool _didAttach = false;
87
  Set<String> _dartDependencies;
88
  final String dillOutputPath;
89

90
  final Map<String, List<int>> benchmarkData = <String, List<int>>{};
91 92
  // The initial launch is from a snapshot.
  bool _runningFromSnapshot = true;
93
  DateTime firstBuildTime;
94

95 96 97 98 99
  void _addBenchmarkData(String name, int value) {
    benchmarkData[name] ??= <int>[];
    benchmarkData[name].add(value);
  }

100
  Future<bool> _refreshDartDependencies() async {
101 102 103 104
    if (!hotRunnerConfig.computeDartDependencies) {
      // Disabled.
      return true;
    }
105 106 107 108
    if (_dartDependencies != null) {
      // Already computed.
      return true;
    }
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124

    try {
      // Will return immediately if pubspec.yaml is up-to-date.
      await pubGet(
        context: PubContext.pubGet,
        directory: projectRootPath,
      );
    } on ToolExit catch (error) {
      printError(
        'Unable to reload your application because "flutter packages get" failed to update '
        'package dependencies.\n'
        '$error'
      );
      return false;
    }

125
    final DartDependencySetBuilder dartDependencySetBuilder =
126
        DartDependencySetBuilder(mainPath, packagesFilePath);
127
    try {
128
      _dartDependencies = Set<String>.from(dartDependencySetBuilder.build());
129 130 131 132 133
    } on DartDependencyException catch (error) {
      printError(
        'Your application could not be compiled, because its dependencies could not be established.\n'
        '$error'
      );
134 135 136 137 138
      return false;
    }
    return true;
  }

139
  Future<void> _reloadSourcesService(String isolateId,
140
      { bool force = false, bool pause = false }) async {
141 142
    // TODO(cbernaschina): check that isolateId is the id of the UI isolate.
    final OperationResult result = await restart(pauseAfterRestart: pause);
143
    if (!result.isOk) {
144
      throw rpc.RpcException(
145 146 147 148 149 150
        rpc_error_code.INTERNAL_ERROR,
        'Unable to reload sources',
      );
    }
  }

151 152 153 154 155 156 157 158 159 160 161
  Future<void> _restartService({ bool pause = false }) async {
    final OperationResult result =
      await restart(fullRestart: true, pauseAfterRestart: pause);
    if (!result.isOk) {
      throw rpc.RpcException(
        rpc_error_code.INTERNAL_ERROR,
        'Unable to restart',
      );
    }
  }

162 163 164 165 166 167 168 169 170
  Future<String> _compileExpressionService(String isolateId, String expression,
      List<String> definitions, List<String> typeDefinitions,
      String libraryUri, String klass, bool isStatic,
      ) async {
    for (FlutterDevice device in flutterDevices) {
      if (device.generator != null) {
        final CompilerOutput compilerOutput =
            await device.generator.compileExpression(expression, definitions,
                typeDefinitions, libraryUri, klass, isStatic);
171
        if (compilerOutput != null && compilerOutput.outputFilename != null) {
172 173 174 175
          return base64.encode(fs.file(compilerOutput.outputFilename).readAsBytesSync());
        }
      }
    }
176
    throw 'Failed to compile $expression';
177 178
  }

179
  // Returns the exit code of the flutter tool process, like [run].
180
  @override
181
  Future<int> attach({
182
    Completer<DebugConnectionInfo> connectionInfoCompleter,
183
    Completer<void> appStartedCompleter,
184
  }) async {
185
    _didAttach = true;
186
    try {
187 188
      await connectToServiceProtocol(
        reloadSources: _reloadSourcesService,
189
        restart: _restartService,
190 191
        compileExpression: _compileExpressionService,
      );
192 193 194 195 196
    } catch (error) {
      printError('Error connecting to the service protocol: $error');
      return 2;
    }

197 198
    for (FlutterDevice device in flutterDevices)
      device.initLogReader();
199
    try {
200
      final List<Uri> baseUris = await _initDevFS();
201
      if (connectionInfoCompleter != null) {
202
        // Only handle one debugger connection.
203
        connectionInfoCompleter.complete(
204
          DebugConnectionInfo(
205 206 207
            httpUri: flutterDevices.first.observatoryUris.first,
            wsUri: flutterDevices.first.vmServices.first.wsAddress,
            baseUri: baseUris.first.toString()
208 209 210 211 212 213 214
          )
        );
      }
    } catch (error) {
      printError('Error initializing DevFS: $error');
      return 3;
    }
215
    final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start();
216
    final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: true);
217 218 219 220
    _addBenchmarkData(
      'hotReloadInitialDevFSSyncMilliseconds',
      initialUpdateDevFSsTimer.elapsed.inMilliseconds,
    );
221
    if (!devfsResult.success)
222 223
      return 3;

224
    await refreshViews();
225
    for (FlutterDevice device in flutterDevices) {
226 227 228 229
      // VM must have accepted the kernel binary, there will be no reload
      // report, so we let incremental compiler know that source code was accepted.
      if (device.generator != null)
        device.generator.accept();
230 231 232
      for (FlutterView view in device.views)
        printTrace('Connected to $view.');
    }
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250

    if (stayResident) {
      setupTerminal();
      registerSignalHandlers();
    }

    appStartedCompleter?.complete();

    if (benchmarkMode) {
      // We are running in benchmark mode.
      printStatus('Running in benchmark mode.');
      // Measure time to perform a hot restart.
      printStatus('Benchmarking hot restart');
      await restart(fullRestart: true);
      // TODO(johnmccutchan): Modify script entry point.
      printStatus('Benchmarking hot reload');
      // Measure time to perform a hot reload.
      await restart(fullRestart: false);
251 252 253 254 255 256 257 258
      if (stayResident) {
        await waitForAppToFinish();
      } else {
        printStatus('Benchmark completed. Exiting application.');
        await _cleanupDevFS();
        await stopEchoingDeviceLog();
        await stopApp();
      }
259 260
      final File benchmarkOutput = fs.file('hot_benchmark.json');
      benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
261
      return 0;
262 263
    }

264
    int result = 0;
265
    if (stayResident)
266
      result = await waitForAppToFinish();
267
    await cleanupAtFinish();
268
    return result;
269 270
  }

271 272
  @override
  Future<int> run({
273
    Completer<DebugConnectionInfo> connectionInfoCompleter,
274
    Completer<void> appStartedCompleter,
275
    String route,
276
    bool shouldBuild = true
277
  }) async {
278 279
    if (!fs.isFileSync(mainPath)) {
      String message = 'Tried to run $mainPath, but that file does not exist.';
280 281 282 283 284 285
      if (target == null)
        message += '\nConsider using the -t option to specify the Dart file to start.';
      printError(message);
      return 1;
    }

286
    // Determine the Dart dependencies eagerly.
287
    if (!await _refreshDartDependencies()) {
288 289 290 291
      // Some kind of source level error or missing file in the Dart code.
      return 1;
    }

292
    firstBuildTime = DateTime.now();
293

294 295 296 297 298 299 300 301 302
    for (FlutterDevice device in flutterDevices) {
      final int result = await device.runHot(
        hotRunner: this,
        route: route,
        shouldBuild: shouldBuild,
      );
      if (result != 0) {
        return result;
      }
303 304
    }

305 306
    return attach(
      connectionInfoCompleter: connectionInfoCompleter,
307
      appStartedCompleter: appStartedCompleter,
308
    );
309 310 311
  }

  @override
312
  Future<void> handleTerminalCommand(String code) async {
313
    final String lower = code.toLowerCase();
314
    if (lower == 'r') {
315 316 317 318 319 320 321 322 323 324
      OperationResult result;
      if (code == 'R') {
        // If hot restart is not supported for all devices, ignore the command.
        if (!canHotRestart) {
          return;
        }
        result = await restart(fullRestart: true);
      } else {
        result = await restart(fullRestart: false);
      }
325 326 327 328
      if (!result.isOk) {
        // TODO(johnmccutchan): Attempt to determine the number of errors that
        // occurred and tighten this message.
        printStatus('Try again after fixing the above error(s).', emphasis: true);
329
      }
330 331 332 333 334 335
    } else if (lower == 'l') {
      final List<FlutterView> views = flutterDevices.expand((FlutterDevice d) => d.views).toList();
      printStatus('Connected ${pluralize('view', views.length)}:');
      for (FlutterView v in views) {
        printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2);
      }
336 337 338
    }
  }

339
  Future<List<Uri>> _initDevFS() async {
340
    final String fsName = fs.path.basename(projectRootPath);
341 342 343 344 345 346 347 348 349 350
    final List<Uri> devFSUris = <Uri>[];
    for (FlutterDevice device in flutterDevices) {
      final Uri uri = await device.setupDevFS(
        fsName,
        fs.directory(projectRootPath),
        packagesFilePath: packagesFilePath
      );
      devFSUris.add(uri);
    }
    return devFSUris;
351
  }
352

353
  Future<UpdateFSReport> _updateDevFS({ bool fullRestart = false }) async {
354
    if (!await _refreshDartDependencies()) {
355
      // Did not update DevFS because of a Dart source error.
356
      return UpdateFSReport(success: false);
357
    }
358
    final bool isFirstUpload = assetBundle.wasBuiltOnce() == false;
359
    final bool rebuildBundle = assetBundle.needsBuild();
360
    if (rebuildBundle) {
361
      printTrace('Updating assets');
362
      final int result = await assetBundle.build();
363
      if (result != 0)
364
        return UpdateFSReport(success: false);
365
    }
366

367
    final UpdateFSReport results = UpdateFSReport(success: true);
368
    for (FlutterDevice device in flutterDevices) {
369
      results.incorporateResults(await device.updateDevFS(
370 371
        mainPath: mainPath,
        target: target,
372
        bundle: assetBundle,
373 374 375
        firstBuildTime: firstBuildTime,
        bundleFirstUpload: isFirstUpload,
        bundleDirty: isFirstUpload == false && rebuildBundle,
376
        fileFilter: _dartDependencies,
377 378
        fullRestart: fullRestart,
        projectRootPath: projectRootPath,
379
        pathToReload: getReloadPath(fullRestart: fullRestart),
380 381
      ));
    }
382 383
    if (!results.success) {
      return results;
384
    }
385

386 387 388 389
    if (!hotRunnerConfig.stableDartDependencies) {
      // Clear the set after the sync so they are recomputed next time.
      _dartDependencies = null;
    }
390
    return results;
391 392
  }

393 394
  Future<void> _evictDirtyAssets() {
    final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[];
395 396
    for (FlutterDevice device in flutterDevices) {
      if (device.devFS.assetPathsToEvict.isEmpty)
397 398 399 400 401
        continue;
      if (device.views.first.uiIsolate == null) {
        printError('Application isolate not found for $device');
        continue;
      }
402
      for (String assetPath in device.devFS.assetPathsToEvict)
403
        futures.add(device.views.first.uiIsolate.flutterEvictAsset(assetPath));
404 405
      device.devFS.assetPathsToEvict.clear();
    }
406
    return Future.wait<Map<String, dynamic>>(futures);
407 408
  }

409 410 411 412 413
  void _resetDirtyAssets() {
    for (FlutterDevice device in flutterDevices)
      device.devFS.assetPathsToEvict.clear();
  }

414
  Future<void> _cleanupDevFS() async {
415
    final List<Future<void>> futures = <Future<void>>[];
416 417
    for (FlutterDevice device in flutterDevices) {
      if (device.devFS != null) {
418 419
        // Cleanup the devFS, but don't wait indefinitely.
        // We ignore any errors, because it's not clear what we would do anyway.
420
        futures.add(device.devFS.destroy()
421 422
          .timeout(const Duration(milliseconds: 250))
          .catchError((dynamic error) {
423
            printTrace('Ignored error while cleaning up DevFS: $error');
424
          }));
425 426 427
      }
      device.devFS = null;
    }
428
    await Future.wait(futures);
429 430
  }

431
  Future<void> _launchInView(FlutterDevice device,
432
                             Uri entryUri,
433
                             Uri packagesUri,
434 435
                             Uri assetsDirectoryUri) {
    final List<Future<void>> futures = <Future<void>>[];
436
    for (FlutterView view in device.views)
437 438
      futures.add(view.runFromSource(entryUri, packagesUri, assetsDirectoryUri));
    final Completer<void> completer = Completer<void>();
439
    Future.wait(futures).whenComplete(() { completer.complete(null); });
440
    return completer.future;
441 442
  }

443
  Future<void> _launchFromDevFS(String mainScript) async {
444
    final String entryUri = fs.path.relative(mainScript, from: projectRootPath);
445
    final List<Future<void>> futures = <Future<void>>[];
446 447 448
    for (FlutterDevice device in flutterDevices) {
      final Uri deviceEntryUri = device.devFS.baseUri.resolveUri(
        fs.path.toUri(entryUri));
449
      final Uri devicePackagesUri = device.devFS.baseUri.resolve('.packages');
450 451
      final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri(
        fs.path.toUri(getAssetBuildDirectory()));
452
      futures.add(_launchInView(device,
453 454
                          deviceEntryUri,
                          devicePackagesUri,
455
                          deviceAssetsDirectoryUri));
456
    }
457 458 459 460 461 462 463 464 465
    await Future.wait(futures);
    if (benchmarkMode) {
      futures.clear();
      for (FlutterDevice device in flutterDevices)
        for (FlutterView view in device.views)
          futures.add(view.flushUIThreadTasks());
      await Future.wait(futures);
    }

466 467
  }

468 469 470 471 472 473
  Future<OperationResult> _restartFromSources({ String reason }) async {
    final Map<String, String> analyticsParameters =
      reason == null
        ? null
        : <String, String>{ kEventReloadReasonParameterName: reason };

474 475 476 477 478
    if (!_isPaused()) {
      printTrace('Refreshing active FlutterViews before restarting.');
      await refreshViews();
    }

479
    final Stopwatch restartTimer = Stopwatch()..start();
480 481
    // TODO(aam): Add generator reset logic once we switch to using incremental
    // compiler for full application recompilation on restart.
482 483
    final UpdateFSReport updatedDevFS = await _updateDevFS(fullRestart: true);
    if (!updatedDevFS.success) {
484 485 486 487
      for (FlutterDevice device in flutterDevices) {
        if (device.generator != null)
          device.generator.reject();
      }
488
      return OperationResult(1, 'DevFS synchronization failed');
489 490 491 492 493 494 495 496
    }
    _resetDirtyAssets();
    for (FlutterDevice device in flutterDevices) {
      // VM must have accepted the kernel binary, there will be no reload
      // report, so we let incremental compiler know that source code was accepted.
      if (device.generator != null)
        device.generator.accept();
    }
497
    // Check if the isolate is paused and resume it.
498
    final List<Future<void>> futures = <Future<void>>[];
499 500 501 502
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
        if (view.uiIsolate != null) {
          // Reload the isolate.
503 504 505 506 507 508 509 510 511
          final Completer<void> completer = Completer<void>();
          futures.add(completer.future);
          view.uiIsolate.reload().then((ServiceObject _) { // ignore: unawaited_futures
            final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
            if ((pauseEvent != null) && pauseEvent.isPauseEvent) {
              // Resume the isolate so that it can be killed by the embedder.
              return view.uiIsolate.resume();
            }
          }).whenComplete(() { completer.complete(null); });
512
        }
513 514
      }
    }
515
    await Future.wait(futures);
516 517
    // We are now running from source.
    _runningFromSnapshot = false;
518
    await _launchFromDevFS(mainPath + '.dill');
519
    restartTimer.stop();
520
    printTrace('Hot restart performed in ${getElapsedAsMilliseconds(restartTimer.elapsed)}.');
521 522
    // We are now running from sources.
    _runningFromSnapshot = false;
523 524
    _addBenchmarkData('hotRestartMillisecondsToFrame',
        restartTimer.elapsed.inMilliseconds);
525
    flutterUsage.sendEvent('hot', 'restart', parameters: analyticsParameters);
526
    flutterUsage.sendTiming('hot', 'restart', restartTimer.elapsed);
527
    return OperationResult.ok;
528 529 530
  }

  /// Returns [true] if the reload was successful.
531 532
  /// Prints errors if [printErrors] is [true].
  static bool validateReloadReport(Map<String, dynamic> reloadReport,
533
      { bool printErrors = true }) {
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551
    if (reloadReport == null) {
      if (printErrors)
        printError('Hot reload did not receive reload report.');
      return false;
    }
    if (!(reloadReport['type'] == 'ReloadReport' &&
          (reloadReport['success'] == true ||
           (reloadReport['success'] == false &&
            (reloadReport['details'] is Map<String, dynamic> &&
             reloadReport['details']['notices'] is List<dynamic> &&
             reloadReport['details']['notices'].isNotEmpty &&
             reloadReport['details']['notices'].every(
               (dynamic item) => item is Map<String, dynamic> && item['message'] is String
             )
            )
           )
          )
         )) {
552 553
      if (printErrors)
        printError('Hot reload received invalid response: $reloadReport');
554 555
      return false;
    }
556
    if (!reloadReport['success']) {
557 558 559 560 561
      if (printErrors) {
        printError('Hot reload was rejected:');
        for (Map<String, dynamic> notice in reloadReport['details']['notices'])
          printError('${notice['message']}');
      }
562 563 564 565 566
      return false;
    }
    return true;
  }

567 568 569
  @override
  bool get supportsRestart => true;

570
  @override
571
  Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) async {
572
    final Stopwatch timer = Stopwatch()..start();
573
    if (fullRestart) {
574 575 576
      if (!canHotRestart) {
        return OperationResult(1, 'hotRestart not supported');
      }
577
      final Status status = logger.startProgress(
578
        'Performing hot restart...',
579
        timeout: kFastOperation,
580
        progressId: 'hot.restart',
581
      );
Devon Carew's avatar
Devon Carew committed
582
      try {
583
        if (!(await hotRunnerConfig.setupHotRestart()))
584
          return OperationResult(1, 'setupHotRestart failed');
585
        final OperationResult result = await _restartFromSources(reason: reason);
586 587
        if (!result.isOk)
          return result;
588
      } finally {
589
        status.cancel();
Devon Carew's avatar
Devon Carew committed
590
      }
591
      printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
592
      return OperationResult.ok;
593
    } else {
594 595
      final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
      final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
596
      Status status = logger.startProgress(
597
        '$progressPrefix hot reload...',
598 599
        timeout: kFastOperation,
        progressId: 'hot.reload',
600
      );
601
      OperationResult result;
602
      bool showTime = true;
Devon Carew's avatar
Devon Carew committed
603
      try {
604 605 606 607 608 609 610 611 612 613 614 615 616
        result = await _reloadSources(
          pause: pauseAfterRestart,
          reason: reason,
          onSlow: (String message) {
            status?.cancel();
            status = logger.startProgress(
              message,
              timeout: kSlowOperation,
              progressId: 'hot.reload',
            );
            showTime = false;
          },
        );
617
      } finally {
618
        status.cancel();
Devon Carew's avatar
Devon Carew committed
619
      }
620 621 622 623 624 625 626
      if (result.isOk) {
        if (showTime) {
          printStatus('${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.');
        } else {
          printStatus('${result.message}.');
        }
      }
627 628
      if (result.hintMessage != null)
        printStatus('\n${result.hintMessage}');
629
      return result;
630 631
    }
  }
632

633 634
  Future<OperationResult> _reloadSources({ bool pause = false, String reason, void Function(String message) onSlow }) async {
    final Map<String, String> analyticsParameters = <String, String>{};
635 636 637
    if (reason != null) {
      analyticsParameters[kEventReloadReasonParameterName] = reason;
    }
638 639 640 641 642 643
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
        if (view.uiIsolate == null)
          throw 'Application isolate not found';
      }
    }
644

645 646 647 648 649 650
    // The initial launch is from a script snapshot. When we reload from source
    // on top of a script snapshot, the first reload will be a worst case reload
    // because all of the sources will end up being dirty (library paths will
    // change from host path to a device path). Subsequent reloads will
    // not be affected, so we resume reporting reload times on the second
    // reload.
651
    bool shouldReportReloadTime = !_runningFromSnapshot;
652
    final Stopwatch reloadTimer = Stopwatch()..start();
653

654 655 656 657 658
    if (!_isPaused()) {
      printTrace('Refreshing active FlutterViews before reloading.');
      await refreshViews();
    }

659
    final Stopwatch devFSTimer = Stopwatch()..start();
660
    final UpdateFSReport updatedDevFS = await _updateDevFS();
661
    // Record time it took to synchronize to DevFS.
662
    _addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds);
663
    if (!updatedDevFS.success)
664
      return OperationResult(1, 'DevFS synchronization failed');
Devon Carew's avatar
Devon Carew committed
665
    String reloadMessage;
666
    final Stopwatch vmReloadTimer = Stopwatch()..start();
667
    try {
668
      final String entryPath = fs.path.relative(
669
        getReloadPath(fullRestart: false),
670 671
        from: projectRootPath,
      );
672
      final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[];
673
      for (FlutterDevice device in flutterDevices) {
674 675 676 677 678
        if (_runningFromSnapshot) {
          // Asset directory has to be set only once when we switch from
          // running from snapshot to running from uploaded files.
          await device.resetAssetDirectory();
        }
679 680 681 682
        final Completer<DeviceReloadReport> completer = Completer<DeviceReloadReport>();
        allReportsFutures.add(completer.future);
        final List<Future<Map<String, dynamic>>> reportFutures = device.reloadSources(
          entryPath, pause: pause
683
        );
684 685 686 687 688 689 690 691 692
        Future.wait(reportFutures).then((List<Map<String, dynamic>> reports) { // ignore: unawaited_futures
          // TODO(aam): Investigate why we are validating only first reload report,
          // which seems to be current behavior
          final Map<String, dynamic> firstReport = reports.first;
          // Don't print errors because they will be printed further down when
          // `validateReloadReport` is called again.
          device.updateReloadStatus(validateReloadReport(firstReport, printErrors: false));
          completer.complete(DeviceReloadReport(device, reports));
        });
693
      }
694 695 696 697 698 699 700 701
      final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures);
      for (DeviceReloadReport report in reports) {
        final Map<String, dynamic> reloadReport = report.reports[0];
        if (!validateReloadReport(reloadReport)) {
          // Reload failed.
          flutterUsage.sendEvent('hot', 'reload-reject');
          return OperationResult(1, 'Reload rejected');
        } else {
702 703 704 705 706 707 708 709 710 711 712 713
          // Collect stats that help understand scale of update for this hot reload request.
          // For example, [syncedLibraryCount]/[finalLibraryCount] indicates how
          // many libraries were affected by the hot reload request.
          // Relation of [invalidatedSourcesCount] to [syncedLibraryCount] should help
          // understand sync/transfer "overhead" of updating this number of source files.
          final Map<String, dynamic> details = reloadReport['details'];
          analyticsParameters[kEventReloadFinalLibraryCount] = "${details['finalLibraryCount']}";
          analyticsParameters[kEventReloadSyncedLibraryCount] = "${details['receivedLibraryCount']}";
          analyticsParameters[kEventReloadSyncedClassesCount] = "${details['receivedClassesCount']}";
          analyticsParameters[kEventReloadSyncedProceduresCount] = "${details['receivedProceduresCount']}";
          analyticsParameters[kEventReloadSyncedBytes] = '${updatedDevFS.syncedBytes}';
          analyticsParameters[kEventReloadInvalidatedSourcesCount] = '${updatedDevFS.invalidatedSourcesCount}';
714
          analyticsParameters[kEventReloadTransferTimeInMs] = '${devFSTimer.elapsed.inMilliseconds}';
715 716 717 718 719
          final int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'];
          final int finalLibraryCount = reloadReport['details']['finalLibraryCount'];
          printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
          reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
        }
720
      }
721 722
    } on Map<String, dynamic> catch (error, stackTrace) {
      printTrace('Hot reload failed: $error\n$stackTrace');
723
      final int errorCode = error['code'];
724
      String errorMessage = error['message'];
725
      if (errorCode == Isolate.kIsolateReloadBarred) {
726 727 728 729
        errorMessage = 'Unable to hot reload application due to an unrecoverable error in '
                       'the source code. Please address the error and then use "R" to '
                       'restart the app.\n'
                       '$errorMessage (error code: $errorCode)';
730
        flutterUsage.sendEvent('hot', 'reload-barred');
731
        return OperationResult(errorCode, errorMessage);
732
      }
733 734 735
      return OperationResult(errorCode, '$errorMessage (error code: $errorCode)');
    } catch (error, stackTrace) {
      printTrace('Hot reload failed: $error\n$stackTrace');
736
      return OperationResult(1, '$error');
737
    }
738
    // Record time it took for the VM to reload the sources.
739 740
    _addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds);

741
    final Stopwatch reassembleTimer = Stopwatch()..start();
742
    // Reload the isolate.
743
    final List<Future<void>> allDevices = <Future<void>>[];
744
    for (FlutterDevice device in flutterDevices) {
745
      printTrace('Sending reload events to ${device.device.name}');
746
      final List<Future<ServiceObject>> futuresViews = <Future<ServiceObject>>[];
747 748
      for (FlutterView view in device.views) {
        printTrace('Sending reload event to "${view.uiIsolate.name}"');
749
        futuresViews.add(view.uiIsolate.reload());
750
      }
751 752 753 754 755
      final Completer<void> deviceCompleter = Completer<void>();
      Future.wait(futuresViews).whenComplete(() { // ignore: unawaited_futures
        deviceCompleter.complete(device.refreshViews());
      });
      allDevices.add(deviceCompleter.future);
756
    }
757
    await Future.wait(allDevices);
758 759
    // We are now running from source.
    _runningFromSnapshot = false;
760
    // Check if any isolates are paused.
761
    final List<FlutterView> reassembleViews = <FlutterView>[];
762 763
    String serviceEventKind;
    int pausedIsolatesFound = 0;
764 765
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
766 767
        // Check if the isolate is paused, and if so, don't reassemble. Ignore the
        // PostPauseEvent event - the client requesting the pause will resume the app.
768
        final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
769
        if (pauseEvent != null && pauseEvent.isPauseEvent && pauseEvent.kind != ServiceEvent.kPausePostRequest) {
770 771 772 773 774 775 776 777
          pausedIsolatesFound += 1;
          if (serviceEventKind == null) {
            serviceEventKind = pauseEvent.kind;
          } else if (serviceEventKind != pauseEvent.kind) {
            serviceEventKind = ''; // many kinds
          }
        } else {
          reassembleViews.add(view);
778
        }
779 780
      }
    }
781 782 783 784 785 786 787
    if (pausedIsolatesFound > 0) {
      if (onSlow != null)
        onSlow('${_describePausedIsolates(pausedIsolatesFound, serviceEventKind)}; interface might not update.');
      if (reassembleViews.isEmpty) {
        printTrace('Skipping reassemble because all isolates are paused.');
        return OperationResult(OperationResult.ok.code, reloadMessage);
      }
788
    }
789
    assert(reassembleViews.isNotEmpty);
790
    printTrace('Evicting dirty assets');
791
    await _evictDirtyAssets();
792
    printTrace('Reassembling application');
793
    bool failedReassemble = false;
794
    final List<Future<void>> futures = <Future<void>>[];
795
    for (FlutterView view in reassembleViews) {
796 797 798 799 800
      futures.add(() async {
        try {
          await view.uiIsolate.flutterReassemble();
        } catch (error) {
          failedReassemble = true;
801
          printError('Reassembling ${view.uiIsolate.name} failed: $error');
802
          return;
803
        }
804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845
        try {
          await view.uiIsolate.uiWindowScheduleFrame();
        } catch (error) {
          failedReassemble = true;
          printError('Scheduling a frame for ${view.uiIsolate.name} failed: $error');
        }
      }());
    }
    final Future<void> reassembleFuture = Future.wait<void>(futures).then<void>((List<void> values) { });
    await reassembleFuture.timeout(
      const Duration(seconds: 2),
      onTimeout: () async {
        if (pausedIsolatesFound > 0) {
          shouldReportReloadTime = false;
          return; // probably no point waiting, they're probably deadlocked and we've already warned.
        }
        // Check if any isolate is newly paused.
        printTrace('This is taking a long time; will now check for paused isolates.');
        int postReloadPausedIsolatesFound = 0;
        String serviceEventKind;
        for (FlutterView view in reassembleViews) {
          await view.uiIsolate.reload();
          final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
          if (pauseEvent != null && pauseEvent.isPauseEvent) {
            postReloadPausedIsolatesFound += 1;
            if (serviceEventKind == null) {
              serviceEventKind = pauseEvent.kind;
            } else if (serviceEventKind != pauseEvent.kind) {
              serviceEventKind = ''; // many kinds
            }
          }
        }
        printTrace('Found $postReloadPausedIsolatesFound newly paused isolate(s).');
        if (postReloadPausedIsolatesFound == 0) {
          await reassembleFuture; // must just be taking a long time... keep waiting!
          return;
        }
        shouldReportReloadTime = false;
        if (onSlow != null)
          onSlow('${_describePausedIsolates(postReloadPausedIsolatesFound, serviceEventKind)}.');
      },
    );
846
    // Record time it took for Flutter to reassemble the application.
847
    _addBenchmarkData('hotReloadFlutterReassembleMilliseconds', reassembleTimer.elapsed.inMilliseconds);
848

849
    reloadTimer.stop();
850 851 852 853 854 855
    final Duration reloadDuration = reloadTimer.elapsed;
    final int reloadInMs = reloadDuration.inMilliseconds;

    analyticsParameters[kEventReloadOverallTimeInMs] = '$reloadInMs';
    flutterUsage.sendEvent('hot', 'reload', parameters: analyticsParameters);

856 857 858 859 860 861 862
    if (shouldReportReloadTime) {
      printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadDuration)}.');
      // Record complete time it took for the reload.
      _addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs);
    }
    // Only report timings if we reloaded a single view without any errors.
    if ((reassembleViews.length == 1) && !failedReassemble && shouldReportReloadTime)
863
      flutterUsage.sendTiming('hot', 'reload', reloadDuration);
864

865
    return OperationResult(
866
      failedReassemble ? 1 : OperationResult.ok.code,
867
      reloadMessage,
868
    );
869 870
  }

871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900
  String _describePausedIsolates(int pausedIsolatesFound, String serviceEventKind) {
    assert(pausedIsolatesFound > 0);
    final StringBuffer message = StringBuffer();
    bool plural;
    if (pausedIsolatesFound == 1) {
      if (flutterDevices.length == 1 && flutterDevices.single.views.length == 1) {
        message.write('The application is ');
      } else {
        message.write('An isolate is ');
      }
      plural = false;
    } else {
      message.write('$pausedIsolatesFound isolates are ');
      plural = true;
    }
    assert(serviceEventKind != null);
    switch (serviceEventKind) {
      case ServiceEvent.kPauseStart: message.write('paused (probably due to --start-paused)'); break;
      case ServiceEvent.kPauseExit: message.write('paused because ${ plural ? 'they have' : 'it has' } terminated'); break;
      case ServiceEvent.kPauseBreakpoint: message.write('paused in the debugger on a breakpoint'); break;
      case ServiceEvent.kPauseInterrupted: message.write('paused due in the debugger'); break;
      case ServiceEvent.kPauseException: message.write('paused in the debugger after an exception was thrown'); break;
      case ServiceEvent.kPausePostRequest: message.write('paused'); break;
      case '': message.write('paused for various reasons'); break;
      default:
        message.write('paused');
    }
    return message.toString();
  }

901 902 903 904 905 906 907 908 909 910 911 912 913 914
  bool _isPaused() {
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
        if (view.uiIsolate != null) {
          final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
          if (pauseEvent != null && pauseEvent.isPauseEvent) {
            return true;
          }
        }
      }
    }
    return false;
  }

915
  @override
916
  void printHelp({ @required bool details }) {
917
    const String fire = '🔥';
918 919 920 921
    String rawMessage = '  To hot reload changes while running, press "r". ';
    if (canHotRestart) {
      rawMessage += 'To hot restart (and rebuild state), press "R".';
    }
922
    final String message = terminal.color(
923
      fire + terminal.bolden(rawMessage),
924
      TerminalColor.red,
925
    );
926
    printStatus(message);
927 928 929 930 931
    for (FlutterDevice device in flutterDevices) {
      final String dname = device.device.name;
      for (Uri uri in device.observatoryUris)
        printStatus('An Observatory debugger and profiler on $dname is available at: $uri');
    }
932 933 934
    final String quitMessage = _didAttach
        ? 'To detach, press "d"; to quit, press "q".'
        : 'To quit, press "q".';
935
    if (details) {
936
      printHelpDetails();
937
      printStatus('To repeat this help message, press "h". $quitMessage');
938
    } else {
939
      printStatus('For a more detailed help message, press "h". $quitMessage');
940
    }
941 942 943
  }

  @override
944
  Future<void> cleanupAfterSignal() async {
945
    await stopEchoingDeviceLog();
946
    await hotRunnerConfig.runPreShutdownOperations();
947 948 949 950 951
    if (_didAttach) {
      appFinished();
    } else {
      await stopApp();
    }
952 953
  }

954
  @override
955 956 957 958
  Future<void> preStop() async {
    await _cleanupDevFS();
    await hotRunnerConfig.runPreShutdownOperations();
  }
959

960
  @override
961
  Future<void> cleanupAtFinish() async {
962 963 964 965
    await _cleanupDevFS();
    await stopEchoingDeviceLog();
  }
}