Unverified Commit 31a9626c authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

[O] Removing all timeouts (mark II) (#26736)

These are essentially self-inflicted race conditions. Instead of timeouts we're going to try a more verbose logging mechanism that points out when things are taking a long time.
parent fa79c813
...@@ -119,7 +119,7 @@ Notably, it will start and stop gradle, for instance. ...@@ -119,7 +119,7 @@ Notably, it will start and stop gradle, for instance.
To run all tests defined in `manifest.yaml`, use option `-a` (`--all`): To run all tests defined in `manifest.yaml`, use option `-a` (`--all`):
```sh ```sh
dart bin/run.dart -a ../../bin/cache/dart-sdk/bin/dart bin/run.dart -a
``` ```
## Running specific tests ## Running specific tests
...@@ -128,7 +128,7 @@ To run a test, use option `-t` (`--task`): ...@@ -128,7 +128,7 @@ To run a test, use option `-t` (`--task`):
```sh ```sh
# from the .../flutter/dev/devicelab directory # from the .../flutter/dev/devicelab directory
dart bin/run.dart -t {NAME_OR_PATH_OF_TEST} ../../bin/cache/dart-sdk/bin/dart bin/run.dart -t {NAME_OR_PATH_OF_TEST}
``` ```
Where `NAME_OR_PATH_OF_TEST` can be either of: Where `NAME_OR_PATH_OF_TEST` can be either of:
...@@ -142,7 +142,7 @@ Where `NAME_OR_PATH_OF_TEST` can be either of: ...@@ -142,7 +142,7 @@ Where `NAME_OR_PATH_OF_TEST` can be either of:
To run multiple tests, repeat option `-t` (`--task`) multiple times: To run multiple tests, repeat option `-t` (`--task`) multiple times:
```sh ```sh
dart bin/run.dart -t test1 -t test2 -t test3 ../../bin/cache/dart-sdk/bin/dart bin/run.dart -t test1 -t test2 -t test3
``` ```
To run tests from a specific stage, use option `-s` (`--stage`). To run tests from a specific stage, use option `-s` (`--stage`).
...@@ -151,7 +151,7 @@ Currently there are only three stages defined, `devicelab`, ...@@ -151,7 +151,7 @@ Currently there are only three stages defined, `devicelab`,
```sh ```sh
dart bin/run.dart -s {NAME_OF_STAGE} ../../bin/cache/dart-sdk/bin/dart bin/run.dart -s {NAME_OF_STAGE}
``` ```
# Reproducing broken builds locally # Reproducing broken builds locally
...@@ -162,7 +162,7 @@ failing test is `flutter_gallery__transition_perf`. This name can be passed to ...@@ -162,7 +162,7 @@ failing test is `flutter_gallery__transition_perf`. This name can be passed to
the `run.dart` command. For example: the `run.dart` command. For example:
```sh ```sh
dart bin/run.dart -t flutter_gallery__transition_perf ../../bin/cache/dart-sdk/bin/dart bin/run.dart -t flutter_gallery__transition_perf
``` ```
# Writing tests # Writing tests
......
...@@ -74,18 +74,18 @@ void main() { ...@@ -74,18 +74,18 @@ void main() {
run.stdin.write('P'); run.stdin.write('P');
await driver.drive('none'); await driver.drive('none');
final Future<String> reloadStartingText = final Future<String> reloadStartingText =
stdout.stream.firstWhere((String line) => line.endsWith('hot reload...')); stdout.stream.firstWhere((String line) => line.endsWith('] Initializing hot reload...'));
final Future<String> reloadEndingText = final Future<String> reloadEndingText =
stdout.stream.firstWhere((String line) => line.contains('Hot reload performed in ')); stdout.stream.firstWhere((String line) => line.contains('] Reloaded ') && line.endsWith('ms.'));
print('test: pressing "r" to perform a hot reload...'); print('test: pressing "r" to perform a hot reload...');
run.stdin.write('r'); run.stdin.write('r');
await reloadStartingText; await reloadStartingText;
await reloadEndingText; await reloadEndingText;
await driver.drive('none'); await driver.drive('none');
final Future<String> restartStartingText = final Future<String> restartStartingText =
stdout.stream.firstWhere((String line) => line.endsWith('hot restart...')); stdout.stream.firstWhere((String line) => line.endsWith('Performing hot restart...'));
final Future<String> restartEndingText = final Future<String> restartEndingText =
stdout.stream.firstWhere((String line) => line.contains('Hot restart performed in ')); stdout.stream.firstWhere((String line) => line.contains('] Restarted application in '));
print('test: pressing "R" to perform a full reload...'); print('test: pressing "R" to perform a full reload...');
run.stdin.write('R'); run.stdin.write('R');
await restartStartingText; await restartStartingText;
......
...@@ -29,7 +29,7 @@ Future<void> main() async { ...@@ -29,7 +29,7 @@ Future<void> main() async {
final SerializableFinder summary = find.byValueKey('summary'); final SerializableFinder summary = find.byValueKey('summary');
// Wait for calibration to complete and fab to appear. // Wait for calibration to complete and fab to appear.
await driver.waitFor(fab, timeout: const Duration(seconds: 40)); await driver.waitFor(fab);
final String calibrationResult = await driver.getText(summary); final String calibrationResult = await driver.getText(summary);
final Match matchCalibration = calibrationRegExp.matchAsPrefix(calibrationResult); final Match matchCalibration = calibrationRegExp.matchAsPrefix(calibrationResult);
...@@ -59,7 +59,7 @@ Future<void> main() async { ...@@ -59,7 +59,7 @@ Future<void> main() async {
expect(double.parse(matchFast.group(1)), closeTo(flutterFrameRate * 2.0, 5.0)); expect(double.parse(matchFast.group(1)), closeTo(flutterFrameRate * 2.0, 5.0));
expect(double.parse(matchFast.group(2)), closeTo(flutterFrameRate, 10.0)); expect(double.parse(matchFast.group(2)), closeTo(flutterFrameRate, 10.0));
expect(int.parse(matchFast.group(3)), 1); expect(int.parse(matchFast.group(3)), 1);
}, timeout: const Timeout(Duration(minutes: 1))); });
tearDownAll(() async { tearDownAll(() async {
driver?.close(); driver?.close();
......
...@@ -268,8 +268,7 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB ...@@ -268,8 +268,7 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
void initServiceExtensions() { void initServiceExtensions() {
super.initServiceExtensions(); super.initServiceExtensions();
const bool isReleaseMode = bool.fromEnvironment('dart.vm.product'); profile(() {
if (!isReleaseMode) {
registerSignalServiceExtension( registerSignalServiceExtension(
name: 'debugDumpApp', name: 'debugDumpApp',
callback: () { callback: () {
...@@ -294,11 +293,14 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB ...@@ -294,11 +293,14 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
name: 'didSendFirstFrameEvent', name: 'didSendFirstFrameEvent',
callback: (_) async { callback: (_) async {
return <String, dynamic>{ return <String, dynamic>{
// This is defined to return a STRING, not a boolean.
// Devtools, the Intellij plugin, and the flutter tool all depend
// on it returning a string and not a boolean.
'enabled': _needToReportFirstFrame ? 'false' : 'true' 'enabled': _needToReportFirstFrame ? 'false' : 'true'
}; };
}, },
); );
} });
assert(() { assert(() {
registerBoolServiceExtension( registerBoolServiceExtension(
...@@ -540,22 +542,25 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB ...@@ -540,22 +542,25 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
/// Whether the first frame has finished rendering. /// Whether the first frame has finished rendering.
/// ///
/// Only valid in profile and debug builds, it can't be used in release /// Only useful in profile and debug builds; in release builds, this always
/// builds. /// return false. This can be deferred using [deferFirstFrameReport] and
/// It can be deferred using [deferFirstFrameReport] and /// [allowFirstFrameReport]. The value is set at the end of the call to
/// [allowFirstFrameReport]. /// [drawFrame].
/// The value is set at the end of the call to [drawFrame]. ///
/// This value can also be obtained over the VM service protocol as
/// `ext.flutter.didSendFirstFrameEvent`.
bool get debugDidSendFirstFrameEvent => !_needToReportFirstFrame; bool get debugDidSendFirstFrameEvent => !_needToReportFirstFrame;
/// Tell the framework not to report the frame it is building as a "useful" /// Tell the framework not to report the frame it is building as a "useful"
/// first frame until there is a corresponding call to [allowFirstFrameReport]. /// first frame until there is a corresponding call to [allowFirstFrameReport].
/// ///
/// This is used by [WidgetsApp] to report the first frame. /// This is used by [WidgetsApp] to avoid reporting frames that aren't useful
// /// during startup as the "first frame".
// TODO(ianh): This method should only be available in debug and profile modes.
void deferFirstFrameReport() { void deferFirstFrameReport() {
assert(_deferFirstFrameReportCount >= 0); profile(() {
_deferFirstFrameReportCount += 1; assert(_deferFirstFrameReportCount >= 0);
_deferFirstFrameReportCount += 1;
});
} }
/// When called after [deferFirstFrameReport]: tell the framework to report /// When called after [deferFirstFrameReport]: tell the framework to report
...@@ -564,12 +569,13 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB ...@@ -564,12 +569,13 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
/// This method may only be called once for each corresponding call /// This method may only be called once for each corresponding call
/// to [deferFirstFrameReport]. /// to [deferFirstFrameReport].
/// ///
/// This is used by [WidgetsApp] to report the first frame. /// This is used by [WidgetsApp] to report when the first useful frame is
// /// painted.
// TODO(ianh): This method should only be available in debug and profile modes.
void allowFirstFrameReport() { void allowFirstFrameReport() {
assert(_deferFirstFrameReportCount >= 1); profile(() {
_deferFirstFrameReportCount -= 1; assert(_deferFirstFrameReportCount >= 1);
_deferFirstFrameReportCount -= 1;
});
} }
void _handleBuildScheduled() { void _handleBuildScheduled() {
...@@ -691,13 +697,13 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB ...@@ -691,13 +697,13 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
return true; return true;
}()); }());
} }
// TODO(ianh): Following code should not be included in release mode, only profile and debug modes. profile(() {
// See https://github.com/dart-lang/sdk/issues/27192 if (_needToReportFirstFrame && _reportFirstFrame) {
if (_needToReportFirstFrame && _reportFirstFrame) { developer.Timeline.instantSync('Widgets completed first useful frame');
developer.Timeline.instantSync('Widgets completed first useful frame'); developer.postEvent('Flutter.FirstFrame', <String, dynamic>{});
developer.postEvent('Flutter.FirstFrame', <String, dynamic>{}); _needToReportFirstFrame = false;
_needToReportFirstFrame = false; }
} });
} }
/// The [Element] that is at the root of the hierarchy (and which wraps the /// The [Element] that is at the root of the hierarchy (and which wraps the
......
...@@ -9,16 +9,24 @@ import 'package:meta/meta.dart'; ...@@ -9,16 +9,24 @@ import 'package:meta/meta.dart';
abstract class Command { abstract class Command {
/// Abstract const constructor. This constructor enables subclasses to provide /// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions. /// const constructors so that they can be used in const expressions.
const Command({ Duration timeout }) const Command({ this.timeout });
: timeout = timeout ?? const Duration(seconds: 5);
/// Deserializes this command from the value generated by [serialize]. /// Deserializes this command from the value generated by [serialize].
Command.deserialize(Map<String, String> json) Command.deserialize(Map<String, String> json)
: timeout = Duration(milliseconds: int.parse(json['timeout'])); : timeout = _parseTimeout(json);
static Duration _parseTimeout(Map<String, String> json) {
final String timeout = json['timeout'];
if (timeout == null)
return null;
return Duration(milliseconds: int.parse(timeout));
}
/// The maximum amount of time to wait for the command to complete. /// The maximum amount of time to wait for the command to complete.
/// ///
/// Defaults to 5 seconds. /// Defaults to no timeout, because it is common for operations to take oddly
/// long in test environments (e.g. because the test host is overloaded), and
/// having timeouts essentially means having race conditions.
final Duration timeout; final Duration timeout;
/// Identifies the type of the command object and of the handler. /// Identifies the type of the command object and of the handler.
...@@ -26,10 +34,14 @@ abstract class Command { ...@@ -26,10 +34,14 @@ abstract class Command {
/// Serializes this command to parameter name/value pairs. /// Serializes this command to parameter name/value pairs.
@mustCallSuper @mustCallSuper
Map<String, String> serialize() => <String, String>{ Map<String, String> serialize() {
'command': kind, final Map<String, String> result = <String, String>{
'timeout': '${timeout.inMilliseconds}', 'command': kind,
}; };
if (timeout != null)
result['timeout'] = '${timeout.inMilliseconds}';
return result;
}
} }
/// An object sent from a Flutter application back to the Flutter Driver in /// An object sent from a Flutter application back to the Flutter Driver in
......
...@@ -177,7 +177,10 @@ class FlutterDriverExtension { ...@@ -177,7 +177,10 @@ class FlutterDriverExtension {
if (commandHandler == null || commandDeserializer == null) if (commandHandler == null || commandDeserializer == null)
throw 'Extension $_extensionMethod does not support command $commandKind'; throw 'Extension $_extensionMethod does not support command $commandKind';
final Command command = commandDeserializer(params); final Command command = commandDeserializer(params);
final Result response = await commandHandler(command).timeout(command.timeout); Future<Result> responseFuture = commandHandler(command);
if (command.timeout != null)
responseFuture = responseFuture.timeout(command.timeout);
final Result response = await responseFuture;
return _makeResponse(response?.toJson()); return _makeResponse(response?.toJson());
} on TimeoutException catch (error, stackTrace) { } on TimeoutException catch (error, stackTrace) {
final String msg = 'Timeout while executing $commandKind: $error\n$stackTrace'; final String msg = 'Timeout while executing $commandKind: $error\n$stackTrace';
......
...@@ -11,13 +11,13 @@ import 'package:flutter_driver/src/driver/timeline.dart'; ...@@ -11,13 +11,13 @@ import 'package:flutter_driver/src/driver/timeline.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:vm_service_client/vm_service_client.dart'; import 'package:vm_service_client/vm_service_client.dart';
import 'package:quiver/testing/async.dart';
import 'common.dart'; import 'common.dart';
/// Magical timeout value that's different from the default. /// Magical timeout value that's different from the default.
const Duration _kTestTimeout = Duration(milliseconds: 1234); const Duration _kTestTimeout = Duration(milliseconds: 1234);
const String _kSerializedTestTimeout = '1234'; const String _kSerializedTestTimeout = '1234';
const Duration _kDefaultCommandTimeout = Duration(seconds: 5);
void main() { void main() {
group('FlutterDriver.connect', () { group('FlutterDriver.connect', () {
...@@ -358,17 +358,19 @@ void main() { ...@@ -358,17 +358,19 @@ void main() {
group('sendCommand error conditions', () { group('sendCommand error conditions', () {
test('local timeout', () async { test('local timeout', () async {
final List<String> log = <String>[];
final StreamSubscription<LogRecord> logSub = flutterDriverLog.listen((LogRecord s) => log.add(s.toString()));
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
// completer never competed to trigger timeout // completer never completed to trigger timeout
return Completer<Map<String, dynamic>>().future; return Completer<Map<String, dynamic>>().future;
}); });
try { FakeAsync().run((FakeAsync time) {
await driver.waitFor(find.byTooltip('foo'), timeout: const Duration(milliseconds: 100)); driver.waitFor(find.byTooltip('foo'));
fail('expected an exception'); expect(log, <String>[]);
} catch (error) { time.elapse(const Duration(hours: 1));
expect(error is DriverError, isTrue); });
expect(error.message, 'Failed to fulfill WaitFor: Flutter application not responding'); expect(log, <String>['[warning] FlutterDriver: waitFor message is taking a long time to complete...']);
} await logSub.cancel();
}); });
test('remote error', () async { test('remote error', () async {
...@@ -389,7 +391,6 @@ void main() { ...@@ -389,7 +391,6 @@ void main() {
}); });
group('FlutterDriver with custom timeout', () { group('FlutterDriver with custom timeout', () {
const double kTestMultiplier = 3.0;
MockVMServiceClient mockClient; MockVMServiceClient mockClient;
MockPeer mockPeer; MockPeer mockPeer;
MockIsolate mockIsolate; MockIsolate mockIsolate;
...@@ -399,21 +400,20 @@ void main() { ...@@ -399,21 +400,20 @@ void main() {
mockClient = MockVMServiceClient(); mockClient = MockVMServiceClient();
mockPeer = MockPeer(); mockPeer = MockPeer();
mockIsolate = MockIsolate(); mockIsolate = MockIsolate();
driver = FlutterDriver.connectedTo(mockClient, mockPeer, mockIsolate, timeoutMultiplier: kTestMultiplier); driver = FlutterDriver.connectedTo(mockClient, mockPeer, mockIsolate);
}); });
test('multiplies the timeout', () async { test('GetHealth has no default timeout', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, String>{ expect(i.positionalArguments[1], <String, String>{
'command': 'get_health', 'command': 'get_health',
'timeout': '${(_kDefaultCommandTimeout * kTestMultiplier).inMilliseconds}',
}); });
return makeMockResponse(<String, dynamic>{'status': 'ok'}); return makeMockResponse(<String, dynamic>{'status': 'ok'});
}); });
await driver.checkHealth(); await driver.checkHealth();
}); });
test('does not multiply explicit timeouts', () async { test('does not interfere with explicit timeouts', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, String>{ expect(i.positionalArguments[1], <String, String>{
'command': 'get_health', 'command': 'get_health',
......
...@@ -88,15 +88,13 @@ class AndroidDevice extends Device { ...@@ -88,15 +88,13 @@ class AndroidDevice extends Device {
propCommand, propCommand,
stdoutEncoding: latin1, stdoutEncoding: latin1,
stderrEncoding: latin1, stderrEncoding: latin1,
).timeout(const Duration(seconds: 5)); );
if (result.exitCode == 0) { if (result.exitCode == 0) {
_properties = parseAdbDeviceProperties(result.stdout); _properties = parseAdbDeviceProperties(result.stdout);
} else { } else {
printError('Error retrieving device properties for $name:'); printError('Error retrieving device properties for $name:');
printError(result.stderr); printError(result.stderr);
} }
} on TimeoutException catch (_) {
throwToolExit('adb not responding');
} on ProcessException catch (error) { } on ProcessException catch (error) {
printError('Error retrieving device properties for $name: $error'); printError('Error retrieving device properties for $name: $error');
} }
...@@ -279,7 +277,7 @@ class AndroidDevice extends Device { ...@@ -279,7 +277,7 @@ class AndroidDevice extends Device {
if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion()) if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
return false; return false;
final Status status = logger.startProgress('Installing ${fs.path.relative(apk.file.path)}...', expectSlowOperation: true); final Status status = logger.startProgress('Installing ${fs.path.relative(apk.file.path)}...', timeout: kSlowOperation);
final RunResult installResult = await runAsync(adbCommandForDevice(<String>['install', '-t', '-r', apk.file.path])); final RunResult installResult = await runAsync(adbCommandForDevice(<String>['install', '-t', '-r', apk.file.path]));
status.stop(); status.stop();
// Some versions of adb exit with exit code 0 even on failure :( // Some versions of adb exit with exit code 0 even on failure :(
......
...@@ -51,9 +51,10 @@ class AndroidEmulator extends Emulator { ...@@ -51,9 +51,10 @@ class AndroidEmulator extends Emulator {
throw '${runResult.stdout}\n${runResult.stderr}'.trimRight(); throw '${runResult.stdout}\n${runResult.stderr}'.trimRight();
} }
}); });
// emulator continues running on a successful launch so if we // The emulator continues running on a successful launch, so if it hasn't
// haven't quit within 3 seconds we assume that's a success and just // quit within 3 seconds we assume that's a success and just return. This
// return. // means that on a slow machine, a failure that takes more than three
// seconds won't be recognized as such... :-/
return Future.any<void>(<Future<void>>[ return Future.any<void>(<Future<void>>[
launchResult, launchResult,
Future<void>.delayed(const Duration(seconds: 3)) Future<void>.delayed(const Duration(seconds: 3))
......
...@@ -50,32 +50,41 @@ class AndroidWorkflow implements Workflow { ...@@ -50,32 +50,41 @@ class AndroidWorkflow implements Workflow {
class AndroidValidator extends DoctorValidator { class AndroidValidator extends DoctorValidator {
AndroidValidator(): super('Android toolchain - develop for Android devices',); AndroidValidator(): super('Android toolchain - develop for Android devices',);
@override
String get slowWarning => '${_task ?? 'This'} is taking a long time...';
String _task;
/// Returns false if we cannot determine the Java version or if the version /// Returns false if we cannot determine the Java version or if the version
/// is not compatible. /// is not compatible.
Future<bool> _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) async { Future<bool> _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) async {
if (!processManager.canRun(javaBinary)) { _task = 'Checking Java status';
messages.add(ValidationMessage.error(userMessages.androidCantRunJavaBinary(javaBinary)));
return false;
}
String javaVersion;
try { try {
printTrace('java -version'); if (!processManager.canRun(javaBinary)) {
final ProcessResult result = await processManager.run(<String>[javaBinary, '-version']); messages.add(ValidationMessage.error(userMessages.androidCantRunJavaBinary(javaBinary)));
if (result.exitCode == 0) { return false;
final List<String> versionLines = result.stderr.split('\n');
javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
} }
} catch (error) { String javaVersion;
printTrace(error.toString()); try {
} printTrace('java -version');
if (javaVersion == null) { final ProcessResult result = await processManager.run(<String>[javaBinary, '-version']);
// Could not determine the java version. if (result.exitCode == 0) {
messages.add(ValidationMessage.error(userMessages.androidUnknownJavaVersion)); final List<String> versionLines = result.stderr.split('\n');
return false; javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
}
} catch (error) {
printTrace(error.toString());
}
if (javaVersion == null) {
// Could not determine the java version.
messages.add(ValidationMessage.error(userMessages.androidUnknownJavaVersion));
return false;
}
messages.add(ValidationMessage(userMessages.androidJavaVersion(javaVersion)));
// TODO(johnmccutchan): Validate version.
return true;
} finally {
_task = null;
} }
messages.add(ValidationMessage(userMessages.androidJavaVersion(javaVersion)));
// TODO(johnmccutchan): Validate version.
return true;
} }
@override @override
...@@ -149,6 +158,9 @@ class AndroidValidator extends DoctorValidator { ...@@ -149,6 +158,9 @@ class AndroidValidator extends DoctorValidator {
class AndroidLicenseValidator extends DoctorValidator { class AndroidLicenseValidator extends DoctorValidator {
AndroidLicenseValidator(): super('Android license subvalidator',); AndroidLicenseValidator(): super('Android license subvalidator',);
@override
String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...';
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validate() async {
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<ValidationMessage> messages = <ValidationMessage>[];
...@@ -208,10 +220,8 @@ class AndroidLicenseValidator extends DoctorValidator { ...@@ -208,10 +220,8 @@ class AndroidLicenseValidator extends DoctorValidator {
Future<LicensesAccepted> get licensesAccepted async { Future<LicensesAccepted> get licensesAccepted async {
LicensesAccepted status; LicensesAccepted status;
void _onLine(String line) { void _handleLine(String line) {
if (status == null && licenseAccepted.hasMatch(line)) { if (licenseCounts.hasMatch(line)) {
status = LicensesAccepted.all;
} else if (licenseCounts.hasMatch(line)) {
final Match match = licenseCounts.firstMatch(line); final Match match = licenseCounts.firstMatch(line);
if (match.group(1) != match.group(2)) { if (match.group(1) != match.group(2)) {
status = LicensesAccepted.some; status = LicensesAccepted.some;
...@@ -219,9 +229,12 @@ class AndroidLicenseValidator extends DoctorValidator { ...@@ -219,9 +229,12 @@ class AndroidLicenseValidator extends DoctorValidator {
status = LicensesAccepted.none; status = LicensesAccepted.none;
} }
} else if (licenseNotAccepted.hasMatch(line)) { } else if (licenseNotAccepted.hasMatch(line)) {
// In case the format changes, a more general match will keep doctor // The licenseNotAccepted pattern is trying to match the same line as
// mostly working. // licenseCounts, but is more general. In case the format changes, a
// more general match may keep doctor mostly working.
status = LicensesAccepted.none; status = LicensesAccepted.none;
} else if (licenseAccepted.hasMatch(line)) {
status ??= LicensesAccepted.all;
} }
} }
...@@ -235,19 +248,14 @@ class AndroidLicenseValidator extends DoctorValidator { ...@@ -235,19 +248,14 @@ class AndroidLicenseValidator extends DoctorValidator {
final Future<void> output = process.stdout final Future<void> output = process.stdout
.transform<String>(const Utf8Decoder(allowMalformed: true)) .transform<String>(const Utf8Decoder(allowMalformed: true))
.transform<String>(const LineSplitter()) .transform<String>(const LineSplitter())
.listen(_onLine) .listen(_handleLine)
.asFuture<void>(null); .asFuture<void>(null);
final Future<void> errors = process.stderr final Future<void> errors = process.stderr
.transform<String>(const Utf8Decoder(allowMalformed: true)) .transform<String>(const Utf8Decoder(allowMalformed: true))
.transform<String>(const LineSplitter()) .transform<String>(const LineSplitter())
.listen(_onLine) .listen(_handleLine)
.asFuture<void>(null); .asFuture<void>(null);
try { await Future.wait<void>(<Future<void>>[output, errors]);
await Future.wait<void>(<Future<void>>[output, errors]).timeout(const Duration(seconds: 30));
} catch (TimeoutException) {
printTrace(userMessages.androidLicensesTimeout(androidSdk.sdkManagerPath));
processManager.killPid(process.pid);
}
return status ?? LicensesAccepted.unknown; return status ?? LicensesAccepted.unknown;
} }
...@@ -261,9 +269,10 @@ class AndroidLicenseValidator extends DoctorValidator { ...@@ -261,9 +269,10 @@ class AndroidLicenseValidator extends DoctorValidator {
_ensureCanRunSdkManager(); _ensureCanRunSdkManager();
final Version sdkManagerVersion = Version.parse(androidSdk.sdkManagerVersion); final Version sdkManagerVersion = Version.parse(androidSdk.sdkManagerVersion);
if (sdkManagerVersion == null || sdkManagerVersion.major < 26) if (sdkManagerVersion == null || sdkManagerVersion.major < 26) {
// SDK manager is found, but needs to be updated. // SDK manager is found, but needs to be updated.
throwToolExit(userMessages.androidSdkOutdated(androidSdk.sdkManagerPath)); throwToolExit(userMessages.androidSdkOutdated(androidSdk.sdkManagerPath));
}
final Process process = await runCommand( final Process process = await runCommand(
<String>[androidSdk.sdkManagerPath, '--licenses'], <String>[androidSdk.sdkManagerPath, '--licenses'],
......
...@@ -97,7 +97,7 @@ Future<GradleProject> _readGradleProject() async { ...@@ -97,7 +97,7 @@ Future<GradleProject> _readGradleProject() async {
final FlutterProject flutterProject = await FlutterProject.current(); final FlutterProject flutterProject = await FlutterProject.current();
final String gradle = await _ensureGradle(flutterProject); final String gradle = await _ensureGradle(flutterProject);
updateLocalProperties(project: flutterProject); updateLocalProperties(project: flutterProject);
final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true); final Status status = logger.startProgress('Resolving dependencies...', timeout: kSlowOperation);
GradleProject project; GradleProject project;
try { try {
final RunResult propertiesRunResult = await runCheckedAsync( final RunResult propertiesRunResult = await runCheckedAsync(
...@@ -175,7 +175,7 @@ Future<String> _ensureGradle(FlutterProject project) async { ...@@ -175,7 +175,7 @@ Future<String> _ensureGradle(FlutterProject project) async {
// of validating the Gradle executable. This may take several seconds. // of validating the Gradle executable. This may take several seconds.
Future<String> _initializeGradle(FlutterProject project) async { Future<String> _initializeGradle(FlutterProject project) async {
final Directory android = project.android.hostAppGradleRoot; final Directory android = project.android.hostAppGradleRoot;
final Status status = logger.startProgress('Initializing gradle...', expectSlowOperation: true); final Status status = logger.startProgress('Initializing gradle...', timeout: kSlowOperation);
String gradle = _locateGradlewExecutable(android); String gradle = _locateGradlewExecutable(android);
if (gradle == null) { if (gradle == null) {
injectGradleWrapper(android); injectGradleWrapper(android);
...@@ -314,8 +314,8 @@ Future<void> buildGradleProject({ ...@@ -314,8 +314,8 @@ Future<void> buildGradleProject({
Future<void> _buildGradleProjectV1(FlutterProject project, String gradle) async { Future<void> _buildGradleProjectV1(FlutterProject project, String gradle) async {
// Run 'gradlew build'. // Run 'gradlew build'.
final Status status = logger.startProgress( final Status status = logger.startProgress(
"Running 'gradlew build'...", 'Running \'gradlew build\'...',
expectSlowOperation: true, timeout: kSlowOperation,
multilineOutput: true, multilineOutput: true,
); );
final int exitCode = await runCommandAndStreamOutput( final int exitCode = await runCommandAndStreamOutput(
...@@ -365,8 +365,8 @@ Future<void> _buildGradleProjectV2( ...@@ -365,8 +365,8 @@ Future<void> _buildGradleProjectV2(
} }
} }
final Status status = logger.startProgress( final Status status = logger.startProgress(
"Gradle task '$assembleTask'...", 'Running Gradle task \'$assembleTask\'...',
expectSlowOperation: true, timeout: kSlowOperation,
multilineOutput: true, multilineOutput: true,
); );
final String gradlePath = fs.file(gradle).absolute.path; final String gradlePath = fs.file(gradle).absolute.path;
......
...@@ -107,7 +107,7 @@ class AppContext { ...@@ -107,7 +107,7 @@ class AppContext {
/// Gets the value associated with the specified [type], or `null` if no /// Gets the value associated with the specified [type], or `null` if no
/// such value has been associated. /// such value has been associated.
dynamic operator [](Type type) { Object operator [](Type type) {
dynamic value = _generateIfNecessary(type, _overrides); dynamic value = _generateIfNecessary(type, _overrides);
if (value == null && _parent != null) if (value == null && _parent != null)
value = _parent[type]; value = _parent[type];
......
...@@ -36,9 +36,7 @@ RecordingFileSystem getRecordingFileSystem(String location) { ...@@ -36,9 +36,7 @@ RecordingFileSystem getRecordingFileSystem(String location) {
final RecordingFileSystem fileSystem = RecordingFileSystem( final RecordingFileSystem fileSystem = RecordingFileSystem(
delegate: _kLocalFs, destination: dir); delegate: _kLocalFs, destination: dir);
addShutdownHook(() async { addShutdownHook(() async {
await fileSystem.recording.flush( await fileSystem.recording.flush();
pendingResultTimeout: const Duration(seconds: 5),
);
}, ShutdownStage.SERIALIZE_RECORDING); }, ShutdownStage.SERIALIZE_RECORDING);
return fileSystem; return fileSystem;
} }
......
...@@ -162,10 +162,7 @@ class Stdio { ...@@ -162,10 +162,7 @@ class Stdio {
bool get supportsAnsiEscapes => hasTerminal ? io.stdout.supportsAnsiEscapes : false; bool get supportsAnsiEscapes => hasTerminal ? io.stdout.supportsAnsiEscapes : false;
} }
io.IOSink get stderr => context[Stdio].stderr;
Stream<List<int>> get stdin => context[Stdio].stdin;
io.IOSink get stdout => context[Stdio].stdout;
Stdio get stdio => context[Stdio]; Stdio get stdio => context[Stdio];
io.IOSink get stdout => stdio.stdout;
Stream<List<int>> get stdin => stdio.stdin;
io.IOSink get stderr => stdio.stderr;
...@@ -37,7 +37,7 @@ Future<List<int>> _attempt(Uri url, {bool onlyHeaders = false}) async { ...@@ -37,7 +37,7 @@ Future<List<int>> _attempt(Uri url, {bool onlyHeaders = false}) async {
printTrace('Downloading: $url'); printTrace('Downloading: $url');
HttpClient httpClient; HttpClient httpClient;
if (context[HttpClientFactory] != null) { if (context[HttpClientFactory] != null) {
httpClient = context[HttpClientFactory](); httpClient = (context[HttpClientFactory] as HttpClientFactory)(); // ignore: avoid_as
} else { } else {
httpClient = HttpClient(); httpClient = HttpClient();
} }
......
...@@ -203,14 +203,6 @@ Future<int> runInteractively(List<String> command, { ...@@ -203,14 +203,6 @@ Future<int> runInteractively(List<String> command, {
return await process.exitCode; return await process.exitCode;
} }
Future<void> runAndKill(List<String> cmd, Duration timeout) {
final Future<Process> proc = runDetached(cmd);
return Future<void>.delayed(timeout, () async {
printTrace('Intentionally killing ${cmd[0]}');
processManager.killPid((await proc).pid);
});
}
Future<Process> runDetached(List<String> cmd) { Future<Process> runDetached(List<String> cmd) {
_traceCommand(cmd); _traceCommand(cmd);
final Future<Process> proc = processManager.start( final Future<Process> proc = processManager.start(
......
...@@ -5,8 +5,6 @@ ...@@ -5,8 +5,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert' show AsciiDecoder; import 'dart:convert' show AsciiDecoder;
import 'package:quiver/strings.dart';
import '../globals.dart'; import '../globals.dart';
import 'context.dart'; import 'context.dart';
import 'io.dart' as io; import 'io.dart' as io;
...@@ -172,31 +170,32 @@ class AnsiTerminal { ...@@ -172,31 +170,32 @@ class AnsiTerminal {
/// Return keystrokes from the console. /// Return keystrokes from the console.
/// ///
/// Useful when the console is in [singleCharMode]. /// Useful when the console is in [singleCharMode].
Stream<String> get onCharInput { Stream<String> get keystrokes {
_broadcastStdInString ??= io.stdin.transform<String>(const AsciiDecoder(allowInvalid: true)).asBroadcastStream(); _broadcastStdInString ??= io.stdin.transform<String>(const AsciiDecoder(allowInvalid: true)).asBroadcastStream();
return _broadcastStdInString; return _broadcastStdInString;
} }
/// Prompts the user to input a character within the accepted list. Re-prompts /// Prompts the user to input a character within a given list. Re-prompts if
/// if entered character is not in the list. /// entered character is not in the list.
/// ///
/// The [prompt] is the text displayed prior to waiting for user input. The /// The `prompt`, if non-null, is the text displayed prior to waiting for user
/// [defaultChoiceIndex], if given, will be the character appearing in /// input each time. If `prompt` is non-null and `displayAcceptedCharacters`
/// [acceptedCharacters] in the index given if the user presses enter without /// is true, the accepted keys are printed next to the `prompt`.
/// any key input. Setting [displayAcceptedCharacters] also prints the
/// accepted keys next to the [prompt].
/// ///
/// Throws a [TimeoutException] if a `timeout` is provided and its duration /// The returned value is the user's input; if `defaultChoiceIndex` is not
/// expired without user input. Duration resets per key press. /// null, and the user presses enter without any other input, the return value
/// will be the character in `acceptedCharacters` at the index given by
/// `defaultChoiceIndex`.
Future<String> promptForCharInput( Future<String> promptForCharInput(
List<String> acceptedCharacters, { List<String> acceptedCharacters, {
String prompt, String prompt,
int defaultChoiceIndex, int defaultChoiceIndex,
bool displayAcceptedCharacters = true, bool displayAcceptedCharacters = true,
Duration timeout,
}) async { }) async {
assert(acceptedCharacters != null); assert(acceptedCharacters != null);
assert(acceptedCharacters.isNotEmpty); assert(acceptedCharacters.isNotEmpty);
assert(prompt == null || prompt.isNotEmpty);
assert(displayAcceptedCharacters != null);
List<String> charactersToDisplay = acceptedCharacters; List<String> charactersToDisplay = acceptedCharacters;
if (defaultChoiceIndex != null) { if (defaultChoiceIndex != null) {
assert(defaultChoiceIndex >= 0 && defaultChoiceIndex < acceptedCharacters.length); assert(defaultChoiceIndex >= 0 && defaultChoiceIndex < acceptedCharacters.length);
...@@ -206,17 +205,14 @@ class AnsiTerminal { ...@@ -206,17 +205,14 @@ class AnsiTerminal {
} }
String choice; String choice;
singleCharMode = true; singleCharMode = true;
while (isEmpty(choice) || choice.length != 1 || !acceptedCharacters.contains(choice)) { while (choice == null || choice.length > 1 || !acceptedCharacters.contains(choice)) {
if (isNotEmpty(prompt)) { if (prompt != null) {
printStatus(prompt, emphasis: true, newline: false); printStatus(prompt, emphasis: true, newline: false);
if (displayAcceptedCharacters) if (displayAcceptedCharacters)
printStatus(' [${charactersToDisplay.join("|")}]', newline: false); printStatus(' [${charactersToDisplay.join("|")}]', newline: false);
printStatus(': ', emphasis: true, newline: false); printStatus(': ', emphasis: true, newline: false);
} }
Future<String> inputFuture = onCharInput.first; choice = await keystrokes.first;
if (timeout != null)
inputFuture = inputFuture.timeout(timeout);
choice = await inputFuture;
printStatus(choice); printStatus(choice);
} }
singleCharMode = false; singleCharMode = false;
......
...@@ -299,7 +299,7 @@ abstract class CachedArtifact { ...@@ -299,7 +299,7 @@ abstract class CachedArtifact {
Future<void> _downloadArchive(String message, Uri url, Directory location, bool verifier(File f), void extractor(File f, Directory d)) { Future<void> _downloadArchive(String message, Uri url, Directory location, bool verifier(File f), void extractor(File f, Directory d)) {
return _withDownloadFile('${flattenNameSubdirs(url)}', (File tempFile) async { return _withDownloadFile('${flattenNameSubdirs(url)}', (File tempFile) async {
if (!verifier(tempFile)) { if (!verifier(tempFile)) {
final Status status = logger.startProgress(message, expectSlowOperation: true); final Status status = logger.startProgress(message, timeout: kSlowOperation);
try { try {
await _downloadFile(url, tempFile); await _downloadFile(url, tempFile);
status.stop(); status.stop();
...@@ -648,7 +648,7 @@ Future<void> _downloadFile(Uri url, File location) async { ...@@ -648,7 +648,7 @@ Future<void> _downloadFile(Uri url, File location) async {
} }
Future<bool> _doesRemoteExist(String message, Uri url) async { Future<bool> _doesRemoteExist(String message, Uri url) async {
final Status status = logger.startProgress(message, expectSlowOperation: true); final Status status = logger.startProgress(message, timeout: kSlowOperation);
final bool exists = await doesRemoteFileExist(url); final bool exists = await doesRemoteFileExist(url);
status.stop(); status.stop();
return exists; return exists;
......
...@@ -74,11 +74,12 @@ class AnalyzeContinuously extends AnalyzeBase { ...@@ -74,11 +74,12 @@ class AnalyzeContinuously extends AnalyzeBase {
analysisStatus?.cancel(); analysisStatus?.cancel();
if (!firstAnalysis) if (!firstAnalysis)
printStatus('\n'); printStatus('\n');
analysisStatus = logger.startProgress('Analyzing $analysisTarget...'); analysisStatus = logger.startProgress('Analyzing $analysisTarget...', timeout: kSlowOperation);
analyzedPaths.clear(); analyzedPaths.clear();
analysisTimer = Stopwatch()..start(); analysisTimer = Stopwatch()..start();
} else { } else {
analysisStatus?.stop(); analysisStatus?.stop();
analysisStatus = null;
analysisTimer.stop(); analysisTimer.stop();
logger.printStatus(terminal.clearScreen(), newline: false); logger.printStatus(terminal.clearScreen(), newline: false);
......
...@@ -107,7 +107,7 @@ class AnalyzeOnce extends AnalyzeBase { ...@@ -107,7 +107,7 @@ class AnalyzeOnce extends AnalyzeBase {
? '${directories.length} ${directories.length == 1 ? 'directory' : 'directories'}' ? '${directories.length} ${directories.length == 1 ? 'directory' : 'directories'}'
: fs.path.basename(directories.first); : fs.path.basename(directories.first);
final Status progress = argResults['preamble'] final Status progress = argResults['preamble']
? logger.startProgress('Analyzing $message...') ? logger.startProgress('Analyzing $message...', timeout: kSlowOperation)
: null; : null;
await analysisCompleter.future; await analysisCompleter.future;
......
...@@ -135,15 +135,14 @@ class AttachCommand extends FlutterCommand { ...@@ -135,15 +135,14 @@ class AttachCommand extends FlutterCommand {
if (device is FuchsiaDevice) { if (device is FuchsiaDevice) {
attachLogger = true; attachLogger = true;
final String module = argResults['module']; final String module = argResults['module'];
if (module == null) { if (module == null)
throwToolExit('\'--module\' is requried for attaching to a Fuchsia device'); throwToolExit('\'--module\' is required for attaching to a Fuchsia device');
}
usesIpv6 = device.ipv6; usesIpv6 = device.ipv6;
FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol; FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
try { try {
isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module); isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
observatoryUri = await isolateDiscoveryProtocol.uri; observatoryUri = await isolateDiscoveryProtocol.uri;
printStatus('Done.'); printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
} catch (_) { } catch (_) {
isolateDiscoveryProtocol?.dispose(); isolateDiscoveryProtocol?.dispose();
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList(); final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
...@@ -163,7 +162,7 @@ class AttachCommand extends FlutterCommand { ...@@ -163,7 +162,7 @@ class AttachCommand extends FlutterCommand {
observatoryUri = await observatoryDiscovery.uri; observatoryUri = await observatoryDiscovery.uri;
// Determine ipv6 status from the scanned logs. // Determine ipv6 status from the scanned logs.
usesIpv6 = observatoryDiscovery.ipv6; usesIpv6 = observatoryDiscovery.ipv6;
printStatus('Done.'); printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
} finally { } finally {
await observatoryDiscovery?.cancel(); await observatoryDiscovery?.cancel();
} }
...@@ -210,20 +209,30 @@ class AttachCommand extends FlutterCommand { ...@@ -210,20 +209,30 @@ class AttachCommand extends FlutterCommand {
if (attachLogger) { if (attachLogger) {
flutterDevice.startEchoingDeviceLog(); flutterDevice.startEchoingDeviceLog();
} }
int result;
if (daemon != null) { if (daemon != null) {
AppInstance app; AppInstance app;
try { try {
app = await daemon.appDomain.launch(runner, runner.attach, app = await daemon.appDomain.launch(
device, null, true, fs.currentDirectory); runner,
runner.attach,
device,
null,
true,
fs.currentDirectory,
);
} catch (error) { } catch (error) {
throwToolExit(error.toString()); throwToolExit(error.toString());
} }
final int result = await app.runner.waitForAppToFinish(); result = await app.runner.waitForAppToFinish();
if (result != 0) assert(result != null);
throwToolExit(null, exitCode: result);
} else { } else {
await runner.attach(); result = await runner.attach();
assert(result != null);
} }
if (result != 0)
throwToolExit(null, exitCode: result);
} finally { } finally {
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList(); final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
for (ForwardedPort port in ports) { for (ForwardedPort port in ports) {
......
...@@ -4,11 +4,6 @@ ...@@ -4,11 +4,6 @@
import 'dart:async'; import 'dart:async';
import 'package:meta/meta.dart';
import '../base/file_system.dart';
import '../base/utils.dart';
import '../globals.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import 'build_aot.dart'; import 'build_aot.dart';
import 'build_apk.dart'; import 'build_apk.dart';
...@@ -41,25 +36,4 @@ abstract class BuildSubCommand extends FlutterCommand { ...@@ -41,25 +36,4 @@ abstract class BuildSubCommand extends FlutterCommand {
BuildSubCommand() { BuildSubCommand() {
requiresPubspecYaml(); requiresPubspecYaml();
} }
@override
@mustCallSuper
Future<FlutterCommandResult> runCommand() async {
if (isRunningOnBot) {
final File dotPackages = fs.file('.packages');
printStatus('Contents of .packages:');
if (dotPackages.existsSync())
printStatus(dotPackages.readAsStringSync());
else
printError('File not found: ${dotPackages.absolute.path}');
final File pubspecLock = fs.file('pubspec.lock');
printStatus('Contents of pubspec.lock:');
if (pubspecLock.existsSync())
printStatus(pubspecLock.readAsStringSync());
else
printError('File not found: ${pubspecLock.absolute.path}');
}
return null;
}
} }
...@@ -57,8 +57,6 @@ class BuildAotCommand extends BuildSubCommand { ...@@ -57,8 +57,6 @@ class BuildAotCommand extends BuildSubCommand {
@override @override
Future<FlutterCommandResult> runCommand() async { Future<FlutterCommandResult> runCommand() async {
await super.runCommand();
final String targetPlatform = argResults['target-platform']; final String targetPlatform = argResults['target-platform'];
final TargetPlatform platform = getTargetPlatformForName(targetPlatform); final TargetPlatform platform = getTargetPlatformForName(targetPlatform);
if (platform == null) if (platform == null)
...@@ -71,7 +69,7 @@ class BuildAotCommand extends BuildSubCommand { ...@@ -71,7 +69,7 @@ class BuildAotCommand extends BuildSubCommand {
final String typeName = artifacts.getEngineType(platform, buildMode); final String typeName = artifacts.getEngineType(platform, buildMode);
status = logger.startProgress( status = logger.startProgress(
'Building AOT snapshot in ${getFriendlyModeName(getBuildMode())} mode ($typeName)...', 'Building AOT snapshot in ${getFriendlyModeName(getBuildMode())} mode ($typeName)...',
expectSlowOperation: true, timeout: kSlowOperation,
); );
} }
final String outputPath = argResults['output-dir'] ?? getAotBuildDirectory(); final String outputPath = argResults['output-dir'] ?? getAotBuildDirectory();
......
...@@ -42,7 +42,6 @@ class BuildApkCommand extends BuildSubCommand { ...@@ -42,7 +42,6 @@ class BuildApkCommand extends BuildSubCommand {
@override @override
Future<FlutterCommandResult> runCommand() async { Future<FlutterCommandResult> runCommand() async {
await super.runCommand();
await buildApk( await buildApk(
project: await FlutterProject.current(), project: await FlutterProject.current(),
target: targetFile, target: targetFile,
......
...@@ -40,7 +40,6 @@ class BuildAppBundleCommand extends BuildSubCommand { ...@@ -40,7 +40,6 @@ class BuildAppBundleCommand extends BuildSubCommand {
@override @override
Future<FlutterCommandResult> runCommand() async { Future<FlutterCommandResult> runCommand() async {
await super.runCommand();
await buildAppBundle( await buildAppBundle(
project: await FlutterProject.current(), project: await FlutterProject.current(),
target: targetFile, target: targetFile,
......
...@@ -65,8 +65,6 @@ class BuildBundleCommand extends BuildSubCommand { ...@@ -65,8 +65,6 @@ class BuildBundleCommand extends BuildSubCommand {
@override @override
Future<FlutterCommandResult> runCommand() async { Future<FlutterCommandResult> runCommand() async {
await super.runCommand();
final String targetPlatform = argResults['target-platform']; final String targetPlatform = argResults['target-platform'];
final TargetPlatform platform = getTargetPlatformForName(targetPlatform); final TargetPlatform platform = getTargetPlatformForName(targetPlatform);
if (platform == null) if (platform == null)
......
...@@ -20,12 +20,9 @@ class BuildFlxCommand extends BuildSubCommand { ...@@ -20,12 +20,9 @@ class BuildFlxCommand extends BuildSubCommand {
@override @override
Future<FlutterCommandResult> runCommand() async { Future<FlutterCommandResult> runCommand() async {
await super.runCommand();
printError("'build flx' is no longer supported. Instead, use 'build " printError("'build flx' is no longer supported. Instead, use 'build "
"bundle' to build and assemble the application code and resources " "bundle' to build and assemble the application code and resources "
'for your app.'); 'for your app.');
return null; return null;
} }
} }
...@@ -53,7 +53,6 @@ class BuildIOSCommand extends BuildSubCommand { ...@@ -53,7 +53,6 @@ class BuildIOSCommand extends BuildSubCommand {
final bool forSimulator = argResults['simulator']; final bool forSimulator = argResults['simulator'];
defaultBuildMode = forSimulator ? BuildMode.debug : BuildMode.release; defaultBuildMode = forSimulator ? BuildMode.debug : BuildMode.release;
await super.runCommand();
if (getCurrentHostPlatform() != HostPlatform.darwin_x64) if (getCurrentHostPlatform() != HostPlatform.darwin_x64)
throwToolExit('Building for iOS is only supported on the Mac.'); throwToolExit('Building for iOS is only supported on the Mac.');
......
...@@ -383,16 +383,22 @@ class AppDomain extends Domain { ...@@ -383,16 +383,22 @@ class AppDomain extends Domain {
} }
return launch( return launch(
runner, runner,
({ Completer<DebugConnectionInfo> connectionInfoCompleter, ({
Completer<void> appStartedCompleter }) => runner.run( Completer<DebugConnectionInfo> connectionInfoCompleter,
connectionInfoCompleter: connectionInfoCompleter, Completer<void> appStartedCompleter,
appStartedCompleter: appStartedCompleter, }) {
route: route), return runner.run(
device, connectionInfoCompleter: connectionInfoCompleter,
projectDirectory, appStartedCompleter: appStartedCompleter,
enableHotReload, route: route,
cwd); );
},
device,
projectDirectory,
enableHotReload,
cwd,
);
} }
Future<AppInstance> launch( Future<AppInstance> launch(
...@@ -428,17 +434,19 @@ class AppDomain extends Domain { ...@@ -428,17 +434,19 @@ class AppDomain extends Domain {
}); });
} }
final Completer<void> appStartedCompleter = Completer<void>(); final Completer<void> appStartedCompleter = Completer<void>();
// We don't want to wait for this future to complete and callbacks won't fail. // We don't want to wait for this future to complete, and callbacks won't fail,
// As it just writes to stdout. // as it just writes to stdout.
appStartedCompleter.future.then<void>((_) { // ignore: unawaited_futures appStartedCompleter.future // ignore: unawaited_futures
_sendAppEvent(app, 'started'); .then<void>((void value) {
}); _sendAppEvent(app, 'started');
});
await app._runInZone<void>(this, () async { await app._runInZone<void>(this, () async {
try { try {
await runOrAttach( await runOrAttach(
connectionInfoCompleter: connectionInfoCompleter, connectionInfoCompleter: connectionInfoCompleter,
appStartedCompleter: appStartedCompleter); appStartedCompleter: appStartedCompleter,
);
_sendAppEvent(app, 'stop'); _sendAppEvent(app, 'stop');
} catch (error, trace) { } catch (error, trace) {
_sendAppEvent(app, 'stop', <String, dynamic>{ _sendAppEvent(app, 'stop', <String, dynamic>{
...@@ -515,14 +523,15 @@ class AppDomain extends Domain { ...@@ -515,14 +523,15 @@ class AppDomain extends Domain {
if (app == null) if (app == null)
throw "app '$appId' not found"; throw "app '$appId' not found";
return app.stop().timeout(const Duration(seconds: 5)).then<bool>((_) { return app.stop().then<bool>(
return true; (void value) => true,
}).catchError((dynamic error) { onError: (dynamic error, StackTrace stack) {
_sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true }); _sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
app.closeLogger(); app.closeLogger();
_apps.remove(app); _apps.remove(app);
return false; return false;
}); },
);
} }
Future<bool> detach(Map<String, dynamic> args) async { Future<bool> detach(Map<String, dynamic> args) async {
...@@ -532,14 +541,15 @@ class AppDomain extends Domain { ...@@ -532,14 +541,15 @@ class AppDomain extends Domain {
if (app == null) if (app == null)
throw "app '$appId' not found"; throw "app '$appId' not found";
return app.detach().timeout(const Duration(seconds: 5)).then<bool>((_) { return app.detach().then<bool>(
return true; (void value) => true,
}).catchError((dynamic error) { onError: (dynamic error, StackTrace stack) {
_sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true }); _sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
app.closeLogger(); app.closeLogger();
_apps.remove(app); _apps.remove(app);
return false; return false;
}); },
);
} }
AppInstance _getApp(String id) { AppInstance _getApp(String id) {
...@@ -772,13 +782,14 @@ class NotifyingLogger extends Logger { ...@@ -772,13 +782,14 @@ class NotifyingLogger extends Logger {
@override @override
Status startProgress( Status startProgress(
String message, { String message, {
@required Duration timeout,
String progressId, String progressId,
bool expectSlowOperation = false,
bool multilineOutput, bool multilineOutput,
int progressIndicatorPadding = kDefaultStatusPadding, int progressIndicatorPadding = kDefaultStatusPadding,
}) { }) {
assert(timeout != null);
printStatus(message); printStatus(message);
return Status(); return SilentStatus(timeout: timeout);
} }
void dispose() { void dispose() {
...@@ -948,11 +959,12 @@ class _AppRunLogger extends Logger { ...@@ -948,11 +959,12 @@ class _AppRunLogger extends Logger {
@override @override
Status startProgress( Status startProgress(
String message, { String message, {
@required Duration timeout,
String progressId, String progressId,
bool expectSlowOperation = false,
bool multilineOutput, bool multilineOutput,
int progressIndicatorPadding = 52, int progressIndicatorPadding = 52,
}) { }) {
assert(timeout != null);
final int id = _nextProgressId++; final int id = _nextProgressId++;
_sendProgressEvent(<String, dynamic>{ _sendProgressEvent(<String, dynamic>{
...@@ -961,13 +973,16 @@ class _AppRunLogger extends Logger { ...@@ -961,13 +973,16 @@ class _AppRunLogger extends Logger {
'message': message, 'message': message,
}); });
_status = Status(onFinish: () { _status = SilentStatus(
_status = null; timeout: timeout,
_sendProgressEvent(<String, dynamic>{ onFinish: () {
'id': id.toString(), _status = null;
'progressId': progressId, _sendProgressEvent(<String, dynamic>{
'finished': true 'id': id.toString(),
}); 'progressId': progressId,
'finished': true,
},
);
})..start(); })..start();
return _status; return _status;
} }
......
...@@ -29,11 +29,11 @@ class LogsCommand extends FlutterCommand { ...@@ -29,11 +29,11 @@ class LogsCommand extends FlutterCommand {
Device device; Device device;
@override @override
Future<FlutterCommandResult> verifyThenRunCommand() async { Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
device = await findTargetDevice(); device = await findTargetDevice();
if (device == null) if (device == null)
throwToolExit(null); throwToolExit(null);
return super.verifyThenRunCommand(); return super.verifyThenRunCommand(commandPath);
} }
@override @override
......
...@@ -17,9 +17,9 @@ import '../resident_runner.dart'; ...@@ -17,9 +17,9 @@ import '../resident_runner.dart';
import '../run_cold.dart'; import '../run_cold.dart';
import '../run_hot.dart'; import '../run_hot.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../tracing.dart';
import 'daemon.dart'; import 'daemon.dart';
// TODO(mklim): Test this, flutter/flutter#23031.
abstract class RunCommandBase extends FlutterCommand { abstract class RunCommandBase extends FlutterCommand {
// Used by run and drive commands. // Used by run and drive commands.
RunCommandBase({ bool verboseHelp = false }) { RunCommandBase({ bool verboseHelp = false }) {
...@@ -30,7 +30,7 @@ abstract class RunCommandBase extends FlutterCommand { ...@@ -30,7 +30,7 @@ abstract class RunCommandBase extends FlutterCommand {
argParser argParser
..addFlag('trace-startup', ..addFlag('trace-startup',
negatable: false, negatable: false,
help: 'Start tracing during startup.', help: 'Trace application startup, then exit, saving the trace to a file.',
) )
..addOption('route', ..addOption('route',
help: 'Which route to load when running the app.', help: 'Which route to load when running the app.',
...@@ -90,6 +90,14 @@ class RunCommand extends RunCommandBase { ...@@ -90,6 +90,14 @@ class RunCommand extends RunCommandBase {
help: 'Enable tracing of Skia code. This is useful when debugging ' help: 'Enable tracing of Skia code. This is useful when debugging '
'the GPU thread. By default, Flutter will not log skia code.', 'the GPU thread. By default, Flutter will not log skia code.',
) )
..addFlag('await-first-frame-when-tracing',
defaultsTo: true,
help: 'Whether to wait for the first frame when tracing startup ("--trace-startup"), '
'or just dump the trace as soon as the application is running. The first frame '
'is detected by looking for a Timeline event with the name '
'"${Tracing.firstUsefulFrameEventName}". '
'By default, the widgets library\'s binding takes care of sending this event. ',
)
..addFlag('use-test-fonts', ..addFlag('use-test-fonts',
negatable: true, negatable: true,
help: 'Enable (and default to) the "Ahem" font. This is a special font ' help: 'Enable (and default to) the "Ahem" font. This is a special font '
...@@ -108,7 +116,7 @@ class RunCommand extends RunCommandBase { ...@@ -108,7 +116,7 @@ class RunCommand extends RunCommandBase {
) )
..addFlag('track-widget-creation', ..addFlag('track-widget-creation',
hide: !verboseHelp, hide: !verboseHelp,
help: 'Track widget creation locations. Requires Dart 2.0 functionality.', help: 'Track widget creation locations.',
) )
..addOption('project-root', ..addOption('project-root',
hide: !verboseHelp, hide: !verboseHelp,
...@@ -123,18 +131,18 @@ class RunCommand extends RunCommandBase { ...@@ -123,18 +131,18 @@ class RunCommand extends RunCommandBase {
..addFlag('hot', ..addFlag('hot',
negatable: true, negatable: true,
defaultsTo: kHotReloadDefault, defaultsTo: kHotReloadDefault,
help: 'Run with support for hot reloading.', help: 'Run with support for hot reloading. Only available for debug mode. Not available with "--trace-startup".',
)
..addOption('pid-file',
help: 'Specify a file to write the process id to. '
'You can send SIGUSR1 to trigger a hot reload '
'and SIGUSR2 to trigger a hot restart.',
) )
..addFlag('resident', ..addFlag('resident',
negatable: true, negatable: true,
defaultsTo: true, defaultsTo: true,
hide: !verboseHelp, hide: !verboseHelp,
help: 'Stay resident after launching the application.', help: 'Stay resident after launching the application. Not available with "--trace-startup".',
)
..addOption('pid-file',
help: 'Specify a file to write the process id to. '
'You can send SIGUSR1 to trigger a hot reload '
'and SIGUSR2 to trigger a hot restart.',
) )
..addFlag('benchmark', ..addFlag('benchmark',
negatable: false, negatable: false,
...@@ -206,7 +214,7 @@ class RunCommand extends RunCommandBase { ...@@ -206,7 +214,7 @@ class RunCommand extends RunCommandBase {
bool shouldUseHotMode() { bool shouldUseHotMode() {
final bool hotArg = argResults['hot'] ?? false; final bool hotArg = argResults['hot'] ?? false;
final bool shouldUseHotMode = hotArg; final bool shouldUseHotMode = hotArg && !traceStartup;
return getBuildInfo().isDebug && shouldUseHotMode; return getBuildInfo().isDebug && shouldUseHotMode;
} }
...@@ -214,6 +222,7 @@ class RunCommand extends RunCommandBase { ...@@ -214,6 +222,7 @@ class RunCommand extends RunCommandBase {
argResults['use-application-binary'] != null; argResults['use-application-binary'] != null;
bool get stayResident => argResults['resident']; bool get stayResident => argResults['resident'];
bool get awaitFirstFrameWhenTracing => argResults['await-first-frame-when-tracing'];
@override @override
Future<void> validateCommand() async { Future<void> validateCommand() async {
...@@ -359,6 +368,7 @@ class RunCommand extends RunCommandBase { ...@@ -359,6 +368,7 @@ class RunCommand extends RunCommandBase {
target: targetFile, target: targetFile,
debuggingOptions: _createDebuggingOptions(), debuggingOptions: _createDebuggingOptions(),
traceStartup: traceStartup, traceStartup: traceStartup,
awaitFirstFrameWhenTracing: awaitFirstFrameWhenTracing,
applicationBinary: applicationBinaryPath == null applicationBinary: applicationBinaryPath == null
? null ? null
: fs.file(applicationBinaryPath), : fs.file(applicationBinaryPath),
......
...@@ -64,7 +64,7 @@ class ScreenshotCommand extends FlutterCommand { ...@@ -64,7 +64,7 @@ class ScreenshotCommand extends FlutterCommand {
Device device; Device device;
@override @override
Future<FlutterCommandResult> verifyThenRunCommand() async { Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
device = await findTargetDevice(); device = await findTargetDevice();
if (device == null) if (device == null)
throwToolExit('Must have a connected device'); throwToolExit('Must have a connected device');
...@@ -72,7 +72,7 @@ class ScreenshotCommand extends FlutterCommand { ...@@ -72,7 +72,7 @@ class ScreenshotCommand extends FlutterCommand {
throwToolExit('Screenshot not supported for ${device.name}.'); throwToolExit('Screenshot not supported for ${device.name}.');
if (argResults[_kType] != _kDeviceType && argResults[_kObservatoryPort] == null) if (argResults[_kType] != _kDeviceType && argResults[_kObservatoryPort] == null)
throwToolExit('Observatory port must be specified for screenshot type ${argResults[_kType]}'); throwToolExit('Observatory port must be specified for screenshot type ${argResults[_kType]}');
return super.verifyThenRunCommand(); return super.verifyThenRunCommand(commandPath);
} }
@override @override
......
...@@ -87,7 +87,7 @@ class UpdatePackagesCommand extends FlutterCommand { ...@@ -87,7 +87,7 @@ class UpdatePackagesCommand extends FlutterCommand {
Future<void> _downloadCoverageData() async { Future<void> _downloadCoverageData() async {
final Status status = logger.startProgress( final Status status = logger.startProgress(
'Downloading lcov data for package:flutter...', 'Downloading lcov data for package:flutter...',
expectSlowOperation: true, timeout: kSlowOperation,
); );
final String urlBase = platform.environment['FLUTTER_STORAGE_BASE_URL'] ?? 'https://storage.googleapis.com'; final String urlBase = platform.environment['FLUTTER_STORAGE_BASE_URL'] ?? 'https://storage.googleapis.com';
final List<int> data = await fetchUrl(Uri.parse('$urlBase/flutter_infra/flutter/coverage/lcov.info')); final List<int> data = await fetchUrl(Uri.parse('$urlBase/flutter_infra/flutter/coverage/lcov.info'));
......
...@@ -92,7 +92,7 @@ Future<void> pubGet({ ...@@ -92,7 +92,7 @@ Future<void> pubGet({
final String command = upgrade ? 'upgrade' : 'get'; final String command = upgrade ? 'upgrade' : 'get';
final Status status = logger.startProgress( final Status status = logger.startProgress(
'Running "flutter packages $command" in ${fs.path.basename(directory)}...', 'Running "flutter packages $command" in ${fs.path.basename(directory)}...',
expectSlowOperation: true, timeout: kSlowOperation,
); );
final List<String> args = <String>['--verbosity=warning']; final List<String> args = <String>['--verbosity=warning'];
if (FlutterCommand.current != null && FlutterCommand.current.globalResults['verbose']) if (FlutterCommand.current != null && FlutterCommand.current.globalResults['verbose'])
......
...@@ -156,7 +156,7 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { ...@@ -156,7 +156,7 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery {
final List<Device> devices = await pollingGetDevices().timeout(_pollingTimeout); final List<Device> devices = await pollingGetDevices().timeout(_pollingTimeout);
_items.updateWithNewList(devices); _items.updateWithNewList(devices);
} on TimeoutException { } on TimeoutException {
printTrace('Device poll timed out.'); printTrace('Device poll timed out. Will retry.');
} }
}, _pollingInterval); }, _pollingInterval);
} }
......
...@@ -188,7 +188,10 @@ class Doctor { ...@@ -188,7 +188,10 @@ class Doctor {
for (ValidatorTask validatorTask in startValidatorTasks()) { for (ValidatorTask validatorTask in startValidatorTasks()) {
final DoctorValidator validator = validatorTask.validator; final DoctorValidator validator = validatorTask.validator;
final Status status = Status.withSpinner(); final Status status = Status.withSpinner(
timeout: kFastOperation,
slowWarningCallback: () => validator.slowWarning,
);
ValidationResult result; ValidationResult result;
try { try {
result = await validatorTask.result; result = await validatorTask.result;
...@@ -290,6 +293,8 @@ abstract class DoctorValidator { ...@@ -290,6 +293,8 @@ abstract class DoctorValidator {
final String title; final String title;
String get slowWarning => 'This is taking an unexpectedly long time...';
Future<ValidationResult> validate(); Future<ValidationResult> validate();
} }
...@@ -302,6 +307,10 @@ class GroupedValidator extends DoctorValidator { ...@@ -302,6 +307,10 @@ class GroupedValidator extends DoctorValidator {
final List<DoctorValidator> subValidators; final List<DoctorValidator> subValidators;
@override
String get slowWarning => _currentSlowWarning;
String _currentSlowWarning = 'Initializing...';
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validate() async {
final List<ValidatorTask> tasks = <ValidatorTask>[]; final List<ValidatorTask> tasks = <ValidatorTask>[];
...@@ -311,8 +320,10 @@ class GroupedValidator extends DoctorValidator { ...@@ -311,8 +320,10 @@ class GroupedValidator extends DoctorValidator {
final List<ValidationResult> results = <ValidationResult>[]; final List<ValidationResult> results = <ValidationResult>[];
for (ValidatorTask subValidator in tasks) { for (ValidatorTask subValidator in tasks) {
_currentSlowWarning = subValidator.validator.slowWarning;
results.add(await subValidator.result); results.add(await subValidator.result);
} }
_currentSlowWarning = 'Merging results...';
return _mergeValidationResults(results); return _mergeValidationResults(results);
} }
...@@ -675,6 +686,9 @@ class IntelliJValidatorOnMac extends IntelliJValidator { ...@@ -675,6 +686,9 @@ class IntelliJValidatorOnMac extends IntelliJValidator {
class DeviceValidator extends DoctorValidator { class DeviceValidator extends DoctorValidator {
DeviceValidator() : super('Connected device'); DeviceValidator() : super('Connected device');
@override
String get slowWarning => 'Scanning for devices is taking a long time...';
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validate() async {
final List<Device> devices = await deviceManager.getAllConnectedDevices().toList(); final List<Device> devices = await deviceManager.getAllConnectedDevices().toList();
......
...@@ -325,7 +325,7 @@ class FuchsiaIsolateDiscoveryProtocol { ...@@ -325,7 +325,7 @@ class FuchsiaIsolateDiscoveryProtocol {
} }
_status ??= logger.startProgress( _status ??= logger.startProgress(
'Waiting for a connection from $_isolateName on ${_device.name}...', 'Waiting for a connection from $_isolateName on ${_device.name}...',
expectSlowOperation: true, timeout: null, // could take an arbitrary amount of time
); );
_pollingTimer ??= Timer(_pollDuration, _findIsolate); _pollingTimer ??= Timer(_pollDuration, _findIsolate);
return _foundUri.future.then((Uri uri) { return _foundUri.future.then((Uri uri) {
......
...@@ -237,7 +237,7 @@ class CocoaPods { ...@@ -237,7 +237,7 @@ class CocoaPods {
} }
Future<void> _runPodInstall(IosProject iosProject, String engineDirectory) async { Future<void> _runPodInstall(IosProject iosProject, String engineDirectory) async {
final Status status = logger.startProgress('Running pod install...', expectSlowOperation: true); final Status status = logger.startProgress('Running pod install...', timeout: kSlowOperation);
final ProcessResult result = await processManager.run( final ProcessResult result = await processManager.run(
<String>['pod', 'install', '--verbose'], <String>['pod', 'install', '--verbose'],
workingDirectory: iosProject.hostAppRoot.path, workingDirectory: iosProject.hostAppRoot.path,
......
...@@ -26,8 +26,6 @@ const String _kIdeviceinstallerInstructions = ...@@ -26,8 +26,6 @@ const String _kIdeviceinstallerInstructions =
'To work with iOS devices, please install ideviceinstaller. To install, run:\n' 'To work with iOS devices, please install ideviceinstaller. To install, run:\n'
'brew install ideviceinstaller.'; 'brew install ideviceinstaller.';
const Duration kPortForwardTimeout = Duration(seconds: 10);
class IOSDeploy { class IOSDeploy {
const IOSDeploy(); const IOSDeploy();
...@@ -297,7 +295,7 @@ class IOSDevice extends Device { ...@@ -297,7 +295,7 @@ class IOSDevice extends Device {
int installationResult = -1; int installationResult = -1;
Uri localObservatoryUri; Uri localObservatoryUri;
final Status installStatus = logger.startProgress('Installing and launching...', expectSlowOperation: true); final Status installStatus = logger.startProgress('Installing and launching...', timeout: kSlowOperation);
if (!debuggingOptions.debuggingEnabled) { if (!debuggingOptions.debuggingEnabled) {
// If debugging is not enabled, just launch the application and continue. // If debugging is not enabled, just launch the application and continue.
......
...@@ -470,7 +470,7 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -470,7 +470,7 @@ Future<XcodeBuildResult> buildXcodeProject({
initialBuildStatus.cancel(); initialBuildStatus.cancel();
buildSubStatus = logger.startProgress( buildSubStatus = logger.startProgress(
line, line,
expectSlowOperation: true, timeout: kSlowOperation,
progressIndicatorPadding: kDefaultStatusPadding - 7, progressIndicatorPadding: kDefaultStatusPadding - 7,
); );
} }
...@@ -485,7 +485,7 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -485,7 +485,7 @@ Future<XcodeBuildResult> buildXcodeProject({
} }
final Stopwatch buildStopwatch = Stopwatch()..start(); final Stopwatch buildStopwatch = Stopwatch()..start();
initialBuildStatus = logger.startProgress('Starting Xcode build...'); initialBuildStatus = logger.startProgress('Starting Xcode build...', timeout: kFastOperation);
final RunResult buildResult = await runAsync( final RunResult buildResult = await runAsync(
buildCommands, buildCommands,
workingDirectory: app.project.hostAppRoot.path, workingDirectory: app.project.hostAppRoot.path,
......
...@@ -9,7 +9,6 @@ import 'package:meta/meta.dart'; ...@@ -9,7 +9,6 @@ import 'package:meta/meta.dart';
import 'application_package.dart'; import 'application_package.dart';
import 'artifacts.dart'; import 'artifacts.dart';
import 'asset.dart'; import 'asset.dart';
import 'base/common.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'base/io.dart'; import 'base/io.dart';
import 'base/logger.dart'; import 'base/logger.dart';
...@@ -76,12 +75,14 @@ class FlutterDevice { ...@@ -76,12 +75,14 @@ class FlutterDevice {
if (vmServices != null) if (vmServices != null)
return; return;
final List<VMService> localVmServices = List<VMService>(observatoryUris.length); final List<VMService> localVmServices = List<VMService>(observatoryUris.length);
for (int i = 0; i < observatoryUris.length; i++) { for (int i = 0; i < observatoryUris.length; i += 1) {
printTrace('Connecting to service protocol: ${observatoryUris[i]}'); printTrace('Connecting to service protocol: ${observatoryUris[i]}');
localVmServices[i] = await VMService.connect(observatoryUris[i], localVmServices[i] = await VMService.connect(
reloadSources: reloadSources, observatoryUris[i],
restart: restart, reloadSources: reloadSources,
compileExpression: compileExpression); restart: restart,
compileExpression: compileExpression,
);
printTrace('Successfully connected to service protocol: ${observatoryUris[i]}'); printTrace('Successfully connected to service protocol: ${observatoryUris[i]}');
} }
vmServices = localVmServices; vmServices = localVmServices;
...@@ -102,9 +103,13 @@ class FlutterDevice { ...@@ -102,9 +103,13 @@ class FlutterDevice {
return vmServices return vmServices
.where((VMService service) => !service.isClosed) .where((VMService service) => !service.isClosed)
.expand<FlutterView>((VMService service) => viewFilter != null .expand<FlutterView>(
? service.vm.allViewsWithName(viewFilter) (VMService service) {
: service.vm.views) return viewFilter != null
? service.vm.allViewsWithName(viewFilter)
: service.vm.views;
},
)
.toList(); .toList();
} }
...@@ -120,13 +125,16 @@ class FlutterDevice { ...@@ -120,13 +125,16 @@ class FlutterDevice {
final List<FlutterView> flutterViews = views; final List<FlutterView> flutterViews = views;
if (flutterViews == null || flutterViews.isEmpty) if (flutterViews == null || flutterViews.isEmpty)
return; return;
final List<Future<void>> futures = <Future<void>>[];
for (FlutterView view in flutterViews) { for (FlutterView view in flutterViews) {
if (view != null && view.uiIsolate != null) { if (view != null && view.uiIsolate != null) {
// Manage waits specifically below. futures.add(view.uiIsolate.flutterExit());
view.uiIsolate.flutterExit(); // ignore: unawaited_futures
} }
} }
await Future<void>.delayed(const Duration(milliseconds: 100)); // The flutterExit message only returns if it fails, so just wait a few
// seconds then assume it worked.
// TODO(ianh): We should make this return once the VM service disconnects.
await Future.wait(futures).timeout(const Duration(seconds: 2), onTimeout: () { });
} }
Future<Uri> setupDevFS(String fsName, Future<Uri> setupDevFS(String fsName,
...@@ -390,7 +398,7 @@ class FlutterDevice { ...@@ -390,7 +398,7 @@ class FlutterDevice {
}) async { }) async {
final Status devFSStatus = logger.startProgress( final Status devFSStatus = logger.startProgress(
'Syncing files to device ${device.name}...', 'Syncing files to device ${device.name}...',
expectSlowOperation: true, timeout: kFastOperation,
); );
UpdateFSReport report; UpdateFSReport report;
try { try {
...@@ -482,11 +490,14 @@ abstract class ResidentRunner { ...@@ -482,11 +490,14 @@ abstract class ResidentRunner {
} }
/// Start the app and keep the process running during its lifetime. /// Start the app and keep the process running during its lifetime.
///
/// Returns the exit code that we should use for the flutter tool process; 0
/// for success, 1 for user error (e.g. bad arguments), 2 for other failures.
Future<int> run({ Future<int> run({
Completer<DebugConnectionInfo> connectionInfoCompleter, Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter, Completer<void> appStartedCompleter,
String route, String route,
bool shouldBuild = true bool shouldBuild = true,
}); });
Future<int> attach({ Future<int> attach({
...@@ -506,7 +517,7 @@ abstract class ResidentRunner { ...@@ -506,7 +517,7 @@ abstract class ResidentRunner {
await _debugSaveCompilationTrace(); await _debugSaveCompilationTrace();
await stopEchoingDeviceLog(); await stopEchoingDeviceLog();
await preStop(); await preStop();
return stopApp(); await stopApp();
} }
Future<void> detach() async { Future<void> detach() async {
...@@ -571,7 +582,7 @@ abstract class ResidentRunner { ...@@ -571,7 +582,7 @@ abstract class ResidentRunner {
} }
Future<void> _screenshot(FlutterDevice device) async { Future<void> _screenshot(FlutterDevice device) async {
final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...'); final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...', timeout: kFastOperation);
final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png'); final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
try { try {
if (supportsServiceProtocol && isRunningDebug) { if (supportsServiceProtocol && isRunningDebug) {
...@@ -686,14 +697,18 @@ abstract class ResidentRunner { ...@@ -686,14 +697,18 @@ abstract class ResidentRunner {
} }
/// If the [reloadSources] parameter is not null the 'reloadSources' service /// If the [reloadSources] parameter is not null the 'reloadSources' service
/// will be registered /// will be registered.
//
// Failures should be indicated by completing the future with an error, using
// a string as the error object, which will be used by the caller (attach())
// to display an error message.
Future<void> connectToServiceProtocol({ Future<void> connectToServiceProtocol({
ReloadSources reloadSources, ReloadSources reloadSources,
Restart restart, Restart restart,
CompileExpression compileExpression, CompileExpression compileExpression,
}) async { }) async {
if (!debuggingOptions.debuggingEnabled) if (!debuggingOptions.debuggingEnabled)
return Future<void>.error('Error the service protocol is not enabled.'); throw 'The service protocol is not enabled.';
bool viewFound = false; bool viewFound = false;
for (FlutterDevice device in flutterDevices) { for (FlutterDevice device in flutterDevices) {
...@@ -704,13 +719,15 @@ abstract class ResidentRunner { ...@@ -704,13 +719,15 @@ abstract class ResidentRunner {
); );
await device.getVMs(); await device.getVMs();
await device.refreshViews(); await device.refreshViews();
if (device.views.isEmpty) if (device.views.isNotEmpty)
printStatus('No Flutter views available on ${device.device.name}');
else
viewFound = true; viewFound = true;
} }
if (!viewFound) if (!viewFound) {
throwToolExit('No Flutter view is available'); if (flutterDevices.length == 1)
throw 'No Flutter view is available on ${flutterDevices.first.device.name}.';
throw 'No Flutter view is available on any device '
'(${flutterDevices.map<String>((FlutterDevice device) => device.device.name).join(', ')}).';
}
// Listen for service protocol connection to close. // Listen for service protocol connection to close.
for (FlutterDevice device in flutterDevices) { for (FlutterDevice device in flutterDevices) {
...@@ -861,12 +878,13 @@ abstract class ResidentRunner { ...@@ -861,12 +878,13 @@ abstract class ResidentRunner {
printHelp(details: false); printHelp(details: false);
} }
terminal.singleCharMode = true; terminal.singleCharMode = true;
terminal.onCharInput.listen(processTerminalInput); terminal.keystrokes.listen(processTerminalInput);
} }
} }
Future<int> waitForAppToFinish() async { Future<int> waitForAppToFinish() async {
final int exitCode = await _finished.future; final int exitCode = await _finished.future;
assert(exitCode != null);
await cleanupAtFinish(); await cleanupAtFinish();
return exitCode; return exitCode;
} }
...@@ -887,8 +905,10 @@ abstract class ResidentRunner { ...@@ -887,8 +905,10 @@ abstract class ResidentRunner {
Future<void> preStop() async { } Future<void> preStop() async { }
Future<void> stopApp() async { Future<void> stopApp() async {
final List<Future<void>> futures = <Future<void>>[];
for (FlutterDevice device in flutterDevices) for (FlutterDevice device in flutterDevices)
await device.stopApps(); futures.add(device.stopApps());
await Future.wait(futures);
appFinished(); appFinished();
} }
......
...@@ -21,6 +21,7 @@ class ColdRunner extends ResidentRunner { ...@@ -21,6 +21,7 @@ class ColdRunner extends ResidentRunner {
DebuggingOptions debuggingOptions, DebuggingOptions debuggingOptions,
bool usesTerminalUI = true, bool usesTerminalUI = true,
this.traceStartup = false, this.traceStartup = false,
this.awaitFirstFrameWhenTracing = true,
this.applicationBinary, this.applicationBinary,
bool saveCompilationTrace = false, bool saveCompilationTrace = false,
bool stayResident = true, bool stayResident = true,
...@@ -34,6 +35,7 @@ class ColdRunner extends ResidentRunner { ...@@ -34,6 +35,7 @@ class ColdRunner extends ResidentRunner {
ipv6: ipv6); ipv6: ipv6);
final bool traceStartup; final bool traceStartup;
final bool awaitFirstFrameWhenTracing;
final File applicationBinary; final File applicationBinary;
bool _didAttach = false; bool _didAttach = false;
...@@ -66,8 +68,14 @@ class ColdRunner extends ResidentRunner { ...@@ -66,8 +68,14 @@ class ColdRunner extends ResidentRunner {
} }
// Connect to observatory. // Connect to observatory.
if (debuggingOptions.debuggingEnabled) if (debuggingOptions.debuggingEnabled) {
await connectToServiceProtocol(); try {
await connectToServiceProtocol();
} on String catch (message) {
printError(message);
return 2;
}
}
if (flutterDevices.first.observatoryUris != null) { if (flutterDevices.first.observatoryUris != null) {
// For now, only support one debugger connection. // For now, only support one debugger connection.
...@@ -91,13 +99,11 @@ class ColdRunner extends ResidentRunner { ...@@ -91,13 +99,11 @@ class ColdRunner extends ResidentRunner {
// Only trace startup for the first device. // Only trace startup for the first device.
final FlutterDevice device = flutterDevices.first; final FlutterDevice device = flutterDevices.first;
if (device.vmServices != null && device.vmServices.isNotEmpty) { if (device.vmServices != null && device.vmServices.isNotEmpty) {
printStatus('Downloading startup trace info for ${device.device.name}'); printStatus('Tracing startup on ${device.device.name}.');
try { await downloadStartupTrace(
await downloadStartupTrace(device.vmServices.first); device.vmServices.first,
} catch (error) { awaitFirstFrame: awaitFirstFrameWhenTracing,
printError('Error downloading startup trace: $error'); );
return 2;
}
} }
appFinished(); appFinished();
} else if (stayResident) { } else if (stayResident) {
...@@ -107,7 +113,7 @@ class ColdRunner extends ResidentRunner { ...@@ -107,7 +113,7 @@ class ColdRunner extends ResidentRunner {
appStartedCompleter?.complete(); appStartedCompleter?.complete();
if (stayResident) if (stayResident && !traceStartup)
return waitForAppToFinish(); return waitForAppToFinish();
await cleanupAtFinish(); await cleanupAtFinish();
return 0; return 0;
......
This diff is collapsed.
...@@ -482,19 +482,18 @@ abstract class FlutterCommand extends Command<void> { ...@@ -482,19 +482,18 @@ abstract class FlutterCommand extends Command<void> {
body: () async { body: () async {
if (flutterUsage.isFirstRun) if (flutterUsage.isFirstRun)
flutterUsage.printWelcome(); flutterUsage.printWelcome();
final String commandPath = await usagePath;
FlutterCommandResult commandResult; FlutterCommandResult commandResult;
try { try {
commandResult = await verifyThenRunCommand(); commandResult = await verifyThenRunCommand(commandPath);
} on ToolExit { } on ToolExit {
commandResult = const FlutterCommandResult(ExitStatus.fail); commandResult = const FlutterCommandResult(ExitStatus.fail);
rethrow; rethrow;
} finally { } finally {
final DateTime endTime = systemClock.now(); final DateTime endTime = systemClock.now();
printTrace(userMessages.flutterElapsedTime(name, getElapsedAsMilliseconds(endTime.difference(startTime)))); printTrace(userMessages.flutterElapsedTime(name, getElapsedAsMilliseconds(endTime.difference(startTime))));
// This is checking the result of the call to 'usagePath' printTrace('"flutter $name" took ${getElapsedAsMilliseconds(endTime.difference(startTime))}.');
// (a Future<String>), and not the result of evaluating the Future. if (commandPath != null) {
if (usagePath != null) {
final List<String> labels = <String>[]; final List<String> labels = <String>[];
if (commandResult?.exitStatus != null) if (commandResult?.exitStatus != null)
labels.add(getEnumName(commandResult.exitStatus)); labels.add(getEnumName(commandResult.exitStatus));
...@@ -528,7 +527,7 @@ abstract class FlutterCommand extends Command<void> { ...@@ -528,7 +527,7 @@ abstract class FlutterCommand extends Command<void> {
/// then call this method to execute the command /// then call this method to execute the command
/// rather than calling [runCommand] directly. /// rather than calling [runCommand] directly.
@mustCallSuper @mustCallSuper
Future<FlutterCommandResult> verifyThenRunCommand() async { Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
await validateCommand(); await validateCommand();
// Populate the cache. We call this before pub get below so that the sky_engine // Populate the cache. We call this before pub get below so that the sky_engine
...@@ -544,8 +543,6 @@ abstract class FlutterCommand extends Command<void> { ...@@ -544,8 +543,6 @@ abstract class FlutterCommand extends Command<void> {
setupApplicationPackages(); setupApplicationPackages();
final String commandPath = await usagePath;
if (commandPath != null) { if (commandPath != null) {
final Map<String, String> additionalUsageValues = await usageValues; final Map<String, String> additionalUsageValues = await usageValues;
flutterUsage.sendCommand(commandPath, parameters: additionalUsageValues); flutterUsage.sendCommand(commandPath, parameters: additionalUsageValues);
......
...@@ -57,13 +57,7 @@ class CoverageCollector extends TestWatcher { ...@@ -57,13 +57,7 @@ class CoverageCollector extends TestWatcher {
if (result == null) if (result == null)
throw Exception('Failed to collect coverage.'); throw Exception('Failed to collect coverage.');
data = result; data = result;
}) });
.timeout(
const Duration(minutes: 10),
onTimeout: () {
throw Exception('Timed out while collecting coverage.');
},
);
await Future.any<void>(<Future<void>>[ processComplete, collectionComplete ]); await Future.any<void>(<Future<void>>[ processComplete, collectionComplete ]);
assert(data != null); assert(data != null);
...@@ -77,12 +71,8 @@ class CoverageCollector extends TestWatcher { ...@@ -77,12 +71,8 @@ class CoverageCollector extends TestWatcher {
/// ///
/// This will not start any collection tasks. It us up to the caller of to /// This will not start any collection tasks. It us up to the caller of to
/// call [collectCoverage] for each process first. /// call [collectCoverage] for each process first.
///
/// If [timeout] is specified, the future will timeout (with a
/// [TimeoutException]) after the specified duration.
Future<String> finalizeCoverage({ Future<String> finalizeCoverage({
coverage.Formatter formatter, coverage.Formatter formatter,
Duration timeout,
Directory coverageDirectory, Directory coverageDirectory,
}) async { }) async {
printTrace('formating coverage data'); printTrace('formating coverage data');
...@@ -102,9 +92,8 @@ class CoverageCollector extends TestWatcher { ...@@ -102,9 +92,8 @@ class CoverageCollector extends TestWatcher {
} }
Future<bool> collectCoverageData(String coveragePath, { bool mergeCoverageData = false, Directory coverageDirectory }) async { Future<bool> collectCoverageData(String coveragePath, { bool mergeCoverageData = false, Directory coverageDirectory }) async {
final Status status = logger.startProgress('Collecting coverage information...'); final Status status = logger.startProgress('Collecting coverage information...', timeout: kFastOperation);
final String coverageData = await finalizeCoverage( final String coverageData = await finalizeCoverage(
timeout: const Duration(seconds: 30),
coverageDirectory: coverageDirectory, coverageDirectory: coverageDirectory,
); );
status.stop(); status.stop();
......
...@@ -33,11 +33,17 @@ import 'watcher.dart'; ...@@ -33,11 +33,17 @@ import 'watcher.dart';
/// The timeout we give the test process to connect to the test harness /// The timeout we give the test process to connect to the test harness
/// once the process has entered its main method. /// once the process has entered its main method.
const Duration _kTestStartupTimeout = Duration(minutes: 1); ///
/// We time out test execution because we expect some tests to hang and we want
/// to know which test hung, rather than have the entire test harness just do
/// nothing for a few hours until the user (or CI environment) gets bored.
const Duration _kTestStartupTimeout = Duration(minutes: 5);
/// The timeout we give the test process to start executing Dart code. When the /// The timeout we give the test process to start executing Dart code. When the
/// CPU is under severe load, this can take a while, but it's not indicative of /// CPU is under severe load, this can take a while, but it's not indicative of
/// any problem with Flutter, so we give it a large timeout. /// any problem with Flutter, so we give it a large timeout.
///
/// See comment under [_kTestStartupTimeout] regarding timeouts.
const Duration _kTestProcessTimeout = Duration(minutes: 5); const Duration _kTestProcessTimeout = Duration(minutes: 5);
/// Message logged by the test process to signal that its main method has begun /// Message logged by the test process to signal that its main method has begun
...@@ -288,13 +294,11 @@ class _Compiler { ...@@ -288,13 +294,11 @@ class _Compiler {
firstCompile = true; firstCompile = true;
} }
suppressOutput = false; suppressOutput = false;
final CompilerOutput compilerOutput = await handleTimeout<CompilerOutput>( final CompilerOutput compilerOutput = await compiler.recompile(
compiler.recompile( request.path,
request.path, <String>[request.path],
<String>[request.path], outputPath: outputDill.path,
outputPath: outputDill.path), );
request.path,
);
final String outputPath = compilerOutput?.outputFilename; final String outputPath = compilerOutput?.outputFilename;
// In case compiler didn't produce output or reported compilation // In case compiler didn't produce output or reported compilation
...@@ -337,7 +341,7 @@ class _Compiler { ...@@ -337,7 +341,7 @@ class _Compiler {
Future<String> compile(String mainDart) { Future<String> compile(String mainDart) {
final Completer<String> completer = Completer<String>(); final Completer<String> completer = Completer<String>();
compilerController.add(_CompilationRequest(mainDart, completer)); compilerController.add(_CompilationRequest(mainDart, completer));
return handleTimeout<String>(completer.future, mainDart); return completer.future;
} }
Future<void> _shutdown() async { Future<void> _shutdown() async {
...@@ -353,13 +357,6 @@ class _Compiler { ...@@ -353,13 +357,6 @@ class _Compiler {
await _shutdown(); await _shutdown();
await compilerController.close(); await compilerController.close();
} }
static Future<T> handleTimeout<T>(Future<T> value, String path) {
return value.timeout(const Duration(minutes: 5), onTimeout: () {
printError('Compilation of $path timed out after 5 minutes.');
return null;
});
}
} }
class _FlutterPlatform extends PlatformPlugin { class _FlutterPlatform extends PlatformPlugin {
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'base/logger.dart';
import 'base/utils.dart'; import 'base/utils.dart';
import 'build_info.dart'; import 'build_info.dart';
import 'globals.dart'; import 'globals.dart';
...@@ -18,6 +19,8 @@ const String _kFirstUsefulFrameEventName = 'Widgets completed first useful frame ...@@ -18,6 +19,8 @@ const String _kFirstUsefulFrameEventName = 'Widgets completed first useful frame
class Tracing { class Tracing {
Tracing(this.vmService); Tracing(this.vmService);
static const String firstUsefulFrameEventName = _kFirstUsefulFrameEventName;
static Future<Tracing> connect(Uri uri) async { static Future<Tracing> connect(Uri uri) async {
final VMService observatory = await VMService.connect(uri); final VMService observatory = await VMService.connect(uri);
return Tracing(observatory); return Tracing(observatory);
...@@ -32,49 +35,46 @@ class Tracing { ...@@ -32,49 +35,46 @@ class Tracing {
/// Stops tracing; optionally wait for first frame. /// Stops tracing; optionally wait for first frame.
Future<Map<String, dynamic>> stopTracingAndDownloadTimeline({ Future<Map<String, dynamic>> stopTracingAndDownloadTimeline({
bool waitForFirstFrame = false bool awaitFirstFrame = false
}) async { }) async {
Map<String, dynamic> timeline; if (awaitFirstFrame) {
final Status status = logger.startProgress(
if (!waitForFirstFrame) { 'Waiting for application to render first frame...',
// Stop tracing immediately and get the timeline timeout: kFastOperation,
await vmService.vm.setVMTimelineFlags(<String>[]);
timeline = await vmService.vm.getVMTimeline();
} else {
final Completer<void> whenFirstFrameRendered = Completer<void>();
(await vmService.onTimelineEvent).listen((ServiceEvent timelineEvent) {
final List<Map<String, dynamic>> events = timelineEvent.timelineEvents;
for (Map<String, dynamic> event in events) {
if (event['name'] == _kFirstUsefulFrameEventName)
whenFirstFrameRendered.complete();
}
});
await whenFirstFrameRendered.future.timeout(
const Duration(seconds: 10),
onTimeout: () {
printError(
'Timed out waiting for the first frame event. Either the '
'application failed to start, or the event was missed because '
'"flutter run" took too long to subscribe to timeline events.'
);
return null;
}
); );
try {
timeline = await vmService.vm.getVMTimeline(); final Completer<void> whenFirstFrameRendered = Completer<void>();
(await vmService.onTimelineEvent).listen((ServiceEvent timelineEvent) {
await vmService.vm.setVMTimelineFlags(<String>[]); final List<Map<String, dynamic>> events = timelineEvent.timelineEvents;
for (Map<String, dynamic> event in events) {
if (event['name'] == _kFirstUsefulFrameEventName)
whenFirstFrameRendered.complete();
}
});
bool done = false;
for (FlutterView view in vmService.vm.views) {
if (await view.uiIsolate.flutterAlreadyPaintedFirstUsefulFrame()) {
done = true;
break;
}
}
if (!done)
await whenFirstFrameRendered.future;
} catch (exception) {
status.cancel();
rethrow;
}
status.stop();
} }
final Map<String, dynamic> timeline = await vmService.vm.getVMTimeline();
await vmService.vm.setVMTimelineFlags(<String>[]);
return timeline; return timeline;
} }
} }
/// Download the startup trace information from the given observatory client and /// Download the startup trace information from the given observatory client and
/// store it to build/start_up_info.json. /// store it to build/start_up_info.json.
Future<void> downloadStartupTrace(VMService observatory) async { Future<void> downloadStartupTrace(VMService observatory, { bool awaitFirstFrame = true }) async {
final String traceInfoFilePath = fs.path.join(getBuildDirectory(), 'start_up_info.json'); final String traceInfoFilePath = fs.path.join(getBuildDirectory(), 'start_up_info.json');
final File traceInfoFile = fs.file(traceInfoFilePath); final File traceInfoFile = fs.file(traceInfoFilePath);
...@@ -89,45 +89,53 @@ Future<void> downloadStartupTrace(VMService observatory) async { ...@@ -89,45 +89,53 @@ Future<void> downloadStartupTrace(VMService observatory) async {
final Tracing tracing = Tracing(observatory); final Tracing tracing = Tracing(observatory);
final Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline( final Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline(
waitForFirstFrame: true awaitFirstFrame: awaitFirstFrame,
); );
int extractInstantEventTimestamp(String eventName) { int extractInstantEventTimestamp(String eventName) {
final List<Map<String, dynamic>> events = final List<Map<String, dynamic>> events =
List<Map<String, dynamic>>.from(timeline['traceEvents']); List<Map<String, dynamic>>.from(timeline['traceEvents']);
final Map<String, dynamic> event = events.firstWhere( final Map<String, dynamic> event = events.firstWhere(
(Map<String, dynamic> event) => event['name'] == eventName, orElse: () => null (Map<String, dynamic> event) => event['name'] == eventName, orElse: () => null
); );
return event == null ? null : event['ts']; return event == null ? null : event['ts'];
} }
String message = 'No useful metrics were gathered.';
final int engineEnterTimestampMicros = extractInstantEventTimestamp(_kFlutterEngineMainEnterEventName); final int engineEnterTimestampMicros = extractInstantEventTimestamp(_kFlutterEngineMainEnterEventName);
final int frameworkInitTimestampMicros = extractInstantEventTimestamp(_kFrameworkInitEventName); final int frameworkInitTimestampMicros = extractInstantEventTimestamp(_kFrameworkInitEventName);
final int firstFrameTimestampMicros = extractInstantEventTimestamp(_kFirstUsefulFrameEventName);
if (engineEnterTimestampMicros == null) { if (engineEnterTimestampMicros == null) {
printTrace('Engine start event is missing in the timeline: $timeline'); printTrace('Engine start event is missing in the timeline: $timeline');
throw 'Engine start event is missing in the timeline. Cannot compute startup time.'; throw 'Engine start event is missing in the timeline. Cannot compute startup time.';
} }
if (firstFrameTimestampMicros == null) {
printTrace('First frame event is missing in the timeline: $timeline');
throw 'First frame event is missing in the timeline. Cannot compute startup time.';
}
final int timeToFirstFrameMicros = firstFrameTimestampMicros - engineEnterTimestampMicros;
final Map<String, dynamic> traceInfo = <String, dynamic>{ final Map<String, dynamic> traceInfo = <String, dynamic>{
'engineEnterTimestampMicros': engineEnterTimestampMicros, 'engineEnterTimestampMicros': engineEnterTimestampMicros,
'timeToFirstFrameMicros': timeToFirstFrameMicros,
}; };
if (frameworkInitTimestampMicros != null) { if (frameworkInitTimestampMicros != null) {
traceInfo['timeToFrameworkInitMicros'] = frameworkInitTimestampMicros - engineEnterTimestampMicros; final int timeToFrameworkInitMicros = frameworkInitTimestampMicros - engineEnterTimestampMicros;
traceInfo['timeAfterFrameworkInitMicros'] = firstFrameTimestampMicros - frameworkInitTimestampMicros; traceInfo['timeToFrameworkInitMicros'] = timeToFrameworkInitMicros;
message = 'Time to framework init: ${timeToFrameworkInitMicros ~/ 1000}ms.';
}
if (awaitFirstFrame) {
final int firstFrameTimestampMicros = extractInstantEventTimestamp(_kFirstUsefulFrameEventName);
if (firstFrameTimestampMicros == null) {
printTrace('First frame event is missing in the timeline: $timeline');
throw 'First frame event is missing in the timeline. Cannot compute startup time.';
}
final int timeToFirstFrameMicros = firstFrameTimestampMicros - engineEnterTimestampMicros;
traceInfo['timeToFirstFrameMicros'] = timeToFirstFrameMicros;
message = 'Time to first frame: ${timeToFirstFrameMicros ~/ 1000}ms.';
if (frameworkInitTimestampMicros != null)
traceInfo['timeAfterFrameworkInitMicros'] = firstFrameTimestampMicros - frameworkInitTimestampMicros;
} }
traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo)); traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo));
printStatus('Time to first frame: ${timeToFirstFrameMicros ~/ 1000}ms.'); printStatus(message);
printStatus('Saved startup trace info in ${traceInfoFile.path}.'); printStatus('Saved startup trace info in ${traceInfoFile.path}.');
} }
...@@ -14,7 +14,6 @@ class VsCodeValidator extends DoctorValidator { ...@@ -14,7 +14,6 @@ class VsCodeValidator extends DoctorValidator {
final VsCode _vsCode; final VsCode _vsCode;
static Iterable<DoctorValidator> get installedValidators { static Iterable<DoctorValidator> get installedValidators {
return VsCode return VsCode
.allInstalled() .allInstalled()
......
...@@ -158,7 +158,7 @@ void main() { ...@@ -158,7 +158,7 @@ void main() {
fallbacks: <Type, Generator>{ fallbacks: <Type, Generator>{
int: () => int.parse(context[String]), int: () => int.parse(context[String]),
String: () => '${context[double]}', String: () => '${context[double]}',
double: () => context[int] * 1.0, double: () => (context[int] as int) * 1.0, // ignore: avoid_as
}, },
); );
try { try {
......
...@@ -165,7 +165,7 @@ Stream<String> mockStdInStream; ...@@ -165,7 +165,7 @@ Stream<String> mockStdInStream;
class TestTerminal extends AnsiTerminal { class TestTerminal extends AnsiTerminal {
@override @override
Stream<String> get onCharInput { Stream<String> get keystrokes {
return mockStdInStream; return mockStdInStream;
} }
} }
...@@ -90,5 +90,5 @@ void main() { ...@@ -90,5 +90,5 @@ void main() {
expect(result, isList); expect(result, isList);
expect(result, isNotEmpty); expect(result, isNotEmpty);
}); });
}, timeout: const Timeout.factor(2)); }, timeout: const Timeout.factor(10)); // This test uses the `flutter` tool, which could be blocked behind the startup lock for a long time.
} }
...@@ -28,13 +28,15 @@ void main() { ...@@ -28,13 +28,15 @@ void main() {
}); });
test('can step over statements', () async { test('can step over statements', () async {
await _flutter.run(withDebugger: true); await _flutter.run(withDebugger: true, startPaused: true);
await _flutter.addBreakpoint(_project.breakpointUri, _project.breakpointLine);
await _flutter.resume();
await _flutter.waitForPause(); // Now we should be on the breakpoint.
// Stop at the initial breakpoint that the expected steps are based on. expect((await _flutter.getSourceLocation()).line, equals(_project.breakpointLine));
await _flutter.breakAt(_project.breakpointUri, _project.breakpointLine, restart: true);
// Issue 5 steps, ensuring that we end up on the annotated lines each time. // Issue 5 steps, ensuring that we end up on the annotated lines each time.
for (int i = 1; i <= _project.numberOfSteps; i++) { for (int i = 1; i <= _project.numberOfSteps; i += 1) {
await _flutter.stepOverOrOverAsyncSuspension(); await _flutter.stepOverOrOverAsyncSuspension();
final SourcePosition location = await _flutter.getSourceLocation(); final SourcePosition location = await _flutter.getSourceLocation();
final int actualLine = location.line; final int actualLine = location.line;
...@@ -47,5 +49,5 @@ void main() { ...@@ -47,5 +49,5 @@ void main() {
reason: 'After $i steps, debugger should stop at $expectedLine but stopped at $actualLine'); reason: 'After $i steps, debugger should stop at $expectedLine but stopped at $actualLine');
} }
}); });
}, timeout: const Timeout.factor(3)); }, timeout: const Timeout.factor(10)); // The DevFS sync takes a really long time, so these tests can be slow.
} }
...@@ -32,16 +32,18 @@ void main() { ...@@ -32,16 +32,18 @@ void main() {
tryToDelete(tempDir); tryToDelete(tempDir);
}); });
Future<Isolate> breakInBuildMethod(FlutterTestDriver flutter) async { Future<void> breakInBuildMethod(FlutterTestDriver flutter) async {
return _flutter.breakAt( await _flutter.breakAt(
_project.buildMethodBreakpointUri, _project.buildMethodBreakpointUri,
_project.buildMethodBreakpointLine); _project.buildMethodBreakpointLine,
);
} }
Future<Isolate> breakInTopLevelFunction(FlutterTestDriver flutter) async { Future<void> breakInTopLevelFunction(FlutterTestDriver flutter) async {
return _flutter.breakAt( await _flutter.breakAt(
_project.topLevelFunctionBreakpointUri, _project.topLevelFunctionBreakpointUri,
_project.topLevelFunctionBreakpointLine); _project.topLevelFunctionBreakpointLine,
);
} }
test('can evaluate trivial expressions in top level function', () async { test('can evaluate trivial expressions in top level function', () async {
...@@ -79,7 +81,7 @@ void main() { ...@@ -79,7 +81,7 @@ void main() {
await breakInBuildMethod(_flutter); await breakInBuildMethod(_flutter);
await evaluateComplexReturningExpressions(_flutter); await evaluateComplexReturningExpressions(_flutter);
}); });
}, timeout: const Timeout.factor(6)); }, timeout: const Timeout.factor(10)); // The DevFS sync takes a really long time, so these tests can be slow.
group('flutter test expression evaluation', () { group('flutter test expression evaluation', () {
Directory tempDir; Directory tempDir;
...@@ -124,7 +126,7 @@ void main() { ...@@ -124,7 +126,7 @@ void main() {
await evaluateComplexReturningExpressions(_flutter); await evaluateComplexReturningExpressions(_flutter);
}); });
// Skipped due to https://github.com/flutter/flutter/issues/26518 // Skipped due to https://github.com/flutter/flutter/issues/26518
}, timeout: const Timeout.factor(6)); }, timeout: const Timeout.factor(10), skip: true); // The DevFS sync takes a really long time, so these tests can be slow.
} }
Future<void> evaluateTrivialExpressions(FlutterTestDriver flutter) async { Future<void> evaluateTrivialExpressions(FlutterTestDriver flutter) async {
......
...@@ -18,8 +18,8 @@ void main() { ...@@ -18,8 +18,8 @@ void main() {
setUp(() async { setUp(() async {
tempDir = createResolvedTempDirectorySync('attach_test.'); tempDir = createResolvedTempDirectorySync('attach_test.');
await _project.setUpIn(tempDir); await _project.setUpIn(tempDir);
_flutterRun = FlutterRunTestDriver(tempDir, logPrefix: 'RUN'); _flutterRun = FlutterRunTestDriver(tempDir, logPrefix: ' RUN ');
_flutterAttach = FlutterRunTestDriver(tempDir, logPrefix: 'ATTACH'); _flutterAttach = FlutterRunTestDriver(tempDir, logPrefix: 'ATTACH ');
}); });
tearDown(() async { tearDown(() async {
...@@ -58,5 +58,5 @@ void main() { ...@@ -58,5 +58,5 @@ void main() {
await _flutterAttach.attach(_flutterRun.vmServicePort); await _flutterAttach.attach(_flutterRun.vmServicePort);
await _flutterAttach.hotReload(); await _flutterAttach.hotReload();
}); });
}, timeout: const Timeout.factor(6)); }, timeout: const Timeout.factor(10)); // The DevFS sync takes a really long time, so these tests can be slow.
} }
...@@ -55,5 +55,5 @@ void main() { ...@@ -55,5 +55,5 @@ void main() {
await _flutter.run(pidFile: pidFile); await _flutter.run(pidFile: pidFile);
expect(pidFile.existsSync(), isTrue); expect(pidFile.existsSync(), isTrue);
}); });
}, timeout: const Timeout.factor(6)); }, timeout: const Timeout.factor(10)); // The DevFS sync takes a really long time, so these tests can be slow.
} }
...@@ -45,5 +45,5 @@ void main() { ...@@ -45,5 +45,5 @@ void main() {
await Future<void>.delayed(requiredLifespan); await Future<void>.delayed(requiredLifespan);
expect(_flutter.hasExited, equals(false)); expect(_flutter.hasExited, equals(false));
}); });
}, timeout: const Timeout.factor(6)); }, timeout: const Timeout.factor(10)); // The DevFS sync takes a really long time, so these tests can be slow.
} }
...@@ -19,15 +19,22 @@ class BasicProject extends Project { ...@@ -19,15 +19,22 @@ class BasicProject extends Project {
@override @override
final String main = r''' final String main = r'''
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
void main() => runApp(new MyApp()); Future<void> main() async {
while (true) {
runApp(new MyApp());
await Future.delayed(const Duration(milliseconds: 50));
}
}
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
topLevelFunction(); topLevelFunction();
return new MaterialApp( // BREAKPOINT return new MaterialApp( // BUILD BREAKPOINT
title: 'Flutter Demo', title: 'Flutter Demo',
home: new Container(), home: new Container(),
); );
...@@ -39,9 +46,9 @@ class BasicProject extends Project { ...@@ -39,9 +46,9 @@ class BasicProject extends Project {
} }
'''; ''';
Uri get buildMethodBreakpointUri => breakpointUri; Uri get buildMethodBreakpointUri => mainDart;
int get buildMethodBreakpointLine => breakpointLine; int get buildMethodBreakpointLine => lineContaining(main, '// BUILD BREAKPOINT');
Uri get topLevelFunctionBreakpointUri => breakpointUri; Uri get topLevelFunctionBreakpointUri => mainDart;
int get topLevelFunctionBreakpointLine => lineContaining(main, '// TOP LEVEL BREAKPOINT'); int get topLevelFunctionBreakpointLine => lineContaining(main, '// TOP LEVEL BREAKPOINT');
} }
...@@ -22,33 +22,63 @@ class HotReloadProject extends Project { ...@@ -22,33 +22,63 @@ class HotReloadProject extends Project {
@override @override
final String main = r''' final String main = r'''
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runApp(new MyApp()); void main() => runApp(new MyApp());
int count = 1;
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Do not remove this line, it's uncommented by a test to verify that hot // This method gets called each time we hot reload, during reassemble.
// reloading worked.
// Do not remove the next line, it's uncommented by a test to verify that
// hot reloading worked:
// printHotReloadWorked(); // printHotReloadWorked();
return new MaterialApp( // BREAKPOINT print('((((TICK $count))))');
// tick 1 = startup warmup frame
// tick 2 = hot reload warmup reassemble frame
// after that there's a post-hot-reload frame scheduled by the tool that
// doesn't trigger this to rebuild, but does trigger the first callback
// below, then that callback schedules another frame on which we do the
// breakpoint.
// tick 3 = second hot reload warmup reassemble frame (pre breakpoint)
if (count == 2) {
SchedulerBinding.instance.scheduleFrameCallback((Duration timestamp) {
SchedulerBinding.instance.scheduleFrameCallback((Duration timestamp) {
print('breakpoint line'); // SCHEDULED BREAKPOINT
});
});
}
count += 1;
return MaterialApp( // BUILD BREAKPOINT
title: 'Flutter Demo', title: 'Flutter Demo',
home: new Container(), home: Container(),
); );
} }
} }
printHotReloadWorked() { void printHotReloadWorked() {
// The call to this function is uncommented by a test to verify that hot // The call to this function is uncommented by a test to verify that hot
// reloading worked. // reloading worked.
print('(((((RELOAD WORKED)))))'); print('(((((RELOAD WORKED)))))');
} }
'''; ''';
Uri get scheduledBreakpointUri => mainDart;
int get scheduledBreakpointLine => lineContaining(main, '// SCHEDULED BREAKPOINT');
Uri get buildBreakpointUri => mainDart;
int get buildBreakpointLine => lineContaining(main, '// BUILD BREAKPOINT');
void uncommentHotReloadPrint() { void uncommentHotReloadPrint() {
final String newMainContents = main.replaceAll( final String newMainContents = main.replaceAll(
'// printHotReloadWorked();', 'printHotReloadWorked();'); '// printHotReloadWorked();',
'printHotReloadWorked();'
);
writeFile(fs.path.join(dir.path, 'lib', 'main.dart'), newMainContents); writeFile(fs.path.join(dir.path, 'lib', 'main.dart'), newMainContents);
} }
} }
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