run_hot.dart 38 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/async_guard.dart';
12
import 'base/common.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
class HotRunnerConfig {
  /// Should the hot runner assume that the minimal Dart dependencies do not change?
  bool stableDartDependencies = false;
32 33 34 35 36
  /// 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;
  }
37 38 39 40 41
  /// A hook for implementations to perform any necessary operations right
  /// before the runner is about to be shut down.
  Future<void> runPreShutdownOperations() async {
    return;
  }
42 43
}

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

46 47
const bool kHotReloadDefault = true;

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

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

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

79
  final bool benchmarkMode;
80
  final File applicationBinary;
81
  final bool hostIsIde;
82
  bool _didAttach = false;
83

84
  final Map<String, List<int>> benchmarkData = <String, List<int>>{};
85 86
  // The initial launch is from a snapshot.
  bool _runningFromSnapshot = true;
87
  DateTime firstBuildTime;
88

89 90 91 92 93
  void _addBenchmarkData(String name, int value) {
    benchmarkData[name] ??= <int>[];
    benchmarkData[name].add(value);
  }

94 95 96 97 98
  Future<void> _reloadSourcesService(
    String isolateId, {
    bool force = false,
    bool pause = false,
  }) async {
99 100
    // TODO(cbernaschina): check that isolateId is the id of the UI isolate.
    final OperationResult result = await restart(pauseAfterRestart: pause);
101
    if (!result.isOk) {
102
      throw rpc.RpcException(
103 104 105 106 107 108
        rpc_error_code.INTERNAL_ERROR,
        'Unable to reload sources',
      );
    }
  }

109 110 111 112 113 114 115 116 117 118 119
  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',
      );
    }
  }

120 121 122 123 124 125 126 127 128
  Future<String> _compileExpressionService(
    String isolateId,
    String expression,
    List<String> definitions,
    List<String> typeDefinitions,
    String libraryUri,
    String klass,
    bool isStatic,
  ) async {
129 130 131 132 133
    for (FlutterDevice device in flutterDevices) {
      if (device.generator != null) {
        final CompilerOutput compilerOutput =
            await device.generator.compileExpression(expression, definitions,
                typeDefinitions, libraryUri, klass, isStatic);
134
        if (compilerOutput != null && compilerOutput.outputFilename != null) {
135 136 137 138
          return base64.encode(fs.file(compilerOutput.outputFilename).readAsBytesSync());
        }
      }
    }
139
    throw 'Failed to compile $expression';
140 141
  }

142
  // Returns the exit code of the flutter tool process, like [run].
143
  @override
144
  Future<int> attach({
145
    Completer<DebugConnectionInfo> connectionInfoCompleter,
146
    Completer<void> appStartedCompleter,
147
  }) async {
148
    _didAttach = true;
149
    try {
150 151
      await connectToServiceProtocol(
        reloadSources: _reloadSourcesService,
152
        restart: _restartService,
153 154
        compileExpression: _compileExpressionService,
      );
155 156
    } catch (error) {
      printError('Error connecting to the service protocol: $error');
157 158 159 160 161 162 163
      // 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.');
      }
164 165 166
      return 2;
    }

167 168
    for (FlutterDevice device in flutterDevices)
      device.initLogReader();
169
    try {
170
      final List<Uri> baseUris = await _initDevFS();
171
      if (connectionInfoCompleter != null) {
172
        // Only handle one debugger connection.
173
        connectionInfoCompleter.complete(
174
          DebugConnectionInfo(
175 176
            httpUri: flutterDevices.first.observatoryUris.first,
            wsUri: flutterDevices.first.vmServices.first.wsAddress,
177
            baseUri: baseUris.first.toString(),
178 179 180 181 182 183 184
          )
        );
      }
    } catch (error) {
      printError('Error initializing DevFS: $error');
      return 3;
    }
185
    final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start();
186
    final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: true);
