run_hot.dart 43.3 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
  // The initial launch is from a snapshot.
  bool _runningFromSnapshot = true;
98
  DateTime firstBuildTime;
99

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

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

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

131 132 133 134 135 136 137 138 139
  Future<String> _compileExpressionService(
    String isolateId,
    String expression,
    List<String> definitions,
    List<String> typeDefinitions,
    String libraryUri,
    String klass,
    bool isStatic,
  ) async {
140
    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
    } 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 201 202 203
    flutterUsage.sendTiming('hot', 'ui', stopwatch.elapsed);
    return OperationResult.ok;
  }

204
  // Returns the exit code of the flutter tool process, like [run].
205
  @override
206
  Future<int> attach({
207
    Completer<DebugConnectionInfo> connectionInfoCompleter,
208
    Completer<void> appStartedCompleter,
209
  }) async {
210
    _didAttach = true;
211
    try {
212 213
      await connectToServiceProtocol(
        reloadSources: _reloadSourcesService,
214
        restart: _restartService,
215
        compileExpression: _compileExpressionService,
216
        reloadMethod: reloadMethod,
217
      );
218
    } catch (error) {
219
      globals.printError('Error connecting to the service protocol: $error');
220
      // https://github.com/flutter/flutter/issues/33050
221
      // TODO(blasten): Remove this check once https://issuetracker.google.com/issues/132325318 has been fixed.
222 223
      if (await hasDeviceRunningAndroidQ(flutterDevices) &&
          error.toString().contains(kAndroidQHttpConnectionClosedExp)) {
224 225
        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.');
226
      }
227 228 229
      return 2;
    }

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

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

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

283 284 285 286
    appStartedCompleter?.complete();

    if (benchmarkMode) {
      // We are running in benchmark mode.
287
      globals.printStatus('Running in benchmark mode.');
288
      // Measure time to perform a hot restart.
289
      globals.printStatus('Benchmarking hot restart');
290
      await restart(fullRestart: true, benchmarkMode: true);
291
      globals.printStatus('Benchmarking hot reload');
292 293
      // Measure time to perform a hot reload.
      await restart(fullRestart: false);
294 295 296
      if (stayResident) {
        await waitForAppToFinish();
      } else {
297
        globals.printStatus('Benchmark completed. Exiting application.');
298 299
        await _cleanupDevFS();
        await stopEchoingDeviceLog();
300
        await exitApp();
301
      }
302
      final File benchmarkOutput = globals.fs.file('hot_benchmark.json');
303
      benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
304
      return 0;
305
    }
306
    writeVmserviceFile();
307

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

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

331
    firstBuildTime = DateTime.now();
332

333
    final List<Future<bool>> startupTasks = <Future<bool>>[];
334
    for (final FlutterDevice device in flutterDevices) {
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
      // 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 ??
              getDefaultApplicationKernelPath(trackWidgetCreation: device.trackWidgetCreation),
            packagesFilePath : packagesFilePath,
          ).then((CompilerOutput output) => output?.errorCount == 0)
        );
      }
      startupTasks.add(device.runHot(
351 352
        hotRunner: this,
        route: route,
353 354 355 356 357 358
      ).then((int result) => result == 0));
    }
    try {
      final List<bool> results = await Future.wait(startupTasks);
      if (!results.every((bool passed) => passed)) {
        return 1;
359
      }
360 361 362
    } on Exception catch (err) {
      globals.printError(err.toString());
      return 1;
363 364
    }

365 366
    return attach(
      connectionInfoCompleter: connectionInfoCompleter,
367
      appStartedCompleter: appStartedCompleter,
368
    );
369 370
  }

371
  Future<List<Uri>> _initDevFS() async {
372
    final String fsName = globals.fs.path.basename(projectRootPath);
373
    return <Uri>[
374
      for (final FlutterDevice device in flutterDevices)
375 376
        await device.setupDevFS(
          fsName,
377
          globals.fs.directory(projectRootPath),
378 379 380
          packagesFilePath: packagesFilePath,
        ),
    ];
381
  }
382

