run_hot.dart 41.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// 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
import 'package:pool/pool.dart';
11

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

29 30 31 32 33 34
ProjectFileInvalidator get projectFileInvalidator => context.get<ProjectFileInvalidator>() ?? _defaultInvalidator;
final ProjectFileInvalidator _defaultInvalidator = ProjectFileInvalidator(
  fileSystem: fs,
  platform: platform,
  logger: logger,
);
35 36 37

HotRunnerConfig get hotRunnerConfig => context.get<HotRunnerConfig>();

38 39 40
class HotRunnerConfig {
  /// Should the hot runner assume that the minimal Dart dependencies do not change?
  bool stableDartDependencies = false;
41 42 43 44

  /// Whether the hot runner should scan for modified files asynchronously.
  bool asyncScanning = false;

45 46 47 48 49
  /// 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;
  }
50 51 52 53 54
  /// A hook for implementations to perform any necessary operations right
  /// before the runner is about to be shut down.
  Future<void> runPreShutdownOperations() async {
    return;
  }
55 56
}

57 58
const bool kHotReloadDefault = true;

59 60 61 62 63 64 65
class DeviceReloadReport {
  DeviceReloadReport(this.device, this.reports);

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

66
// TODO(mklim): Test this, flutter/flutter#23031.
67 68
class HotRunner extends ResidentRunner {
  HotRunner(
69
    List<FlutterDevice> devices, {
70 71
    String target,
    DebuggingOptions debuggingOptions,
72
    this.benchmarkMode = false,
73
    this.applicationBinary,
74
    this.hostIsIde = false,
75 76
    String projectRootPath,
    String packagesFilePath,
77
    String dillOutputPath,
78 79
    bool stayResident = true,
    bool ipv6 = false,
80
  }) : super(devices,
81 82
             target: target,
             debuggingOptions: debuggingOptions,
83 84
             projectRootPath: projectRootPath,
             packagesFilePath: packagesFilePath,
85
             stayResident: stayResident,
86
             hotMode: true,
87
             dillOutputPath: dillOutputPath,
88
             ipv6: ipv6);
89

90
  final bool benchmarkMode;
91
  final File applicationBinary;
92
  final bool hostIsIde;
93
  bool _didAttach = false;
94

95
  final Map<String, List<int>> benchmarkData = <String, List<int>>{};
96 97
  // The initial launch is from a snapshot.
  bool _runningFromSnapshot = true;
98
  DateTime firstBuildTime;
99

100 101 102 103 104
  void _addBenchmarkData(String name, int value) {
    benchmarkData[name] ??= <int>[];
    benchmarkData[name].add(value);
  }

105 106 107 108 109
  Future<void> _reloadSourcesService(
    String isolateId, {
    bool force = false,
    bool pause = false,
  }) async {
110
    // TODO(cbernaschina): check that isolateId is the id of the UI isolate.
111
    final OperationResult result = await restart(pause: pause);
112
    if (!result.isOk) {
113
      throw rpc.RpcException(
114 115 116 117 118 119
        rpc_error_code.INTERNAL_ERROR,
        'Unable to reload sources',
      );
    }
  }

120 121
  Future<void> _restartService({ bool pause = false }) async {
    final OperationResult result =
122
      await restart(fullRestart: true, pause: pause);
123 124 125 126 127 128 129 130
    if (!result.isOk) {
      throw rpc.RpcException(
        rpc_error_code.INTERNAL_ERROR,
        'Unable to restart',
      );
    }
  }

131 132 133 134 135 136 137 138 139
  Future<String> _compileExpressionService(
    String isolateId,
    String expression,
    List<String> definitions,
    List<String> typeDefinitions,
    String libraryUri,
    String klass,
    bool isStatic,
  ) async {
140 141 142 143 144
    for (FlutterDevice device in flutterDevices) {
      if (device.generator != null) {
        final CompilerOutput compilerOutput =
            await device.generator.compileExpression(expression, definitions,
                typeDefinitions, libraryUri, klass, isStatic);
145
        if (compilerOutput != null && compilerOutput.outputFilename != null) {
146 147 148 149
          return base64.encode(fs.file(compilerOutput.outputFilename).readAsBytesSync());
        }
      }
    }
150
    throw 'Failed to compile $expression';
151 152
  }

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 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
  @override
  Future<OperationResult> reloadMethod({String libraryId, String classId}) async {
    final Stopwatch stopwatch = Stopwatch()..start();
    final UpdateFSReport results = UpdateFSReport(success: true);
    final List<Uri> invalidated =  <Uri>[Uri.parse(libraryId)];
    for (FlutterDevice device in flutterDevices) {
      results.incorporateResults(await device.updateDevFS(
        mainPath: mainPath,
        target: target,
        bundle: assetBundle,
        firstBuildTime: firstBuildTime,
        bundleFirstUpload: false,
        bundleDirty: false,
        fullRestart: false,
        projectRootPath: projectRootPath,
        pathToReload: getReloadPath(fullRestart: false),
        invalidatedFiles: invalidated,
        dillOutputPath: dillOutputPath,
      ));
    }
    if (!results.success) {
      return OperationResult(1, 'Failed to compile');
    }
    try {
      final String entryPath = fs.path.relative(
        getReloadPath(fullRestart: false),
        from: projectRootPath,
      );
      for (FlutterDevice device in flutterDevices) {
        final List<Future<Map<String, dynamic>>> reportFutures = device.reloadSources(
          entryPath, pause: false,
        );
        final List<Map<String, dynamic>> reports = await Future.wait(reportFutures);
        final Map<String, dynamic> firstReport = reports.first;
        await device.updateReloadStatus(validateReloadReport(firstReport, printErrors: false));
      }
    } catch (error) {
      return OperationResult(1, error.toString());
    }

    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
        await view.uiIsolate.flutterFastReassemble(classId);
      }
    }