187 188 189 190
    _addBenchmarkData(
      'hotReloadInitialDevFSSyncMilliseconds',
      initialUpdateDevFSsTimer.elapsed.inMilliseconds,
    );
191
    if (!devfsResult.success)
192 193
      return 3;

194
    await refreshViews();
195
    for (FlutterDevice device in flutterDevices) {
196 197 198 199
      // 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();
200 201 202
      for (FlutterView view in device.views)
        printTrace('Connected to $view.');
    }
203 204 205 206 207 208 209 210

    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');
211
      await restart(fullRestart: true, benchmarkMode: true);
212 213 214
      printStatus('Benchmarking hot reload');
      // Measure time to perform a hot reload.
      await restart(fullRestart: false);
215 216 217 218 219 220
      if (stayResident) {
        await waitForAppToFinish();
      } else {
        printStatus('Benchmark completed. Exiting application.');
        await _cleanupDevFS();
        await stopEchoingDeviceLog();
221
        await exitApp();
222
      }
223 224
      final File benchmarkOutput = fs.file('hot_benchmark.json');
      benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
225
      return 0;
226 227
    }

228
    int result = 0;
229
    if (stayResident)
230
      result = await waitForAppToFinish();
231
    await cleanupAtFinish();
232
    return result;
233 234
  }

235 236
  @override
  Future<int> run({
237
    Completer<DebugConnectionInfo> connectionInfoCompleter,
238
    Completer<void> appStartedCompleter,
239
    String route,
240
  }) async {
241 242
    if (!fs.isFileSync(mainPath)) {
      String message = 'Tried to run $mainPath, but that file does not exist.';
243 244 245 246 247 248
      if (target == null)
        message += '\nConsider using the -t option to specify the Dart file to start.';
      printError(message);
      return 1;
    }

249
    firstBuildTime = DateTime.now();
250

251 252 253 254 255 256 257 258
    for (FlutterDevice device in flutterDevices) {
      final int result = await device.runHot(
        hotRunner: this,
        route: route,
      );
      if (result != 0) {
        return result;
      }
259 260
    }

261 262
    return attach(
      connectionInfoCompleter: connectionInfoCompleter,
263
      appStartedCompleter: appStartedCompleter,
264
    );
265 266
  }

267
  Future<List<Uri>> _initDevFS() async {
268
    final String fsName = fs.path.basename(projectRootPath);
269 270 271 272 273
    final List<Uri> devFSUris = <Uri>[];
    for (FlutterDevice device in flutterDevices) {
      final Uri uri = await device.setupDevFS(
        fsName,
        fs.directory(projectRootPath),
274
        packagesFilePath: packagesFilePath,
275 276 277 278
      );
      devFSUris.add(uri);
    }
    return devFSUris;
279
  }
280

281
  Future<UpdateFSReport> _updateDevFS({ bool fullRestart = false }) async {
282
    final bool isFirstUpload = assetBundle.wasBuiltOnce() == false;
283
    final bool rebuildBundle = assetBundle.needsBuild();
284
    if (rebuildBundle) {
285
      printTrace('Updating assets');
286
      final int result = await assetBundle.build();
287
      if (result != 0)
288
        return UpdateFSReport(success: false);
289
    }
290 291 292 293 294

    // Picking up first device's compiler as a source of truth - compilers
    // for all devices should be in sync.
    final List<Uri> invalidatedFiles = ProjectFileInvalidator.findInvalidated(
      lastCompiled: flutterDevices[0].devFS.lastCompiled,
295 296 297
      urisToMonitor: flutterDevices[0].devFS.sources,
      packagesPath: packagesFilePath,
    );
298
    final UpdateFSReport results = UpdateFSReport(success: true);
299
    for (FlutterDevice device in flutterDevices) {
300
      results.incorporateResults(await device.updateDevFS(
301 302
        mainPath: mainPath,
        target: target,
303
        bundle: assetBundle,
304 305 306
        firstBuildTime: firstBuildTime,
        bundleFirstUpload: isFirstUpload,
        bundleDirty: isFirstUpload == false && rebuildBundle,
307 308
        fullRestart: fullRestart,
        projectRootPath: projectRootPath,
309
        pathToReload: getReloadPath(fullRestart: fullRestart),
310
        invalidatedFiles: invalidatedFiles,
311
        dillOutputPath: dillOutputPath,
312 313
      ));
    }
314
    return results;
315 316
  }

