Flutter run restart (#4105) * working on making a faster flutter run restart * clean up todos; fire events on isolate changes * use the Flutter.FrameworkInitialization event * review comments
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index 5ab2e80..1ae3359 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -6,6 +6,8 @@ import 'dart:convert'; import 'dart:io'; +import 'package:path/path.dart' as path; + import '../android/android_sdk.dart'; import '../application_package.dart'; import '../base/os.dart'; @@ -14,6 +16,7 @@ import '../device.dart'; import '../flx.dart' as flx; import '../globals.dart'; +import '../observatory.dart'; import '../protocol_discovery.dart'; import 'adb.dart'; import 'android.dart'; @@ -370,6 +373,39 @@ } @override + Future<bool> restartApp( + ApplicationPackage package, + LaunchResult result, { + String mainPath, + Observatory observatory + }) async { + Directory tempDir = await Directory.systemTemp.createTemp('flutter_tools'); + + try { + String snapshotPath = path.join(tempDir.path, 'snapshot_blob.bin'); + int result = await flx.createSnapshot(mainPath: mainPath, snapshotPath: snapshotPath); + + if (result != 0) { + printError('Failed to run the Flutter compiler; exit code: $result'); + return false; + } + + AndroidApk apk = package; + String androidActivity = apk.launchActivity; + bool success = await refreshSnapshot(androidActivity, snapshotPath); + + if (!success) { + printError('Error refreshing snapshot on $this.'); + return false; + } + + return true; + } finally { + tempDir.deleteSync(recursive: true); + } + } + + @override Future<bool> stopApp(ApplicationPackage app) { List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]); return runCommandAndStreamOutput(command).then((int exitCode) => exitCode == 0); @@ -416,7 +452,13 @@ return false; } - runCheckedSync(adbCommandForDevice(<String>['push', snapshotPath, _deviceSnapshotPath])); + RunResult result = await runAsync( + adbCommandForDevice(<String>['push', snapshotPath, _deviceSnapshotPath]) + ); + if (result.exitCode != 0) { + printStatus(result.toString()); + return false; + } List<String> cmd = adbCommandForDevice(<String>[ 'shell', 'am', 'start', @@ -426,9 +468,14 @@ '--es', 'snapshot', _deviceSnapshotPath, activity, ]); + result = await runAsync(cmd); + if (result.exitCode != 0) { + printStatus(result.toString()); + return false; + } - RegExp errorRegExp = new RegExp(r'^Error: .*$', multiLine: true); - Match errorMatch = errorRegExp.firstMatch(runCheckedSync(cmd)); + final RegExp errorRegExp = new RegExp(r'^Error: .*$', multiLine: true); + Match errorMatch = errorRegExp.firstMatch(result.processResult.stdout); if (errorMatch != null) { printError(errorMatch.group(0)); return false;
diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart index 184dd1f..d4287a1 100644 --- a/packages/flutter_tools/lib/src/base/process.dart +++ b/packages/flutter_tools/lib/src/base/process.dart
@@ -32,8 +32,7 @@ RegExp filter, StringConverter mapFunction }) async { - Process process = await runCommand(cmd, - workingDirectory: workingDirectory); + Process process = await runCommand(cmd, workingDirectory: workingDirectory); process.stdout .transform(UTF8.decoder) .transform(const LineSplitter()) @@ -84,6 +83,18 @@ ); } +Future<RunResult> runAsync(List<String> cmd, { String workingDirectory }) async { + printTrace(cmd.join(' ')); + ProcessResult results = await Process.run( + cmd[0], + cmd.getRange(1, cmd.length).toList(), + workingDirectory: workingDirectory + ); + RunResult runResults = new RunResult(results); + printTrace(runResults.toString()); + return runResults; +} + /// Run cmd and return stdout. String runSync(List<String> cmd, { String workingDirectory }) { return _runWithLoggingSync(cmd, workingDirectory: workingDirectory); @@ -146,3 +157,21 @@ @override String toString() => message; } + +class RunResult { + RunResult(this.processResult); + + final ProcessResult processResult; + + int get exitCode => processResult.exitCode; + + @override + String toString() { + StringBuffer out = new StringBuffer(); + if (processResult.stdout.isNotEmpty) + out.writeln(processResult.stdout); + if (processResult.stderr.isNotEmpty) + out.writeln(processResult.stderr); + return out.toString().trimRight(); + } +}
diff --git a/packages/flutter_tools/lib/src/commands/analyze.dart b/packages/flutter_tools/lib/src/commands/analyze.dart index cd4eaea..0a5c156 100644 --- a/packages/flutter_tools/lib/src/commands/analyze.dart +++ b/packages/flutter_tools/lib/src/commands/analyze.dart
@@ -17,7 +17,6 @@ import '../globals.dart'; import '../runner/flutter_command.dart'; - bool isDartFile(FileSystemEntity entry) => entry is File && entry.path.endsWith('.dart'); typedef bool FileFilter(FileSystemEntity entity);
diff --git a/packages/flutter_tools/lib/src/commands/refresh.dart b/packages/flutter_tools/lib/src/commands/refresh.dart index 5306ce2..2dfbc7d 100644 --- a/packages/flutter_tools/lib/src/commands/refresh.dart +++ b/packages/flutter_tools/lib/src/commands/refresh.dart
@@ -39,11 +39,8 @@ Directory tempDir = await Directory.systemTemp.createTemp('flutter_tools'); try { String snapshotPath = path.join(tempDir.path, 'snapshot_blob.bin'); + int result = await createSnapshot(mainPath: argResults['target'], snapshotPath: snapshotPath); - int result = await createSnapshot( - mainPath: argResults['target'], - snapshotPath: snapshotPath - ); if (result != 0) { printError('Failed to run the Flutter compiler. Exit code: $result'); return result;
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 75f9fe0..ceee469 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -295,10 +295,18 @@ StreamSubscription<String> _loggingSubscription; Observatory observatory; - String _isolateId; /// Start the app and keep the process running during its lifetime. - Future<int> run({ bool traceStartup: false, bool benchmark: false }) async { + Future<int> run({ bool traceStartup: false, bool benchmark: false }) { + // Don't let uncaught errors kill the process. + return runZoned(() { + return _run(traceStartup: traceStartup, benchmark: benchmark); + }, onError: (dynamic error) { + printError('Exception from flutter run: $error'); + }); + } + + Future<int> _run({ bool traceStartup: false, bool benchmark: false }) async { String mainPath = findMainDartFile(target); if (!FileSystemEntity.isFileSync(mainPath)) { String message = 'Tried to run $mainPath, but that file does not exist.'; @@ -319,7 +327,7 @@ return 1; } - Stopwatch stopwatch = new Stopwatch()..start(); + Stopwatch startTime = new Stopwatch()..start(); // TODO(devoncarew): We shouldn't have to do type checks here. if (device is AndroidDevice) { @@ -377,7 +385,7 @@ return 2; } - stopwatch.stop(); + startTime.stop(); _exitCompleter = new Completer<int>(); @@ -386,21 +394,21 @@ observatory = await Observatory.connect(result.observatoryPort); printTrace('Connected to observatory port: ${result.observatoryPort}.'); - observatory.onIsolateEvent.listen((Event event) { - if (event['isolate'] != null) - _isolateId = event['isolate']['id']; + observatory.onExtensionEvent.listen((Event event) { + printTrace(event.toString()); }); - observatory.streamListen('Isolate'); + + observatory.onIsolateEvent.listen((Event event) { + printTrace(event.toString()); + }); + + if (benchmark) + await observatory.waitFirstIsolate; // Listen for observatory connection close. observatory.done.whenComplete(() { _handleExit(); }); - - observatory.getVM().then((VM vm) { - if (vm.isolates.isNotEmpty) - _isolateId = vm.isolates.first['id']; - }); } printStatus('Application running.'); @@ -425,7 +433,7 @@ _printHelp(); } else if (lower == 'r' || code == AnsiTerminal.KEY_F5) { // F5, refresh - _handleRefresh(); + _handleRefresh(package, result, mainPath); } else if (lower == 'q' || code == AnsiTerminal.KEY_F10) { // F10, exit _handleExit(); @@ -441,18 +449,31 @@ } if (benchmark) { - _writeBenchmark(stopwatch); - new Future<Null>.delayed(new Duration(seconds: 2)).then((_) { - _handleExit(); - }); + await new Future<Null>.delayed(new Duration(seconds: 4)); + + // Touch the file. + File mainFile = new File(mainPath); + mainFile.writeAsBytesSync(mainFile.readAsBytesSync()); + + Stopwatch restartTime = new Stopwatch()..start(); + bool restarted = await _handleRefresh(package, result, mainPath); + restartTime.stop(); + _writeBenchmark(startTime, restarted ? restartTime : null); + await new Future<Null>.delayed(new Duration(seconds: 2)); + _handleExit(); } return _exitCompleter.future.then((int exitCode) async { - if (observatory != null && !observatory.isClosed && _isolateId != null) { - observatory.flutterExit(_isolateId); - - // WebSockets do not have a flush() method. - await new Future<Null>.delayed(new Duration(milliseconds: 100)); + try { + if (observatory != null && !observatory.isClosed) { + if (observatory.isolates.isNotEmpty) { + observatory.flutterExit(observatory.firstIsolateId); + // The Dart WebSockets API does not have a flush() method. + await new Future<Null>.delayed(new Duration(milliseconds: 100)); + } + } + } catch (error) { + stderr.writeln(error.toString()); } return exitCode; @@ -463,15 +484,33 @@ printStatus('Type "h" or F1 for help, "r" or F5 to restart the app, and "q", F10, or ctrl-c to quit.'); } - void _handleRefresh() { + Future<bool> _handleRefresh(ApplicationPackage package, LaunchResult result, String mainPath) async { if (observatory == null) { printError('Debugging is not enabled.'); + return false; } else { - printStatus('Re-starting application...'); + Status status = logger.startProgress('Re-starting application...'); - observatory.isolateReload(_isolateId).catchError((dynamic error) { - printError('Error restarting app: $error'); - }); + Future<Event> extensionAddedEvent = observatory.onExtensionEvent + .where((Event event) => event.extensionKind == 'Flutter.FrameworkInitialization') + .first; + + bool restartResult = await device.restartApp( + package, + result, + mainPath: mainPath, + observatory: observatory + ); + + status.stop(showElapsedTime: true); + + if (restartResult) { + // TODO(devoncarew): We should restore the route here. + + await extensionAddedEvent; + } + + return restartResult; } } @@ -533,11 +572,15 @@ printStatus('Saved startup trace info in ${traceInfoFile.path}.'); } -void _writeBenchmark(Stopwatch stopwatch) { +void _writeBenchmark(Stopwatch startTime, [Stopwatch restartTime]) { final String benchmarkOut = 'refresh_benchmark.json'; Map<String, dynamic> data = <String, dynamic>{ - 'time': stopwatch.elapsedMilliseconds + 'start': startTime.elapsedMilliseconds, + 'time': (restartTime ?? startTime).elapsedMilliseconds // time and restart are the same }; + if (restartTime != null) + data['restart'] = restartTime.elapsedMilliseconds; + new File(benchmarkOut).writeAsStringSync(toPrettyJson(data)); printStatus('Run benchmark written to $benchmarkOut ($data).'); }
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 3528682..f67c0d3 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart
@@ -13,6 +13,7 @@ import 'base/utils.dart'; import 'build_info.dart'; import 'globals.dart'; +import 'observatory.dart'; import 'ios/devices.dart'; import 'ios/simulators.dart'; @@ -185,6 +186,15 @@ Map<String, dynamic> platformArgs }); + /// Restart the given app; the application will already have been launched with + /// [startApp]. + Future<bool> restartApp( + ApplicationPackage package, + LaunchResult result, { + String mainPath, + Observatory observatory + }); + /// Stop an app package on the current device. Future<bool> stopApp(ApplicationPackage app);
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 9b66c8f..321ab3a 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -12,6 +12,7 @@ import '../build_info.dart'; import '../device.dart'; import '../globals.dart'; +import '../observatory.dart'; import 'mac.dart'; const String _ideviceinstallerInstructions = @@ -199,6 +200,21 @@ } @override + Future<bool> restartApp( + ApplicationPackage package, + LaunchResult result, { + String mainPath, + Observatory observatory + }) async { + return observatory.isolateReload(observatory.firstIsolateId).then((Response response) { + return true; + }).catchError((dynamic error) { + printError('Error restarting app: $error'); + return false; + }); + } + + @override Future<bool> stopApp(ApplicationPackage app) async { // Currently we don't have a way to stop an app running on iOS. return false;
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 7a6df56..f566a14 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -15,6 +15,7 @@ import '../device.dart'; import '../flx.dart' as flx; import '../globals.dart'; +import '../observatory.dart'; import '../protocol_discovery.dart'; import 'mac.dart'; @@ -561,6 +562,21 @@ } @override + Future<bool> restartApp( + ApplicationPackage package, + LaunchResult result, { + String mainPath, + Observatory observatory + }) { + return observatory.isolateReload(observatory.firstIsolateId).then((Response response) { + return true; + }).catchError((dynamic error) { + printError('Error restarting app: $error'); + return false; + }); + } + + @override Future<bool> stopApp(ApplicationPackage app) async { // Currently we don't have a way to stop an app running on iOS. return false;
diff --git a/packages/flutter_tools/lib/src/observatory.dart b/packages/flutter_tools/lib/src/observatory.dart index 27b83a4..93e9b10 100644 --- a/packages/flutter_tools/lib/src/observatory.dart +++ b/packages/flutter_tools/lib/src/observatory.dart
@@ -13,6 +13,15 @@ peer.registerMethod('streamNotify', (rpc.Parameters event) { _handleStreamNotify(event.asMap); }); + + onIsolateEvent.listen((Event event) { + if (event.kind == 'IsolateStart') { + _addIsolate(event.isolate); + } else if (event.kind == 'IsolateExit') { + String removedId = event.isolate.id; + isolates.removeWhere((IsolateRef ref) => ref.id == removedId); + } + }); } static Future<Observatory> connect(int port) async { @@ -26,19 +35,30 @@ final rpc.Peer peer; final int port; + List<IsolateRef> isolates = <IsolateRef>[]; + Completer<IsolateRef> _waitFirstIsolateCompleter; + Map<String, StreamController<Event>> _eventControllers = <String, StreamController<Event>>{}; + Set<String> _listeningFor = new Set<String>(); + bool get isClosed => peer.isClosed; Future<Null> get done => peer.done; + String get firstIsolateId => isolates.isEmpty ? null : isolates.first.id; + // Events + Stream<Event> get onExtensionEvent => onEvent('Extension'); // IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, ServiceExtensionAdded - Stream<Event> get onIsolateEvent => _getEventController('Isolate').stream; - Stream<Event> get onTimelineEvent => _getEventController('Timeline').stream; + Stream<Event> get onIsolateEvent => onEvent('Isolate'); + Stream<Event> get onTimelineEvent => onEvent('Timeline'); // Listen for a specific event name. - Stream<Event> onEvent(String streamName) => _getEventController(streamName).stream; + Stream<Event> onEvent(String streamId) { + streamListen(streamId); + return _getEventController(streamId).stream; + } StreamController<Event> _getEventController(String eventName) { StreamController<Event> controller = _eventControllers[eventName]; @@ -54,16 +74,31 @@ _getEventController(data['streamId']).add(event); } + Future<IsolateRef> get waitFirstIsolate async { + if (isolates.isNotEmpty) + return isolates.first; + + _waitFirstIsolateCompleter = new Completer<IsolateRef>(); + + getVM().then((VM vm) { + for (IsolateRef isolate in vm.isolates) + _addIsolate(isolate); + }); + + return _waitFirstIsolateCompleter.future; + } + // Requests Future<Response> sendRequest(String method, [Map<String, dynamic> args]) { return peer.sendRequest(method, args).then((dynamic result) => new Response(result)); } - Future<Response> streamListen(String streamId) { - return sendRequest('streamListen', <String, dynamic>{ - 'streamId': streamId - }); + Future<Null> streamListen(String streamId) async { + if (!_listeningFor.contains(streamId)) { + _listeningFor.add(streamId); + sendRequest('streamListen', <String, dynamic>{ 'streamId': streamId }); + } } Future<VM> getVM() { @@ -97,6 +132,17 @@ 'isolateId': isolateId }).then((dynamic result) => new Response(result)); } + + void _addIsolate(IsolateRef isolate) { + if (!isolates.contains(isolate)) { + isolates.add(isolate); + + if (_waitFirstIsolateCompleter != null) { + _waitFirstIsolateCompleter.complete(isolate); + _waitFirstIsolateCompleter = null; + } + } + } } class Response { @@ -104,6 +150,8 @@ final Map<String, dynamic> response; + String get type => response['type']; + dynamic operator[](String key) => response[key]; @override @@ -113,18 +161,35 @@ class VM extends Response { VM(Map<String, dynamic> response) : super(response); - List<dynamic> get isolates => response['isolates']; + List<IsolateRef> get isolates => response['isolates'].map((dynamic ref) => new IsolateRef(ref)).toList(); } -class Event { - Event(this.event); +class Event extends Response { + Event(Map<String, dynamic> response) : super(response); - final Map<String, dynamic> event; + String get kind => response['kind']; + IsolateRef get isolate => new IsolateRef.from(response['isolate']); - String get kind => event['kind']; + /// Only valid for [kind] == `Extension`. + String get extensionKind => response['extensionKind']; +} - dynamic operator[](String key) => event[key]; +class IsolateRef extends Response { + IsolateRef(Map<String, dynamic> response) : super(response); + factory IsolateRef.from(dynamic ref) => ref == null ? null : new IsolateRef(ref); + + String get id => response['id']; @override - String toString() => event.toString(); + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other is! IsolateRef) + return false; + final IsolateRef typedOther = other; + return id == typedOther.id; + } + + @override + int get hashCode => id.hashCode; }