run_hot.dart 43.5 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
import 'package:platform/platform.dart';
8 9
import 'package:json_rpc_2/error_code.dart' as rpc_error_code;
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
10
import 'package:meta/meta.dart';
11
import 'package:pool/pool.dart';
12
import 'base/async_guard.dart';
13

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

29 30
ProjectFileInvalidator get projectFileInvalidator => context.get<ProjectFileInvalidator>() ?? _defaultInvalidator;
final ProjectFileInvalidator _defaultInvalidator = ProjectFileInvalidator(
31 32 33
  fileSystem: globals.fs,
  platform: globals.platform,
  logger: globals.logger,
34
);
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
  DateTime firstBuildTime;
98
  bool _shouldResetAssetDirectory = true;
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
    for (final FlutterDevice device in flutterDevices) {
141 142 143 144
      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
          return base64.encode(globals.fs.file(compilerOutput.outputFilename).readAsBytesSync());
147 148 149
        }
      }
    }
150
    throw 'Failed to compile $expression';
151 152
  }

153 154 155 156 157
  @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)];
158
    for (final FlutterDevice device in flutterDevices) {
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
      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 {
177
      final String entryPath = globals.fs.path.relative(
178 179 180
        getReloadPath(fullRestart: false),
        from: projectRootPath,
      );
181
      for (final FlutterDevice device in flutterDevices) {
182 183 184 185 186 187 188
        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));
      }
189
    } on Exception catch (error) {
190 191 192
      return OperationResult(1, error.toString());
    }

193 194
    for (final FlutterDevice device in flutterDevices) {
      for (final FlutterView view in device.views) {
195 196 197 198
        await view.uiIsolate.flutterFastReassemble(classId);
      }
    }

199
    globals.printStatus('reloadMethod took ${stopwatch.elapsedMilliseconds}');
200
    globals.flutterUsage.sendTiming('hot', 'ui', stopwatch.elapsed);
201 202 203
    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 220 221 222
    // Catches all exceptions, non-Exception objects are rethrown.
    } catch (error) { // ignore: avoid_catches_without_on_clauses
      if (error is! Exception && error is! String) {
        rethrow;
      }
223
      globals.printError('Error connecting to the service protocol: $error');
224
      // https://github.com/flutter/flutter/issues/33050
225 226
      // TODO(blasten): Remove this check once
      // https://issuetracker.google.com/issues/132325318 has been fixed.
227 228
      if (await hasDeviceRunningAndroidQ(flutterDevices) &&
          error.toString().contains(kAndroidQHttpConnectionClosedExp)) {
229 230 231 232 233 234 235 236
        globals.printStatus(
          '🔨 If you are using an emulator running Android Q Beta, '
          'consider using an emulator running API level 29 or lower.',
        );
        globals.printStatus(
          'Learn more about the status of this issue on '
          'https://issuetracker.google.com/issues/132325318.',
        );
237
      }
238 239 240
      return 2;
    }

241
    for (final FlutterDevice device in flutterDevices) {
242
      await device.initLogReader();
243
    }
244
    try {
245
      final List<Uri> baseUris = await _initDevFS();
246
      if (connectionInfoCompleter != null) {
247
        // Only handle one debugger connection.
248
        connectionInfoCompleter.complete(
249
          DebugConnectionInfo(
250 251
            httpUri: flutterDevices.first.vmService.httpAddress,
            wsUri: flutterDevices.first.vmService.wsAddress,
252
            baseUri: baseUris.first.toString(),
253
          ),
254 255
        );
      }
256
    } on Exception catch (error) {
257
      globals.printError('Error initializing DevFS: $error');
258 259
      return 3;
    }
260
    final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start();
261
    final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: true);
262 263 264 265
    _addBenchmarkData(
      'hotReloadInitialDevFSSyncMilliseconds',
      initialUpdateDevFSsTimer.elapsed.inMilliseconds,
    );
266
    if (!devfsResult.success) {
267
      return 3;
268
    }
269

270
    await refreshViews();
271
    for (final FlutterDevice device in flutterDevices) {
272 273
      // VM must have accepted the kernel binary, there will be no reload
      // report, so we let incremental compiler know that source code was accepted.
274
      if (device.generator != null) {
275
        device.generator.accept();
276
      }
277
      for (final FlutterView view in device.views) {
278
        globals.printTrace('Connected to $view.');
279
      }
280
    }
281

282 283 284 285 286 287 288 289 290 291 292 293
    // 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,
      );
    }