383
  Future<UpdateFSReport> _updateDevFS({ bool fullRestart = false }) async {
384
    final bool isFirstUpload = !assetBundle.wasBuiltOnce();
385
    final bool rebuildBundle = assetBundle.needsBuild();
386
    if (rebuildBundle) {
387
      globals.printTrace('Updating assets');
388
      final int result = await assetBundle.build();
389
      if (result != 0) {
390
        return UpdateFSReport(success: false);
391
      }
392
    }
393 394 395

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

421
  void _resetDirtyAssets() {
422
    for (final FlutterDevice device in flutterDevices) {
423
      device.devFS.assetPathsToEvict.clear();
424
    }
425 426
  }

427
  Future<void> _cleanupDevFS() async {
428
    final List<Future<void>> futures = <Future<void>>[];
429
    for (final FlutterDevice device in flutterDevices) {
430
      if (device.devFS != null) {
431 432
        // Cleanup the devFS, but don't wait indefinitely.
        // We ignore any errors, because it's not clear what we would do anyway.
433
        futures.add(device.devFS.destroy()
434 435
          .timeout(const Duration(milliseconds: 250))
          .catchError((dynamic error) {
436
            globals.printTrace('Ignored error while cleaning up DevFS: $error');
437
          }));
438 439 440
      }
      device.devFS = null;
    }
441
    await Future.wait(futures);
442 443
  }

444 445 446 447 448 449
  Future<void> _launchInView(
    FlutterDevice device,
    Uri entryUri,
    Uri packagesUri,
    Uri assetsDirectoryUri,
  ) {
450
    return Future.wait(<Future<void>>[
451
      for (final FlutterView view in device.views)
452 453
        view.runFromSource(entryUri, packagesUri, assetsDirectoryUri),
    ]);
454 455
  }

456
  Future<void> _launchFromDevFS(String mainScript) async {
457
    final String entryUri = globals.fs.path.relative(mainScript, from: projectRootPath);
458
    final List<Future<void>> futures = <Future<void>>[];
459
    for (final FlutterDevice device in flutterDevices) {
460
      final Uri deviceEntryUri = device.devFS.baseUri.resolveUri(
461
        globals.fs.path.toUri(entryUri));
462
      final Uri devicePackagesUri = device.devFS.baseUri.resolve('.packages');
463
      final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri(
464
        globals.fs.path.toUri(getAssetBuildDirectory()));
465
      futures.add(_launchInView(device,
466 467
                          deviceEntryUri,
                          devicePackagesUri,
468
                          deviceAssetsDirectoryUri));
469
    }
470 471 472
    await Future.wait(futures);
    if (benchmarkMode) {
      futures.clear();
473 474
      for (final FlutterDevice device in flutterDevices) {
        for (final FlutterView view in device.views) {
475
          futures.add(view.flushUIThreadTasks());
476 477
        }
      }
478 479
      await Future.wait(futures);
    }
480 481
  }

482 483
  Future<OperationResult> _restartFromSources({
    String reason,
484
    bool benchmarkMode = false,
485
  }) async {
486
    if (!_isPaused()) {
487
      globals.printTrace('Refreshing active FlutterViews before restarting.');
488 489 490
      await refreshViews();
    }

491
    final Stopwatch restartTimer = Stopwatch()..start();
492 493
    // TODO(aam): Add generator reset logic once we switch to using incremental
    // compiler for full application recompilation on restart.
494 495
    final UpdateFSReport updatedDevFS = await _updateDevFS(fullRestart: true);
    if (!updatedDevFS.success) {
496
      for (final FlutterDevice device in flutterDevices) {
497
        if (device.generator != null) {
498
          await device.generator.reject();
499
        }
500
      }
501
      return OperationResult(1, 'DevFS synchronization failed');
502 503
    }
    _resetDirtyAssets();
504
    for (final FlutterDevice device in flutterDevices) {
505 506
      // VM must have accepted the kernel binary, there will be no reload
      // report, so we let incremental compiler know that source code was accepted.
507
      if (device.generator != null) {
508
        device.generator.accept();
509
      }
510
    }
511
    // Check if the isolate is paused and resume it.
512
    final List<Future<void>> futures = <Future<void>>[];
513 514
    for (final FlutterDevice device in flutterDevices) {
      for (final FlutterView view in device.views) {
515 516
        if (view.uiIsolate == null) {
          continue;
517
        }
518
        // Reload the isolate.
519 520 521 522 523 524 525 526
        futures.add(view.uiIsolate.reload().then((ServiceObject _) {
          final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
          if ((pauseEvent != null) && pauseEvent.isPauseEvent) {
            // Resume the isolate so that it can be killed by the embedder.
            return view.uiIsolate.resume();
          }
          return null;
        }));
527 528
      }
    }
529
    await Future.wait(futures);
530

531 532
    // We are now running from source.
    _runningFromSnapshot = false;
533
    await _launchFromDevFS(mainPath + '.dill');
534
    restartTimer.stop();
535
    globals.printTrace('Hot restart performed in ${getElapsedAsMilliseconds(restartTimer.elapsed)}.');
536 537
    // We are now running from sources.
    _runningFromSnapshot = false;
538 539
    _addBenchmarkData('hotRestartMillisecondsToFrame',
        restartTimer.elapsed.inMilliseconds);
540 541

    // Send timing analytics.
542
    flutterUsage.sendTiming('hot', 'restart', restartTimer.elapsed);
543 544 545 546

    // In benchmark mode, make sure all stream notifications have finished.
    if (benchmarkMode) {
      final List<Future<void>> isolateNotifications = <Future<void>>[];
547 548
      for (final FlutterDevice device in flutterDevices) {
        for (final FlutterView view in device.views) {
549
          isolateNotifications.add(
550 551
            view.owner.vm.vmService.onIsolateEvent
              .then((Stream<ServiceEvent> serviceEvents) async {
552
              await for (final ServiceEvent serviceEvent in serviceEvents) {
553 554
                if (serviceEvent.owner.name.contains('_spawn')
                  && serviceEvent.kind == ServiceEvent.kIsolateExit) {
555 556 557 558 559 560 561 562 563
                  return;
                }
              }
            }),
          );
        }
      }
      await Future.wait(isolateNotifications);
    }
564
    return OperationResult.ok;
565 566 567
  }

  /// Returns [true] if the reload was successful.
568
  /// Prints errors if [printErrors] is [true].
569 570 571 572
  static bool validateReloadReport(
    Map<String, dynamic> reloadReport, {
    bool printErrors = true,
  }) {
573
    if (reloadReport == null) {
574
      if (printErrors) {
575
        globals.printError('Hot reload did not receive reload report.');
576
      }
577 578 579 580 581 582 583
      return false;
    }
    if (!(reloadReport['type'] == 'ReloadReport' &&
          (reloadReport['success'] == true ||
           (reloadReport['success'] == false &&
            (reloadReport['details'] is Map<String, dynamic> &&
             reloadReport['details']['notices'] is List<dynamic> &&
584 585
             (reloadReport['details']['notices'] as List<dynamic>).isNotEmpty &&
             (reloadReport['details']['notices'] as List<dynamic>).every(
586 587 588 589 590 591
               (dynamic item) => item is Map<String, dynamic> && item['message'] is String
             )
            )
           )
          )
         )) {
592
      if (printErrors) {
593
        globals.printError('Hot reload received invalid response: $reloadReport');
594
      }
595 596
      return false;
    }
597
    if (!(reloadReport['success'] as bool)) {
598
      if (printErrors) {
599
        globals.printError('Hot reload was rejected:');
600
        for (final Map<String, dynamic> notice in reloadReport['details']['notices']) {
601
          globals.printError('${notice['message']}');
602
        }
603
      }
604 605 606 607 608
      return false;
    }
    return true;
  }

609 610 611
  @override
  bool get supportsRestart => true;

612
  @override
613 614 615
  Future<OperationResult> restart({
    bool fullRestart = false,
    String reason,
616
    bool benchmarkMode = false,
617 618
    bool silent = false,
    bool pause = false,
619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636
  }) 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;
    }
