[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.
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index fdb9ebf..bca4b00 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -88,15 +88,13 @@
propCommand,
stdoutEncoding: latin1,
stderrEncoding: latin1,
- ).timeout(const Duration(seconds: 5));
+ );
if (result.exitCode == 0) {
_properties = parseAdbDeviceProperties(result.stdout);
} else {
printError('Error retrieving device properties for $name:');
printError(result.stderr);
}
- } on TimeoutException catch (_) {
- throwToolExit('adb not responding');
} on ProcessException catch (error) {
printError('Error retrieving device properties for $name: $error');
}
@@ -279,7 +277,7 @@
if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
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]));
status.stop();
// Some versions of adb exit with exit code 0 even on failure :(
diff --git a/packages/flutter_tools/lib/src/android/android_emulator.dart b/packages/flutter_tools/lib/src/android/android_emulator.dart
index 6a0e24d..bce4062 100644
--- a/packages/flutter_tools/lib/src/android/android_emulator.dart
+++ b/packages/flutter_tools/lib/src/android/android_emulator.dart
@@ -51,9 +51,10 @@
throw '${runResult.stdout}\n${runResult.stderr}'.trimRight();
}
});
- // emulator continues running on a successful launch so if we
- // haven't quit within 3 seconds we assume that's a success and just
- // return.
+ // The emulator continues running on a successful launch, so if it hasn't
+ // quit within 3 seconds we assume that's a success and just return. This
+ // 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>>[
launchResult,
Future<void>.delayed(const Duration(seconds: 3))
diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart
index 6fccde7..d1978a2 100644
--- a/packages/flutter_tools/lib/src/android/android_workflow.dart
+++ b/packages/flutter_tools/lib/src/android/android_workflow.dart
@@ -50,32 +50,41 @@
class AndroidValidator extends DoctorValidator {
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
/// is not compatible.
Future<bool> _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) async {
- if (!processManager.canRun(javaBinary)) {
- messages.add(ValidationMessage.error(userMessages.androidCantRunJavaBinary(javaBinary)));
- return false;
- }
- String javaVersion;
+ _task = 'Checking Java status';
try {
- printTrace('java -version');
- final ProcessResult result = await processManager.run(<String>[javaBinary, '-version']);
- if (result.exitCode == 0) {
- final List<String> versionLines = result.stderr.split('\n');
- javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
+ if (!processManager.canRun(javaBinary)) {
+ messages.add(ValidationMessage.error(userMessages.androidCantRunJavaBinary(javaBinary)));
+ return false;
}
- } catch (error) {
- printTrace(error.toString());
+ String javaVersion;
+ try {
+ printTrace('java -version');
+ final ProcessResult result = await processManager.run(<String>[javaBinary, '-version']);
+ if (result.exitCode == 0) {
+ final List<String> versionLines = result.stderr.split('\n');
+ 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;
}
- 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;
}
@override
@@ -150,6 +159,9 @@
AndroidLicenseValidator(): super('Android license subvalidator',);
@override
+ String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...';
+
+ @override
Future<ValidationResult> validate() async {
final List<ValidationMessage> messages = <ValidationMessage>[];
@@ -208,10 +220,8 @@
Future<LicensesAccepted> get licensesAccepted async {
LicensesAccepted status;
- void _onLine(String line) {
- if (status == null && licenseAccepted.hasMatch(line)) {
- status = LicensesAccepted.all;
- } else if (licenseCounts.hasMatch(line)) {
+ void _handleLine(String line) {
+ if (licenseCounts.hasMatch(line)) {
final Match match = licenseCounts.firstMatch(line);
if (match.group(1) != match.group(2)) {
status = LicensesAccepted.some;
@@ -219,9 +229,12 @@
status = LicensesAccepted.none;
}
} else if (licenseNotAccepted.hasMatch(line)) {
- // In case the format changes, a more general match will keep doctor
- // mostly working.
+ // The licenseNotAccepted pattern is trying to match the same line as
+ // licenseCounts, but is more general. In case the format changes, a
+ // more general match may keep doctor mostly working.
status = LicensesAccepted.none;
+ } else if (licenseAccepted.hasMatch(line)) {
+ status ??= LicensesAccepted.all;
}
}
@@ -235,19 +248,14 @@
final Future<void> output = process.stdout
.transform<String>(const Utf8Decoder(allowMalformed: true))
.transform<String>(const LineSplitter())
- .listen(_onLine)
+ .listen(_handleLine)
.asFuture<void>(null);
final Future<void> errors = process.stderr
.transform<String>(const Utf8Decoder(allowMalformed: true))
.transform<String>(const LineSplitter())
- .listen(_onLine)
+ .listen(_handleLine)
.asFuture<void>(null);
- try {
- await Future.wait<void>(<Future<void>>[output, errors]).timeout(const Duration(seconds: 30));
- } catch (TimeoutException) {
- printTrace(userMessages.androidLicensesTimeout(androidSdk.sdkManagerPath));
- processManager.killPid(process.pid);
- }
+ await Future.wait<void>(<Future<void>>[output, errors]);
return status ?? LicensesAccepted.unknown;
}
@@ -261,9 +269,10 @@
_ensureCanRunSdkManager();
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.
throwToolExit(userMessages.androidSdkOutdated(androidSdk.sdkManagerPath));
+ }
final Process process = await runCommand(
<String>[androidSdk.sdkManagerPath, '--licenses'],
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 781f719..60ddf91 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -97,7 +97,7 @@
final FlutterProject flutterProject = await FlutterProject.current();
final String gradle = await _ensureGradle(flutterProject);
updateLocalProperties(project: flutterProject);
- final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true);
+ final Status status = logger.startProgress('Resolving dependencies...', timeout: kSlowOperation);
GradleProject project;
try {
final RunResult propertiesRunResult = await runCheckedAsync(
@@ -175,7 +175,7 @@
// of validating the Gradle executable. This may take several seconds.
Future<String> _initializeGradle(FlutterProject project) async {
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);
if (gradle == null) {
injectGradleWrapper(android);
@@ -314,8 +314,8 @@
Future<void> _buildGradleProjectV1(FlutterProject project, String gradle) async {
// Run 'gradlew build'.
final Status status = logger.startProgress(
- "Running 'gradlew build'...",
- expectSlowOperation: true,
+ 'Running \'gradlew build\'...',
+ timeout: kSlowOperation,
multilineOutput: true,
);
final int exitCode = await runCommandAndStreamOutput(
@@ -365,8 +365,8 @@
}
}
final Status status = logger.startProgress(
- "Gradle task '$assembleTask'...",
- expectSlowOperation: true,
+ 'Running Gradle task \'$assembleTask\'...',
+ timeout: kSlowOperation,
multilineOutput: true,
);
final String gradlePath = fs.file(gradle).absolute.path;
diff --git a/packages/flutter_tools/lib/src/base/context.dart b/packages/flutter_tools/lib/src/base/context.dart
index df3de18..5bc60d5 100644
--- a/packages/flutter_tools/lib/src/base/context.dart
+++ b/packages/flutter_tools/lib/src/base/context.dart
@@ -107,7 +107,7 @@
/// Gets the value associated with the specified [type], or `null` if no
/// such value has been associated.
- dynamic operator [](Type type) {
+ Object operator [](Type type) {
dynamic value = _generateIfNecessary(type, _overrides);
if (value == null && _parent != null)
value = _parent[type];
diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart
index 0aeebed..3123e35 100644
--- a/packages/flutter_tools/lib/src/base/file_system.dart
+++ b/packages/flutter_tools/lib/src/base/file_system.dart
@@ -36,9 +36,7 @@
final RecordingFileSystem fileSystem = RecordingFileSystem(
delegate: _kLocalFs, destination: dir);
addShutdownHook(() async {
- await fileSystem.recording.flush(
- pendingResultTimeout: const Duration(seconds: 5),
- );
+ await fileSystem.recording.flush();
}, ShutdownStage.SERIALIZE_RECORDING);
return fileSystem;
}
diff --git a/packages/flutter_tools/lib/src/base/io.dart b/packages/flutter_tools/lib/src/base/io.dart
index 70ca4fe..a3de883 100644
--- a/packages/flutter_tools/lib/src/base/io.dart
+++ b/packages/flutter_tools/lib/src/base/io.dart
@@ -162,10 +162,7 @@
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];
+io.IOSink get stdout => stdio.stdout;
+Stream<List<int>> get stdin => stdio.stdin;
+io.IOSink get stderr => stdio.stderr;
diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart
index eb8d92f..06b7807 100644
--- a/packages/flutter_tools/lib/src/base/logger.dart
+++ b/packages/flutter_tools/lib/src/base/logger.dart
@@ -12,6 +12,8 @@
import 'utils.dart';
const int kDefaultStatusPadding = 59;
+const Duration kFastOperation = Duration(seconds: 2);
+const Duration kSlowOperation = Duration(minutes: 2);
typedef VoidCallback = void Function();
@@ -24,24 +26,30 @@
bool get hasTerminal => stdio.hasTerminal;
- /// Display an error [message] to the user. Commands should use this if they
+ /// Display an error `message` to the user. Commands should use this if they
/// fail in some way.
///
- /// The [message] argument is printed to the stderr in red by default.
- /// The [stackTrace] argument is the stack trace that will be printed if
+ /// The `message` argument is printed to the stderr in red by default.
+ ///
+ /// The `stackTrace` argument is the stack trace that will be printed if
/// supplied.
- /// The [emphasis] argument will cause the output message be printed in bold text.
- /// The [color] argument will print the message in the supplied color instead
+ ///
+ /// The `emphasis` argument will cause the output message be printed in bold text.
+ ///
+ /// The `color` argument will print the message in the supplied color instead
/// of the default of red. Colors will not be printed if the output terminal
/// doesn't support them.
- /// The [indent] argument specifies the number of spaces to indent the overall
+ ///
+ /// The `indent` argument specifies the number of spaces to indent the overall
/// message. If wrapping is enabled in [outputPreferences], then the wrapped
/// lines will be indented as well.
- /// If [hangingIndent] is specified, then any wrapped lines will be indented
+ ///
+ /// If `hangingIndent` is specified, then any wrapped lines will be indented
/// by this much more than the first line, if wrapping is enabled in
/// [outputPreferences].
- /// If [wrap] is specified, then it overrides the
- /// [outputPreferences.wrapText] setting.
+ ///
+ /// If `wrap` is specified, then it overrides the
+ /// `outputPreferences.wrapText` setting.
void printError(
String message, {
StackTrace stackTrace,
@@ -55,24 +63,31 @@
/// Display normal output of the command. This should be used for things like
/// progress messages, success messages, or just normal command output.
///
- /// The [message] argument is printed to the stderr in red by default.
- /// The [stackTrace] argument is the stack trace that will be printed if
+ /// The `message` argument is printed to the stderr in red by default.
+ ///
+ /// The `stackTrace` argument is the stack trace that will be printed if
/// supplied.
- /// If the [emphasis] argument is true, it will cause the output message be
+ ///
+ /// If the `emphasis` argument is true, it will cause the output message be
/// printed in bold text. Defaults to false.
- /// The [color] argument will print the message in the supplied color instead
+ ///
+ /// The `color` argument will print the message in the supplied color instead
/// of the default of red. Colors will not be printed if the output terminal
/// doesn't support them.
- /// If [newline] is true, then a newline will be added after printing the
+ ///
+ /// If `newline` is true, then a newline will be added after printing the
/// status. Defaults to true.
- /// The [indent] argument specifies the number of spaces to indent the overall
+ ///
+ /// The `indent` argument specifies the number of spaces to indent the overall
/// message. If wrapping is enabled in [outputPreferences], then the wrapped
/// lines will be indented as well.
- /// If [hangingIndent] is specified, then any wrapped lines will be indented
+ ///
+ /// If `hangingIndent` is specified, then any wrapped lines will be indented
/// by this much more than the first line, if wrapping is enabled in
/// [outputPreferences].
- /// If [wrap] is specified, then it overrides the
- /// [outputPreferences.wrapText] setting.
+ ///
+ /// If `wrap` is specified, then it overrides the
+ /// `outputPreferences.wrapText` setting.
void printStatus(
String message, {
bool emphasis,
@@ -89,17 +104,25 @@
/// Start an indeterminate progress display.
///
- /// [message] is the message to display to the user; [progressId] provides an ID which can be
- /// used to identify this type of progress (`hot.reload`, `hot.restart`, ...).
+ /// The `message` argument is the message to display to the user.
///
- /// [progressIndicatorPadding] can optionally be used to specify spacing
- /// between the [message] and the progress indicator.
+ /// The `timeout` argument sets a duration after which an additional message
+ /// may be shown saying that the operation is taking a long time. (Not all
+ /// [Status] subclasses show such a message.) Set this to null if the
+ /// operation can legitimately take an abritrary amount of time (e.g. waiting
+ /// for the user).
+ ///
+ /// The `progressId` argument provides an ID that can be used to identify
+ /// this type of progress (e.g. `hot.reload`, `hot.restart`).
+ ///
+ /// The `progressIndicatorPadding` can optionally be used to specify spacing
+ /// between the `message` and the progress indicator, if any.
Status startProgress(
String message, {
+ @required Duration timeout,
String progressId,
- bool expectSlowOperation,
- bool multilineOutput,
- int progressIndicatorPadding,
+ bool multilineOutput = false,
+ int progressIndicatorPadding = kDefaultStatusPadding,
});
}
@@ -119,17 +142,16 @@
int hangingIndent,
bool wrap,
}) {
+ _status?.pause();
message ??= '';
message = wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap);
- _status?.cancel();
- _status = null;
if (emphasis == true)
message = terminal.bolden(message);
message = terminal.color(message, color ?? TerminalColor.red);
stderr.writeln(message);
- if (stackTrace != null) {
+ if (stackTrace != null)
stderr.writeln(stackTrace.toString());
- }
+ _status?.resume();
}
@override
@@ -142,10 +164,9 @@
int hangingIndent,
bool wrap,
}) {
+ _status?.pause();
message ??= '';
message = wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap);
- _status?.cancel();
- _status = null;
if (emphasis == true)
message = terminal.bolden(message);
if (color != null)
@@ -153,6 +174,7 @@
if (newline != false)
message = '$message\n';
writeToStdOut(message);
+ _status?.resume();
}
@protected
@@ -166,21 +188,23 @@
@override
Status startProgress(
String message, {
+ @required Duration timeout,
String progressId,
- bool expectSlowOperation,
- bool multilineOutput,
- int progressIndicatorPadding,
+ bool multilineOutput = false,
+ int progressIndicatorPadding = kDefaultStatusPadding,
}) {
- expectSlowOperation ??= false;
- progressIndicatorPadding ??= kDefaultStatusPadding;
+ assert(progressIndicatorPadding != null);
if (_status != null) {
// Ignore nested progresses; return a no-op status object.
- return Status(onFinish: _clearStatus)..start();
+ return SilentStatus(
+ timeout: timeout,
+ onFinish: _clearStatus,
+ )..start();
}
if (terminal.supportsColor) {
_status = AnsiStatus(
message: message,
- expectSlowOperation: expectSlowOperation,
+ timeout: timeout,
multilineOutput: multilineOutput,
padding: progressIndicatorPadding,
onFinish: _clearStatus,
@@ -188,7 +212,7 @@
} else {
_status = SummaryStatus(
message: message,
- expectSlowOperation: expectSlowOperation,
+ timeout: timeout,
padding: progressIndicatorPadding,
onFinish: _clearStatus,
)..start();
@@ -270,13 +294,14 @@
@override
Status startProgress(
String message, {
+ @required Duration timeout,
String progressId,
- bool expectSlowOperation,
- bool multilineOutput,
- int progressIndicatorPadding,
+ bool multilineOutput = false,
+ int progressIndicatorPadding = kDefaultStatusPadding,
}) {
+ assert(progressIndicatorPadding != null);
printStatus(message);
- return Status()..start();
+ return SilentStatus(timeout: timeout)..start();
}
/// Clears all buffers.
@@ -337,15 +362,30 @@
@override
Status startProgress(
String message, {
+ @required Duration timeout,
String progressId,
- bool expectSlowOperation,
- bool multilineOutput,
- int progressIndicatorPadding,
+ bool multilineOutput = false,
+ int progressIndicatorPadding = kDefaultStatusPadding,
}) {
+ assert(progressIndicatorPadding != null);
printStatus(message);
- return Status(onFinish: () {
- printTrace('$message (completed)');
- })..start();
+ final Stopwatch timer = Stopwatch()..start();
+ return SilentStatus(
+ timeout: timeout,
+ onFinish: () {
+ String time;
+ if (timeout == null || timeout > kFastOperation) {
+ time = getElapsedAsSeconds(timer.elapsed);
+ } else {
+ time = getElapsedAsMilliseconds(timer.elapsed);
+ }
+ if (timeout != null && timer.elapsed > timeout) {
+ printTrace('$message (completed in $time, longer than expected)');
+ } else {
+ printTrace('$message (completed in $time)');
+ }
+ },
+ )..start();
}
void _emit(_LogType type, String message, [StackTrace stackTrace]) {
@@ -383,10 +423,15 @@
enum _LogType { error, status, trace }
+typedef SlowWarningCallback = String Function();
+
/// A [Status] class begins when start is called, and may produce progress
/// information asynchronously.
///
-/// The [Status] class itself never has any output.
+/// Some subclasses change output once [timeout] has expired, to indicate that
+/// something is taking longer than expected.
+///
+/// The [SilentStatus] class never has any output.
///
/// The [AnsiSpinner] subclass shows a spinner, and replaces it with a single
/// space character when stopped or canceled.
@@ -395,51 +440,159 @@
/// information when stopped. When canceled, the information isn't shown. In
/// either case, a newline is printed.
///
+/// The [SummaryStatus] subclass shows only a static message (without an
+/// indicator), then updates it when the operation ends.
+///
/// Generally, consider `logger.startProgress` instead of directly creating
/// a [Status] or one of its subclasses.
-class Status {
- Status({this.onFinish});
+abstract class Status {
+ Status({ @required this.timeout, this.onFinish });
- /// A straight [Status] or an [AnsiSpinner] (depending on whether the
+ /// A [SilentStatus] or an [AnsiSpinner] (depending on whether the
/// terminal is fancy enough), already started.
- factory Status.withSpinner({ VoidCallback onFinish }) {
+ factory Status.withSpinner({
+ @required Duration timeout,
+ VoidCallback onFinish,
+ SlowWarningCallback slowWarningCallback,
+ }) {
if (terminal.supportsColor)
- return AnsiSpinner(onFinish: onFinish)..start();
- return Status(onFinish: onFinish)..start();
+ return AnsiSpinner(timeout: timeout, onFinish: onFinish, slowWarningCallback: slowWarningCallback)..start();
+ return SilentStatus(timeout: timeout, onFinish: onFinish)..start();
}
+ final Duration timeout;
final VoidCallback onFinish;
- bool _isStarted = false;
+ @protected
+ final Stopwatch _stopwatch = Stopwatch();
+
+ @protected
+ bool get seemsSlow => timeout != null && _stopwatch.elapsed > timeout;
+
+ @protected
+ String get elapsedTime {
+ if (timeout == null || timeout > kFastOperation)
+ return getElapsedAsSeconds(_stopwatch.elapsed);
+ return getElapsedAsMilliseconds(_stopwatch.elapsed);
+ }
/// Call to start spinning.
void start() {
- assert(!_isStarted);
- _isStarted = true;
+ assert(!_stopwatch.isRunning);
+ _stopwatch.start();
}
/// Call to stop spinning after success.
void stop() {
- assert(_isStarted);
- _isStarted = false;
- if (onFinish != null)
- onFinish();
+ finish();
}
/// Call to cancel the spinner after failure or cancellation.
void cancel() {
- assert(_isStarted);
- _isStarted = false;
+ finish();
+ }
+
+ /// Call to clear the current line but not end the progress.
+ void pause() { }
+
+ /// Call to resume after a pause.
+ void resume() { }
+
+ @protected
+ void finish() {
+ assert(_stopwatch.isRunning);
+ _stopwatch.stop();
if (onFinish != null)
onFinish();
}
}
+/// A [SilentStatus] shows nothing.
+class SilentStatus extends Status {
+ SilentStatus({
+ @required Duration timeout,
+ VoidCallback onFinish,
+ }) : super(timeout: timeout, onFinish: onFinish);
+}
+
+/// Constructor writes [message] to [stdout]. On [cancel] or [stop], will call
+/// [onFinish]. On [stop], will additionally print out summary information.
+class SummaryStatus extends Status {
+ SummaryStatus({
+ this.message = '',
+ @required Duration timeout,
+ this.padding = kDefaultStatusPadding,
+ VoidCallback onFinish,
+ }) : assert(message != null),
+ assert(padding != null),
+ super(timeout: timeout, onFinish: onFinish);
+
+ final String message;
+ final int padding;
+
+ bool _messageShowingOnCurrentLine = false;
+
+ @override
+ void start() {
+ _printMessage();
+ super.start();
+ }
+
+ void _printMessage() {
+ assert(!_messageShowingOnCurrentLine);
+ stdout.write('${message.padRight(padding)} ');
+ _messageShowingOnCurrentLine = true;
+ }
+
+ @override
+ void stop() {
+ if (!_messageShowingOnCurrentLine)
+ _printMessage();
+ super.stop();
+ writeSummaryInformation();
+ stdout.write('\n');
+ }
+
+ @override
+ void cancel() {
+ super.cancel();
+ if (_messageShowingOnCurrentLine)
+ stdout.write('\n');
+ }
+
+ /// Prints a (minimum) 8 character padded time.
+ ///
+ /// If [timeout] is less than or equal to [kFastOperation], the time is in
+ /// seconds; otherwise, milliseconds. If the time is longer than [timeout],
+ /// appends "(!)" to the time.
+ ///
+ /// Examples: ` 0.5s`, ` 150ms`, ` 1,600ms`, ` 3.1s (!)`
+ void writeSummaryInformation() {
+ assert(_messageShowingOnCurrentLine);
+ stdout.write(elapsedTime.padLeft(_kTimePadding));
+ if (seemsSlow)
+ stdout.write(' (!)');
+ }
+
+ @override
+ void pause() {
+ super.pause();
+ stdout.write('\n');
+ _messageShowingOnCurrentLine = false;
+ }
+}
+
/// An [AnsiSpinner] is a simple animation that does nothing but implement a
-/// ASCII/Unicode spinner. When stopped or canceled, the animation erases
-/// itself.
+/// terminal spinner. When stopped or canceled, the animation erases itself.
+///
+/// If the timeout expires, a customizable warning is shown (but the spinner
+/// continues otherwise unabated).
class AnsiSpinner extends Status {
- AnsiSpinner({VoidCallback onFinish}) : super(onFinish: onFinish);
+ AnsiSpinner({
+ @required Duration timeout,
+ VoidCallback onFinish,
+ this.slowWarningCallback,
+ }) : super(timeout: timeout, onFinish: onFinish);
int ticks = 0;
Timer timer;
@@ -449,73 +602,123 @@
? <String>[r'-', r'\', r'|', r'/']
: <String>['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
- String get _backspace => '\b' * _animation[0].length;
- String get _clear => ' ' * _animation[0].length;
+ static const String _defaultSlowWarning = '(This is taking an unexpectedly long time.)';
+ final SlowWarningCallback slowWarningCallback;
- void _callback(Timer timer) {
- stdout.write('$_backspace${_animation[ticks++ % _animation.length]}');
- }
+ String _slowWarning = '';
+
+ String get _currentAnimationFrame => _animation[ticks % _animation.length];
+ int get _currentLength => _currentAnimationFrame.length + _slowWarning.length;
+ String get _backspace => '\b' * _currentLength;
+ String get _clear => ' ' * _currentLength;
+
+ @protected
+ int get spinnerIndent => 0;
@override
void start() {
super.start();
assert(timer == null);
- stdout.write(' ');
+ _startSpinner();
+ }
+
+ void _startSpinner() {
+ stdout.write(_clear * (spinnerIndent + 1)); // for _callback to backspace over
timer = Timer.periodic(const Duration(milliseconds: 100), _callback);
_callback(timer);
}
- @override
- void stop() {
+ void _callback(Timer timer) {
+ assert(this.timer == timer);
+ assert(timer != null);
assert(timer.isActive);
- timer.cancel();
- stdout.write('$_backspace$_clear$_backspace');
- super.stop();
+ stdout.write('${_backspace * (spinnerIndent + 1)}');
+ ticks += 1;
+ stdout.write('${_clear * spinnerIndent}$_currentAnimationFrame');
+ if (seemsSlow) {
+ if (slowWarningCallback != null) {
+ _slowWarning = ' ' + slowWarningCallback();
+ } else {
+ _slowWarning = ' ' + _defaultSlowWarning;
+ }
+ stdout.write(_slowWarning);
+ }
}
@override
- void cancel() {
+ void finish() {
+ assert(timer != null);
assert(timer.isActive);
timer.cancel();
- stdout.write('$_backspace$_clear$_backspace');
- super.cancel();
+ timer = null;
+ _clearSpinner();
+ super.finish();
+ }
+
+ void _clearSpinner() {
+ final int width = spinnerIndent + 1;
+ stdout.write('${_backspace * width}${_clear * width}${_backspace * width}');
+ }
+
+ @override
+ void pause() {
+ assert(timer != null);
+ assert(timer.isActive);
+ _clearSpinner();
+ timer.cancel();
+ }
+
+ @override
+ void resume() {
+ assert(timer != null);
+ assert(!timer.isActive);
+ _startSpinner();
}
}
-/// Constructor writes [message] to [stdout] with padding, then starts as an
-/// [AnsiSpinner]. On [cancel] or [stop], will call [onFinish].
-/// On [stop], will additionally print out summary information in
-/// milliseconds if [expectSlowOperation] is false, as seconds otherwise.
+const int _kTimePadding = 8; // should fit "99,999ms"
+
+/// Constructor writes [message] to [stdout] with padding, then starts an
+/// indeterminate progress indicator animation (it's a subclass of
+/// [AnsiSpinner]).
+///
+/// On [cancel] or [stop], will call [onFinish]. On [stop], will
+/// additionally print out summary information.
class AnsiStatus extends AnsiSpinner {
AnsiStatus({
- String message,
- bool expectSlowOperation,
- bool multilineOutput,
- int padding,
+ this.message = '',
+ @required Duration timeout,
+ this.multilineOutput = false,
+ this.padding = kDefaultStatusPadding,
VoidCallback onFinish,
- }) : message = message ?? '',
- padding = padding ?? 0,
- expectSlowOperation = expectSlowOperation ?? false,
- multilineOutput = multilineOutput ?? false,
- super(onFinish: onFinish);
+ }) : assert(message != null),
+ assert(multilineOutput != null),
+ assert(padding != null),
+ super(timeout: timeout, onFinish: onFinish);
final String message;
- final bool expectSlowOperation;
final bool multilineOutput;
final int padding;
- Stopwatch stopwatch;
-
static const String _margin = ' ';
@override
+ int get spinnerIndent => _kTimePadding - 1;
+
+ int _totalMessageLength;
+
+ @override
void start() {
- assert(stopwatch == null || !stopwatch.isRunning);
- stopwatch = Stopwatch()..start();
- stdout.write('${message.padRight(padding)}$_margin');
+ _startStatus();
super.start();
}
+ void _startStatus() {
+ final String line = '${message.padRight(padding)}$_margin';
+ _totalMessageLength = line.length;
+ stdout.write(line);
+ }
+
@override
void stop() {
super.stop();
@@ -531,74 +734,31 @@
/// Print summary information when a task is done.
///
- /// If [multilineOutput] is false, backs up 4 characters and prints a
- /// (minimum) 5 character padded time. If [expectSlowOperation] is true, the
- /// time is in seconds; otherwise, milliseconds. Only backs up 4 characters
- /// because [super.cancel] backs up one.
+ /// If [multilineOutput] is false, replaces the spinner with the summary message.
///
/// If [multilineOutput] is true, then it prints the message again on a new
- /// line before writing the elapsed time, and doesn't back up at all.
+ /// line before writing the elapsed time.
void writeSummaryInformation() {
- final String prefix = multilineOutput
- ? '\n${'$message Done'.padRight(padding - 4)}$_margin'
- : '\b\b\b\b';
- if (expectSlowOperation) {
- stdout.write('$prefix${getElapsedAsSeconds(stopwatch.elapsed).padLeft(5)}');
- } else {
- stdout.write('$prefix${getElapsedAsMilliseconds(stopwatch.elapsed).padLeft(5)}');
- }
+ if (multilineOutput)
+ stdout.write('\n${'$message Done'.padRight(padding)}$_margin');
+ stdout.write(elapsedTime.padLeft(_kTimePadding));
+ if (seemsSlow)
+ stdout.write(' (!)');
}
-}
-/// Constructor writes [message] to [stdout]. On [cancel] or [stop], will call
-/// [onFinish]. On [stop], will additionally print out summary information in
-/// milliseconds if [expectSlowOperation] is false, as seconds otherwise.
-class SummaryStatus extends Status {
- SummaryStatus({
- String message,
- bool expectSlowOperation,
- int padding,
- VoidCallback onFinish,
- }) : message = message ?? '',
- padding = padding ?? 0,
- expectSlowOperation = expectSlowOperation ?? false,
- super(onFinish: onFinish);
-
- final String message;
- final bool expectSlowOperation;
- final int padding;
-
- Stopwatch stopwatch;
-
- @override
- void start() {
- stopwatch = Stopwatch()..start();
- stdout.write('${message.padRight(padding)} ');
- super.start();
+ void _clearStatus() {
+ stdout.write('${_backspace * _totalMessageLength}${_clear * _totalMessageLength}${_backspace * _totalMessageLength}');
}
@override
- void stop() {
- super.stop();
- writeSummaryInformation();
- stdout.write('\n');
+ void pause() {
+ super.pause();
+ _clearStatus();
}
@override
- void cancel() {
- super.cancel();
- stdout.write('\n');
- }
-
- /// Prints a (minimum) 5 character padded time. If [expectSlowOperation] is
- /// true, the time is in seconds; otherwise, milliseconds.
- ///
- /// Example: ' 0.5s', '150ms', '1600ms'
- void writeSummaryInformation() {
- if (expectSlowOperation) {
- stdout.write(getElapsedAsSeconds(stopwatch.elapsed).padLeft(5));
- } else {
- stdout.write(getElapsedAsMilliseconds(stopwatch.elapsed).padLeft(5));
- }
+ void resume() {
+ _startStatus();
+ super.resume();
}
}
diff --git a/packages/flutter_tools/lib/src/base/net.dart b/packages/flutter_tools/lib/src/base/net.dart
index 57ec065..5196fc79 100644
--- a/packages/flutter_tools/lib/src/base/net.dart
+++ b/packages/flutter_tools/lib/src/base/net.dart
@@ -37,7 +37,7 @@
printTrace('Downloading: $url');
HttpClient httpClient;
if (context[HttpClientFactory] != null) {
- httpClient = context[HttpClientFactory]();
+ httpClient = (context[HttpClientFactory] as HttpClientFactory)(); // ignore: avoid_as
} else {
httpClient = HttpClient();
}
diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart
index 37ef5bf..e61b4c1 100644
--- a/packages/flutter_tools/lib/src/base/process.dart
+++ b/packages/flutter_tools/lib/src/base/process.dart
@@ -203,14 +203,6 @@
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) {
_traceCommand(cmd);
final Future<Process> proc = processManager.start(
diff --git a/packages/flutter_tools/lib/src/base/terminal.dart b/packages/flutter_tools/lib/src/base/terminal.dart
index 8ad874c..6f9d542 100644
--- a/packages/flutter_tools/lib/src/base/terminal.dart
+++ b/packages/flutter_tools/lib/src/base/terminal.dart
@@ -5,8 +5,6 @@
import 'dart:async';
import 'dart:convert' show AsciiDecoder;
-import 'package:quiver/strings.dart';
-
import '../globals.dart';
import 'context.dart';
import 'io.dart' as io;
@@ -172,31 +170,32 @@
/// Return keystrokes from the console.
///
/// 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();
return _broadcastStdInString;
}
- /// Prompts the user to input a character within the accepted list. Re-prompts
- /// if entered character is not in the list.
+ /// Prompts the user to input a character within a given list. Re-prompts if
+ /// entered character is not in the list.
///
- /// The [prompt] is the text displayed prior to waiting for user input. The
- /// [defaultChoiceIndex], if given, will be the character appearing in
- /// [acceptedCharacters] in the index given if the user presses enter without
- /// any key input. Setting [displayAcceptedCharacters] also prints the
- /// accepted keys next to the [prompt].
+ /// The `prompt`, if non-null, is the text displayed prior to waiting for user
+ /// input each time. If `prompt` is non-null and `displayAcceptedCharacters`
+ /// is true, the accepted keys are printed next to the `prompt`.
///
- /// Throws a [TimeoutException] if a `timeout` is provided and its duration
- /// expired without user input. Duration resets per key press.
+ /// The returned value is the user's input; if `defaultChoiceIndex` is not
+ /// 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(
List<String> acceptedCharacters, {
String prompt,
int defaultChoiceIndex,
bool displayAcceptedCharacters = true,
- Duration timeout,
}) async {
assert(acceptedCharacters != null);
assert(acceptedCharacters.isNotEmpty);
+ assert(prompt == null || prompt.isNotEmpty);
+ assert(displayAcceptedCharacters != null);
List<String> charactersToDisplay = acceptedCharacters;
if (defaultChoiceIndex != null) {
assert(defaultChoiceIndex >= 0 && defaultChoiceIndex < acceptedCharacters.length);
@@ -206,17 +205,14 @@
}
String choice;
singleCharMode = true;
- while (isEmpty(choice) || choice.length != 1 || !acceptedCharacters.contains(choice)) {
- if (isNotEmpty(prompt)) {
+ while (choice == null || choice.length > 1 || !acceptedCharacters.contains(choice)) {
+ if (prompt != null) {
printStatus(prompt, emphasis: true, newline: false);
if (displayAcceptedCharacters)
printStatus(' [${charactersToDisplay.join("|")}]', newline: false);
printStatus(': ', emphasis: true, newline: false);
}
- Future<String> inputFuture = onCharInput.first;
- if (timeout != null)
- inputFuture = inputFuture.timeout(timeout);
- choice = await inputFuture;
+ choice = await keystrokes.first;
printStatus(choice);
}
singleCharMode = false;
diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart
index a8b0cb4..dd1db59 100644
--- a/packages/flutter_tools/lib/src/cache.dart
+++ b/packages/flutter_tools/lib/src/cache.dart
@@ -299,7 +299,7 @@
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 {
if (!verifier(tempFile)) {
- final Status status = logger.startProgress(message, expectSlowOperation: true);
+ final Status status = logger.startProgress(message, timeout: kSlowOperation);
try {
await _downloadFile(url, tempFile);
status.stop();
@@ -648,7 +648,7 @@
}
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);
status.stop();
return exists;
diff --git a/packages/flutter_tools/lib/src/commands/analyze_continuously.dart b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart
index dda2ab8..1be67c6 100644
--- a/packages/flutter_tools/lib/src/commands/analyze_continuously.dart
+++ b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart
@@ -74,11 +74,12 @@
analysisStatus?.cancel();
if (!firstAnalysis)
printStatus('\n');
- analysisStatus = logger.startProgress('Analyzing $analysisTarget...');
+ analysisStatus = logger.startProgress('Analyzing $analysisTarget...', timeout: kSlowOperation);
analyzedPaths.clear();
analysisTimer = Stopwatch()..start();
} else {
analysisStatus?.stop();
+ analysisStatus = null;
analysisTimer.stop();
logger.printStatus(terminal.clearScreen(), newline: false);
diff --git a/packages/flutter_tools/lib/src/commands/analyze_once.dart b/packages/flutter_tools/lib/src/commands/analyze_once.dart
index 3a536e5..e75d379 100644
--- a/packages/flutter_tools/lib/src/commands/analyze_once.dart
+++ b/packages/flutter_tools/lib/src/commands/analyze_once.dart
@@ -107,7 +107,7 @@
? '${directories.length} ${directories.length == 1 ? 'directory' : 'directories'}'
: fs.path.basename(directories.first);
final Status progress = argResults['preamble']
- ? logger.startProgress('Analyzing $message...')
+ ? logger.startProgress('Analyzing $message...', timeout: kSlowOperation)
: null;
await analysisCompleter.future;
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index 0cba158..732c0cd 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -135,15 +135,14 @@
if (device is FuchsiaDevice) {
attachLogger = true;
final String module = argResults['module'];
- if (module == null) {
- throwToolExit('\'--module\' is requried for attaching to a Fuchsia device');
- }
+ if (module == null)
+ throwToolExit('\'--module\' is required for attaching to a Fuchsia device');
usesIpv6 = device.ipv6;
FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
try {
isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
observatoryUri = await isolateDiscoveryProtocol.uri;
- printStatus('Done.');
+ printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
} catch (_) {
isolateDiscoveryProtocol?.dispose();
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
@@ -163,7 +162,7 @@
observatoryUri = await observatoryDiscovery.uri;
// Determine ipv6 status from the scanned logs.
usesIpv6 = observatoryDiscovery.ipv6;
- printStatus('Done.');
+ printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
} finally {
await observatoryDiscovery?.cancel();
}
@@ -210,20 +209,30 @@
if (attachLogger) {
flutterDevice.startEchoingDeviceLog();
}
+
+ int result;
if (daemon != null) {
AppInstance app;
try {
- app = await daemon.appDomain.launch(runner, runner.attach,
- device, null, true, fs.currentDirectory);
+ app = await daemon.appDomain.launch(
+ runner,
+ runner.attach,
+ device,
+ null,
+ true,
+ fs.currentDirectory,
+ );
} catch (error) {
throwToolExit(error.toString());
}
- final int result = await app.runner.waitForAppToFinish();
- if (result != 0)
- throwToolExit(null, exitCode: result);
+ result = await app.runner.waitForAppToFinish();
+ assert(result != null);
} else {
- await runner.attach();
+ result = await runner.attach();
+ assert(result != null);
}
+ if (result != 0)
+ throwToolExit(null, exitCode: result);
} finally {
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
for (ForwardedPort port in ports) {
diff --git a/packages/flutter_tools/lib/src/commands/build.dart b/packages/flutter_tools/lib/src/commands/build.dart
index 11c21f3..c31f741 100644
--- a/packages/flutter_tools/lib/src/commands/build.dart
+++ b/packages/flutter_tools/lib/src/commands/build.dart
@@ -4,11 +4,6 @@
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 'build_aot.dart';
import 'build_apk.dart';
@@ -41,25 +36,4 @@
BuildSubCommand() {
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;
- }
}
diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart
index e7d40f7..f541aae 100644
--- a/packages/flutter_tools/lib/src/commands/build_aot.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aot.dart
@@ -57,8 +57,6 @@
@override
Future<FlutterCommandResult> runCommand() async {
- await super.runCommand();
-
final String targetPlatform = argResults['target-platform'];
final TargetPlatform platform = getTargetPlatformForName(targetPlatform);
if (platform == null)
@@ -71,7 +69,7 @@
final String typeName = artifacts.getEngineType(platform, buildMode);
status = logger.startProgress(
'Building AOT snapshot in ${getFriendlyModeName(getBuildMode())} mode ($typeName)...',
- expectSlowOperation: true,
+ timeout: kSlowOperation,
);
}
final String outputPath = argResults['output-dir'] ?? getAotBuildDirectory();
diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart
index d76a421..2660932 100644
--- a/packages/flutter_tools/lib/src/commands/build_apk.dart
+++ b/packages/flutter_tools/lib/src/commands/build_apk.dart
@@ -42,7 +42,6 @@
@override
Future<FlutterCommandResult> runCommand() async {
- await super.runCommand();
await buildApk(
project: await FlutterProject.current(),
target: targetFile,
diff --git a/packages/flutter_tools/lib/src/commands/build_appbundle.dart b/packages/flutter_tools/lib/src/commands/build_appbundle.dart
index d9f22ca..bf8979a 100644
--- a/packages/flutter_tools/lib/src/commands/build_appbundle.dart
+++ b/packages/flutter_tools/lib/src/commands/build_appbundle.dart
@@ -40,7 +40,6 @@
@override
Future<FlutterCommandResult> runCommand() async {
- await super.runCommand();
await buildAppBundle(
project: await FlutterProject.current(),
target: targetFile,
diff --git a/packages/flutter_tools/lib/src/commands/build_bundle.dart b/packages/flutter_tools/lib/src/commands/build_bundle.dart
index 4093787..2fff12f 100644
--- a/packages/flutter_tools/lib/src/commands/build_bundle.dart
+++ b/packages/flutter_tools/lib/src/commands/build_bundle.dart
@@ -65,8 +65,6 @@
@override
Future<FlutterCommandResult> runCommand() async {
- await super.runCommand();
-
final String targetPlatform = argResults['target-platform'];
final TargetPlatform platform = getTargetPlatformForName(targetPlatform);
if (platform == null)
diff --git a/packages/flutter_tools/lib/src/commands/build_flx.dart b/packages/flutter_tools/lib/src/commands/build_flx.dart
index 33e6946..d6a2274 100644
--- a/packages/flutter_tools/lib/src/commands/build_flx.dart
+++ b/packages/flutter_tools/lib/src/commands/build_flx.dart
@@ -20,12 +20,9 @@
@override
Future<FlutterCommandResult> runCommand() async {
- await super.runCommand();
-
printError("'build flx' is no longer supported. Instead, use 'build "
"bundle' to build and assemble the application code and resources "
'for your app.');
-
return null;
}
}
diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart
index 539eb76..b90e332 100644
--- a/packages/flutter_tools/lib/src/commands/build_ios.dart
+++ b/packages/flutter_tools/lib/src/commands/build_ios.dart
@@ -53,7 +53,6 @@
final bool forSimulator = argResults['simulator'];
defaultBuildMode = forSimulator ? BuildMode.debug : BuildMode.release;
- await super.runCommand();
if (getCurrentHostPlatform() != HostPlatform.darwin_x64)
throwToolExit('Building for iOS is only supported on the Mac.');
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index 55f331f..2a2d9d5 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -383,16 +383,22 @@
}
return launch(
- runner,
- ({ Completer<DebugConnectionInfo> connectionInfoCompleter,
- Completer<void> appStartedCompleter }) => runner.run(
- connectionInfoCompleter: connectionInfoCompleter,
- appStartedCompleter: appStartedCompleter,
- route: route),
- device,
- projectDirectory,
- enableHotReload,
- cwd);
+ runner,
+ ({
+ Completer<DebugConnectionInfo> connectionInfoCompleter,
+ Completer<void> appStartedCompleter,
+ }) {
+ return runner.run(
+ connectionInfoCompleter: connectionInfoCompleter,
+ appStartedCompleter: appStartedCompleter,
+ route: route,
+ );
+ },
+ device,
+ projectDirectory,
+ enableHotReload,
+ cwd,
+ );
}
Future<AppInstance> launch(
@@ -428,17 +434,19 @@
});
}
final Completer<void> appStartedCompleter = Completer<void>();
- // We don't want to wait for this future to complete and callbacks won't fail.
- // As it just writes to stdout.
- appStartedCompleter.future.then<void>((_) { // ignore: unawaited_futures
- _sendAppEvent(app, 'started');
- });
+ // We don't want to wait for this future to complete, and callbacks won't fail,
+ // as it just writes to stdout.
+ appStartedCompleter.future // ignore: unawaited_futures
+ .then<void>((void value) {
+ _sendAppEvent(app, 'started');
+ });
await app._runInZone<void>(this, () async {
try {
await runOrAttach(
- connectionInfoCompleter: connectionInfoCompleter,
- appStartedCompleter: appStartedCompleter);
+ connectionInfoCompleter: connectionInfoCompleter,
+ appStartedCompleter: appStartedCompleter,
+ );
_sendAppEvent(app, 'stop');
} catch (error, trace) {
_sendAppEvent(app, 'stop', <String, dynamic>{
@@ -515,14 +523,15 @@
if (app == null)
throw "app '$appId' not found";
- return app.stop().timeout(const Duration(seconds: 5)).then<bool>((_) {
- return true;
- }).catchError((dynamic error) {
- _sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
- app.closeLogger();
- _apps.remove(app);
- return false;
- });
+ return app.stop().then<bool>(
+ (void value) => true,
+ onError: (dynamic error, StackTrace stack) {
+ _sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
+ app.closeLogger();
+ _apps.remove(app);
+ return false;
+ },
+ );
}
Future<bool> detach(Map<String, dynamic> args) async {
@@ -532,14 +541,15 @@
if (app == null)
throw "app '$appId' not found";
- return app.detach().timeout(const Duration(seconds: 5)).then<bool>((_) {
- return true;
- }).catchError((dynamic error) {
- _sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
- app.closeLogger();
- _apps.remove(app);
- return false;
- });
+ return app.detach().then<bool>(
+ (void value) => true,
+ onError: (dynamic error, StackTrace stack) {
+ _sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
+ app.closeLogger();
+ _apps.remove(app);
+ return false;
+ },
+ );
}
AppInstance _getApp(String id) {
@@ -772,13 +782,14 @@
@override
Status startProgress(
String message, {
+ @required Duration timeout,
String progressId,
- bool expectSlowOperation = false,
bool multilineOutput,
int progressIndicatorPadding = kDefaultStatusPadding,
}) {
+ assert(timeout != null);
printStatus(message);
- return Status();
+ return SilentStatus(timeout: timeout);
}
void dispose() {
@@ -948,11 +959,12 @@
@override
Status startProgress(
String message, {
+ @required Duration timeout,
String progressId,
- bool expectSlowOperation = false,
bool multilineOutput,
int progressIndicatorPadding = 52,
}) {
+ assert(timeout != null);
final int id = _nextProgressId++;
_sendProgressEvent(<String, dynamic>{
@@ -961,13 +973,16 @@
'message': message,
});
- _status = Status(onFinish: () {
- _status = null;
- _sendProgressEvent(<String, dynamic>{
- 'id': id.toString(),
- 'progressId': progressId,
- 'finished': true
- });
+ _status = SilentStatus(
+ timeout: timeout,
+ onFinish: () {
+ _status = null;
+ _sendProgressEvent(<String, dynamic>{
+ 'id': id.toString(),
+ 'progressId': progressId,
+ 'finished': true,
+ },
+ );
})..start();
return _status;
}
diff --git a/packages/flutter_tools/lib/src/commands/logs.dart b/packages/flutter_tools/lib/src/commands/logs.dart
index 77759f6..e3d3c2b 100644
--- a/packages/flutter_tools/lib/src/commands/logs.dart
+++ b/packages/flutter_tools/lib/src/commands/logs.dart
@@ -29,11 +29,11 @@
Device device;
@override
- Future<FlutterCommandResult> verifyThenRunCommand() async {
+ Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
device = await findTargetDevice();
if (device == null)
throwToolExit(null);
- return super.verifyThenRunCommand();
+ return super.verifyThenRunCommand(commandPath);
}
@override
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 3ae3b2d..16b764e 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -17,9 +17,9 @@
import '../run_cold.dart';
import '../run_hot.dart';
import '../runner/flutter_command.dart';
+import '../tracing.dart';
import 'daemon.dart';
-// TODO(mklim): Test this, flutter/flutter#23031.
abstract class RunCommandBase extends FlutterCommand {
// Used by run and drive commands.
RunCommandBase({ bool verboseHelp = false }) {
@@ -30,7 +30,7 @@
argParser
..addFlag('trace-startup',
negatable: false,
- help: 'Start tracing during startup.',
+ help: 'Trace application startup, then exit, saving the trace to a file.',
)
..addOption('route',
help: 'Which route to load when running the app.',
@@ -90,6 +90,14 @@
help: 'Enable tracing of Skia code. This is useful when debugging '
'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',
negatable: true,
help: 'Enable (and default to) the "Ahem" font. This is a special font '
@@ -108,7 +116,7 @@
)
..addFlag('track-widget-creation',
hide: !verboseHelp,
- help: 'Track widget creation locations. Requires Dart 2.0 functionality.',
+ help: 'Track widget creation locations.',
)
..addOption('project-root',
hide: !verboseHelp,
@@ -123,18 +131,18 @@
..addFlag('hot',
negatable: true,
defaultsTo: kHotReloadDefault,
- help: 'Run with support for hot reloading.',
- )
- ..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.',
+ help: 'Run with support for hot reloading. Only available for debug mode. Not available with "--trace-startup".',
)
..addFlag('resident',
negatable: true,
defaultsTo: true,
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',
negatable: false,
@@ -206,7 +214,7 @@
bool shouldUseHotMode() {
final bool hotArg = argResults['hot'] ?? false;
- final bool shouldUseHotMode = hotArg;
+ final bool shouldUseHotMode = hotArg && !traceStartup;
return getBuildInfo().isDebug && shouldUseHotMode;
}
@@ -214,6 +222,7 @@
argResults['use-application-binary'] != null;
bool get stayResident => argResults['resident'];
+ bool get awaitFirstFrameWhenTracing => argResults['await-first-frame-when-tracing'];
@override
Future<void> validateCommand() async {
@@ -359,6 +368,7 @@
target: targetFile,
debuggingOptions: _createDebuggingOptions(),
traceStartup: traceStartup,
+ awaitFirstFrameWhenTracing: awaitFirstFrameWhenTracing,
applicationBinary: applicationBinaryPath == null
? null
: fs.file(applicationBinaryPath),
diff --git a/packages/flutter_tools/lib/src/commands/screenshot.dart b/packages/flutter_tools/lib/src/commands/screenshot.dart
index c2dc3d1..20a2d1f 100644
--- a/packages/flutter_tools/lib/src/commands/screenshot.dart
+++ b/packages/flutter_tools/lib/src/commands/screenshot.dart
@@ -64,7 +64,7 @@
Device device;
@override
- Future<FlutterCommandResult> verifyThenRunCommand() async {
+ Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
device = await findTargetDevice();
if (device == null)
throwToolExit('Must have a connected device');
@@ -72,7 +72,7 @@
throwToolExit('Screenshot not supported for ${device.name}.');
if (argResults[_kType] != _kDeviceType && argResults[_kObservatoryPort] == null)
throwToolExit('Observatory port must be specified for screenshot type ${argResults[_kType]}');
- return super.verifyThenRunCommand();
+ return super.verifyThenRunCommand(commandPath);
}
@override
diff --git a/packages/flutter_tools/lib/src/commands/update_packages.dart b/packages/flutter_tools/lib/src/commands/update_packages.dart
index 73b62de..b084fb43 100644
--- a/packages/flutter_tools/lib/src/commands/update_packages.dart
+++ b/packages/flutter_tools/lib/src/commands/update_packages.dart
@@ -87,7 +87,7 @@
Future<void> _downloadCoverageData() async {
final Status status = logger.startProgress(
'Downloading lcov data for package:flutter...',
- expectSlowOperation: true,
+ timeout: kSlowOperation,
);
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'));
diff --git a/packages/flutter_tools/lib/src/dart/pub.dart b/packages/flutter_tools/lib/src/dart/pub.dart
index 062bc12..ce61edac 100644
--- a/packages/flutter_tools/lib/src/dart/pub.dart
+++ b/packages/flutter_tools/lib/src/dart/pub.dart
@@ -92,7 +92,7 @@
final String command = upgrade ? 'upgrade' : 'get';
final Status status = logger.startProgress(
'Running "flutter packages $command" in ${fs.path.basename(directory)}...',
- expectSlowOperation: true,
+ timeout: kSlowOperation,
);
final List<String> args = <String>['--verbosity=warning'];
if (FlutterCommand.current != null && FlutterCommand.current.globalResults['verbose'])
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index f6418b2..94030fe 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -156,7 +156,7 @@
final List<Device> devices = await pollingGetDevices().timeout(_pollingTimeout);
_items.updateWithNewList(devices);
} on TimeoutException {
- printTrace('Device poll timed out.');
+ printTrace('Device poll timed out. Will retry.');
}
}, _pollingInterval);
}
diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart
index 090e9e6..a790c83 100644
--- a/packages/flutter_tools/lib/src/doctor.dart
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -188,7 +188,10 @@
for (ValidatorTask validatorTask in startValidatorTasks()) {
final DoctorValidator validator = validatorTask.validator;
- final Status status = Status.withSpinner();
+ final Status status = Status.withSpinner(
+ timeout: kFastOperation,
+ slowWarningCallback: () => validator.slowWarning,
+ );
ValidationResult result;
try {
result = await validatorTask.result;
@@ -290,6 +293,8 @@
final String title;
+ String get slowWarning => 'This is taking an unexpectedly long time...';
+
Future<ValidationResult> validate();
}
@@ -303,6 +308,10 @@
final List<DoctorValidator> subValidators;
@override
+ String get slowWarning => _currentSlowWarning;
+ String _currentSlowWarning = 'Initializing...';
+
+ @override
Future<ValidationResult> validate() async {
final List<ValidatorTask> tasks = <ValidatorTask>[];
for (DoctorValidator validator in subValidators) {
@@ -311,8 +320,10 @@
final List<ValidationResult> results = <ValidationResult>[];
for (ValidatorTask subValidator in tasks) {
+ _currentSlowWarning = subValidator.validator.slowWarning;
results.add(await subValidator.result);
}
+ _currentSlowWarning = 'Merging results...';
return _mergeValidationResults(results);
}
@@ -676,6 +687,9 @@
DeviceValidator() : super('Connected device');
@override
+ String get slowWarning => 'Scanning for devices is taking a long time...';
+
+ @override
Future<ValidationResult> validate() async {
final List<Device> devices = await deviceManager.getAllConnectedDevices().toList();
List<ValidationMessage> messages;
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
index eb508e0..84bc1ba 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
@@ -325,7 +325,7 @@
}
_status ??= logger.startProgress(
'Waiting for a connection from $_isolateName on ${_device.name}...',
- expectSlowOperation: true,
+ timeout: null, // could take an arbitrary amount of time
);
_pollingTimer ??= Timer(_pollDuration, _findIsolate);
return _foundUri.future.then((Uri uri) {
diff --git a/packages/flutter_tools/lib/src/ios/cocoapods.dart b/packages/flutter_tools/lib/src/ios/cocoapods.dart
index ce1cbbe..92b7a3e 100644
--- a/packages/flutter_tools/lib/src/ios/cocoapods.dart
+++ b/packages/flutter_tools/lib/src/ios/cocoapods.dart
@@ -237,7 +237,7 @@
}
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(
<String>['pod', 'install', '--verbose'],
workingDirectory: iosProject.hostAppRoot.path,
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index c55cb85..aa8317e 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -26,8 +26,6 @@
'To work with iOS devices, please install ideviceinstaller. To install, run:\n'
'brew install ideviceinstaller.';
-const Duration kPortForwardTimeout = Duration(seconds: 10);
-
class IOSDeploy {
const IOSDeploy();
@@ -297,7 +295,7 @@
int installationResult = -1;
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 debugging is not enabled, just launch the application and continue.
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 845a696..f7d3267 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -470,7 +470,7 @@
initialBuildStatus.cancel();
buildSubStatus = logger.startProgress(
line,
- expectSlowOperation: true,
+ timeout: kSlowOperation,
progressIndicatorPadding: kDefaultStatusPadding - 7,
);
}
@@ -485,7 +485,7 @@
}
final Stopwatch buildStopwatch = Stopwatch()..start();
- initialBuildStatus = logger.startProgress('Starting Xcode build...');
+ initialBuildStatus = logger.startProgress('Starting Xcode build...', timeout: kFastOperation);
final RunResult buildResult = await runAsync(
buildCommands,
workingDirectory: app.project.hostAppRoot.path,
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 6cc1db3..eecb057 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -9,7 +9,6 @@
import 'application_package.dart';
import 'artifacts.dart';
import 'asset.dart';
-import 'base/common.dart';
import 'base/file_system.dart';
import 'base/io.dart';
import 'base/logger.dart';
@@ -76,12 +75,14 @@
if (vmServices != null)
return;
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]}');
- localVmServices[i] = await VMService.connect(observatoryUris[i],
- reloadSources: reloadSources,
- restart: restart,
- compileExpression: compileExpression);
+ localVmServices[i] = await VMService.connect(
+ observatoryUris[i],
+ reloadSources: reloadSources,
+ restart: restart,
+ compileExpression: compileExpression,
+ );
printTrace('Successfully connected to service protocol: ${observatoryUris[i]}');
}
vmServices = localVmServices;
@@ -102,9 +103,13 @@
return vmServices
.where((VMService service) => !service.isClosed)
- .expand<FlutterView>((VMService service) => viewFilter != null
- ? service.vm.allViewsWithName(viewFilter)
- : service.vm.views)
+ .expand<FlutterView>(
+ (VMService service) {
+ return viewFilter != null
+ ? service.vm.allViewsWithName(viewFilter)
+ : service.vm.views;
+ },
+ )
.toList();
}
@@ -120,13 +125,16 @@
final List<FlutterView> flutterViews = views;
if (flutterViews == null || flutterViews.isEmpty)
return;
+ final List<Future<void>> futures = <Future<void>>[];
for (FlutterView view in flutterViews) {
if (view != null && view.uiIsolate != null) {
- // Manage waits specifically below.
- view.uiIsolate.flutterExit(); // ignore: unawaited_futures
+ futures.add(view.uiIsolate.flutterExit());
}
}
- 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,
@@ -390,7 +398,7 @@
}) async {
final Status devFSStatus = logger.startProgress(
'Syncing files to device ${device.name}...',
- expectSlowOperation: true,
+ timeout: kFastOperation,
);
UpdateFSReport report;
try {
@@ -482,11 +490,14 @@
}
/// 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({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
String route,
- bool shouldBuild = true
+ bool shouldBuild = true,
});
Future<int> attach({
@@ -506,7 +517,7 @@
await _debugSaveCompilationTrace();
await stopEchoingDeviceLog();
await preStop();
- return stopApp();
+ await stopApp();
}
Future<void> detach() async {
@@ -571,7 +582,7 @@
}
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');
try {
if (supportsServiceProtocol && isRunningDebug) {
@@ -686,14 +697,18 @@
}
/// 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({
ReloadSources reloadSources,
Restart restart,
CompileExpression compileExpression,
}) async {
if (!debuggingOptions.debuggingEnabled)
- return Future<void>.error('Error the service protocol is not enabled.');
+ throw 'The service protocol is not enabled.';
bool viewFound = false;
for (FlutterDevice device in flutterDevices) {
@@ -704,13 +719,15 @@
);
await device.getVMs();
await device.refreshViews();
- if (device.views.isEmpty)
- printStatus('No Flutter views available on ${device.device.name}');
- else
+ if (device.views.isNotEmpty)
viewFound = true;
}
- if (!viewFound)
- throwToolExit('No Flutter view is available');
+ if (!viewFound) {
+ 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.
for (FlutterDevice device in flutterDevices) {
@@ -861,12 +878,13 @@
printHelp(details: false);
}
terminal.singleCharMode = true;
- terminal.onCharInput.listen(processTerminalInput);
+ terminal.keystrokes.listen(processTerminalInput);
}
}
Future<int> waitForAppToFinish() async {
final int exitCode = await _finished.future;
+ assert(exitCode != null);
await cleanupAtFinish();
return exitCode;
}
@@ -887,8 +905,10 @@
Future<void> preStop() async { }
Future<void> stopApp() async {
+ final List<Future<void>> futures = <Future<void>>[];
for (FlutterDevice device in flutterDevices)
- await device.stopApps();
+ futures.add(device.stopApps());
+ await Future.wait(futures);
appFinished();
}
diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart
index 7fc18db..87b6213 100644
--- a/packages/flutter_tools/lib/src/run_cold.dart
+++ b/packages/flutter_tools/lib/src/run_cold.dart
@@ -21,6 +21,7 @@
DebuggingOptions debuggingOptions,
bool usesTerminalUI = true,
this.traceStartup = false,
+ this.awaitFirstFrameWhenTracing = true,
this.applicationBinary,
bool saveCompilationTrace = false,
bool stayResident = true,
@@ -34,6 +35,7 @@
ipv6: ipv6);
final bool traceStartup;
+ final bool awaitFirstFrameWhenTracing;
final File applicationBinary;
bool _didAttach = false;
@@ -66,8 +68,14 @@
}
// Connect to observatory.
- if (debuggingOptions.debuggingEnabled)
- await connectToServiceProtocol();
+ if (debuggingOptions.debuggingEnabled) {
+ try {
+ await connectToServiceProtocol();
+ } on String catch (message) {
+ printError(message);
+ return 2;
+ }
+ }
if (flutterDevices.first.observatoryUris != null) {
// For now, only support one debugger connection.
@@ -91,13 +99,11 @@
// Only trace startup for the first device.
final FlutterDevice device = flutterDevices.first;
if (device.vmServices != null && device.vmServices.isNotEmpty) {
- printStatus('Downloading startup trace info for ${device.device.name}');
- try {
- await downloadStartupTrace(device.vmServices.first);
- } catch (error) {
- printError('Error downloading startup trace: $error');
- return 2;
- }
+ printStatus('Tracing startup on ${device.device.name}.');
+ await downloadStartupTrace(
+ device.vmServices.first,
+ awaitFirstFrame: awaitFirstFrameWhenTracing,
+ );
}
appFinished();
} else if (stayResident) {
@@ -107,7 +113,7 @@
appStartedCompleter?.complete();
- if (stayResident)
+ if (stayResident && !traceStartup)
return waitForAppToFinish();
await cleanupAtFinish();
return 0;
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 103790d..c5f1039 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -176,6 +176,7 @@
throw 'Failed to compile $expression';
}
+ // Returns the exit code of the flutter tool process, like [run].
@override
Future<int> attach({
Completer<DebugConnectionInfo> connectionInfoCompleter,
@@ -260,10 +261,11 @@
return 0;
}
+ int result = 0;
if (stayResident)
- return waitForAppToFinish();
+ result = await waitForAppToFinish();
await cleanupAtFinish();
- return 0;
+ return result;
}
@override
@@ -409,22 +411,21 @@
device.devFS.assetPathsToEvict.clear();
}
- Future<void> _cleanupDevFS() {
+ Future<void> _cleanupDevFS() async {
final List<Future<void>> futures = <Future<void>>[];
for (FlutterDevice device in flutterDevices) {
if (device.devFS != null) {
- // Cleanup the devFS; don't wait indefinitely, and ignore any errors.
+ // Cleanup the devFS, but don't wait indefinitely.
+ // We ignore any errors, because it's not clear what we would do anyway.
futures.add(device.devFS.destroy()
.timeout(const Duration(milliseconds: 250))
.catchError((dynamic error) {
- printTrace('$error');
+ printTrace('Ignored error while cleaning up DevFS: $error');
}));
}
device.devFS = null;
}
- final Completer<void> completer = Completer<void>();
- Future.wait(futures).whenComplete(() { completer.complete(null); } ); // ignore: unawaited_futures
- return completer.future;
+ await Future.wait(futures);
}
Future<void> _launchInView(FlutterDevice device,
@@ -575,6 +576,7 @@
}
final Status status = logger.startProgress(
'Performing hot restart...',
+ timeout: kFastOperation,
progressId: 'hot.restart',
);
try {
@@ -591,26 +593,45 @@
} else {
final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
- final Status status = logger.startProgress(
+ Status status = logger.startProgress(
'$progressPrefix hot reload...',
- progressId: 'hot.reload'
+ timeout: kFastOperation,
+ progressId: 'hot.reload',
);
OperationResult result;
+ bool showTime = true;
try {
- result = await _reloadSources(pause: pauseAfterRestart, reason: reason);
+ result = await _reloadSources(
+ pause: pauseAfterRestart,
+ reason: reason,
+ onSlow: (String message) {
+ status?.cancel();
+ status = logger.startProgress(
+ message,
+ timeout: kSlowOperation,
+ progressId: 'hot.reload',
+ );
+ showTime = false;
+ },
+ );
} finally {
status.cancel();
}
- if (result.isOk)
- printStatus('${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.');
+ if (result.isOk) {
+ if (showTime) {
+ printStatus('${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.');
+ } else {
+ printStatus('${result.message}.');
+ }
+ }
if (result.hintMessage != null)
printStatus('\n${result.hintMessage}');
return result;
}
}
- Future<OperationResult> _reloadSources({ bool pause = false, String reason }) async {
- final Map<String, String> analyticsParameters = <String, String> {};
+ Future<OperationResult> _reloadSources({ bool pause = false, String reason, void Function(String message) onSlow }) async {
+ final Map<String, String> analyticsParameters = <String, String>{};
if (reason != null) {
analyticsParameters[kEventReloadReasonParameterName] = reason;
}
@@ -621,25 +642,24 @@
}
}
- if (!_isPaused()) {
- printTrace('Refreshing active FlutterViews before reloading.');
- await refreshViews();
- }
-
// The initial launch is from a script snapshot. When we reload from source
// on top of a script snapshot, the first reload will be a worst case reload
// because all of the sources will end up being dirty (library paths will
// change from host path to a device path). Subsequent reloads will
// not be affected, so we resume reporting reload times on the second
// reload.
- final bool shouldReportReloadTime = !_runningFromSnapshot;
+ bool shouldReportReloadTime = !_runningFromSnapshot;
final Stopwatch reloadTimer = Stopwatch()..start();
+ if (!_isPaused()) {
+ printTrace('Refreshing active FlutterViews before reloading.');
+ await refreshViews();
+ }
+
final Stopwatch devFSTimer = Stopwatch()..start();
final UpdateFSReport updatedDevFS = await _updateDevFS();
// Record time it took to synchronize to DevFS.
- _addBenchmarkData('hotReloadDevFSSyncMilliseconds',
- devFSTimer.elapsed.inMilliseconds);
+ _addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds);
if (!updatedDevFS.success)
return OperationResult(1, 'DevFS synchronization failed');
String reloadMessage;
@@ -656,7 +676,6 @@
// running from snapshot to running from uploaded files.
await device.resetAssetDirectory();
}
-
final Completer<DeviceReloadReport> completer = Completer<DeviceReloadReport>();
allReportsFutures.add(completer.future);
final List<Future<Map<String, dynamic>>> reportFutures = device.reloadSources(
@@ -672,7 +691,6 @@
completer.complete(DeviceReloadReport(device, reports));
});
}
-
final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures);
for (DeviceReloadReport report in reports) {
final Map<String, dynamic> reloadReport = report.reports[0];
@@ -700,29 +718,26 @@
reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
}
}
- } on Map<String, dynamic> catch (error, st) {
- printError('Hot reload failed: $error\n$st');
+ } on Map<String, dynamic> catch (error, stackTrace) {
+ printTrace('Hot reload failed: $error\n$stackTrace');
final int errorCode = error['code'];
- final String errorMessage = error['message'];
+ String errorMessage = error['message'];
if (errorCode == Isolate.kIsolateReloadBarred) {
- printError(
- 'Unable to hot reload application due to an unrecoverable error in '
- 'the source code. Please address the error and then use "R" to '
- 'restart the app.'
- );
+ errorMessage = 'Unable to hot reload application due to an unrecoverable error in '
+ 'the source code. Please address the error and then use "R" to '
+ 'restart the app.\n'
+ '$errorMessage (error code: $errorCode)';
flutterUsage.sendEvent('hot', 'reload-barred');
return OperationResult(errorCode, errorMessage);
}
-
- printError('Hot reload failed:\ncode = $errorCode\nmessage = $errorMessage\n$st');
- return OperationResult(errorCode, errorMessage);
- } catch (error, st) {
- printError('Hot reload failed: $error\n$st');
+ return OperationResult(errorCode, '$errorMessage (error code: $errorCode)');
+ } catch (error, stackTrace) {
+ printTrace('Hot reload failed: $error\n$stackTrace');
return OperationResult(1, '$error');
}
// Record time it took for the VM to reload the sources.
- _addBenchmarkData('hotReloadVMReloadMilliseconds',
- vmReloadTimer.elapsed.inMilliseconds);
+ _addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds);
+
final Stopwatch reassembleTimer = Stopwatch()..start();
// Reload the isolate.
final List<Future<void>> allDevices = <Future<void>>[];
@@ -739,53 +754,97 @@
});
allDevices.add(deviceCompleter.future);
}
-
await Future.wait(allDevices);
// We are now running from source.
_runningFromSnapshot = false;
- // Check if the isolate is paused.
-
+ // Check if any isolates are paused.
final List<FlutterView> reassembleViews = <FlutterView>[];
+ String serviceEventKind;
+ int pausedIsolatesFound = 0;
for (FlutterDevice device in flutterDevices) {
for (FlutterView view in device.views) {
// Check if the isolate is paused, and if so, don't reassemble. Ignore the
// PostPauseEvent event - the client requesting the pause will resume the app.
final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
if (pauseEvent != null && pauseEvent.isPauseEvent && pauseEvent.kind != ServiceEvent.kPausePostRequest) {
- continue;
+ pausedIsolatesFound += 1;
+ if (serviceEventKind == null) {
+ serviceEventKind = pauseEvent.kind;
+ } else if (serviceEventKind != pauseEvent.kind) {
+ serviceEventKind = ''; // many kinds
+ }
+ } else {
+ reassembleViews.add(view);
}
- reassembleViews.add(view);
}
}
- if (reassembleViews.isEmpty) {
- printTrace('Skipping reassemble because all isolates are paused.');
- return OperationResult(OperationResult.ok.code, reloadMessage);
+ if (pausedIsolatesFound > 0) {
+ if (onSlow != null)
+ onSlow('${_describePausedIsolates(pausedIsolatesFound, serviceEventKind)}; interface might not update.');
+ if (reassembleViews.isEmpty) {
+ printTrace('Skipping reassemble because all isolates are paused.');
+ return OperationResult(OperationResult.ok.code, reloadMessage);
+ }
}
+ assert(reassembleViews.isNotEmpty);
printTrace('Evicting dirty assets');
await _evictDirtyAssets();
printTrace('Reassembling application');
- bool reassembleAndScheduleErrors = false;
- bool reassembleTimedOut = false;
+ bool failedReassemble = false;
final List<Future<void>> futures = <Future<void>>[];
for (FlutterView view in reassembleViews) {
- futures.add(view.uiIsolate.flutterReassemble().then((_) {
- return view.uiIsolate.uiWindowScheduleFrame();
- }).catchError((dynamic error) {
- if (error is TimeoutException) {
- reassembleTimedOut = true;
- printTrace('Reassembling ${view.uiIsolate.name} took too long.');
- printStatus('Hot reloading ${view.uiIsolate.name} took too long; the reload may have failed.');
- } else {
- reassembleAndScheduleErrors = true;
+ futures.add(() async {
+ try {
+ await view.uiIsolate.flutterReassemble();
+ } catch (error) {
+ failedReassemble = true;
printError('Reassembling ${view.uiIsolate.name} failed: $error');
+ return;
}
- }));
+ try {
+ await view.uiIsolate.uiWindowScheduleFrame();
+ } catch (error) {
+ failedReassemble = true;
+ printError('Scheduling a frame for ${view.uiIsolate.name} failed: $error');
+ }
+ }());
}
- await Future.wait(futures);
-
+ final Future<void> reassembleFuture = Future.wait<void>(futures).then<void>((List<void> values) { });
+ await reassembleFuture.timeout(
+ const Duration(seconds: 2),
+ onTimeout: () async {
+ if (pausedIsolatesFound > 0) {
+ shouldReportReloadTime = false;
+ return; // probably no point waiting, they're probably deadlocked and we've already warned.
+ }
+ // Check if any isolate is newly paused.
+ printTrace('This is taking a long time; will now check for paused isolates.');
+ int postReloadPausedIsolatesFound = 0;
+ String serviceEventKind;
+ for (FlutterView view in reassembleViews) {
+ await view.uiIsolate.reload();
+ final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
+ if (pauseEvent != null && pauseEvent.isPauseEvent) {
+ postReloadPausedIsolatesFound += 1;
+ if (serviceEventKind == null) {
+ serviceEventKind = pauseEvent.kind;
+ } else if (serviceEventKind != pauseEvent.kind) {
+ serviceEventKind = ''; // many kinds
+ }
+ }
+ }
+ printTrace('Found $postReloadPausedIsolatesFound newly paused isolate(s).');
+ if (postReloadPausedIsolatesFound == 0) {
+ await reassembleFuture; // must just be taking a long time... keep waiting!
+ return;
+ }
+ shouldReportReloadTime = false;
+ if (onSlow != null)
+ onSlow('${_describePausedIsolates(postReloadPausedIsolatesFound, serviceEventKind)}.');
+ },
+ );
// Record time it took for Flutter to reassemble the application.
- _addBenchmarkData('hotReloadFlutterReassembleMilliseconds',
- reassembleTimer.elapsed.inMilliseconds);
+ _addBenchmarkData('hotReloadFlutterReassembleMilliseconds', reassembleTimer.elapsed.inMilliseconds);
reloadTimer.stop();
final Duration reloadDuration = reloadTimer.elapsed;
@@ -794,23 +853,51 @@
analyticsParameters[kEventReloadOverallTimeInMs] = '$reloadInMs';
flutterUsage.sendEvent('hot', 'reload', parameters: analyticsParameters);
- printTrace('Hot reload performed in $reloadInMs.');
- // Record complete time it took for the reload.
- _addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs);
- // Only report timings if we reloaded a single view without any
- // errors or timeouts.
- if ((reassembleViews.length == 1) &&
- !reassembleAndScheduleErrors &&
- !reassembleTimedOut &&
- shouldReportReloadTime)
+ if (shouldReportReloadTime) {
+ printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadDuration)}.');
+ // Record complete time it took for the reload.
+ _addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs);
+ }
+ // Only report timings if we reloaded a single view without any errors.
+ if ((reassembleViews.length == 1) && !failedReassemble && shouldReportReloadTime)
flutterUsage.sendTiming('hot', 'reload', reloadDuration);
return OperationResult(
- reassembleAndScheduleErrors ? 1 : OperationResult.ok.code,
+ failedReassemble ? 1 : OperationResult.ok.code,
reloadMessage,
);
}
+ String _describePausedIsolates(int pausedIsolatesFound, String serviceEventKind) {
+ assert(pausedIsolatesFound > 0);
+ final StringBuffer message = StringBuffer();
+ bool plural;
+ if (pausedIsolatesFound == 1) {
+ if (flutterDevices.length == 1 && flutterDevices.single.views.length == 1) {
+ message.write('The application is ');
+ } else {
+ message.write('An isolate is ');
+ }
+ plural = false;
+ } else {
+ message.write('$pausedIsolatesFound isolates are ');
+ plural = true;
+ }
+ assert(serviceEventKind != null);
+ switch (serviceEventKind) {
+ case ServiceEvent.kPauseStart: message.write('paused (probably due to --start-paused)'); break;
+ case ServiceEvent.kPauseExit: message.write('paused because ${ plural ? 'they have' : 'it has' } terminated'); break;
+ case ServiceEvent.kPauseBreakpoint: message.write('paused in the debugger on a breakpoint'); break;
+ case ServiceEvent.kPauseInterrupted: message.write('paused due in the debugger'); break;
+ case ServiceEvent.kPauseException: message.write('paused in the debugger after an exception was thrown'); break;
+ case ServiceEvent.kPausePostRequest: message.write('paused'); break;
+ case '': message.write('paused for various reasons'); break;
+ default:
+ message.write('paused');
+ }
+ return message.toString();
+ }
+
bool _isPaused() {
for (FlutterDevice device in flutterDevices) {
for (FlutterView view in device.views) {
@@ -822,7 +909,6 @@
}
}
}
-
return false;
}
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index f2faebb..591d557 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -482,19 +482,18 @@
body: () async {
if (flutterUsage.isFirstRun)
flutterUsage.printWelcome();
-
+ final String commandPath = await usagePath;
FlutterCommandResult commandResult;
try {
- commandResult = await verifyThenRunCommand();
+ commandResult = await verifyThenRunCommand(commandPath);
} on ToolExit {
commandResult = const FlutterCommandResult(ExitStatus.fail);
rethrow;
} finally {
final DateTime endTime = systemClock.now();
printTrace(userMessages.flutterElapsedTime(name, getElapsedAsMilliseconds(endTime.difference(startTime))));
- // This is checking the result of the call to 'usagePath'
- // (a Future<String>), and not the result of evaluating the Future.
- if (usagePath != null) {
+ printTrace('"flutter $name" took ${getElapsedAsMilliseconds(endTime.difference(startTime))}.');
+ if (commandPath != null) {
final List<String> labels = <String>[];
if (commandResult?.exitStatus != null)
labels.add(getEnumName(commandResult.exitStatus));
@@ -528,7 +527,7 @@
/// then call this method to execute the command
/// rather than calling [runCommand] directly.
@mustCallSuper
- Future<FlutterCommandResult> verifyThenRunCommand() async {
+ Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
await validateCommand();
// Populate the cache. We call this before pub get below so that the sky_engine
@@ -544,8 +543,6 @@
setupApplicationPackages();
- final String commandPath = await usagePath;
-
if (commandPath != null) {
final Map<String, String> additionalUsageValues = await usageValues;
flutterUsage.sendCommand(commandPath, parameters: additionalUsageValues);
diff --git a/packages/flutter_tools/lib/src/test/coverage_collector.dart b/packages/flutter_tools/lib/src/test/coverage_collector.dart
index fda1785..ff8ca0c 100644
--- a/packages/flutter_tools/lib/src/test/coverage_collector.dart
+++ b/packages/flutter_tools/lib/src/test/coverage_collector.dart
@@ -57,13 +57,7 @@
if (result == null)
throw Exception('Failed to collect coverage.');
data = result;
- })
- .timeout(
- const Duration(minutes: 10),
- onTimeout: () {
- throw Exception('Timed out while collecting coverage.');
- },
- );
+ });
await Future.any<void>(<Future<void>>[ processComplete, collectionComplete ]);
assert(data != null);
@@ -77,12 +71,8 @@
///
/// This will not start any collection tasks. It us up to the caller of to
/// call [collectCoverage] for each process first.
- ///
- /// If [timeout] is specified, the future will timeout (with a
- /// [TimeoutException]) after the specified duration.
Future<String> finalizeCoverage({
coverage.Formatter formatter,
- Duration timeout,
Directory coverageDirectory,
}) async {
printTrace('formating coverage data');
@@ -102,9 +92,8 @@
}
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(
- timeout: const Duration(seconds: 30),
coverageDirectory: coverageDirectory,
);
status.stop();
diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart
index e01414e..5886f03 100644
--- a/packages/flutter_tools/lib/src/test/flutter_platform.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart
@@ -33,11 +33,17 @@
/// The timeout we give the test process to connect to the test harness
/// 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
/// 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.
+///
+/// See comment under [_kTestStartupTimeout] regarding timeouts.
const Duration _kTestProcessTimeout = Duration(minutes: 5);
/// Message logged by the test process to signal that its main method has begun
@@ -288,13 +294,11 @@
firstCompile = true;
}
suppressOutput = false;
- final CompilerOutput compilerOutput = await handleTimeout<CompilerOutput>(
- compiler.recompile(
- request.path,
- <String>[request.path],
- outputPath: outputDill.path),
- request.path,
- );
+ final CompilerOutput compilerOutput = await compiler.recompile(
+ request.path,
+ <String>[request.path],
+ outputPath: outputDill.path,
+ );
final String outputPath = compilerOutput?.outputFilename;
// In case compiler didn't produce output or reported compilation
@@ -337,7 +341,7 @@
Future<String> compile(String mainDart) {
final Completer<String> completer = Completer<String>();
compilerController.add(_CompilationRequest(mainDart, completer));
- return handleTimeout<String>(completer.future, mainDart);
+ return completer.future;
}
Future<void> _shutdown() async {
@@ -353,13 +357,6 @@
await _shutdown();
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 {
diff --git a/packages/flutter_tools/lib/src/tracing.dart b/packages/flutter_tools/lib/src/tracing.dart
index 3fe3953..485fb30 100644
--- a/packages/flutter_tools/lib/src/tracing.dart
+++ b/packages/flutter_tools/lib/src/tracing.dart
@@ -5,6 +5,7 @@
import 'dart:async';
import 'base/file_system.dart';
+import 'base/logger.dart';
import 'base/utils.dart';
import 'build_info.dart';
import 'globals.dart';
@@ -18,6 +19,8 @@
class Tracing {
Tracing(this.vmService);
+ static const String firstUsefulFrameEventName = _kFirstUsefulFrameEventName;
+
static Future<Tracing> connect(Uri uri) async {
final VMService observatory = await VMService.connect(uri);
return Tracing(observatory);
@@ -32,49 +35,46 @@
/// Stops tracing; optionally wait for first frame.
Future<Map<String, dynamic>> stopTracingAndDownloadTimeline({
- bool waitForFirstFrame = false
+ bool awaitFirstFrame = false
}) async {
- Map<String, dynamic> timeline;
-
- if (!waitForFirstFrame) {
- // Stop tracing immediately and get the timeline
- 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;
- }
+ if (awaitFirstFrame) {
+ final Status status = logger.startProgress(
+ 'Waiting for application to render first frame...',
+ timeout: kFastOperation,
);
-
- timeline = await vmService.vm.getVMTimeline();
-
- await vmService.vm.setVMTimelineFlags(<String>[]);
+ try {
+ 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();
+ }
+ });
+ 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;
}
}
/// Download the startup trace information from the given observatory client and
/// 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 File traceInfoFile = fs.file(traceInfoFilePath);
@@ -89,45 +89,53 @@
final Tracing tracing = Tracing(observatory);
final Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline(
- waitForFirstFrame: true
+ awaitFirstFrame: awaitFirstFrame,
);
int extractInstantEventTimestamp(String eventName) {
final List<Map<String, dynamic>> events =
List<Map<String, dynamic>>.from(timeline['traceEvents']);
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'];
}
+ String message = 'No useful metrics were gathered.';
+
final int engineEnterTimestampMicros = extractInstantEventTimestamp(_kFlutterEngineMainEnterEventName);
final int frameworkInitTimestampMicros = extractInstantEventTimestamp(_kFrameworkInitEventName);
- final int firstFrameTimestampMicros = extractInstantEventTimestamp(_kFirstUsefulFrameEventName);
if (engineEnterTimestampMicros == null) {
printTrace('Engine start event is missing in the timeline: $timeline');
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>{
'engineEnterTimestampMicros': engineEnterTimestampMicros,
- 'timeToFirstFrameMicros': timeToFirstFrameMicros,
};
if (frameworkInitTimestampMicros != null) {
- traceInfo['timeToFrameworkInitMicros'] = frameworkInitTimestampMicros - engineEnterTimestampMicros;
- traceInfo['timeAfterFrameworkInitMicros'] = firstFrameTimestampMicros - frameworkInitTimestampMicros;
+ final int timeToFrameworkInitMicros = frameworkInitTimestampMicros - engineEnterTimestampMicros;
+ 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));
- printStatus('Time to first frame: ${timeToFirstFrameMicros ~/ 1000}ms.');
+ printStatus(message);
printStatus('Saved startup trace info in ${traceInfoFile.path}.');
}
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart
index 48a8c8a..f9b53b9 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -15,12 +15,17 @@
import 'package:web_socket_channel/web_socket_channel.dart';
import 'base/common.dart';
+import 'base/context.dart';
import 'base/file_system.dart';
import 'base/io.dart' as io;
import 'base/utils.dart';
import 'globals.dart';
import 'vmservice_record_replay.dart';
+/// Override `WebSocketConnector` in [context] to use a different constructor
+/// for [WebSocket]s (used by tests).
+typedef WebSocketConnector = Future<io.WebSocket> Function(String url);
+
/// A function that opens a two-way communication channel to the specified [uri].
typedef _OpenChannel = Future<StreamChannel<String>> Function(Uri uri);
@@ -58,60 +63,46 @@
const String _kRecordingType = 'vmservice';
Future<StreamChannel<String>> _defaultOpenChannel(Uri uri) async {
- const int _kMaxAttempts = 5;
Duration delay = const Duration(milliseconds: 100);
int attempts = 0;
io.WebSocket socket;
- Future<void> onError(dynamic e) async {
- printTrace('Exception attempting to connect to observatory: $e');
+ Future<void> handleError(dynamic e) async {
+ printTrace('Exception attempting to connect to Observatory: $e');
printTrace('This was attempt #$attempts. Will retry in $delay.');
+ if (attempts == 10)
+ printStatus('This is taking longer than expected...');
+
// Delay next attempt.
await Future<void>.delayed(delay);
- // Back off exponentially.
- delay *= 2;
+ // Back off exponentially, up to 1600ms per attempt.
+ if (delay < const Duration(seconds: 1))
+ delay *= 2;
}
- while (attempts < _kMaxAttempts && socket == null) {
+ final WebSocketConnector constructor = context[WebSocketConnector] ?? io.WebSocket.connect;
+ while (socket == null) {
attempts += 1;
try {
- socket = await io.WebSocket.connect(uri.toString());
+ socket = await constructor(uri.toString());
} on io.WebSocketException catch (e) {
- await onError(e);
+ await handleError(e);
} on io.SocketException catch (e) {
- await onError(e);
+ await handleError(e);
}
}
-
- if (socket == null) {
- throw ToolExit(
- 'Attempted to connect to Dart observatory $_kMaxAttempts times, and '
- 'all attempts failed. Giving up. The URL was $uri'
- );
- }
-
return IOWebSocketChannel(socket).cast<String>();
}
-/// The default VM service request timeout.
-const Duration kDefaultRequestTimeout = Duration(seconds: 30);
-
-/// Used for RPC requests that may take a long time.
-const Duration kLongRequestTimeout = Duration(minutes: 1);
-
-/// Used for RPC requests that should never take a long time.
-const Duration kShortRequestTimeout = Duration(seconds: 5);
-
-// TODO(mklim): Test this, flutter/flutter#23031.
/// A connection to the Dart VM Service.
+// TODO(mklim): Test this, https://github.com/flutter/flutter/issues/23031
class VMService {
VMService(
this._peer,
this.httpAddress,
this.wsAddress,
- this._requestTimeout,
ReloadSources reloadSources,
Restart restart,
CompileExpression compileExpression,
@@ -247,9 +238,6 @@
/// Connect to a Dart VM Service at [httpUri].
///
- /// Requests made via the returned [VMService] time out after [requestTimeout]
- /// amount of time, which is [kDefaultRequestTimeout] by default.
- ///
/// If the [reloadSources] parameter is not null, the 'reloadSources' service
/// will be registered. The VM Service Protocol allows clients to register
/// custom services that can be invoked by other clients through the service
@@ -258,7 +246,6 @@
/// See: https://github.com/dart-lang/sdk/commit/df8bf384eb815cf38450cb50a0f4b62230fba217
static Future<VMService> connect(
Uri httpUri, {
- Duration requestTimeout = kDefaultRequestTimeout,
ReloadSources reloadSources,
Restart restart,
CompileExpression compileExpression,
@@ -266,7 +253,7 @@
final Uri wsUri = httpUri.replace(scheme: 'ws', path: fs.path.join(httpUri.path, 'ws'));
final StreamChannel<String> channel = await _openChannel(wsUri);
final rpc.Peer peer = rpc.Peer.withoutJson(jsonDocument.bind(channel));
- final VMService service = VMService(peer, httpUri, wsUri, requestTimeout, reloadSources, restart, compileExpression);
+ final VMService service = VMService(peer, httpUri, wsUri, reloadSources, restart, compileExpression);
// This call is to ensure we are able to establish a connection instead of
// keeping on trucking and failing farther down the process.
await service._sendRequest('getVersion', const <String, dynamic>{});
@@ -276,7 +263,6 @@
final Uri httpAddress;
final Uri wsAddress;
final rpc.Peer _peer;
- final Duration _requestTimeout;
final Completer<Map<String, dynamic>> _connectionError = Completer<Map<String, dynamic>>();
VM _vm;
@@ -841,28 +827,15 @@
}
/// Invoke the RPC and return the raw response.
- ///
- /// If `timeoutFatal` is false, then a timeout will result in a null return
- /// value. Otherwise, it results in an exception.
Future<Map<String, dynamic>> invokeRpcRaw(String method, {
Map<String, dynamic> params = const <String, dynamic>{},
- Duration timeout,
- bool timeoutFatal = true,
}) async {
printTrace('Sending to VM service: $method($params)');
assert(params != null);
- timeout ??= _vmService._requestTimeout;
try {
- final Map<String, dynamic> result = await _vmService
- ._sendRequest(method, params)
- .timeout(timeout);
+ final Map<String, dynamic> result = await _vmService._sendRequest(method, params);
printTrace('Result: ${_truncate(result.toString(), 250, '...')}');
return result;
- } on TimeoutException {
- printTrace('Request to Dart VM Service timed out: $method($params)');
- if (timeoutFatal)
- throw TimeoutException('Request to Dart VM Service timed out: $method($params)');
- return null;
} on WebSocketChannelException catch (error) {
throwToolExit('Error connecting to observatory: $error');
return null;
@@ -876,12 +849,10 @@
/// Invoke the RPC and return a [ServiceObject] response.
Future<T> invokeRpc<T extends ServiceObject>(String method, {
Map<String, dynamic> params = const <String, dynamic>{},
- Duration timeout,
}) async {
final Map<String, dynamic> response = await invokeRpcRaw(
method,
params: params,
- timeout: timeout,
);
final ServiceObject serviceObject = ServiceObject._fromMap(this, response);
if ((serviceObject != null) && (serviceObject._canCache)) {
@@ -968,7 +939,7 @@
}
Future<Map<String, dynamic>> getVMTimeline() {
- return invokeRpcRaw('_getVMTimeline', timeout: kLongRequestTimeout);
+ return invokeRpcRaw('_getVMTimeline');
}
Future<void> refreshViews({ bool waitForViews = false }) async {
@@ -982,10 +953,7 @@
// When the future returned by invokeRpc() below returns,
// the _viewCache will have been updated.
// This message updates all the views of every isolate.
- await vmService.vm.invokeRpc<ServiceObject>(
- '_flutter.listViews',
- timeout: kLongRequestTimeout,
- );
+ await vmService.vm.invokeRpc<ServiceObject>('_flutter.listViews');
if (_viewCache.values.isNotEmpty || !waitForViews)
return;
failCount += 1;
@@ -1124,8 +1092,6 @@
/// Invoke the RPC and return the raw response.
Future<Map<String, dynamic>> invokeRpcRaw(String method, {
Map<String, dynamic> params,
- Duration timeout,
- bool timeoutFatal = true,
}) {
// Inject the 'isolateId' parameter.
if (params == null) {
@@ -1135,7 +1101,7 @@
} else {
params['isolateId'] = id;
}
- return vm.invokeRpcRaw(method, params: params, timeout: timeout, timeoutFatal: timeoutFatal);
+ return vm.invokeRpcRaw(method, params: params);
}
/// Invoke the RPC and return a ServiceObject response.
@@ -1257,40 +1223,36 @@
Future<Map<String, dynamic>> invokeFlutterExtensionRpcRaw(
String method, {
Map<String, dynamic> params,
- Duration timeout,
- bool timeoutFatal = true,
}
- ) {
- return invokeRpcRaw(method, params: params, timeout: timeout,
- timeoutFatal: timeoutFatal).catchError((dynamic error) {
- if (error is rpc.RpcException) {
- // If an application is not using the framework
- if (error.code == rpc_error_code.METHOD_NOT_FOUND)
- return null;
- throw error;
- }});
+ ) async {
+ try {
+ return await invokeRpcRaw(method, params: params);
+ } on rpc.RpcException catch (e) {
+ // If an application is not using the framework
+ if (e.code == rpc_error_code.METHOD_NOT_FOUND)
+ return null;
+ rethrow;
+ }
}
- // Debug dump extension methods.
-
Future<Map<String, dynamic>> flutterDebugDumpApp() {
- return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpApp', timeout: kLongRequestTimeout);
+ return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpApp');
}
Future<Map<String, dynamic>> flutterDebugDumpRenderTree() {
- return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpRenderTree', timeout: kLongRequestTimeout);
+ return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpRenderTree');
}
Future<Map<String, dynamic>> flutterDebugDumpLayerTree() {
- return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpLayerTree', timeout: kLongRequestTimeout);
+ return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpLayerTree');
}
Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInTraversalOrder() {
- return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInTraversalOrder', timeout: kLongRequestTimeout);
+ return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInTraversalOrder');
}
Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInInverseHitTestOrder() {
- return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', timeout: kLongRequestTimeout);
+ return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder');
}
Future<Map<String, dynamic>> _flutterToggle(String name) async {
@@ -1299,8 +1261,6 @@
state = await invokeFlutterExtensionRpcRaw(
'ext.flutter.$name',
params: <String, dynamic>{ 'enabled': state['enabled'] == 'true' ? 'false' : 'true' },
- timeout: const Duration(milliseconds: 150),
- timeoutFatal: false,
);
}
return state;
@@ -1312,22 +1272,20 @@
Future<Map<String, dynamic>> flutterToggleWidgetInspector() => _flutterToggle('inspector.show');
- Future<void> flutterDebugAllowBanner(bool show) async {
- await invokeFlutterExtensionRpcRaw(
+ Future<Map<String, dynamic>> flutterDebugAllowBanner(bool show) {
+ return invokeFlutterExtensionRpcRaw(
'ext.flutter.debugAllowBanner',
params: <String, dynamic>{ 'enabled': show ? 'true' : 'false' },
- timeout: const Duration(milliseconds: 150),
- timeoutFatal: false,
);
}
- // Reload related extension methods.
Future<Map<String, dynamic>> flutterReassemble() {
- return invokeFlutterExtensionRpcRaw(
- 'ext.flutter.reassemble',
- timeout: kShortRequestTimeout,
- timeoutFatal: true,
- );
+ return invokeFlutterExtensionRpcRaw('ext.flutter.reassemble');
+ }
+
+ Future<bool> flutterAlreadyPaintedFirstUsefulFrame() async {
+ final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw('ext.flutter.didSendFirstFrameEvent');
+ return result['enabled'] == 'true';
}
Future<Map<String, dynamic>> uiWindowScheduleFrame() {
@@ -1335,7 +1293,8 @@
}
Future<Map<String, dynamic>> flutterEvictAsset(String assetPath) {
- return invokeFlutterExtensionRpcRaw('ext.flutter.evict',
+ return invokeFlutterExtensionRpcRaw(
+ 'ext.flutter.evict',
params: <String, dynamic>{
'value': assetPath,
}
@@ -1352,19 +1311,13 @@
// Application control extension methods.
Future<Map<String, dynamic>> flutterExit() {
- return invokeFlutterExtensionRpcRaw(
- 'ext.flutter.exit',
- timeout: const Duration(seconds: 2),
- timeoutFatal: false,
- );
+ return invokeFlutterExtensionRpcRaw('ext.flutter.exit');
}
Future<String> flutterPlatformOverride([String platform]) async {
final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw(
'ext.flutter.platformOverride',
params: platform != null ? <String, dynamic>{ 'value': platform } : <String, String>{},
- timeout: const Duration(seconds: 5),
- timeoutFatal: false,
);
if (result != null && result['value'] is String)
return result['value'];
diff --git a/packages/flutter_tools/lib/src/vscode/vscode_validator.dart b/packages/flutter_tools/lib/src/vscode/vscode_validator.dart
index f955c5f..8fc6434 100644
--- a/packages/flutter_tools/lib/src/vscode/vscode_validator.dart
+++ b/packages/flutter_tools/lib/src/vscode/vscode_validator.dart
@@ -14,7 +14,6 @@
final VsCode _vsCode;
-
static Iterable<DoctorValidator> get installedValidators {
return VsCode
.allInstalled()