294 295 296 297
    appStartedCompleter?.complete();

    if (benchmarkMode) {
      // We are running in benchmark mode.
298
      globals.printStatus('Running in benchmark mode.');
299
      // Measure time to perform a hot restart.
300
      globals.printStatus('Benchmarking hot restart');
301
      await restart(fullRestart: true, benchmarkMode: true);
302
      globals.printStatus('Benchmarking hot reload');
303 304
      // Measure time to perform a hot reload.
      await restart(fullRestart: false);
305 306 307
      if (stayResident) {
        await waitForAppToFinish();
      } else {
308
        globals.printStatus('Benchmark completed. Exiting application.');
309 310
        await _cleanupDevFS();
        await stopEchoingDeviceLog();
311
        await exitApp();
312
      }
313
      final File benchmarkOutput = globals.fs.file('hot_benchmark.json');
314
      benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
315
      return 0;
316
    }
317
    writeVmserviceFile();
318

319
    int result = 0;
320
    if (stayResident) {
321
      result = await waitForAppToFinish();
322
    }
323
    await cleanupAtFinish();
324
    return result;
325 326
  }

327 328
  @override
  Future<int> run({
329
    Completer<DebugConnectionInfo> connectionInfoCompleter,
330
    Completer<void> appStartedCompleter,
331
    String route,
332
  }) async {
333
    if (!globals.fs.isFileSync(mainPath)) {
334
      String message = 'Tried to run $mainPath, but that file does not exist.';
335
      if (target == null) {
336
        message += '\nConsider using the -t option to specify the Dart file to start.';
337
      }
338
      globals.printError(message);
339 340 341
      return 1;
    }

342
    firstBuildTime = DateTime.now();
343

344
    final List<Future<bool>> startupTasks = <Future<bool>>[];
345
    for (final FlutterDevice device in flutterDevices) {
346 347 348 349 350 351 352 353 354 355
      // Here we initialize the frontend_server concurrently with the platform
      // build, reducing overall initialization time. This is safe because the first
      // invocation of the frontend server produces a full dill file that the
      // subsequent invocation in devfs will not overwrite.
      if (device.generator != null) {
        startupTasks.add(
          device.generator.recompile(
            mainPath,
            <Uri>[],
            outputPath: dillOutputPath ??
356
              getDefaultApplicationKernelPath(trackWidgetCreation: debuggingOptions.buildInfo.trackWidgetCreation),
357 358 359 360 361
            packagesFilePath : packagesFilePath,
          ).then((CompilerOutput output) => output?.errorCount == 0)
        );
      }
      startupTasks.add(device.runHot(
362 363
        hotRunner: this,
        route: route,
364 365 366 367 368 369
      ).then((int result) => result == 0));
    }
    try {
      final List<bool> results = await Future.wait(startupTasks);
      if (!results.every((bool passed) => passed)) {
        return 1;
370
      }
371 372 373
    } on Exception catch (err) {
      globals.printError(err.toString());
      return 1;
374 375
    }

376 377
    return attach(
      connectionInfoCompleter: connectionInfoCompleter,
378
      appStartedCompleter: appStartedCompleter,
379
    );
380 381
  }

382
  Future<List<Uri>> _initDevFS() async {
383
    final String fsName = globals.fs.path.basename(projectRootPath);
384
    return <Uri>[
385
      for (final FlutterDevice device in flutterDevices)
386 387
        await device.setupDevFS(
          fsName,
388
          globals.fs.directory(projectRootPath),
389 390 391
          packagesFilePath: packagesFilePath,
        ),
    ];
392
  }
393