    printStatus('reloadMethod took ${stopwatch.elapsedMilliseconds}');
    flutterUsage.sendTiming('hot', 'ui', stopwatch.elapsed);
    return OperationResult.ok;
  }

204
  // Returns the exit code of the flutter tool process, like [run].
205
  @override
206
  Future<int> attach({
207
    Completer<DebugConnectionInfo> connectionInfoCompleter,
208
    Completer<void> appStartedCompleter,
209
  }) async {
210
    _didAttach = true;
211
    try {
212 213
      await connectToServiceProtocol(
        reloadSources: _reloadSourcesService,
214
        restart: _restartService,
215
        compileExpression: _compileExpressionService,
216
        reloadMethod: reloadMethod,
217
      );
218 219
    } catch (error) {
      printError('Error connecting to the service protocol: $error');
220 221 222 223 224 225 226
      // https://github.com/flutter/flutter/issues/33050
      // TODO(blasten): Remove this check once https://issuetracker.google.com/issues/132325318 has been fixed.
      if (await hasDeviceRunningAndroidQ(flutterDevices) &&
          error.toString().contains(kAndroidQHttpConnectionClosedExp)) {
        printStatus('🔨 If you are using an emulator running Android Q Beta, consider using an emulator running API level 29 or lower.');
        printStatus('Learn more about the status of this issue on https://issuetracker.google.com/issues/132325318.');
      }
227 228 229
      return 2;
    }

230
    for (FlutterDevice device in flutterDevices) {
231
      device.initLogReader();
232
    }
233
    try {
234
      final List<Uri> baseUris = await _initDevFS();
235
      if (connectionInfoCompleter != null) {
236
        // Only handle one debugger connection.
237
        connectionInfoCompleter.complete(
238
          DebugConnectionInfo(
239 240
            httpUri: flutterDevices.first.vmService.httpAddress,
            wsUri: flutterDevices.first.vmService.wsAddress,
241
            baseUri: baseUris.first.toString(),
242
          ),
243 244 245 246 247 248
        );
      }
    } catch (error) {
      printError('Error initializing DevFS: $error');
      return 3;
    }
249
    final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start();
250
    final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: true);
251 252 253 254
    _addBenchmarkData(
      'hotReloadInitialDevFSSyncMilliseconds',
      initialUpdateDevFSsTimer.elapsed.inMilliseconds,
    );
255
    if (!devfsResult.success) {
256
      return 3;
257
    }
258

259
    await refreshViews();
260
    for (FlutterDevice device in flutterDevices) {
261 262
      // VM must have accepted the kernel binary, there will be no reload
      // report, so we let incremental compiler know that source code was accepted.
263
      if (device.generator != null) {
264
        device.generator.accept();
265 266
      }
      for (FlutterView view in device.views) {
267
        printTrace('Connected to $view.');
268
      }
269
    }
270

271 272 273 274 275 276 277 278 279 280 281 282
    // In fast-start mode, apps are initialized from a placeholder splashscreen
    // app. We must do a restart here to load the program and assets for the
    // real app.
    if (debuggingOptions.fastStart) {
      await restart(
        fullRestart: true,
        benchmarkMode: !debuggingOptions.startPaused,
        reason: 'restart',
        silent: true,
      );
    }

283 284 285 286 287 288 289
    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');
290
      await restart(fullRestart: true, benchmarkMode: true);
291 292 293
      printStatus('Benchmarking hot reload');
      // Measure time to perform a hot reload.
      await restart(fullRestart: false);
