Commit ec751776 authored by Devon Carew's avatar Devon Carew

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
parent 646b5350
......@@ -6,6 +6,8 @@ import 'dart:async';
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 '../build_info.dart';
import '../device.dart';
import '../flx.dart' as flx;
import '../globals.dart';
import '../observatory.dart';
import '../protocol_discovery.dart';
import 'adb.dart';
import 'android.dart';
......@@ -369,6 +372,39 @@ class AndroidDevice extends Device {
);
}
@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]);
......@@ -416,7 +452,13 @@ class AndroidDevice extends Device {
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 @@ class AndroidDevice extends Device {
'--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;
......
......@@ -32,8 +32,7 @@ Future<int> runCommandAndStreamOutput(List<String> cmd, {
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 @@ String runCheckedSync(List<String> cmd, {
);
}
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 @@ class ProcessExit implements Exception {
@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();
}
}
......@@ -17,7 +17,6 @@ import '../dart/sdk.dart';
import '../globals.dart';
import '../runner/flutter_command.dart';
bool isDartFile(FileSystemEntity entry) => entry is File && entry.path.endsWith('.dart');
typedef bool FileFilter(FileSystemEntity entity);
......
......@@ -39,11 +39,8 @@ class RefreshCommand extends FlutterCommand {
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;
......
......@@ -295,10 +295,18 @@ class _RunAndStayResident {
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 @@ class _RunAndStayResident {
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 @@ class _RunAndStayResident {
return 2;
}
stopwatch.stop();
startTime.stop();
_exitCompleter = new Completer<int>();
......@@ -386,21 +394,21 @@ class _RunAndStayResident {
observatory = await Observatory.connect(result.observatoryPort);
printTrace('Connected to observatory port: ${result.observatoryPort}.');
observatory.onExtensionEvent.listen((Event event) {
printTrace(event.toString());
});
observatory.onIsolateEvent.listen((Event event) {
if (event['isolate'] != null)
_isolateId = event['isolate']['id'];
printTrace(event.toString());
});
observatory.streamListen('Isolate');
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 @@ class _RunAndStayResident {
_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 @@ class _RunAndStayResident {
}
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 @@ class _RunAndStayResident {
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 @@ Future<Null> _downloadStartupTrace(Observatory observatory) async {
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).');
}
......@@ -13,6 +13,7 @@ import 'base/os.dart';
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 @@ abstract class Device {
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);
......
......@@ -12,6 +12,7 @@ import '../base/process.dart';
import '../build_info.dart';
import '../device.dart';
import '../globals.dart';
import '../observatory.dart';
import 'mac.dart';
const String _ideviceinstallerInstructions =
......@@ -198,6 +199,21 @@ class IOSDevice extends Device {
return new LaunchResult.succeeded();
}
@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.
......
......@@ -15,6 +15,7 @@ import '../build_info.dart';
import '../device.dart';
import '../flx.dart' as flx;
import '../globals.dart';
import '../observatory.dart';
import '../protocol_discovery.dart';
import 'mac.dart';
......@@ -560,6 +561,21 @@ class IOSSimulator extends Device {
return (await flx.build(precompiledSnapshot: true)) == 0;
}
@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.
......
......@@ -13,6 +13,15 @@ class Observatory {
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 @@ class Observatory {
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 @@ class Observatory {
_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 @@ class Observatory {
'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 @@ class Response {
final Map<String, dynamic> response;
String get type => response['type'];
dynamic operator[](String key) => response[key];
@override
......@@ -113,18 +161,35 @@ class Response {
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);
String get kind => response['kind'];
IsolateRef get isolate => new IsolateRef.from(response['isolate']);
/// Only valid for [kind] == `Extension`.
String get extensionKind => response['extensionKind'];
}
final Map<String, dynamic> event;
class IsolateRef extends Response {
IsolateRef(Map<String, dynamic> response) : super(response);
factory IsolateRef.from(dynamic ref) => ref == null ? null : new IsolateRef(ref);
String get kind => event['kind'];
String get id => response['id'];
dynamic operator[](String key) => event[key];
@override
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
String toString() => event.toString();
int get hashCode => id.hashCode;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment