run_hot.dart 18.7 KB
Newer Older
1 2 3 4 5 6
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

7
import 'package:meta/meta.dart';
8
import 'package:path/path.dart' as path;
9
import 'package:stack_trace/stack_trace.dart';
10 11

import 'application_package.dart';
12
import 'base/context.dart';
13
import 'base/file_system.dart';
14 15
import 'base/logger.dart';
import 'base/utils.dart';
16
import 'build_info.dart';
17

18
import 'dart/dependencies.dart';
19
import 'devfs.dart';
20 21 22
import 'device.dart';
import 'globals.dart';
import 'resident_runner.dart';
23

24
import 'vmservice.dart';
25
import 'usage.dart';
26

27 28 29 30 31 32 33 34 35
class HotRunnerConfig {
  /// Should the hot runner compute the minimal Dart dependencies?
  bool computeDartDependencies = true;
  /// Should the hot runner assume that the minimal Dart dependencies do not change?
  bool stableDartDependencies = false;
}

HotRunnerConfig get hotRunnerConfig => context[HotRunnerConfig];

36 37
const bool kHotReloadDefault = true;

38 39 40 41 42
class HotRunner extends ResidentRunner {
  HotRunner(
    Device device, {
    String target,
    DebuggingOptions debuggingOptions,
43 44
    bool usesTerminalUI: true,
    this.benchmarkMode: false,
45 46 47
    this.applicationBinary,
    String projectRootPath,
    String packagesFilePath,
48
    String projectAssets,
49
    bool stayResident: true,
50 51 52
  }) : super(device,
             target: target,
             debuggingOptions: debuggingOptions,
53 54 55
             usesTerminalUI: usesTerminalUI,
             projectRootPath: projectRootPath,
             packagesFilePath: packagesFilePath,
56 57
             projectAssets: projectAssets,
             stayResident: stayResident);
58

59 60
  final String applicationBinary;
  bool get prebuiltMode => applicationBinary != null;
61
  Set<String> _dartDependencies;
62
  Uri _observatoryUri;
63

64 65
  final bool benchmarkMode;
  final Map<String, int> benchmarkData = new Map<String, int>();
66 67
  // The initial launch is from a snapshot.
  bool _runningFromSnapshot = true;
68

69
  @override
70
  Future<int> run({
71
    Completer<DebugConnectionInfo> connectionInfoCompleter,
72
    Completer<Null> appStartedCompleter,
73 74 75
    String route,
    bool shouldBuild: true
  }) {
76
    // Don't let uncaught errors kill the process.
77
    return Chain.capture(() {
78
      return _run(
79
        connectionInfoCompleter: connectionInfoCompleter,
80
        appStartedCompleter: appStartedCompleter,
81 82
        route: route,
        shouldBuild: shouldBuild
83 84 85 86 87 88
      );
    }, onError: (dynamic error, StackTrace stackTrace) {
      printError('Exception from flutter run: $error', stackTrace);
    });
  }

89
  bool _refreshDartDependencies() {
90 91 92 93
    if (!hotRunnerConfig.computeDartDependencies) {
      // Disabled.
      return true;
    }
94 95 96 97 98
    if (_dartDependencies != null) {
      // Already computed.
      return true;
    }
    DartDependencySetBuilder dartDependencySetBuilder =
99
        new DartDependencySetBuilder(
100
              mainPath, projectRootPath, packagesFilePath);
101
    try {
102 103 104 105 106 107 108 109 110 111 112 113
      Set<String> dependencies = dartDependencySetBuilder.build();
      _dartDependencies = new Set<String>();
      for (String path in dependencies) {
        // We need to tweak package: uris so that they reflect their devFS
        // location.
        if (path.startsWith('package:')) {
          // Swap out package: for packages/ because we place all package
          // sources under packages/.
          path = path.replaceFirst('package:', 'packages/');
        }
        _dartDependencies.add(path);
      }
114 115
    } catch (error) {
      printStatus('Error detected in application source code:', emphasis: true);
116
      printError('$error');
117 118 119 120 121
      return false;
    }
    return true;
  }

122
  Future<int> _run({
123
    Completer<DebugConnectionInfo> connectionInfoCompleter,
124
    Completer<Null> appStartedCompleter,
125 126
    String route,
    bool shouldBuild: true
127
  }) async {
128 129
    if (!fs.isFileSync(mainPath)) {
      String message = 'Tried to run $mainPath, but that file does not exist.';
130 131 132 133 134 135
      if (target == null)
        message += '\nConsider using the -t option to specify the Dart file to start.';
      printError(message);
      return 1;
    }

136
    package = getApplicationPackageForPlatform(device.platform, applicationBinary: applicationBinary);
137

138
    if (package == null) {
139 140 141 142 143 144 145 146
      String message = 'No application found for ${device.platform}.';
      String hint = getMissingPackageHintForPlatform(device.platform);
      if (hint != null)
        message += '\n$hint';
      printError(message);
      return 1;
    }

147 148 149 150 151 152
    // Determine the Dart dependencies eagerly.
    if (!_refreshDartDependencies()) {
      // Some kind of source level error or missing file in the Dart code.
      return 1;
    }

153 154
    Map<String, dynamic> platformArgs = new Map<String, dynamic>();

155
    await startEchoingDeviceLog(package);
156

157
    String modeName = getModeName(debuggingOptions.buildMode);
158
    printStatus('Launching ${getDisplayPath(mainPath)} on ${device.name} in $modeName mode...');
159

160
    // Start the application.
161
    Future<LaunchResult> futureResult = device.startApp(
162
      package,
163
      debuggingOptions.buildMode,
164
      mainPath: mainPath,
165 166
      debuggingOptions: debuggingOptions,
      platformArgs: platformArgs,
167
      route: route,
168 169
      prebuiltApplication: prebuiltMode,
      applicationNeedsRebuild: shouldBuild || hasDirtyDependencies()
170 171
    );

172 173
    LaunchResult result = await futureResult;

174
    if (!result.started) {
175
      printError('Error launching application on ${device.name}.');
176 177 178 179
      await stopEchoingDeviceLog();
      return 2;
    }

180
    _observatoryUri = result.observatoryUri;
181
    try {
182
      await connectToServiceProtocol(_observatoryUri);
183 184 185 186 187
    } catch (error) {
      printError('Error connecting to the service protocol: $error');
      return 2;
    }

188 189 190 191
    try {
      Uri baseUri = await _initDevFS();
      if (connectionInfoCompleter != null) {
        connectionInfoCompleter.complete(
192
          new DebugConnectionInfo(
193 194
            httpUri: _observatoryUri,
            wsUri: vmService.wsAddress,
195 196
            baseUri: baseUri.toString()
          )
197
        );
198
      }
199 200 201 202
    } catch (error) {
      printError('Error initializing DevFS: $error');
      return 3;
    }
203
    bool devfsResult = await _updateDevFS();
204 205 206
    if (!devfsResult) {
      printError('Could not perform initial file synchronization.');
      return 3;
207 208
    }

209
    await vmService.vm.refreshViews();
210
    printTrace('Connected to ${vmService.vm.mainView}.');
211

212 213 214 215
    if (stayResident) {
      setupTerminal();
      registerSignalHandlers();
    }
216

217 218
    appStartedCompleter?.complete();

219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
    if (benchmarkMode) {
      // We are running in benchmark mode.
      printStatus('Running in benchmark mode.');
      // Measure time to perform a hot restart.
      printStatus('Benchmarking hot restart');
      await restart(fullRestart: true);
      await vmService.vm.refreshViews();
      // TODO(johnmccutchan): Modify script entry point.
      printStatus('Benchmarking hot reload');
      // Measure time to perform a hot reload.
      await restart(fullRestart: false);
      printStatus('Benchmark completed. Exiting application.');
      await _cleanupDevFS();
      await stopEchoingDeviceLog();
      await stopApp();
234
      File benchmarkOutput = fs.file('hot_benchmark.json');
235 236 237
      benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
    }

238 239 240 241
    if (stayResident)
      return waitForAppToFinish();
    await cleanupAtFinish();
    return 0;
242 243 244
  }