317 318 319 320 321
  void _resetDirtyAssets() {
    for (FlutterDevice device in flutterDevices)
      device.devFS.assetPathsToEvict.clear();
  }

322
  Future<void> _cleanupDevFS() async {
323
    final List<Future<void>> futures = <Future<void>>[];
324 325
    for (FlutterDevice device in flutterDevices) {
      if (device.devFS != null) {
326 327
        // Cleanup the devFS, but don't wait indefinitely.
        // We ignore any errors, because it's not clear what we would do anyway.
328
        futures.add(device.devFS.destroy()
329 330
          .timeout(const Duration(milliseconds: 250))
          .catchError((dynamic error) {
331
            printTrace('Ignored error while cleaning up DevFS: $error');
332
          }));
333 334 335
      }
      device.devFS = null;
    }
336
    await Future.wait(futures);
337 338
  }

339 340 341 342 343 344
  Future<void> _launchInView(
    FlutterDevice device,
    Uri entryUri,
    Uri packagesUri,
    Uri assetsDirectoryUri,
  ) {
345
    final List<Future<void>> futures = <Future<void>>[];
346
    for (FlutterView view in device.views)
347 348
      futures.add(view.runFromSource(entryUri, packagesUri, assetsDirectoryUri));
    final Completer<void> completer = Completer<void>();
349
    Future.wait(futures).whenComplete(() { completer.complete(null); });
350
    return completer.future;
351 352
  }

353
  Future<void> _launchFromDevFS(String mainScript) async {
354
    final String entryUri = fs.path.relative(mainScript, from: projectRootPath);
355
    final List<Future<void>> futures = <Future<void>>[];
356 357 358
    for (FlutterDevice device in flutterDevices) {
      final Uri deviceEntryUri = device.devFS.baseUri.resolveUri(
        fs.path.toUri(entryUri));
359
      final Uri devicePackagesUri = device.devFS.baseUri.resolve('.packages');
360 361
      final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri(
        fs.path.toUri(getAssetBuildDirectory()));
362
      futures.add(_launchInView(device,
363 364
                          deviceEntryUri,
                          devicePackagesUri,
365
                          deviceAssetsDirectoryUri));
366
    }
367 368 369 370 371 372 373 374
    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);
    }
375 376
  }

377 378 379 380
  Future<OperationResult> _restartFromSources({
    String reason,
    bool benchmarkMode = false
  }) async {
381 382 383 384 385
    if (!_isPaused()) {
      printTrace('Refreshing active FlutterViews before restarting.');
      await refreshViews();
    }

386
    final Stopwatch restartTimer = Stopwatch()..start();
387 388
    // TODO(aam): Add generator reset logic once we switch to using incremental
    // compiler for full application recompilation on restart.
389 390
    final UpdateFSReport updatedDevFS = await _updateDevFS(fullRestart: true);
    if (!updatedDevFS.success) {
391
      for (FlutterDevice device in flutterDevices) {
392
        if (device.generator != null) {
393
          await device.generator.reject();
394
        }
395
      }
396
      return OperationResult(1, 'DevFS synchronization failed');
397 398 399 400 401
    }
    _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.
402
      if (device.generator != null) {
403
        device.generator.accept();
404
      }
405
    }
406
    // Check if the isolate is paused and resume it.
407
    final List<Future<void>> futures = <Future<void>>[];
408 409
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
410 411
        if (view.uiIsolate == null) {
          continue;
412
        }
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
        // Reload the isolate.
        final Completer<void> completer = Completer<void>();
        futures.add(completer.future);
        unawaited(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;
          },
        ).whenComplete(
          () { completer.complete(null); },
        ));
428 429
      }
    }
430
    await Future.wait(futures);
431 432
    // We are now running from source.
    _runningFromSnapshot = false;
433
    await _launchFromDevFS(mainPath + '.dill');
434
    restartTimer.stop();
435
    printTrace('Hot restart performed in ${getElapsedAsMilliseconds(restartTimer.elapsed)}.');
436 437
    // We are now running from sources.
    _runningFromSnapshot = false;
438 439
    _addBenchmarkData('hotRestartMillisecondsToFrame',
        restartTimer.elapsed.inMilliseconds);
440 441

    // Send timing analytics.
442
    flutterUsage.sendTiming('hot', 'restart', restartTimer.elapsed);
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461

    // 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(
            view.owner.vm.vmService.onIsolateEvent.then((Stream<ServiceEvent> serviceEvents) async {
              await for (ServiceEvent serviceEvent in serviceEvents) {
                if (serviceEvent.owner.name.contains('_spawn') && serviceEvent.kind == ServiceEvent.kIsolateExit) {
                  return;
                }
              }
            }),
          );
        }
      }
      await Future.wait(isolateNotifications);
    }
462
    return OperationResult.ok;
463 464 465
  }

  /// Returns [true] if the reload was successful.
466
  /// Prints errors if [printErrors] is [true].
467 468 469 470
  static bool validateReloadReport(
    Map<String, dynamic> reloadReport, {
    bool printErrors = true,
  }) {
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
    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
             )
            )
           )
          )
         )) {
489 490
      if (printErrors)
        printError('Hot reload received invalid response: $reloadReport');
491 492
      return false;
    }
493
    if (!reloadReport['success']) {
494 495 496 497 498
      if (printErrors) {
        printError('Hot reload was rejected:');
        for (Map<String, dynamic> notice in reloadReport['details']['notices'])
          printError('${notice['message']}');
      }
499 500 501 502 503
      return false;
    }
    return true;
  }

504 505 506
  @override
  bool get supportsRestart => true;

507
  @override
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
  Future<OperationResult> restart({
    bool fullRestart = false,
    bool pauseAfterRestart = false,
    String reason,
    bool benchmarkMode = false
  }) 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;
    }
531
    final Stopwatch timer = Stopwatch()..start();
532
    if (fullRestart) {
533 534 535 536 537 538
      final OperationResult result = await _fullRestartHelper(
        targetPlatform: targetPlatform,
        sdkName: sdkName,
        emulator: emulator,
        reason: reason,
        benchmarkMode: benchmarkMode,
539
      );
540
      printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576
      return result;
    }
    final OperationResult result = await _hotReloadHelper(
      targetPlatform: targetPlatform,
      sdkName: sdkName,
      emulator: emulator,
      reason: reason,
      pauseAfterRestart: pauseAfterRestart,
    );
    if (result.isOk) {
      final String elapsed = getElapsedAsMilliseconds(timer.elapsed);
      printStatus('${result.message} in $elapsed.');
    }
    return result;
  }

  Future<OperationResult> _fullRestartHelper({
    String targetPlatform,
    String sdkName,
    bool emulator,
    String reason,
    bool benchmarkMode,
  }) async {
    if (!canHotRestart) {
      return OperationResult(1, 'hotRestart not supported');
    }
    final Status status = logger.startProgress(
      'Performing hot restart...',
      timeout: timeoutConfiguration.fastOperation,
      progressId: 'hot.restart',
    );
    OperationResult result;
    String restartEvent = 'restart';
    try {
      if (!(await hotRunnerConfig.setupHotRestart())) {
        return OperationResult(1, 'setupHotRestart failed');
Devon Carew's avatar
Devon Carew committed
577
      }
578 579 580 581 582
      // 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(
583 584
        reason: reason,
        benchmarkMode: benchmarkMode,
585
      ));