637
    final Stopwatch timer = Stopwatch()..start();
638
    if (fullRestart) {
639 640 641 642 643 644
      final OperationResult result = await _fullRestartHelper(
        targetPlatform: targetPlatform,
        sdkName: sdkName,
        emulator: emulator,
        reason: reason,
        benchmarkMode: benchmarkMode,
645
        silent: silent,
646
      );
647
      if (!silent) {
648
        globals.printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
649
      }
650 651 652 653 654 655 656
      return result;
    }
    final OperationResult result = await _hotReloadHelper(
      targetPlatform: targetPlatform,
      sdkName: sdkName,
      emulator: emulator,
      reason: reason,
657
      pause: pause,
658 659 660
    );
    if (result.isOk) {
      final String elapsed = getElapsedAsMilliseconds(timer.elapsed);
661
      if (!silent) {
662
        globals.printStatus('${result.message} in $elapsed.');
663
      }
664 665 666 667 668 669 670 671 672 673
    }
    return result;
  }

  Future<OperationResult> _fullRestartHelper({
    String targetPlatform,
    String sdkName,
    bool emulator,
    String reason,
    bool benchmarkMode,
674
    bool silent,
675 676 677 678
  }) async {
    if (!canHotRestart) {
      return OperationResult(1, 'hotRestart not supported');
    }
679 680
    Status status;
    if (!silent) {
681
      status = globals.logger.startProgress(
682 683 684 685 686
        'Performing hot restart...',
        timeout: timeoutConfiguration.fastOperation,
        progressId: 'hot.restart',
      );
    }
687 688 689 690 691
    OperationResult result;
    String restartEvent = 'restart';
    try {
      if (!(await hotRunnerConfig.setupHotRestart())) {
        return OperationResult(1, 'setupHotRestart failed');
Devon Carew's avatar
Devon Carew committed
692
      }
693 694 695 696 697
      // 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(
698 699
        reason: reason,
        benchmarkMode: benchmarkMode,
700
      ));