394
  Future<UpdateFSReport> _updateDevFS({ bool fullRestart = false }) async {
395
    final bool isFirstUpload = !assetBundle.wasBuiltOnce();
396
    final bool rebuildBundle = assetBundle.needsBuild();
397
    if (rebuildBundle) {
398
      globals.printTrace('Updating assets');
399
      final int result = await assetBundle.build();
400
      if (result != 0) {
401
        return UpdateFSReport(success: false);
402
      }
403
    }
404 405 406

    // Picking up first device's compiler as a source of truth - compilers
    // for all devices should be in sync.
407
    final List<Uri> invalidatedFiles = await projectFileInvalidator.findInvalidated(
408
      lastCompiled: flutterDevices[0].devFS.lastCompiled,
409 410
      urisToMonitor: flutterDevices[0].devFS.sources,
      packagesPath: packagesFilePath,
411
      asyncScanning: hotRunnerConfig.asyncScanning,
412
    );
413
    final UpdateFSReport results = UpdateFSReport(success: true);
414
    for (final FlutterDevice device in flutterDevices) {
415
      results.incorporateResults(await device.updateDevFS(
416 417
        mainPath: mainPath,
        target: target,
418
        bundle: assetBundle,
419 420
        firstBuildTime: firstBuildTime,
        bundleFirstUpload: isFirstUpload,
421
        bundleDirty: !isFirstUpload && rebuildBundle,
422 423
        fullRestart: fullRestart,
        projectRootPath: projectRootPath,
424
        pathToReload: getReloadPath(fullRestart: fullRestart),
425
        invalidatedFiles: invalidatedFiles,
426
        dillOutputPath: dillOutputPath,
427 428
      ));
    }
429
    return results;
430 431
  }

432
  void _resetDirtyAssets() {
433
    for (final FlutterDevice device in flutterDevices) {
434
      device.devFS.assetPathsToEvict.clear();
435
    }
436 437
  }

438
  Future<void> _cleanupDevFS() async {
439
    final List<Future<void>> futures = <Future<void>>[];
440
    for (final FlutterDevice device in flutterDevices) {
441
      if (device.devFS != null) {
442 443
        // Cleanup the devFS, but don't wait indefinitely.
        // We ignore any errors, because it's not clear what we would do anyway.
444
        futures.add(device.devFS.destroy()
445 446
          .timeout(const Duration(milliseconds: 250))
          .catchError((dynamic error) {
447
            globals.printTrace('Ignored error while cleaning up DevFS: $error');
448
          }));
449 450 451
      }
      device.devFS = null;
    }
452
    await Future.wait(futures);
453 454
  }

455 456 457 458 459 460
  Future<void> _launchInView(
    FlutterDevice device,
    Uri entryUri,
    Uri packagesUri,
    Uri assetsDirectoryUri,
  ) {
461
    return Future.wait(<Future<void>>[
462
      for (final FlutterView view in device.views)
463 464
        view.runFromSource(entryUri, packagesUri, assetsDirectoryUri),
    ]);
465 466
  }

467
  Future<void> _launchFromDevFS(String mainScript) async {
468
    final String entryUri = globals.fs.path.relative(mainScript, from: projectRootPath);
469
    final List<Future<void>> futures = <Future<void>>[];
470
    for (final FlutterDevice device in flutterDevices) {
471
      final Uri deviceEntryUri = device.devFS.baseUri.resolveUri(
472
        globals.fs.path.toUri(entryUri));
473
      final Uri devicePackagesUri = device.devFS.baseUri.resolve('.packages');
474
      final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri(
475
        globals.fs.path.toUri(getAssetBuildDirectory()));
476
      futures.add(_launchInView(device,
477 478
                          deviceEntryUri,
                          devicePackagesUri,
479
                          deviceAssetsDirectoryUri));
480
    }
481 482 483
    await Future.wait(futures);
    if (benchmarkMode) {
      futures.clear();
484 485
      for (final FlutterDevice device in flutterDevices) {
        for (final FlutterView view in device.views) {
486
          futures.add(view.flushUIThreadTasks());
487 488
        }
      }
489 490
      await Future.wait(futures);
    }
491 492
  }