586 587
      if (!result.isOk) {
        restartEvent = 'restart-failed';
588
      }
589 590 591 592 593 594 595 596 597 598 599
    } 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();
      status.cancel();
600
    }
601
    return result;
602
  }
603

604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644
  Future<OperationResult> _hotReloadHelper({
    String targetPlatform,
    String sdkName,
    bool emulator,
    String reason,
    bool pauseAfterRestart = false,
  }) 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,
        pause: pauseAfterRestart,
        reason: reason,
        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();
645
    }
646 647 648 649 650 651 652 653 654 655 656
    return result;
  }

  Future<OperationResult> _reloadSources({
    String targetPlatform,
    String sdkName,
    bool emulator,
    bool pause = false,
    String reason,
    void Function(String message) onSlow
  }) async {
657 658
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
659
        if (view.uiIsolate == null) {
660
          return OperationResult(2, 'Application isolate not found', fatal: true);
661
        }
662 663
      }
    }
664

665 666 667 668 669 670
    // 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.
671
    bool shouldReportReloadTime = !_runningFromSnapshot;
672
    final Stopwatch reloadTimer = Stopwatch()..start();
673

674 675 676 677 678
    if (!_isPaused()) {
      printTrace('Refreshing active FlutterViews before reloading.');
      await refreshViews();
    }

679
    final Stopwatch devFSTimer = Stopwatch()..start();
680
    final UpdateFSReport updatedDevFS = await _updateDevFS();
681
    // Record time it took to synchronize to DevFS.
682
    _addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds);
683
    if (!updatedDevFS.success) {
684
      return OperationResult(1, 'DevFS synchronization failed');
685
    }
Devon Carew's avatar
Devon Carew committed
686
    String reloadMessage;
687
    final Stopwatch vmReloadTimer = Stopwatch()..start();
688
    Map<String, dynamic> firstReloadDetails;
689
    try {
690
      final String entryPath = fs.path.relative(
691
        getReloadPath(fullRestart: false),
692 693
        from: projectRootPath,
      );
694
      final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[];
695
      for (FlutterDevice device in flutterDevices) {
696 697 698 699 700
        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();
        }
701 702 703
        final Completer<DeviceReloadReport> completer = Completer<DeviceReloadReport>();
        allReportsFutures.add(completer.future);
        final List<Future<Map<String, dynamic>>> reportFutures = device.reloadSources(
704
          entryPath, pause: pause,
705
        );
706 707 708 709 710 711 712 713 714 715 716 717 718
        unawaited(Future.wait(reportFutures).then(
          (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),
            );
            completer.complete(DeviceReloadReport(device, reports));
          },
        ));
719
      }
720 721 722 723 724
      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.
725 726 727 728 729 730 731
          HotEvent('reload-reject',
            targetPlatform: targetPlatform,
            sdkName: sdkName,
            emulator: emulator,
            fullRestart: false,
            reason: reason,
          ).send();
732 733
          return OperationResult(1, 'Reload rejected');
        }
734 735 736 737 738 739 740 741
        // 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.
        firstReloadDetails ??= reloadReport['details'];
        final int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'];
        final int finalLibraryCount = reloadReport['details']['finalLibraryCount'];
        printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
        reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
742
      }
743 744
    } on Map<String, dynamic> catch (error, stackTrace) {
      printTrace('Hot reload failed: $error\n$stackTrace');
745
      final int errorCode = error['code'];
746
      String errorMessage = error['message'];
747
      if (errorCode == Isolate.kIsolateReloadBarred) {
748 749 750 751
        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)';
752 753 754 755 756 757 758
        HotEvent('reload-barred',
          targetPlatform: targetPlatform,
          sdkName: sdkName,
          emulator: emulator,
          fullRestart: false,
          reason: reason,
        ).send();
759
        return OperationResult(errorCode, errorMessage);
760
      }
