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
This commit is contained in:
parent
646b5350d1
commit
ec7517766c
@ -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.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 @@ 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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user