493 494
  Future<OperationResult> _restartFromSources({
    String reason,
495
    bool benchmarkMode = false,
496
  }) async {
497
    if (!_isPaused()) {
498
      globals.printTrace('Refreshing active FlutterViews before restarting.');
499 500 501
      await refreshViews();
    }

502
    final Stopwatch restartTimer = Stopwatch()..start();
503 504
    // TODO(aam): Add generator reset logic once we switch to using incremental
    // compiler for full application recompilation on restart.
505 506
    final UpdateFSReport updatedDevFS = await _updateDevFS(fullRestart: true);
    if (!updatedDevFS.success) {
507
      for (final FlutterDevice device in flutterDevices) {
508
        if (device.generator != null) {
509
          await device.generator.reject();
510
        }
511
      }
512
      return OperationResult(1, 'DevFS synchronization failed');
513 514
    }
    _resetDirtyAssets();
515
    for (final FlutterDevice device in flutterDevices) {
516 517
      // VM must have accepted the kernel binary, there will be no reload
      // report, so we let incremental compiler know that source code was accepted.
518
      if (device.generator != null) {
519
        device.generator.accept();
520
      }
521
    }
522
    // Check if the isolate is paused and resume it.
523
    final List<Future<void>> operations = <Future<void>>[];
524
    for (final FlutterDevice device in flutterDevices) {
525
      final Set<Isolate> uiIsolates = <Isolate>{};
526
      for (final FlutterView view in device.views) {
527 528
        if (view.uiIsolate == null) {
          continue;
529
        }
530
        uiIsolates.add(view.uiIsolate);
531
        // Reload the isolate.
532
        operations.add(view.uiIsolate.reload().then((ServiceObject _) {
533 534 535 536 537 538 539
          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;
        }));
540
      }
541 542 543 544 545 546 547 548 549 550
      // The engine handles killing and recreating isolates that it has spawned
      // ("uiIsolates"). The isolates that were spawned from these uiIsolates
      // will not be restared, and so they must be manually killed.
      for (final Isolate isolate in device?.vmService?.vm?.isolates ?? <Isolate>[]) {
        if (!uiIsolates.contains(isolate)) {
          operations.add(isolate.invokeRpcRaw('kill', params: <String, dynamic>{
            'isolateId': isolate.id,
          }));
        }
      }
551
    }
552
    await Future.wait(operations);
553

554
    await _launchFromDevFS(mainPath + '.dill');
555
    restartTimer.stop();
556
    globals.printTrace('Hot restart performed in ${getElapsedAsMilliseconds(restartTimer.elapsed)}.');
557 558
    _addBenchmarkData('hotRestartMillisecondsToFrame',
        restartTimer.elapsed.inMilliseconds);
559 560

    // Send timing analytics.
561
    globals.flutterUsage.sendTiming('hot', 'restart', restartTimer.elapsed);
562 563 564 565

    // In benchmark mode, make sure all stream notifications have finished.
    if (benchmarkMode) {
      final List<Future<void>> isolateNotifications = <Future<void>>[];
566 567
      for (final FlutterDevice device in flutterDevices) {
        for (final FlutterView view in device.views) {
568
          isolateNotifications.add(
569 570
            view.owner.vm.vmService.onIsolateEvent
              .then((Stream<ServiceEvent> serviceEvents) async {
571
              await for (final ServiceEvent serviceEvent in serviceEvents) {
572 573
                if (serviceEvent.owner.name.contains('_spawn')
                  && serviceEvent.kind == ServiceEvent.kIsolateExit) {
574 575 576 577 578 579 580 581 582
                  return;
                }
              }
            }),
          );
        }
      }
      await Future.wait(isolateNotifications);
    }
583
    return OperationResult.ok;
584 585 586
  }

  /// Returns [true] if the reload was successful.
587
  /// Prints errors if [printErrors] is [true].
588 589 590 591
  static bool validateReloadReport(
    Map<String, dynamic> reloadReport, {
    bool printErrors = true,
  }) {
592
    if (reloadReport == null) {
593
      if (printErrors) {
594
        globals.printError('Hot reload did not receive reload report.');
595
      }
596 597 598 599 600 601 602
      return false;
    }
    if (!(reloadReport['type'] == 'ReloadReport' &&
          (reloadReport['success'] == true ||
           (reloadReport['success'] == false &&
            (reloadReport['details'] is Map<String, dynamic> &&
             reloadReport['details']['notices'] is List<dynamic> &&
603 604
             (reloadReport['details']['notices'] as List<dynamic>).isNotEmpty &&
             (reloadReport['details']['notices'] as List<dynamic>).every(
605 606 607 608 609 610
               (dynamic item) => item is Map<String, dynamic> && item['message'] is String
             )
            )
           )
          )
         )) {
611
      if (printErrors) {
612
        globals.printError('Hot reload received invalid response: $reloadReport');
613
      }
614 615
      return false;
    }
616
    if (!(reloadReport['success'] as bool)) {
617
      if (printErrors) {
618
        globals.printError('Hot reload was rejected:');
619
        for (final Map<String, dynamic> notice in (reloadReport['details']['notices'] as List<dynamic>).cast<Map<String, dynamic>>()) {
620
          globals.printError('${notice['message']}');
621
        }
622
      }
623 624 625 626 627
      return false;
    }
    return true;
  }

628 629 630
  @override
  bool get supportsRestart => true;

631
  @override
632 633 634
  Future<OperationResult> restart({
    bool fullRestart = false,
    String reason,
635
    bool benchmarkMode = false,
636 637
    bool silent = false,
    bool pause = false,
638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655
  }) 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;
    }