294 295 296 297 298 299
      if (stayResident) {
        await waitForAppToFinish();
      } else {
        printStatus('Benchmark completed. Exiting application.');
        await _cleanupDevFS();
        await stopEchoingDeviceLog();
300
        await exitApp();
301
      }
302 303
      final File benchmarkOutput = fs.file('hot_benchmark.json');
      benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
304
      return 0;
305
    }
306
    writeVmserviceFile();
307

308
    int result = 0;
309
    if (stayResident) {
310
      result = await waitForAppToFinish();
311
    }
312
    await cleanupAtFinish();
313
    return result;
314 315
  }

316 317
  @override
  Future<int> run({
318
    Completer<DebugConnectionInfo> connectionInfoCompleter,
319
    Completer<void> appStartedCompleter,
320
    String route,
321
  }) async {
322 323
    if (!fs.isFileSync(mainPath)) {
      String message = 'Tried to run $mainPath, but that file does not exist.';
324
      if (target == null) {
325
        message += '\nConsider using the -t option to specify the Dart file to start.';
326
      }
327 328 329 330
      printError(message);
      return 1;
    }

331
    firstBuildTime = DateTime.now();
332

333
    for (FlutterDevice device in flutterDevices) {
334
      final int result = await device.runHot(
335 336
        hotRunner: this,
        route: route,
337 338 339 340
      );
      if (result != 0) {
        return result;
      }
341 342
    }

343 344
    return attach(
      connectionInfoCompleter: connectionInfoCompleter,
345
      appStartedCompleter: appStartedCompleter,
346
    );
347 348
  }

349
  Future<List<Uri>> _initDevFS() async {
350
    final String fsName = fs.path.basename(projectRootPath);
351 352 353 354 355 356 357 358
    return <Uri>[
      for (FlutterDevice device in flutterDevices)
        await device.setupDevFS(
          fsName,
          fs.directory(projectRootPath),
          packagesFilePath: packagesFilePath,
        ),
    ];
359
  }
360

361
  Future<UpdateFSReport> _updateDevFS({ bool fullRestart = false }) async {
362
    final bool isFirstUpload = !assetBundle.wasBuiltOnce();
363
    final bool rebuildBundle = assetBundle.needsBuild();
364
    if (rebuildBundle) {
365
      printTrace('Updating assets');
366
      final int result = await assetBundle.build();
367
      if (result != 0) {
368
        return UpdateFSReport(success: false);
369
      }
370
    }
371 372 373

    // Picking up first device's compiler as a source of truth - compilers
    // for all devices should be in sync.
374
    final List<Uri> invalidatedFiles = await projectFileInvalidator.findInvalidated(
375
      lastCompiled: flutterDevices[0].devFS.lastCompiled,
376 377
      urisToMonitor: flutterDevices[0].devFS.sources,
      packagesPath: packagesFilePath,
378
      asyncScanning: hotRunnerConfig.asyncScanning,
379
    );
380
    final UpdateFSReport results = UpdateFSReport(success: true);
381
    for (FlutterDevice device in flutterDevices) {
382
      results.incorporateResults(await device.updateDevFS(
383 384
        mainPath: mainPath,
        target: target,
385
        bundle: assetBundle,
386 387
        firstBuildTime: firstBuildTime,
        bundleFirstUpload: isFirstUpload,
388
        bundleDirty: !isFirstUpload && rebuildBundle,
389 390
        fullRestart: fullRestart,
        projectRootPath: projectRootPath,
391
        pathToReload: getReloadPath(fullRestart: fullRestart),
392
        invalidatedFiles: invalidatedFiles,
393
        dillOutputPath: dillOutputPath,
394 395
      ));
    }
396
    return results;
397 398
  }

399
  void _resetDirtyAssets() {
400
    for (FlutterDevice device in flutterDevices) {
401
      device.devFS.assetPathsToEvict.clear();
402
    }
403 404
  }

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

422 423 424 425 426 427
  Future<void> _launchInView(
    FlutterDevice device,
    Uri entryUri,
    Uri packagesUri,
    Uri assetsDirectoryUri,
  ) {
428 429 430 431
    return Future.wait(<Future<void>>[
      for (FlutterView view in device.views)
        view.runFromSource(entryUri, packagesUri, assetsDirectoryUri),
    ]);
432 433
  }

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

460 461
  Future<OperationResult> _restartFromSources({
    String reason,
462
    bool benchmarkMode = false,
463
  }) async {
464 465 466 467 468
    if (!_isPaused()) {
      printTrace('Refreshing active FlutterViews before restarting.');
      await refreshViews();
    }

469
    final Stopwatch restartTimer = Stopwatch()..start();
470 471
    // TODO(aam): Add generator reset logic once we switch to using incremental
    // compiler for full application recompilation on restart.
472 473
    final UpdateFSReport updatedDevFS = await _updateDevFS(fullRestart: true);
    if (!updatedDevFS.success) {
474
      for (FlutterDevice device in flutterDevices) {
475
        if (device.generator != null) {
476
          await device.generator.reject();
477
        }
478
      }
479
      return OperationResult(1, 'DevFS synchronization failed');
480 481 482 483 484
    }
    _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.
485
      if (device.generator != null) {
486
        device.generator.accept();
487
      }
488
    }
489
    // Check if the isolate is paused and resume it.
490
    final List<Future<void>> futures = <Future<void>>[];
491 492
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
493 494
        if (view.uiIsolate == null) {
          continue;
495
        }
496
        // Reload the isolate.
497 498 499 500 501 502 503 504
        futures.add(view.uiIsolate.reload().then((ServiceObject _) {
          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();
          }
          return null;
        }));
505 506
      }
    }
507
    await Future.wait(futures);
508

509 510
    // We are now running from source.
    _runningFromSnapshot = false;
511
    await _launchFromDevFS(mainPath + '.dill');
512
    restartTimer.stop();
513
    printTrace('Hot restart performed in ${getElapsedAsMilliseconds(restartTimer.elapsed)}.');
514 515
    // We are now running from sources.
    _runningFromSnapshot = false;
516 517
    _addBenchmarkData('hotRestartMillisecondsToFrame',
        restartTimer.elapsed.inMilliseconds);
518 519

    // Send timing analytics.
520
    flutterUsage.sendTiming('hot', 'restart', restartTimer.elapsed);
521 522 523 524 525 526 527

    // In benchmark mode, make sure all stream notifications have finished.
    if (benchmarkMode) {
      final List<Future<void>> isolateNotifications = <Future<void>>[];
      for (FlutterDevice device in flutterDevices) {
        for (FlutterView view in device.views) {
          isolateNotifications.add(
528 529
            view.owner.vm.vmService.onIsolateEvent
              .then((Stream<ServiceEvent> serviceEvents) async {
530
              await for (ServiceEvent serviceEvent in serviceEvents) {
531 532
                if (serviceEvent.owner.name.contains('_spawn')
                  && serviceEvent.kind == ServiceEvent.kIsolateExit) {
533 534 535 536 537 538 539 540 541
                  return;
                }
              }
            }),
          );
        }
      }
      await Future.wait(isolateNotifications);
    }
542
    return OperationResult.ok;
543 544 545
  }

  /// Returns [true] if the reload was successful.
546
  /// Prints errors if [printErrors] is [true].
547 548 549 550
  static bool validateReloadReport(
    Map<String, dynamic> reloadReport, {
    bool printErrors = true,
  }) {
551
    if (reloadReport == null) {
552
      if (printErrors) {
553
        printError('Hot reload did not receive reload report.');
554
      }
555 556 557 558 559 560 561
      return false;
    }
    if (!(reloadReport['type'] == 'ReloadReport' &&
          (reloadReport['success'] == true ||
           (reloadReport['success'] == false &&
            (reloadReport['details'] is Map<String, dynamic> &&
             reloadReport['details']['notices'] is List<dynamic> &&
562 563
             (reloadReport['details']['notices'] as List<dynamic>).isNotEmpty &&
             (reloadReport['details']['notices'] as List<dynamic>).every(
564 565 566 567 568 569
               (dynamic item) => item is Map<String, dynamic> && item['message'] is String
             )
            )
           )
          )
         )) {
570
      if (printErrors) {
571
        printError('Hot reload received invalid response: $reloadReport');
572
      }
573 574
      return false;
    }
575
    if (!(reloadReport['success'] as bool)) {
576 577
      if (printErrors) {
        printError('Hot reload was rejected:');
578
        for (Map<String, dynamic> notice in reloadReport['details']['notices']) {
579
          printError('${notice['message']}');
580
        }
581
      }
582 583 584 585 586
      return false;
    }
    return true;
  }

587 588 589
  @override
  bool get supportsRestart => true;

590
  @override
