hot.dart 17.6 KB
Newer Older
1 2 3 4 5
// 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';
6
import 'dart:convert';
7 8 9 10 11 12 13
import 'dart:io';

import 'package:path/path.dart' as path;

import 'application_package.dart';
import 'asset.dart';
import 'base/logger.dart';
14
import 'base/process.dart';
15
import 'base/utils.dart';
16
import 'build_info.dart';
17 18 19
import 'cache.dart';
import 'commands/build_apk.dart';
import 'commands/install.dart';
20
import 'dart/package_map.dart';
21 22 23
import 'device.dart';
import 'globals.dart';
import 'devfs.dart';
24
import 'vmservice.dart';
25
import 'resident_runner.dart';
26
import 'toolchain.dart';
27

28 29
const bool kHotReloadDefault = true;

30 31 32 33 34 35 36 37 38
String getDevFSLoaderScript() {
  return path.absolute(path.join(Cache.flutterRoot,
                                 'packages',
                                 'flutter',
                                 'bin',
                                 'loader',
                                 'loader_app.dart'));
}

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
class StartupDependencySetBuilder {
  StartupDependencySetBuilder(this.mainScriptPath,
                              this.projectRootPath);

  final String mainScriptPath;
  final String projectRootPath;

  Set<String> build() {
    final String skySnapshotPath =
        ToolConfiguration.instance.getHostToolPath(HostTool.SkySnapshot);

    final List<String> args = <String>[
      skySnapshotPath,
      '--packages=${path.absolute(PackageMap.globalPackagesPath)}',
      '--print-deps',
      mainScriptPath
    ];

    String output;
    try {
      output = runCheckedSync(args);
    } catch (e) {
      return null;
    }

    final List<String> lines = LineSplitter.split(output).toList();
    final Set<String> minimalDependencies = new Set<String>();
    for (String line in lines) {
      // We need to convert the uris so that they are relative to the project
      // root and tweak package: uris so that they reflect their devFS location.
      if (line.startsWith('package:')) {
        // Swap out package: for packages/ because we place all package sources
        // under packages/.
        line = line.replaceFirst('package:', 'packages/');
      } else {
        // Ensure paths are relative to the project root.
        line = path.relative(line, from: projectRootPath);
      }
      minimalDependencies.add(line);
    }
    return minimalDependencies;
  }
}


84
class FirstFrameTimer {
85
  FirstFrameTimer(this.vmService);
86 87 88 89

  void start() {
    stopwatch.reset();
    stopwatch.start();
90
    _subscription = vmService.onExtensionEvent.listen(_onExtensionEvent);
91 92 93 94 95
  }

  /// Returns a Future which completes after the first frame event is received.
  Future<Null> firstFrame() => _completer.future;

96
  void _onExtensionEvent(ServiceEvent event) {
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
    if (event.extensionKind == 'Flutter.FirstFrame')
      _stop();
  }

  void _stop() {
    _subscription?.cancel();
    _subscription = null;
    stopwatch.stop();
    _completer.complete(null);
  }

  Duration get elapsed {
    assert(!stopwatch.isRunning);
    return stopwatch.elapsed;
  }

113
  final VMService vmService;
114 115
  final Stopwatch stopwatch = new Stopwatch();
  final Completer<Null> _completer = new Completer<Null>();
116
  StreamSubscription<ServiceEvent> _subscription;
117 118
}

119 120 121 122 123
class HotRunner extends ResidentRunner {
  HotRunner(
    Device device, {
    String target,
    DebuggingOptions debuggingOptions,
124 125
    bool usesTerminalUI: true,
    this.benchmarkMode: false,
126 127 128
  }) : super(device,
             target: target,
             debuggingOptions: debuggingOptions,
129 130 131
             usesTerminalUI: usesTerminalUI) {
    _projectRootPath = Directory.current.path;
  }
132 133 134

  ApplicationPackage _package;
  String _mainPath;
135
  String _projectRootPath;
136
  Set<String> _startupDependencies;
137
  final AssetBundle bundle = new AssetBundle();
138 139
  final bool benchmarkMode;
  final Map<String, int> benchmarkData = new Map<String, int>();
140

141
  @override
142
  Future<int> run({
143
    Completer<DebugConnectionInfo> connectionInfoCompleter,
144 145 146
    String route,
    bool shouldBuild: true
  }) {
147 148 149
    // Don't let uncaught errors kill the process.
    return runZoned(() {
      return _run(
150
        connectionInfoCompleter: connectionInfoCompleter,
151 152
        route: route,
        shouldBuild: shouldBuild
153 154 155 156 157 158 159
      );
    }, onError: (dynamic error, StackTrace stackTrace) {
      printError('Exception from flutter run: $error', stackTrace);
    });
  }