  @override
245
  Future<Null> handleTerminalCommand(String code) async {
246
    final String lower = code.toLowerCase();
247
    if ((lower == 'r') || (code == AnsiTerminal.KEY_F5)) {
Devon Carew's avatar
Devon Carew committed
248
      OperationResult result = await restart(fullRestart: code == 'R');
249 250 251 252
      if (!result.isOk) {
        // TODO(johnmccutchan): Attempt to determine the number of errors that
        // occurred and tighten this message.
        printStatus('Try again after fixing the above error(s).', emphasis: true);
253 254 255 256 257 258
      }
    }
  }

  DevFS _devFS;

259
  Future<Uri> _initDevFS() {
260
    String fsName = path.basename(projectRootPath);
261
    _devFS = new DevFS(vmService,
262
                       fsName,
263 264
                       fs.directory(projectRootPath),
                       packagesFilePath: packagesFilePath);
265 266
    return _devFS.create();
  }
267

268
  Future<bool> _updateDevFS({ DevFSProgressReporter progressReporter }) async {
269 270 271 272
    if (!_refreshDartDependencies()) {
      // Did not update DevFS because of a Dart source error.
      return false;
    }
273
    final bool rebuildBundle = assetBundle.needsBuild();
274
    if (rebuildBundle) {
275
      printTrace('Updating assets');
276
      int result = await assetBundle.build();
277 278
      if (result != 0)
        return false;
279
    }
280
    Status devFSStatus = logger.startProgress('Syncing files to device...');
281
    int bytes = await _devFS.update(progressReporter: progressReporter,
282
                        bundle: assetBundle,
283
                        bundleDirty: rebuildBundle,
284
                        fileFilter: _dartDependencies);
Devon Carew's avatar
Devon Carew committed
285
    devFSStatus.stop();
286 287 288 289
    if (!hotRunnerConfig.stableDartDependencies) {
      // Clear the set after the sync so they are recomputed next time.
      _dartDependencies = null;
    }
290
    printTrace('Synced ${getSizeAsMB(bytes)}.');
291 292 293
    return true;
  }

294
  Future<Null> _evictDirtyAssets() async {
295
    if (_devFS.assetPathsToEvict.isEmpty)
296
      return;
297
    if (currentView.uiIsolate == null)
298
      throw 'Application isolate not found';
299 300
    for (String assetPath in _devFS.assetPathsToEvict) {
      await currentView.uiIsolate.flutterEvictAsset(assetPath);
301
    }
302
    _devFS.assetPathsToEvict.clear();
303 304
  }

305 306
  Future<Null> _cleanupDevFS() async {
    if (_devFS != null) {
307 308 309 310 311 312
      // Cleanup the devFS; don't wait indefinitely, and ignore any errors.
      await _devFS.destroy()
        .timeout(new Duration(milliseconds: 250))
        .catchError((dynamic error) {
          printTrace('$error');
        });
313 314 315 316
    }
    _devFS = null;
  }

317 318 319
  Future<Null> _launchInView(String entryPath,
                             String packagesPath,
                             String assetsDirectoryPath) async {
320
    FlutterView view = vmService.vm.mainView;
321
    return view.runFromSource(entryPath, packagesPath, assetsDirectoryPath);
322 323
  }

324 325
  Future<Null> _launchFromDevFS(ApplicationPackage package,
                                String mainScript) async {
326
    String entryPath = path.relative(mainScript, from: projectRootPath);
327 328 329 330 331
    String deviceEntryPath =
        _devFS.baseUri.resolve(entryPath).toFilePath();
    String devicePackagesPath =
        _devFS.baseUri.resolve('.packages').toFilePath();
    String deviceAssetsDirectoryPath =
332
        _devFS.baseUri.resolve(getAssetBuildDirectory()).toFilePath();
333 334 335 336 337
    await _launchInView(deviceEntryPath,
                        devicePackagesPath,
                        deviceAssetsDirectoryPath);
  }

338
  Future<OperationResult> _restartFromSources() async {
339 340
    Stopwatch restartTimer = new Stopwatch();
    restartTimer.start();
341 342 343
    bool updatedDevFS = await _updateDevFS();
    if (!updatedDevFS)
      return new OperationResult(1, 'Dart Source Error');
344
    await _launchFromDevFS(package, mainPath);
345 346 347
    restartTimer.stop();
    printTrace('Restart performed in '
        '${getElapsedAsMilliseconds(restartTimer.elapsed)}.');
348 349
    // We are now running from sources.
    _runningFromSnapshot = false;
350 351 352
    if (benchmarkMode) {
      benchmarkData['hotRestartMillisecondsToFrame'] =
          restartTimer.elapsed.inMilliseconds;
353
    }
354
    flutterUsage.sendEvent('hot', 'restart');
355
    flutterUsage.sendTiming('hot', 'restart', restartTimer.elapsed);
356
    return OperationResult.ok;
357 358 359
  }

  /// Returns [true] if the reload was successful.
360 361 362 363 364
  static bool validateReloadReport(Map<String, dynamic> reloadReport) {
    if (reloadReport['type'] != 'ReloadReport') {
      printError('Hot reload received invalid response: $reloadReport');
      return false;
    }
365 366
    if (!reloadReport['success']) {
      printError('Hot reload was rejected:');
367
      for (Map<String, dynamic> notice in reloadReport['details']['notices'])
368 369 370 371 372 373
        printError('${notice['message']}');
      return false;
    }
    return true;
  }

374 375 376
  @override
  bool get supportsRestart => true;

377
  @override
Devon Carew's avatar
Devon Carew committed
378
  Future<OperationResult> restart({ bool fullRestart: false, bool pauseAfterRestart: false }) async {
379
    if (fullRestart) {
380
      Status status = logger.startProgress('Performing full restart...', progressId: 'hot.restart');
Devon Carew's avatar
Devon Carew committed
381 382 383 384 385 386 387 388 389
      try {
        await _restartFromSources();
        status.stop();
        printStatus('Restart complete.');
        return OperationResult.ok;
      } catch (error) {
        status.stop();
        rethrow;
      }
390
    } else {
391
      Status status = logger.startProgress('Performing hot reload...', progressId: 'hot.reload');
Devon Carew's avatar
Devon Carew committed
392 393 394 395 396 397 398 399 400 401
      try {
        OperationResult result = await _reloadSources(pause: pauseAfterRestart);
        status.stop();
        if (result.isOk)
          printStatus("${result.message}.");
        return result;
      } catch (error) {
        status.stop();
        rethrow;
      }
402 403
    }
  }
404

Devon Carew's avatar
Devon Carew committed
405
  Future<OperationResult> _reloadSources({ bool pause: false }) async {
406
    if (currentView.uiIsolate == null)
407
      throw 'Application isolate not found';
408 409 410 411 412 413 414
    // 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.
    final bool shouldReportReloadTime = !_runningFromSnapshot;
415 416
    Stopwatch reloadTimer = new Stopwatch();
    reloadTimer.start();
417 418 419 420 421 422 423 424 425
    Stopwatch devFSTimer;
    Stopwatch vmReloadTimer;
    Stopwatch reassembleTimer;
    if (benchmarkMode) {
      devFSTimer = new Stopwatch();
      devFSTimer.start();
      vmReloadTimer = new Stopwatch();
      reassembleTimer = new Stopwatch();
    }
426
    bool updatedDevFS = await _updateDevFS();
427 428 429 430 431 432
    if (benchmarkMode) {
      devFSTimer.stop();
      // Record time it took to synchronize to DevFS.
      benchmarkData['hotReloadDevFSSyncMilliseconds'] =
            devFSTimer.elapsed.inMilliseconds;
    }
433 434
    if (!updatedDevFS)
      return new OperationResult(1, 'Dart Source Error');
Devon Carew's avatar
Devon Carew committed
435
    String reloadMessage;
436
    try {
437
      String entryPath = path.relative(mainPath, from: projectRootPath);
438 439 440 441
      String deviceEntryPath =
          _devFS.baseUri.resolve(entryPath).toFilePath();
      String devicePackagesPath =
          _devFS.baseUri.resolve('.packages').toFilePath();
442 443
      if (benchmarkMode)
        vmReloadTimer.start();
444
      Map<String, dynamic> reloadReport =
445 446 447 448
          await currentView.uiIsolate.reloadSources(
              pause: pause,
              rootLibPath: deviceEntryPath,
              packagesPath: devicePackagesPath);
449
      if (!validateReloadReport(reloadReport)) {
450
        // Reload failed.
451
        flutterUsage.sendEvent('hot', 'reload-reject');
Devon Carew's avatar
Devon Carew committed
452
        return new OperationResult(1, 'reload rejected');
453 454
      } else {
        flutterUsage.sendEvent('hot', 'reload');
Devon Carew's avatar
Devon Carew committed
455 456 457
        int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'];
        int finalLibraryCount = reloadReport['details']['finalLibraryCount'];
        reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
458
      }
459 460
    } catch (error, st) {
      int errorCode = error['code'];
Devon Carew's avatar
Devon Carew committed
461
      String errorMessage = error['message'];
462 463
      if (errorCode == Isolate.kIsolateReloadBarred) {
        printError('Unable to hot reload app due to an unrecoverable error in '
464 465
                   'the source code. Please address the error and then use '
                   '"R" to restart the app.');
466
        flutterUsage.sendEvent('hot', 'reload-barred');
Devon Carew's avatar
Devon Carew committed
467
        return new OperationResult(errorCode, errorMessage);
468
      }
Devon Carew's avatar
Devon Carew committed
469

470
      printError('Hot reload failed:\ncode = $errorCode\nmessage = $errorMessage\n$st');
Devon Carew's avatar
Devon Carew committed
471
      return new OperationResult(errorCode, errorMessage);
472
    }
473 474 475 476 477 478 479 480
    if (benchmarkMode) {
      // Record time it took for the VM to reload the sources.
      vmReloadTimer.stop();
      benchmarkData['hotReloadVMReloadMilliseconds'] =
          vmReloadTimer.elapsed.inMilliseconds;
    }
    if (benchmarkMode)
      reassembleTimer.start();
481 482
    // Reload the isolate.
    await currentView.uiIsolate.reload();
483 484
    // We are now running from source.
    _runningFromSnapshot = false;
485 486 487 488 489
    // Check if the isolate is paused.
    final ServiceEvent pauseEvent = currentView.uiIsolate.pauseEvent;
    if ((pauseEvent != null) && (pauseEvent.isPauseEvent)) {
      // Isolate is paused. Stop here.
      printTrace('Skipping reassemble because isolate is paused.');
490
      return new OperationResult(OperationResult.ok.code, reloadMessage);
491
    }
492
    await _evictDirtyAssets();
493
    printTrace('Reassembling application');
494
    try {
495
      await currentView.uiIsolate.flutterReassemble();
496 497
    } catch (_) {
      printError('Reassembling application failed.');
Devon Carew's avatar
Devon Carew committed
498
      return new OperationResult(1, 'error reassembling application');
499
    }
500 501 502 503 504 505
    try {
      /* ensure that a frame is scheduled */
      await currentView.uiIsolate.uiWindowScheduleFrame();
    } catch (_) {
      /* ignore any errors */
    }
506 507 508
    reloadTimer.stop();
    printTrace('Hot reload performed in '
               '${getElapsedAsMilliseconds(reloadTimer.elapsed)}.');
509

510
    if (benchmarkMode) {
511 512 513 514 515
      // Record time it took for Flutter to reassemble the application.
      reassembleTimer.stop();
      benchmarkData['hotReloadFlutterReassembleMilliseconds'] =
          reassembleTimer.elapsed.inMilliseconds;
      // Record complete time it took for the reload.
516 517
      benchmarkData['hotReloadMillisecondsToFrame'] =
          reloadTimer.elapsed.inMilliseconds;
518
    }
519 520
    if (shouldReportReloadTime)
      flutterUsage.sendTiming('hot', 'reload', reloadTimer.elapsed);
Devon Carew's avatar
Devon Carew committed
521
    return new OperationResult(OperationResult.ok.code, reloadMessage);
522 523 524
  }