761 762 763
      return OperationResult(errorCode, '$errorMessage (error code: $errorCode)');
    } catch (error, stackTrace) {
      printTrace('Hot reload failed: $error\n$stackTrace');
764
      return OperationResult(1, '$error');
765
    }
766
    // Record time it took for the VM to reload the sources.
767
    _addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds);
768
    final Stopwatch reassembleTimer = Stopwatch()..start();
769
    // Reload the isolate.
770
    final List<Future<void>> allDevices = <Future<void>>[];
771
    for (FlutterDevice device in flutterDevices) {
772
      printTrace('Sending reload events to ${device.device.name}');
773
      final List<Future<ServiceObject>> futuresViews = <Future<ServiceObject>>[];
774 775
      for (FlutterView view in device.views) {
        printTrace('Sending reload event to "${view.uiIsolate.name}"');
776
        futuresViews.add(view.uiIsolate.reload());
777
      }
778
      final Completer<void> deviceCompleter = Completer<void>();
779
      unawaited(Future.wait(futuresViews).whenComplete(() {
780
        deviceCompleter.complete(device.refreshViews());
781
      }));
782
      allDevices.add(deviceCompleter.future);
783
    }
784
    await Future.wait(allDevices);
785 786
    // We are now running from source.
    _runningFromSnapshot = false;
787
    // Check if any isolates are paused.
788
    final List<FlutterView> reassembleViews = <FlutterView>[];
789 790
    String serviceEventKind;
    int pausedIsolatesFound = 0;
791 792
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
793 794
        // 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.
795
        final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
796
        if (pauseEvent != null && pauseEvent.isPauseEvent && pauseEvent.kind != ServiceEvent.kPausePostRequest) {
797 798 799 800 801 802 803 804
          pausedIsolatesFound += 1;
          if (serviceEventKind == null) {
            serviceEventKind = pauseEvent.kind;
          } else if (serviceEventKind != pauseEvent.kind) {
            serviceEventKind = ''; // many kinds
          }
        } else {
          reassembleViews.add(view);
805
        }
806 807
      }
    }
808 809 810 811 812 813 814
    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);
      }
815
    }
816
    printTrace('Evicting dirty assets');
817
    await _evictDirtyAssets();
818
    assert(reassembleViews.isNotEmpty);
819
    printTrace('Reassembling application');
820
    bool failedReassemble = false;
821
    final List<Future<void>> futures = <Future<void>>[];
822
    for (FlutterView view in reassembleViews) {
823 824 825 826 827
      futures.add(() async {
        try {
          await view.uiIsolate.flutterReassemble();
        } catch (error) {
          failedReassemble = true;
828
          printError('Reassembling ${view.uiIsolate.name} failed: $error');
829
          return;
830
        }
831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866
      }());
    }
    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)}.');
      },
    );
867
    // Record time it took for Flutter to reassemble the application.
868
    _addBenchmarkData('hotReloadFlutterReassembleMilliseconds', reassembleTimer.elapsed.inMilliseconds);
869

870
    reloadTimer.stop();
871 872 873
    final Duration reloadDuration = reloadTimer.elapsed;
    final int reloadInMs = reloadDuration.inMilliseconds;

874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893
    // 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,
      finalLibraryCount: firstReloadDetails['finalLibraryCount'],
      syncedLibraryCount: firstReloadDetails['receivedLibraryCount'],
      syncedClassesCount: firstReloadDetails['receivedClassesCount'],
      syncedProceduresCount: firstReloadDetails['receivedProceduresCount'],
      syncedBytes: updatedDevFS.syncedBytes,
      invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
      transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
    ).send();
894

895 896 897 898 899 900 901
    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)
902
      flutterUsage.sendTiming('hot', 'reload', reloadDuration);
903
    return OperationResult(
904
      failedReassemble ? 1 : OperationResult.ok.code,
905
      reloadMessage,
906
    );
907 908
  }

909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938
  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();
  }

939 940 941 942 943 944 945 946 947 948 949 950 951 952
  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;
  }

953
  @override
954
  void printHelp({ @required bool details }) {
955
    const String fire = '🔥';
956 957 958 959
    String rawMessage = '  To hot reload changes while running, press "r". ';
    if (canHotRestart) {
      rawMessage += 'To hot restart (and rebuild state), press "R".';
    }
960
    final String message = terminal.color(
961
      fire + terminal.bolden(rawMessage),
962
      TerminalColor.red,
963
    );
964
    printStatus(message);
965 966 967 968 969
    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');
    }
970 971 972
    final String quitMessage = _didAttach
        ? 'To detach, press "d"; to quit, press "q".'
        : 'To quit, press "q".';
973
    if (details) {
974
      printHelpDetails();
975
      printStatus('To repeat this help message, press "h". $quitMessage');
976
    } else {
977
      printStatus('For a more detailed help message, press "h". $quitMessage');
978
    }
979 980
  }

981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997
  Future<void> _evictDirtyAssets() {
    final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[];
    for (FlutterDevice device in flutterDevices) {
      if (device.devFS.assetPathsToEvict.isEmpty)
        continue;
      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);
  }

998
  @override
999
  Future<void> cleanupAfterSignal() async {
1000
    await stopEchoingDeviceLog();
1001
    await hotRunnerConfig.runPreShutdownOperations();
1002 1003 1004
    if (_didAttach) {
      appFinished();
    } else {
1005
      await exitApp();
1006
    }
1007 1008
  }

1009
  @override
1010
  Future<void> preExit() async {
1011 1012 1013
    await _cleanupDevFS();
    await hotRunnerConfig.runPreShutdownOperations();
  }
1014

1015
  @override
1016
  Future<void> cleanupAtFinish() async {
1017 1018 1019 1020
    await _cleanupDevFS();
    await stopEchoingDeviceLog();
  }
}
1021 1022

class ProjectFileInvalidator {
1023
  static const String _pubCachePathLinuxAndMac = '.pub-cache';
1024 1025
  static const String _pubCachePathWindows = 'Pub/Cache';

1026 1027 1028 1029 1030
  static List<Uri> findInvalidated({
    @required DateTime lastCompiled,
    @required List<Uri> urisToMonitor,
    @required String packagesPath,
  }) {
1031 1032 1033 1034 1035
    final List<Uri> invalidatedFiles = <Uri>[];
    int scanned = 0;
    final Stopwatch stopwatch = Stopwatch()..start();
    for (Uri uri in urisToMonitor) {
      if ((platform.isWindows && uri.path.contains(_pubCachePathWindows))
1036
          || uri.path.contains(_pubCachePathLinuxAndMac)) {
1037 1038
        // Don't watch pub cache directories to speed things up a little.
        continue;
1039
      }
1040 1041 1042 1043 1044
      final DateTime updatedAt = fs.statSync(
          uri.toFilePath(windows: platform.isWindows)).modified;
      scanned++;
      if (updatedAt == null) {
        continue;
1045
      }
1046 1047
      if (updatedAt.millisecondsSinceEpoch > lastCompiled.millisecondsSinceEpoch) {
        invalidatedFiles.add(uri);
1048 1049
      }
    }
1050 1051 1052 1053 1054 1055 1056
    // we need to check the .packages file too since it is not used in compilation.
    final DateTime packagesUpdatedAt = fs.statSync(packagesPath).modified;
    if (lastCompiled != null && packagesUpdatedAt != null
        && packagesUpdatedAt.millisecondsSinceEpoch > lastCompiled.millisecondsSinceEpoch) {
      invalidatedFiles.add(fs.file(packagesPath).uri);
      scanned++;
    }
1057
    printTrace('Scanned through $scanned files in ${stopwatch.elapsedMilliseconds}ms');
1058 1059
    return invalidatedFiles;
  }
1060
}