  Future<int> _run({
160
    Completer<DebugConnectionInfo> connectionInfoCompleter,
161 162
    String route,
    bool shouldBuild: true
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
  }) async {
    _mainPath = findMainDartFile(target);
    if (!FileSystemEntity.isFileSync(_mainPath)) {
      String message = 'Tried to run $_mainPath, but that file does not exist.';
      if (target == null)
        message += '\nConsider using the -t option to specify the Dart file to start.';
      printError(message);
      return 1;
    }

    _package = getApplicationPackageForPlatform(device.platform);

    if (_package == null) {
      String message = 'No application found for ${device.platform}.';
      String hint = getMissingPackageHintForPlatform(device.platform);
      if (hint != null)
        message += '\n$hint';
      printError(message);
      return 1;
    }

    // TODO(devoncarew): We shouldn't have to do type checks here.
185
    if (shouldBuild && device is AndroidDevice) {
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
      printTrace('Running build command.');

      int result = await buildApk(
        device.platform,
        target: target,
        buildMode: debuggingOptions.buildMode
      );

      if (result != 0)
        return result;
    }

    // TODO(devoncarew): Move this into the device.startApp() impls.
    if (_package != null) {
      printTrace("Stopping app '${_package.name}' on ${device.name}.");
      // We don't wait for the stop command to complete.
      device.stopApp(_package);
    }

    // Allow any stop commands from above to start work.
    await new Future<Duration>.delayed(Duration.ZERO);

    // TODO(devoncarew): This fails for ios devices - we haven't built yet.
    if (device is AndroidDevice) {
      printTrace('Running install command.');
      if (!(installApp(device, _package, uninstall: false)))
        return 1;
    }

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

    await startEchoingDeviceLog();

219
    printStatus('Launching loader on ${device.name}...');
220

221 222
    // Start the loader.
    Future<LaunchResult> futureResult = device.startApp(
223 224
      _package,
      debuggingOptions.buildMode,
225
      mainPath: getDevFSLoaderScript(),
226 227 228 229 230
      debuggingOptions: debuggingOptions,
      platformArgs: platformArgs,
      route: route
    );

231 232 233 234 235 236 237 238 239 240 241
    // In parallel, compute the minimal dependency set.
    StartupDependencySetBuilder startupDependencySetBuilder =
        new StartupDependencySetBuilder(_mainPath, _projectRootPath);
    _startupDependencies = startupDependencySetBuilder.build();
    if (_startupDependencies == null) {
      printError('Error determining the set of Dart sources necessary to start '
                 'the application. Initial file upload may take a long time.');
    }

    LaunchResult result = await futureResult;

242
    if (!result.started) {
243
      printError('Error launching DevFS loader on ${device.name}.');
244 245 246 247 248 249
      await stopEchoingDeviceLog();
      return 2;
    }

    await connectToServiceProtocol(result.observatoryPort);

250 251 252 253 254 255
    try {
      Uri baseUri = await _initDevFS();
      if (connectionInfoCompleter != null) {
        connectionInfoCompleter.complete(
          new DebugConnectionInfo(result.observatoryPort, baseUri: baseUri.toString())
        );
256
      }
257 258 259 260 261 262 263 264 265
    } catch (error) {
      printError('Error initializing DevFS: $error');
      return 3;
    }
    _loaderShowMessage('Connecting...', progress: 0);
    bool devfsResult = await _updateDevFS(
      progressReporter: (int progress, int max) {
        if (progress % 10 == 0)
          _loaderShowMessage('Syncing files to device...', progress: progress, max: max);
266
      }
267 268 269 270 271
    );
    if (!devfsResult) {
      _loaderShowMessage('Failed.');
      printError('Could not perform initial file synchronization.');
      return 3;
272 273
    }

274 275
    await vmService.vm.refreshViews();
    printStatus('Connected to view \'${vmService.vm.mainView}\'.');
276 277 278 279 280

    printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...');
    _loaderShowMessage('Launching...');
    await _launchFromDevFS(_package, _mainPath);

281 282 283 284 285 286
    printStatus('Application running.');

    setupTerminal();

    registerSignalHandlers();

287 288 289 290
    printStatus('Finishing file synchronization...');
    // Finish the file sync now.
    await _updateDevFS();

291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
    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();
      File benchmarkOutput = new File('hot_benchmark.json');
      benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
    }

310
    return waitForAppToFinish();
311 312 313
  }

  @override