  @override
525
  void printHelp({ @required bool details }) {
526
    const String fire = '🔥';
527
    const String red = '\u001B[31m';
528 529 530 531
    const String bold = '\u001B[0;1m';
    const String reset = '\u001B[0m';
    printStatus(
      '$fire  To hot reload your app on the fly, press "r" or F5. To restart the app entirely, press "R".',
532
      ansiAlternative: '$red$fire$bold  To hot reload your app on the fly, '
533 534
                       'press "r" or F5. To restart the app entirely, press "R".$reset'
    );
535
    printStatus('The Observatory debugger and profiler is available at: $_observatoryUri');
536
    if (details) {
537
      printHelpDetails();
538 539 540 541
      printStatus('To repeat this help message, press "h" or F1. To quit, press "q", F10, or Ctrl-C.');
    } else {
      printStatus('For a more detailed help message, press "h" or F1. To quit, press "q", F10, or Ctrl-C.');
    }
542 543 544 545 546 547 548 549
  }

  @override
  Future<Null> cleanupAfterSignal() async {
    await stopEchoingDeviceLog();
    await stopApp();
  }

550 551 552
  @override
  Future<Null> preStop() => _cleanupDevFS();

553 554 555 556 557 558
  @override
  Future<Null> cleanupAtFinish() async {
    await _cleanupDevFS();
    await stopEchoingDeviceLog();
  }
}