591 592 593
  Future<OperationResult> restart({
    bool fullRestart = false,
    String reason,
594
    bool benchmarkMode = false,
595 596
    bool silent = false,
    bool pause = false,
597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614
  }) async {
    String targetPlatform;
    String sdkName;
    bool emulator;
    if (flutterDevices.length == 1) {
      final Device device = flutterDevices.first.device;
      targetPlatform = getNameForTargetPlatform(await device.targetPlatform);
      sdkName = await device.sdkNameAndVersion;
      emulator = await device.isLocalEmulator;
    } else if (flutterDevices.length > 1) {
      targetPlatform = 'multiple';
      sdkName = 'multiple';
      emulator = false;
    } else {
      targetPlatform = 'unknown';
      sdkName = 'unknown';
      emulator = false;
    }
615
    final Stopwatch timer = Stopwatch()..start();
616
    if (fullRestart) {
617 618 619 620 621 622
      final OperationResult result = await _fullRestartHelper(
        targetPlatform: targetPlatform,
        sdkName: sdkName,
        emulator: emulator,
        reason: reason,
        benchmarkMode: benchmarkMode,
623
        silent: silent,
624
      );
625 626 627
      if (!silent) {
        printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
      }
628 629 630 631 632 633 634
      return result;
    }
    final OperationResult result = await _hotReloadHelper(
      targetPlatform: targetPlatform,
      sdkName: sdkName,
      emulator: emulator,
      reason: reason,
635
      pause: pause,
636 637 638
    );
    if (result.isOk) {
      final String elapsed = getElapsedAsMilliseconds(timer.elapsed);
639 640 641
      if (!silent) {
        printStatus('${result.message} in $elapsed.');
      }
642 643 644 645 646 647 648 649 650 651
    }
    return result;
  }

  Future<OperationResult> _fullRestartHelper({
    String targetPlatform,
    String sdkName,
    bool emulator,
    String reason,
    bool benchmarkMode,
652
    bool silent,
653 654 655 656
  }) async {
    if (!canHotRestart) {
      return OperationResult(1, 'hotRestart not supported');
    }
657 658 659 660 661 662 663 664
    Status status;
    if (!silent) {
      status = logger.startProgress(
        'Performing hot restart...',
        timeout: timeoutConfiguration.fastOperation,
        progressId: 'hot.restart',
      );
    }
665 666 667 668 669
    OperationResult result;
    String restartEvent = 'restart';
    try {
      if (!(await hotRunnerConfig.setupHotRestart())) {
        return OperationResult(1, 'setupHotRestart failed');
Devon Carew's avatar
Devon Carew committed
670
      }
671 672 673 674 675
      // The current implementation of the vmservice and JSON rpc may throw
      // unhandled exceptions into the zone that cannot be caught with a regular
      // try catch. The usage is [asyncGuard] is required to normalize the error
      // handling, at least until we can refactor the underlying code.
      result = await asyncGuard(() => _restartFromSources(
676 677
        reason: reason,
        benchmarkMode: benchmarkMode,
678
      ));
679 680
      if (!result.isOk) {
        restartEvent = 'restart-failed';
681
      }
682 683 684 685 686 687 688 689 690 691
    } on rpc.RpcException {
      restartEvent = 'exception';
      return OperationResult(1, 'hot restart failed to complete', fatal: true);
    } finally {
      HotEvent(restartEvent,
        targetPlatform: targetPlatform,
        sdkName: sdkName,
        emulator: emulator,
        fullRestart: true,
        reason: reason).send();
692
      status?.cancel();
693
    }
694
    return result;
695
  }
696

697 698 699 700 701
  Future<OperationResult> _hotReloadHelper({
    String targetPlatform,
    String sdkName,
    bool emulator,
    String reason,
702
    bool pause,
703 704 705 706 707 708 709 710 711 712 713 714 715 716 717
  }) async {
    final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
    final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
    Status status = logger.startProgress(
      '$progressPrefix hot reload...',
      timeout: timeoutConfiguration.fastOperation,
      progressId: 'hot.reload',
    );
    OperationResult result;
    try {
      result = await _reloadSources(
        targetPlatform: targetPlatform,
        sdkName: sdkName,
        emulator: emulator,
        reason: reason,
718
        pause: pause,
719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
        onSlow: (String message) {
          status?.cancel();
          status = logger.startProgress(
            message,
            timeout: timeoutConfiguration.slowOperation,
            progressId: 'hot.reload',
          );
        },
      );
    } on rpc.RpcException {
      HotEvent('exception',
        targetPlatform: targetPlatform,
        sdkName: sdkName,
        emulator: emulator,
        fullRestart: false,
        reason: reason).send();
      return OperationResult(1, 'hot reload failed to complete', fatal: true);
    } finally {
      status.cancel();
738
    }
739 740 741 742 743 744 745 746 747
    return result;
  }

  Future<OperationResult> _reloadSources({
    String targetPlatform,
    String sdkName,
    bool emulator,
    bool pause = false,
    String reason,
748
    void Function(String message) onSlow,
749
  }) async {
750 751
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
752
        if (view.uiIsolate == null) {
753
          return OperationResult(2, 'Application isolate not found', fatal: true);
754
        }
755 756
      }
    }
757

758 759 760 761 762 763
    // 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.
764
    bool shouldReportReloadTime = !_runningFromSnapshot;
765
    final Stopwatch reloadTimer = Stopwatch()..start();
