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;
}