701 702
      if (!result.isOk) {
        restartEvent = 'restart-failed';
703
      }
704 705 706 707 708 709 710 711 712 713
    } 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();
714
      status?.cancel();
715
    }
716
    return result;
717
  }
718

719 720 721 722 723
  Future<OperationResult> _hotReloadHelper({
    String targetPlatform,
    String sdkName,
    bool emulator,
    String reason,
724
    bool pause,
725 726 727
  }) async {
    final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
    final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
728
    Status status = globals.logger.startProgress(
729 730 731 732 733 734 735 736 737 738 739
      '$progressPrefix hot reload...',
      timeout: timeoutConfiguration.fastOperation,
      progressId: 'hot.reload',
    );
    OperationResult result;
    try {
      result = await _reloadSources(
        targetPlatform: targetPlatform,
        sdkName: sdkName,
        emulator: emulator,
        reason: reason,
740
        pause: pause,
741 742
        onSlow: (String message) {
          status?.cancel();
743
          status = globals.logger.startProgress(
744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759
            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();
760
    }
761 762 763 764 765 766 767 768 769
    return result;
  }

  Future<OperationResult> _reloadSources({
    String targetPlatform,
    String sdkName,
    bool emulator,
    bool pause = false,
    String reason,
770
    void Function(String message) onSlow,
771
  }) async {
772 773
    for (final FlutterDevice device in flutterDevices) {
      for (final FlutterView view in device.views) {
774
        if (view.uiIsolate == null) {
775
          return OperationResult(2, 'Application isolate not found', fatal: true);
776
        }
777 778
      }
    }
779

780 781 782 783 784 785
    // 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.
786
    bool shouldReportReloadTime = !_runningFromSnapshot;
787
    final Stopwatch reloadTimer = Stopwatch()..start();
788

789
    if (!_isPaused()) {
790
      globals.printTrace('Refreshing active FlutterViews before reloading.');
791 792 793
      await refreshViews();
    }

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

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

984
    reloadTimer.stop();
985 986 987
    final Duration reloadDuration = reloadTimer.elapsed;
    final int reloadInMs = reloadDuration.inMilliseconds;

988 989 990 991 992 993 994 995 996 997 998 999
    // 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,
1000 1001 1002 1003
      finalLibraryCount: firstReloadDetails['finalLibraryCount'] as int,
      syncedLibraryCount: firstReloadDetails['receivedLibraryCount'] as int,
      syncedClassesCount: firstReloadDetails['receivedClassesCount'] as int,
      syncedProceduresCount: firstReloadDetails['receivedProceduresCount'] as int,
1004 1005 1006 1007
      syncedBytes: updatedDevFS.syncedBytes,
      invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
      transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
    ).send();
1008

1009
    if (shouldReportReloadTime) {
1010
      globals.printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadDuration)}.');
1011 1012 1013 1014
      // Record complete time it took for the reload.
      _addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs);
    }
    // Only report timings if we reloaded a single view without any errors.
1015
    if ((reassembleViews.length == 1) && !failedReassemble && shouldReportReloadTime) {
1016
      flutterUsage.sendTiming('hot', 'reload', reloadDuration);
1017
    }
1018
    return OperationResult(
1019
      failedReassemble ? 1 : OperationResult.ok.code,
1020
      reloadMessage,
1021
    );
1022 1023
  }

1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053
  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();
  }

1054
  bool _isPaused() {
1055 1056
    for (final FlutterDevice device in flutterDevices) {
      for (final FlutterView view in device.views) {
1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067
        if (view.uiIsolate != null) {
          final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
          if (pauseEvent != null && pauseEvent.isPauseEvent) {
            return true;
          }
        }
      }
    }
    return false;
  }

1068
  @override