766

767 768 769 770 771
    if (!_isPaused()) {
      printTrace('Refreshing active FlutterViews before reloading.');
      await refreshViews();
    }

772
    final Stopwatch devFSTimer = Stopwatch()..start();
773
    final UpdateFSReport updatedDevFS = await _updateDevFS();
774
    // Record time it took to synchronize to DevFS.
775
    _addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds);
776
    if (!updatedDevFS.success) {
777
      return OperationResult(1, 'DevFS synchronization failed');
778
    }
Devon Carew's avatar
Devon Carew committed
779
    String reloadMessage;
780
    final Stopwatch vmReloadTimer = Stopwatch()..start();
781
    Map<String, dynamic> firstReloadDetails;
782
    try {
783
      final String entryPath = fs.path.relative(
784
        getReloadPath(fullRestart: false),
785 786
        from: projectRootPath,
      );
787
      final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[];
788
      for (FlutterDevice device in flutterDevices) {
789 790 791 792 793
        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();
        }
794
        final List<Future<Map<String, dynamic>>> reportFutures = device.reloadSources(
795
          entryPath, pause: pause,
796
        );
797
        allReportsFutures.add(Future.wait(reportFutures).then(
798 799 800 801 802 803 804 805 806
          (List<Map<String, dynamic>> reports) async {
            // 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.
            await device.updateReloadStatus(
              validateReloadReport(firstReport, printErrors: false),
            );
807
            return DeviceReloadReport(device, reports);
808 809
          },
        ));
810
      }
811 812 813 814 815
      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.
816 817 818 819 820 821 822
          HotEvent('reload-reject',
            targetPlatform: targetPlatform,
            sdkName: sdkName,
            emulator: emulator,
            fullRestart: false,
            reason: reason,
          ).send();
823 824
          return OperationResult(1, 'Reload rejected');
        }
825 826 827
        // Collect stats only from the first device. If/when run -d all is
        // refactored, we'll probably need to send one hot reload/restart event
        // per device to analytics.
828 829 830
        firstReloadDetails ??= castStringKeyedMap(reloadReport['details']);
        final int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'] as int;
        final int finalLibraryCount = reloadReport['details']['finalLibraryCount'] as int;
831 832
        printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
        reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
833
      }
834 835
    } on Map<String, dynamic> catch (error, stackTrace) {
      printTrace('Hot reload failed: $error\n$stackTrace');
836 837
      final int errorCode = error['code'] as int;
      String errorMessage = error['message'] as String;
838
      if (errorCode == Isolate.kIsolateReloadBarred) {
839 840 841 842
        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)';
843 844 845 846 847 848 849
        HotEvent('reload-barred',
          targetPlatform: targetPlatform,
          sdkName: sdkName,
          emulator: emulator,
          fullRestart: false,
          reason: reason,
        ).send();
850
        return OperationResult(errorCode, errorMessage);
851
      }
852 853 854
      return OperationResult(errorCode, '$errorMessage (error code: $errorCode)');
    } catch (error, stackTrace) {
      printTrace('Hot reload failed: $error\n$stackTrace');
855
      return OperationResult(1, '$error');
856
    }
857
    // Record time it took for the VM to reload the sources.
858
    _addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds);
859
    final Stopwatch reassembleTimer = Stopwatch()..start();
860
    // Reload the isolate.
861
    final List<Future<void>> allDevices = <Future<void>>[];
862
    for (FlutterDevice device in flutterDevices) {
863
      printTrace('Sending reload events to ${device.device.name}');
864
      final List<Future<ServiceObject>> futuresViews = <Future<ServiceObject>>[];
865 866
      for (FlutterView view in device.views) {
        printTrace('Sending reload event to "${view.uiIsolate.name}"');
867
        futuresViews.add(view.uiIsolate.reload());
868
      }
869 870
      allDevices.add(Future.wait(futuresViews).whenComplete(() {
        return device.refreshViews();
871
      }));
872
    }
873
    await Future.wait(allDevices);
874

875 876
    // We are now running from source.
    _runningFromSnapshot = false;
877
    // Check if any isolates are paused.
878
    final List<FlutterView> reassembleViews = <FlutterView>[];
879 880
    String serviceEventKind;
    int pausedIsolatesFound = 0;
881 882
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
883 884
        // 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.
885
        final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
886
        if (pauseEvent != null && pauseEvent.isPauseEvent && pauseEvent.kind != ServiceEvent.kPausePostRequest) {
887 888 889 890 891 892 893 894
          pausedIsolatesFound += 1;
          if (serviceEventKind == null) {
            serviceEventKind = pauseEvent.kind;
          } else if (serviceEventKind != pauseEvent.kind) {
            serviceEventKind = ''; // many kinds
          }
        } else {
          reassembleViews.add(view);
895
        }