656
    final Stopwatch timer = Stopwatch()..start();
657
    if (fullRestart) {
658 659 660 661 662 663
      final OperationResult result = await _fullRestartHelper(
        targetPlatform: targetPlatform,
        sdkName: sdkName,
        emulator: emulator,
        reason: reason,
        benchmarkMode: benchmarkMode,
664
        silent: silent,
665
      );
666
      if (!silent) {
667
        globals.printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
668
      }
669 670 671 672 673 674 675
      return result;
    }
    final OperationResult result = await _hotReloadHelper(
      targetPlatform: targetPlatform,
      sdkName: sdkName,
      emulator: emulator,
      reason: reason,
676
      pause: pause,
677 678 679
    );
    if (result.isOk) {
      final String elapsed = getElapsedAsMilliseconds(timer.elapsed);
680
      if (!silent) {
681
        globals.printStatus('${result.message} in $elapsed.');
682
      }
683 684 685 686 687 688 689 690 691 692
    }
    return result;
  }

  Future<OperationResult> _fullRestartHelper({
    String targetPlatform,
    String sdkName,
    bool emulator,
    String reason,
    bool benchmarkMode,
693
    bool silent,
694 695 696 697
  }) async {
    if (!canHotRestart) {
      return OperationResult(1, 'hotRestart not supported');
    }
698 699
    Status status;
    if (!silent) {
700
      status = globals.logger.startProgress(
701 702 703 704 705
        'Performing hot restart...',
        timeout: timeoutConfiguration.fastOperation,
        progressId: 'hot.restart',
      );
    }
706 707 708 709 710
    OperationResult result;
    String restartEvent = 'restart';
    try {
      if (!(await hotRunnerConfig.setupHotRestart())) {
        return OperationResult(1, 'setupHotRestart failed');
Devon Carew's avatar
Devon Carew committed
711
      }
712 713 714 715 716
      // 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(
717 718
        reason: reason,
        benchmarkMode: benchmarkMode,
719
      ));
720 721
      if (!result.isOk) {
        restartEvent = 'restart-failed';
722
      }
723 724 725 726 727 728 729 730 731 732
    } 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();
733
      status?.cancel();
734
    }
735
    return result;
736
  }
737