314
  Future<Null> handleTerminalCommand(String code) async {
315
    final String lower = code.toLowerCase();
316
    if ((lower == 'r') || (code == AnsiTerminal.KEY_F5)) {
317
      // F5, restart
318
      if ((code == 'r') || (code == AnsiTerminal.KEY_F5)) {
319
        // lower-case 'r'
320
        await _reloadSources();
321 322
      } else {
        // upper-case 'R'.
323
        await _restartFromSources();
324 325 326 327 328
      }
    }
  }

  void _loaderShowMessage(String message, { int progress, int max }) {
329
    currentView.uiIsolate.flutterLoaderShowMessage(message);
330
    if (progress != null) {
331 332
      currentView.uiIsolate.flutterLoaderSetProgress(progress.toDouble());
      currentView.uiIsolate.flutterLoaderSetProgressMax(max?.toDouble() ?? 0.0);
333
    } else {
334 335
      currentView.uiIsolate.flutterLoaderSetProgress(0.0);
      currentView.uiIsolate.flutterLoaderSetProgressMax(-1.0);
336 337 338 339 340
    }
  }

  DevFS _devFS;

341 342
  Future<Uri> _initDevFS() {
    String fsName = path.basename(_projectRootPath);
343
    _devFS = new DevFS(vmService,
344 345 346 347
                       fsName,
                       new Directory(_projectRootPath));
    return _devFS.create();
  }
348

349
  Future<bool> _updateDevFS({ DevFSProgressReporter progressReporter }) async {
350 351 352 353 354 355
    final bool rebuildBundle = bundle.needsBuild();
    if (rebuildBundle) {
      Status bundleStatus = logger.startProgress('Updating assets...');
      await bundle.build();
      bundleStatus.stop(showElapsedTime: true);
    }
356
    Status devFSStatus = logger.startProgress('Syncing files to device...');
357 358
    await _devFS.update(progressReporter: progressReporter,
                        bundle: bundle,
359 360
                        bundleDirty: rebuildBundle,
                        fileFilter: _startupDependencies);
361
    devFSStatus.stop(showElapsedTime: true);
362 363
    // Clear the minimal set after the first sync.
    _startupDependencies = null;
364 365 366 367
    if (progressReporter != null)
      printStatus('Synced ${getSizeAsMB(_devFS.bytes)}.');
    else
      printTrace('Synced ${getSizeAsMB(_devFS.bytes)}.');
368 369 370
    return true;
  }

371
  Future<Null> _evictDirtyAssets() async {
372
    if (_devFS.dirtyAssetEntries.length == 0)
373
      return;
374
    if (currentView.uiIsolate == null)
375 376
      throw 'Application isolate not found';
    for (DevFSEntry entry in _devFS.dirtyAssetEntries) {
377
      await currentView.uiIsolate.flutterEvictAsset(entry.assetPath);
378 379 380
    }
  }

381 382
  Future<Null> _cleanupDevFS() async {
    if (_devFS != null) {
383 384 385 386 387 388
      // Cleanup the devFS; don't wait indefinitely, and ignore any errors.
      await _devFS.destroy()
        .timeout(new Duration(milliseconds: 250))
        .catchError((dynamic error) {
          printTrace('$error');
        });
389 390 391 392
    }
    _devFS = null;
  }

393 394 395
  Future<Null> _launchInView(String entryPath,
                             String packagesPath,
                             String assetsDirectoryPath) async {
396
    FlutterView view = vmService.vm.mainView;
397
    return view.runFromSource(entryPath, packagesPath, assetsDirectoryPath);
398 399
  }

400 401 402 403 404 405 406 407
  Future<Null> _launchFromDevFS(ApplicationPackage package,
                                String mainScript) async {
    String entryPath = path.relative(mainScript, from: _projectRootPath);
    String deviceEntryPath =
        _devFS.baseUri.resolve(entryPath).toFilePath();
    String devicePackagesPath =
        _devFS.baseUri.resolve('.packages').toFilePath();
    String deviceAssetsDirectoryPath =
408
        _devFS.baseUri.resolve(getAssetBuildDirectory()).toFilePath();
409 410 411 412 413
    await _launchInView(deviceEntryPath,
                        devicePackagesPath,
                        deviceAssetsDirectoryPath);
  }