896 897
      }
    }
898
    if (pausedIsolatesFound > 0) {
899
      if (onSlow != null) {
900
        onSlow('${_describePausedIsolates(pausedIsolatesFound, serviceEventKind)}; interface might not update.');
901
      }
902 903 904 905
      if (reassembleViews.isEmpty) {
        printTrace('Skipping reassemble because all isolates are paused.');
        return OperationResult(OperationResult.ok.code, reloadMessage);
      }
906
    }
907
    printTrace('Evicting dirty assets');
908
    await _evictDirtyAssets();
909
    assert(reassembleViews.isNotEmpty);
910
    printTrace('Reassembling application');
911
    bool failedReassemble = false;
912 913 914 915 916 917 918 919 920 921 922 923
    final List<Future<void>> futures = <Future<void>>[
      for (FlutterView view in reassembleViews)
        () async {
          try {
            await view.uiIsolate.flutterReassemble();
          } catch (error) {
            failedReassemble = true;
            printError('Reassembling ${view.uiIsolate.name} failed: $error');
            return;
          }
        }(),
    ];
924
    final Future<void> reassembleFuture = Future.wait<void>(futures);
925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953
    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;
954
        if (onSlow != null) {
955
          onSlow('${_describePausedIsolates(postReloadPausedIsolatesFound, serviceEventKind)}.');
956
        }
957 958
      },
    );
959
    // Record time it took for Flutter to reassemble the application.
960
    _addBenchmarkData('hotReloadFlutterReassembleMilliseconds', reassembleTimer.elapsed.inMilliseconds);
961

962
    reloadTimer.stop();
963 964 965
    final Duration reloadDuration = reloadTimer.elapsed;
    final int reloadInMs = reloadDuration.inMilliseconds;

966 967 968 969 970 971 972 973 974 975 976 977
    // 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.
    HotEvent('reload',
      targetPlatform: targetPlatform,
      sdkName: sdkName,
      emulator: emulator,
      fullRestart: false,
      reason: reason,
      overallTimeInMs: reloadInMs,
978 979 980 981
      finalLibraryCount: firstReloadDetails['finalLibraryCount'] as int,
      syncedLibraryCount: firstReloadDetails['receivedLibraryCount'] as int,
      syncedClassesCount: firstReloadDetails['receivedClassesCount'] as int,
      syncedProceduresCount: firstReloadDetails['receivedProceduresCount'] as int,
982 983 984 985
      syncedBytes: updatedDevFS.syncedBytes,
      invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
      transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
    ).send();
986

987 988 989 990 991 992
    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.
993
    if ((reassembleViews.length == 1) && !failedReassemble && shouldReportReloadTime) {
994
      flutterUsage.sendTiming('hot', 'reload', reloadDuration);
995
    }
996
    return OperationResult(
997
      failedReassemble ? 1 : OperationResult.ok.code,
998
      reloadMessage,
999
    );
1000 1001
  }

1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031
  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();
  }

1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045
  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;
  }

1046
  @override
1047
  void printHelp({ @required bool details }) {
1048
    const String fire = '🔥';
1049 1050 1051 1052
    String rawMessage = '  To hot reload changes while running, press "r". ';
    if (canHotRestart) {
      rawMessage += 'To hot restart (and rebuild state), press "R".';
    }
1053
    final String message = terminal.color(
1054
      fire + terminal.bolden(rawMessage),
1055
      TerminalColor.red,
1056
    );
1057
    printStatus(message);
1058 1059
    for (FlutterDevice device in flutterDevices) {
      final String dname = device.device.name;
1060 1061
      printStatus('An Observatory debugger and profiler on $dname is '
        'available at: ${device.vmService.httpAddress}');
1062
    }
1063 1064 1065
    final String quitMessage = _didAttach
        ? 'To detach, press "d"; to quit, press "q".'
        : 'To quit, press "q".';
1066
    if (details) {
1067
      printHelpDetails();
1068
      printStatus('To repeat this help message, press "h". $quitMessage');
1069
    } else {
1070
      printStatus('For a more detailed help message, press "h". $quitMessage');
1071
    }
1072 1073
  }

1074 1075 1076
  Future<void> _evictDirtyAssets() {
    final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[];
    for (FlutterDevice device in flutterDevices) {
1077
      if (device.devFS.assetPathsToEvict.isEmpty) {
1078
        continue;
1079
      }
1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091
      if (device.views.first.uiIsolate == null) {
        printError('Application isolate not found for $device');
        continue;
      }
      for (String assetPath in device.devFS.assetPathsToEvict) {
        futures.add(device.views.first.uiIsolate.flutterEvictAsset(assetPath));
      }
      device.devFS.assetPathsToEvict.clear();
    }
    return Future.wait<Map<String, dynamic>>(futures);
  }