738 739 740 741 742
  Future<OperationResult> _hotReloadHelper({
    String targetPlatform,
    String sdkName,
    bool emulator,
    String reason,
743
    bool pause,
744
  }) async {
745
    Status status = globals.logger.startProgress(
746
      'Performing hot reload...',
747 748 749 750 751 752 753 754 755 756
      timeout: timeoutConfiguration.fastOperation,
      progressId: 'hot.reload',
    );
    OperationResult result;
    try {
      result = await _reloadSources(
        targetPlatform: targetPlatform,
        sdkName: sdkName,
        emulator: emulator,
        reason: reason,
757
        pause: pause,
758 759
        onSlow: (String message) {
          status?.cancel();
760
          status = globals.logger.startProgress(
761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776
            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();
777
    }
778 779 780 781 782 783 784 785 786
    return result;
  }

  Future<OperationResult> _reloadSources({
    String targetPlatform,
    String sdkName,
    bool emulator,
    bool pause = false,
    String reason,
787
    void Function(String message) onSlow,
788
  }) async {
789 790
    for (final FlutterDevice device in flutterDevices) {
      for (final FlutterView view in device.views) {
791
        if (view.uiIsolate == null) {
792
          return OperationResult(2, 'Application isolate not found', fatal: true);
793
        }
794 795
      }
    }
796

797
    final Stopwatch reloadTimer = Stopwatch()..start();
798

799
    if (!_isPaused()) {
800
      globals.printTrace('Refreshing active FlutterViews before reloading.');
801 802 803
      await refreshViews();
    }

804
    final Stopwatch devFSTimer = Stopwatch()..start();
805
    final UpdateFSReport updatedDevFS = await _updateDevFS();
806
    // Record time it took to synchronize to DevFS.
807
    bool shouldReportReloadTime = true;
808
    _addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds);
809
    if (!updatedDevFS.success) {
810
      return OperationResult(1, 'DevFS synchronization failed');
811
    }
Devon Carew's avatar
Devon Carew committed
812
    String reloadMessage;
813
    final Stopwatch vmReloadTimer = Stopwatch()..start();
814
    Map<String, dynamic> firstReloadDetails;
815
    try {
816
      final String entryPath = globals.fs.path.relative(
817
        getReloadPath(fullRestart: false),
818 819
        from: projectRootPath,
      );
820
      final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[];
821
      for (final FlutterDevice device in flutterDevices) {
822
        if (_shouldResetAssetDirectory) {
823
          // Asset directory has to be set only once when we switch from
824
          // running from bundle to uploaded files.
825
          await device.resetAssetDirectory();
826
          _shouldResetAssetDirectory = false;
827
        }
828
        final List<Future<Map<String, dynamic>>> reportFutures = device.reloadSources(
829
          entryPath, pause: pause,
830
        );
831
        allReportsFutures.add(Future.wait(reportFutures).then(
832 833 834 835 836 837 838 839 840
          (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),
            );
841
            return DeviceReloadReport(device, reports);
842 843
          },
        ));
844
      }
845
      final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures);
846
      for (final DeviceReloadReport report in reports) {
847 848 849
        final Map<String, dynamic> reloadReport = report.reports[0];
        if (!validateReloadReport(reloadReport)) {
          // Reload failed.
850 851 852 853 854 855 856
          HotEvent('reload-reject',
            targetPlatform: targetPlatform,
            sdkName: sdkName,
            emulator: emulator,
            fullRestart: false,
            reason: reason,
          ).send();
857 858
          return OperationResult(1, 'Reload rejected');
        }
859 860 861
        // 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.
862 863 864
        firstReloadDetails ??= castStringKeyedMap(reloadReport['details']);
        final int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'] as int;
        final int finalLibraryCount = reloadReport['details']['finalLibraryCount'] as int;
865
        globals.printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
866
        reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
867
      }
868
    } on Map<String, dynamic> catch (error, stackTrace) {
869
      globals.printTrace('Hot reload failed: $error\n$stackTrace');
870 871
      final int errorCode = error['code'] as int;
      String errorMessage = error['message'] as String;
872
      if (errorCode == Isolate.kIsolateReloadBarred) {
873 874 875 876
        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)';
877 878 879 880 881 882 883
        HotEvent('reload-barred',
          targetPlatform: targetPlatform,
          sdkName: sdkName,
          emulator: emulator,
          fullRestart: false,
          reason: reason,
        ).send();
884
        return OperationResult(errorCode, errorMessage);
885
      }
886
      return OperationResult(errorCode, '$errorMessage (error code: $errorCode)');
887
    } on Exception catch (error, stackTrace) {
888
      globals.printTrace('Hot reload failed: $error\n$stackTrace');
889
      return OperationResult(1, '$error');
890
    }
891
    // Record time it took for the VM to reload the sources.
892
    _addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds);
893
    final Stopwatch reassembleTimer = Stopwatch()..start();
894
    // Reload the isolate.
895
    final List<Future<void>> allDevices = <Future<void>>[];
896
    for (final FlutterDevice device in flutterDevices) {
897
      globals.printTrace('Sending reload events to ${device.device.name}');
898
      final List<Future<ServiceObject>> futuresViews = <Future<ServiceObject>>[];
899
      for (final FlutterView view in device.views) {
900
        globals.printTrace('Sending reload event to "${view.uiIsolate.name}"');
901
        futuresViews.add(view.uiIsolate.reload());
902
      }
903 904
      allDevices.add(Future.wait(futuresViews).whenComplete(() {
        return device.refreshViews();
905
      }));
906
    }
907
    await Future.wait(allDevices);
908

909
    // Check if any isolates are paused.
910
    final List<FlutterView> reassembleViews = <FlutterView>[];
911 912
    String serviceEventKind;
    int pausedIsolatesFound = 0;
913 914
    for (final FlutterDevice device in flutterDevices) {
      for (final FlutterView view in device.views) {
915 916
        // 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.
917
        final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
918
        if (pauseEvent != null && pauseEvent.isPauseEvent && pauseEvent.kind != ServiceEvent.kPausePostRequest) {
919 920 921 922 923 924 925 926
          pausedIsolatesFound += 1;
          if (serviceEventKind == null) {
            serviceEventKind = pauseEvent.kind;
          } else if (serviceEventKind != pauseEvent.kind) {
            serviceEventKind = ''; // many kinds
          }
        } else {
          reassembleViews.add(view);
927
        }
928 929
      }
    }