414
  Future<Null> _restartFromSources() async {
415
    FirstFrameTimer firstFrameTimer = new FirstFrameTimer(vmService);
416
    firstFrameTimer.start();
417 418
    await _updateDevFS();
    await _launchFromDevFS(_package, _mainPath);
419 420
    bool waitForFrame =
        await currentView.uiIsolate.flutterFrameworkPresent();
421 422
    Status restartStatus =
        logger.startProgress('Waiting for application to start...');
423 424 425 426
    if (waitForFrame) {
      // Wait for the first frame to be rendered.
      await firstFrameTimer.firstFrame();
    }
427
    restartStatus.stop(showElapsedTime: true);
428 429 430 431 432 433 434 435
    if (waitForFrame) {
      printStatus('Restart time: '
                  '${getElapsedAsMilliseconds(firstFrameTimer.elapsed)}');
      if (benchmarkMode) {
        benchmarkData['hotRestartMillisecondsToFrame'] =
            firstFrameTimer.elapsed.inMilliseconds;
      }
      flutterUsage.sendTiming('hot', 'restart', firstFrameTimer.elapsed);
436
    }
437
    flutterUsage.sendEvent('hot', 'restart');
438 439 440 441 442 443
  }

  /// Returns [true] if the reload was successful.
  bool _printReloadReport(Map<String, dynamic> reloadReport) {
    if (!reloadReport['success']) {
      printError('Hot reload was rejected:');
444
      for (Map<String, dynamic> notice in reloadReport['details']['notices'])
445 446 447 448 449
        printError('${notice['message']}');
      return false;
    }
    int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'];
    int finalLibraryCount = reloadReport['details']['finalLibraryCount'];
450
    printStatus('Reloaded $loadedLibraryCount of $finalLibraryCount libraries.');
451 452 453
    return true;
  }

454
  @override
455 456 457 458 459 460 461 462
  Future<bool> restart({ bool fullRestart: false }) async {
    if (fullRestart) {
      await _restartFromSources();
      return true;
    } else {
      return _reloadSources();
    }
  }
463

464
  Future<bool> _reloadSources() async {
465
    if (currentView.uiIsolate == null)
466
      throw 'Application isolate not found';
467
    FirstFrameTimer firstFrameTimer = new FirstFrameTimer(vmService);
468
    firstFrameTimer.start();
469 470
    if (_devFS != null)
      await _updateDevFS();
471
    Status reloadStatus = logger.startProgress('Performing hot reload...');
472 473
    try {
      Map<String, dynamic> reloadReport =
474
          await currentView.uiIsolate.reloadSources();
475 476 477
      reloadStatus.stop(showElapsedTime: true);
      if (!_printReloadReport(reloadReport)) {
        // Reload failed.
478
        flutterUsage.sendEvent('hot', 'reload-reject');
479
        return false;
480 481
      } else {
        flutterUsage.sendEvent('hot', 'reload');
482
      }
483 484 485 486 487 488 489 490 491 492
    } catch (error, st) {
      int errorCode = error['code'];
      if (errorCode == Isolate.kIsolateReloadBarred) {
        printError('Unable to hot reload app due to an unrecoverable error in '
                   'the source code. Please address the error and then '
                   'Use "R" to restart the app.');
        flutterUsage.sendEvent('hot', 'reload-barred');
        return false;
      }
      String errorMessage = error['message'];
493
      reloadStatus.stop(showElapsedTime: true);
494
      printError('Hot reload failed:\ncode = $errorCode\nmessage = $errorMessage\n$st');
495 496
      return false;
    }
497
    await _evictDirtyAssets();
498
    Status reassembleStatus =
499
        logger.startProgress('Reassembling application...');
500
    bool waitForFrame = true;
501
    try {
502
      waitForFrame = (await currentView.uiIsolate.flutterReassemble() != null);
503 504 505 506 507 508
    } catch (_) {
      reassembleStatus.stop(showElapsedTime: true);
      printError('Reassembling application failed.');
      return false;
    }
    reassembleStatus.stop(showElapsedTime: true);
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525
    try {
      /* ensure that a frame is scheduled */
      await currentView.uiIsolate.uiWindowScheduleFrame();
    } catch (_) {
      /* ignore any errors */
    }
    if (waitForFrame) {
      // When the framework is present, we can wait for the first frame
      // event and measure reload itme.
      await firstFrameTimer.firstFrame();
      printStatus('Hot reload time: '
                  '${getElapsedAsMilliseconds(firstFrameTimer.elapsed)}');
      if (benchmarkMode) {
        benchmarkData['hotReloadMillisecondsToFrame'] =
            firstFrameTimer.elapsed.inMilliseconds;
      }
      flutterUsage.sendTiming('hot', 'reload', firstFrameTimer.elapsed);
526
    }
527 528 529 530 531
    return true;
  }

  @override
  void printHelp() {
532 533
    printStatus('Type "h" or F1 for this help message; type "q", F10, or ctrl-c to quit.', emphasis: true);
    printStatus('Type "r" or F5 to perform a hot reload of the app, and "R" to restart the app.', emphasis: true);
534 535 536 537 538 539 540 541 542
    printStatus('Type "w" to print the widget hierarchy of the app, and "t" for the render tree.', emphasis: true);
  }

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

543 544 545
  @override
  Future<Null> preStop() => _cleanupDevFS();

546 547 548 549 550 551
  @override
  Future<Null> cleanupAtFinish() async {
    await _cleanupDevFS();
    await stopEchoingDeviceLog();
  }
}