1069
  void printHelp({ @required bool details }) {
1070
    globals.printStatus('Flutter run key commands.');
1071
    commandHelp.r.print();
1072
    if (canHotRestart) {
1073
      commandHelp.R.print();
1074
    }
1075
    commandHelp.h.print();
1076
    if (_didAttach) {
1077
      commandHelp.d.print();
1078
    }
1079
    commandHelp.c.print();
1080
    commandHelp.q.print();
1081 1082
    if (details) {
      printHelpDetails();
1083 1084 1085 1086 1087 1088 1089 1090
    }
    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}',
      );
1091
    }
1092 1093
  }

1094 1095
  Future<void> _evictDirtyAssets() {
    final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[];
1096
    for (final FlutterDevice device in flutterDevices) {
1097
      if (device.devFS.assetPathsToEvict.isEmpty) {
1098
        continue;
1099
      }
1100
      if (device.views.first.uiIsolate == null) {
1101
        globals.printError('Application isolate not found for $device');
1102 1103
        continue;
      }
1104
      for (final String assetPath in device.devFS.assetPathsToEvict) {
1105 1106 1107 1108 1109 1110 1111
        futures.add(device.views.first.uiIsolate.flutterEvictAsset(assetPath));
      }
      device.devFS.assetPathsToEvict.clear();
    }
    return Future.wait<Map<String, dynamic>>(futures);
  }

1112
  @override
1113
  Future<void> cleanupAfterSignal() async {
1114
    await stopEchoingDeviceLog();
1115
    await hotRunnerConfig.runPreShutdownOperations();
1116 1117 1118
    if (_didAttach) {
      appFinished();
    } else {
1119
      await exitApp();
1120
    }
1121 1122
  }

1123
  @override
1124
  Future<void> preExit() async {
1125 1126
    await _cleanupDevFS();
    await hotRunnerConfig.runPreShutdownOperations();
1127
    await super.preExit();
1128
  }
1129

1130
  @override
1131
  Future<void> cleanupAtFinish() async {
1132
    for (final FlutterDevice flutterDevice in flutterDevices) {
1133
      await flutterDevice.device.dispose();
1134
    }
1135 1136 1137 1138
    await _cleanupDevFS();
    await stopEchoingDeviceLog();
  }
}
1139

1140 1141
/// The [ProjectFileInvalidator] track the dependencies for a running
/// application to determine when they are dirty.
1142
class ProjectFileInvalidator {
1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153
  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;
1154

1155
  static const String _pubCachePathLinuxAndMac = '.pub-cache';
1156 1157
  static const String _pubCachePathWindows = 'Pub/Cache';

1158 1159 1160 1161 1162 1163 1164 1165
  // 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;

1166
  Future<List<Uri>> findInvalidated({
1167 1168 1169
    @required DateTime lastCompiled,
    @required List<Uri> urisToMonitor,
    @required String packagesPath,
1170 1171
    bool asyncScanning = false,
  }) async {
1172 1173 1174 1175
    assert(urisToMonitor != null);
    assert(packagesPath != null);

    if (lastCompiled == null) {
1176
      // Initial load.
1177 1178 1179 1180
      assert(urisToMonitor.isEmpty);
      return <Uri>[];
    }

1181
    final Stopwatch stopwatch = Stopwatch()..start();
1182 1183
    final List<Uri> urisToScan = <Uri>[
      // Don't watch pub cache directories to speed things up a little.
1184
      for (final Uri uri in urisToMonitor)
1185
        if (_isNotInPubCache(uri)) uri,
1186 1187

      // We need to check the .packages file too since it is not used in compilation.
1188
      _fileSystem.file(packagesPath).uri,
1189 1190
    ];
    final List<Uri> invalidatedFiles = <Uri>[];
1191 1192 1193 1194 1195 1196

    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>(
1197 1198
          () => _fileSystem
            .stat(uri.toFilePath(windows: _platform.isWindows))
1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209
            .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) {
1210 1211
        final DateTime updatedAt = _fileSystem.statSync(
            uri.toFilePath(windows: _platform.isWindows)).modified;
1212 1213 1214
        if (updatedAt != null && updatedAt.isAfter(lastCompiled)) {
          invalidatedFiles.add(uri);
        }
1215 1216
      }
    }
1217
    _logger.printTrace(
1218
      'Scanned through ${urisToScan.length} files in '
1219 1220
      '${stopwatch.elapsedMilliseconds}ms'
      '${asyncScanning ? " (async)" : ""}',
1221
    );
1222 1223
    return invalidatedFiles;
  }
1224

1225 1226
  bool _isNotInPubCache(Uri uri) {
    return !(_platform.isWindows && uri.path.contains(_pubCachePathWindows))
1227 1228
        && !uri.path.contains(_pubCachePathLinuxAndMac);
  }
1229
}