930
    if (pausedIsolatesFound > 0) {
931
      if (onSlow != null) {
932
        onSlow('${_describePausedIsolates(pausedIsolatesFound, serviceEventKind)}; interface might not update.');
933
      }
934
      if (reassembleViews.isEmpty) {
935
        globals.printTrace('Skipping reassemble because all isolates are paused.');
936 937
        return OperationResult(OperationResult.ok.code, reloadMessage);
      }
938
    }
939
    globals.printTrace('Evicting dirty assets');
940
    await _evictDirtyAssets();
941
    assert(reassembleViews.isNotEmpty);
942
    globals.printTrace('Reassembling application');
943
    bool failedReassemble = false;
944
    final List<Future<void>> futures = <Future<void>>[
945
      for (final FlutterView view in reassembleViews)
946 947 948
        () async {
          try {
            await view.uiIsolate.flutterReassemble();
949
          } on Exception catch (error) {
950
            failedReassemble = true;
951
            globals.printError('Reassembling ${view.uiIsolate.name} failed: $error');
952 953 954 955
            return;
          }
        }(),
    ];
956
    final Future<void> reassembleFuture = Future.wait<void>(futures);
957 958 959 960 961 962 963 964
    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.
965
        globals.printTrace('This is taking a long time; will now check for paused isolates.');
966 967
        int postReloadPausedIsolatesFound = 0;
        String serviceEventKind;
968
        for (final FlutterView view in reassembleViews) {
969 970 971 972 973 974 975 976 977 978 979
          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
            }
          }
        }
980
        globals.printTrace('Found $postReloadPausedIsolatesFound newly paused isolate(s).');
981 982 983 984 985
        if (postReloadPausedIsolatesFound == 0) {
          await reassembleFuture; // must just be taking a long time... keep waiting!
          return;
        }
        shouldReportReloadTime = false;
986
        if (onSlow != null) {
987
          onSlow('${_describePausedIsolates(postReloadPausedIsolatesFound, serviceEventKind)}.');
988
        }
989 990
      },
    );
991
    // Record time it took for Flutter to reassemble the application.
992
    _addBenchmarkData('hotReloadFlutterReassembleMilliseconds', reassembleTimer.elapsed.inMilliseconds);
993

994
    reloadTimer.stop();
995 996 997
    final Duration reloadDuration = reloadTimer.elapsed;
    final int reloadInMs = reloadDuration.inMilliseconds;

998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009
    // 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,
1010 1011 1012 1013
      finalLibraryCount: firstReloadDetails['finalLibraryCount'] as int,
      syncedLibraryCount: firstReloadDetails['receivedLibraryCount'] as int,
      syncedClassesCount: firstReloadDetails['receivedClassesCount'] as int,
      syncedProceduresCount: firstReloadDetails['receivedProceduresCount'] as int,
1014 1015 1016 1017
      syncedBytes: updatedDevFS.syncedBytes,
      invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
      transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
    ).send();
1018

1019
    if (shouldReportReloadTime) {
1020
      globals.printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadDuration)}.');
1021 1022 1023 1024
      // Record complete time it took for the reload.
      _addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs);
    }
    // Only report timings if we reloaded a single view without any errors.
1025
    if ((reassembleViews.length == 1) && !failedReassemble && shouldReportReloadTime) {
1026
      globals.flutterUsage.sendTiming('hot', 'reload', reloadDuration);
1027
    }
1028
    return OperationResult(
1029
      failedReassemble ? 1 : OperationResult.ok.code,
1030
      reloadMessage,
1031
    );
1032 1033
  }

1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063
  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();
  }

1064
  bool _isPaused() {
1065 1066
    for (final FlutterDevice device in flutterDevices) {
      for (final FlutterView view in device.views) {
1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077
        if (view.uiIsolate != null) {
          final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
          if (pauseEvent != null && pauseEvent.isPauseEvent) {
            return true;
          }
        }
      }
    }
    return false;
  }

1078
  @override
1079
  void printHelp({ @required bool details }) {
1080
    globals.printStatus('Flutter run key commands.');
1081
    commandHelp.r.print();
1082
    if (canHotRestart) {
1083
      commandHelp.R.print();
1084
    }
1085
    commandHelp.h.print();
1086
    if (_didAttach) {
1087
      commandHelp.d.print();
1088
    }
1089
    commandHelp.c.print();
1090
    commandHelp.q.print();
1091 1092
    if (details) {
      printHelpDetails();
1093 1094 1095 1096 1097 1098 1099 1100
    }
    for (final FlutterDevice device in flutterDevices) {
      final String dname = device.device.name;
      // Caution: This log line is parsed by device lab tests.
      globals.printStatus(
        'An Observatory debugger and profiler on $dname is available at: '
        '${device.vmService.httpAddress}',
      );
1101
    }
1102 1103
  }