1092
  @override
1093
  Future<void> cleanupAfterSignal() async {
1094
    await stopEchoingDeviceLog();
1095
    await hotRunnerConfig.runPreShutdownOperations();
1096 1097 1098
    if (_didAttach) {
      appFinished();
    } else {
1099
      await exitApp();
1100
    }
1101 1102
  }

1103
  @override
1104
  Future<void> preExit() async {
1105 1106
    await _cleanupDevFS();
    await hotRunnerConfig.runPreShutdownOperations();
1107
    await super.preExit();
1108
  }
1109

1110
  @override
1111
  Future<void> cleanupAtFinish() async {
1112
    for (FlutterDevice flutterDevice in flutterDevices) {
1113
      await flutterDevice.device.dispose();
1114
    }
1115 1116 1117 1118
    await _cleanupDevFS();
    await stopEchoingDeviceLog();
  }
}
1119

1120 1121
/// The [ProjectFileInvalidator] track the dependencies for a running
/// application to determine when they are dirty.
1122
class ProjectFileInvalidator {
1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133
  ProjectFileInvalidator({
    @required FileSystem fileSystem,
    @required Platform platform,
    @required Logger logger,
  }): _fileSystem = fileSystem,
      _platform = platform,
      _logger = logger;

  final FileSystem _fileSystem;
  final Platform _platform;
  final Logger _logger;
1134

1135
  static const String _pubCachePathLinuxAndMac = '.pub-cache';
1136 1137
  static const String _pubCachePathWindows = 'Pub/Cache';

1138 1139 1140 1141 1142 1143 1144 1145
  // As of writing, Dart supports up to 32 asynchronous I/O threads per
  // isolate.  We also want to avoid hitting platform limits on open file
  // handles/descriptors.
  //
  // This value was chosen based on empirical tests scanning a set of
  // ~2000 files.
  static const int _kMaxPendingStats = 8;

1146
  Future<List<Uri>> findInvalidated({
1147 1148 1149
    @required DateTime lastCompiled,
    @required List<Uri> urisToMonitor,
    @required String packagesPath,
1150 1151
    bool asyncScanning = false,
  }) async {
1152 1153 1154 1155
    assert(urisToMonitor != null);
    assert(packagesPath != null);

    if (lastCompiled == null) {
1156
      // Initial load.
1157 1158 1159 1160
      assert(urisToMonitor.isEmpty);
      return <Uri>[];
    }

1161
    final Stopwatch stopwatch = Stopwatch()..start();
1162 1163
    final List<Uri> urisToScan = <Uri>[
      // Don't watch pub cache directories to speed things up a little.
1164 1165
      for (Uri uri in urisToMonitor)
        if (_isNotInPubCache(uri)) uri,
1166 1167

      // We need to check the .packages file too since it is not used in compilation.
1168
      _fileSystem.file(packagesPath).uri,
1169 1170
    ];
    final List<Uri> invalidatedFiles = <Uri>[];
1171 1172 1173 1174 1175 1176

    if (asyncScanning) {
      final Pool pool = Pool(_kMaxPendingStats);
      final List<Future<void>> waitList = <Future<void>>[];
      for (final Uri uri in urisToScan) {
        waitList.add(pool.withResource<void>(
1177 1178
          () => _fileSystem
            .stat(uri.toFilePath(windows: _platform.isWindows))
1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189
            .then((FileStat stat) {
              final DateTime updatedAt = stat.modified;
              if (updatedAt != null && updatedAt.isAfter(lastCompiled)) {
                invalidatedFiles.add(uri);
              }
            })
        ));
      }
      await Future.wait<void>(waitList);
    } else {
      for (final Uri uri in urisToScan) {
1190 1191
        final DateTime updatedAt = _fileSystem.statSync(
            uri.toFilePath(windows: _platform.isWindows)).modified;
1192 1193 1194
        if (updatedAt != null && updatedAt.isAfter(lastCompiled)) {
          invalidatedFiles.add(uri);
        }
1195 1196
      }
    }
1197
    _logger.printTrace(
1198
      'Scanned through ${urisToScan.length} files in '
1199 1200
      '${stopwatch.elapsedMilliseconds}ms'
      '${asyncScanning ? " (async)" : ""}',
1201
    );
1202 1203
    return invalidatedFiles;
  }
1204

1205 1206
  bool _isNotInPubCache(Uri uri) {
    return !(_platform.isWindows && uri.path.contains(_pubCachePathWindows))
1207 1208
        && !uri.path.contains(_pubCachePathLinuxAndMac);
  }
1209
}