1104 1105
  Future<void> _evictDirtyAssets() {
    final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[];
1106
    for (final FlutterDevice device in flutterDevices) {
1107
      if (device.devFS.assetPathsToEvict.isEmpty) {
1108
        continue;
1109
      }
1110
      if (device.views.first.uiIsolate == null) {
1111
        globals.printError('Application isolate not found for $device');
1112 1113
        continue;
      }
1114
      for (final String assetPath in device.devFS.assetPathsToEvict) {
1115 1116 1117 1118 1119 1120 1121
        futures.add(device.views.first.uiIsolate.flutterEvictAsset(assetPath));
      }
      device.devFS.assetPathsToEvict.clear();
    }
    return Future.wait<Map<String, dynamic>>(futures);
  }

1122
  @override
1123
  Future<void> cleanupAfterSignal() async {
1124
    await stopEchoingDeviceLog();
1125
    await hotRunnerConfig.runPreShutdownOperations();
1126 1127 1128
    if (_didAttach) {
      appFinished();
    } else {
1129
      await exitApp();
1130
    }
1131 1132
  }

1133
  @override
1134
  Future<void> preExit() async {
1135 1136
    await _cleanupDevFS();
    await hotRunnerConfig.runPreShutdownOperations();
1137
    await super.preExit();
1138
  }
1139

1140
  @override
1141
  Future<void> cleanupAtFinish() async {
1142
    for (final FlutterDevice flutterDevice in flutterDevices) {
1143
      await flutterDevice.device.dispose();
1144
    }
1145 1146 1147 1148
    await _cleanupDevFS();
    await stopEchoingDeviceLog();
  }
}
1149

1150 1151
/// The [ProjectFileInvalidator] track the dependencies for a running
/// application to determine when they are dirty.
1152
class ProjectFileInvalidator {
1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163
  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;
1164

1165
  static const String _pubCachePathLinuxAndMac = '.pub-cache';
1166 1167
  static const String _pubCachePathWindows = 'Pub/Cache';

1168 1169 1170 1171 1172 1173 1174 1175
  // 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;

1176
  Future<List<Uri>> findInvalidated({
1177 1178 1179
    @required DateTime lastCompiled,
    @required List<Uri> urisToMonitor,
    @required String packagesPath,
1180 1181
    bool asyncScanning = false,
  }) async {
1182 1183 1184 1185
    assert(urisToMonitor != null);
    assert(packagesPath != null);

    if (lastCompiled == null) {
1186
      // Initial load.
1187 1188 1189 1190
      assert(urisToMonitor.isEmpty);
      return <Uri>[];
    }

1191
    final Stopwatch stopwatch = Stopwatch()..start();
1192 1193
    final List<Uri> urisToScan = <Uri>[
      // Don't watch pub cache directories to speed things up a little.
1194
      for (final Uri uri in urisToMonitor)
1195
        if (_isNotInPubCache(uri)) uri,
1196 1197

      // We need to check the .packages file too since it is not used in compilation.
1198
      _fileSystem.file(packagesPath).uri,
1199 1200
    ];
    final List<Uri> invalidatedFiles = <Uri>[];
1201 1202 1203 1204 1205 1206

    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>(
1207 1208
          () => _fileSystem
            .stat(uri.toFilePath(windows: _platform.isWindows))
1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219
            .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) {
1220 1221
        final DateTime updatedAt = _fileSystem.statSync(
            uri.toFilePath(windows: _platform.isWindows)).modified;
1222 1223 1224
        if (updatedAt != null && updatedAt.isAfter(lastCompiled)) {
          invalidatedFiles.add(uri);
        }
1225 1226
      }
    }
1227
    _logger.printTrace(
1228
      'Scanned through ${urisToScan.length} files in '
1229 1230
      '${stopwatch.elapsedMilliseconds}ms'
      '${asyncScanning ? " (async)" : ""}',
1231
    );
1232 1233
    return invalidatedFiles;
  }
1234

1235 1236
  bool _isNotInPubCache(Uri uri) {
    return !(_platform.isWindows && uri.path.contains(_pubCachePathWindows))
1237 1238
        && !uri.path.contains(_pubCachePathLinuxAndMac);
  }
1239
}