Consolidate observatory code (#3892)

* rename service_protocol.dart to protocol_discovery.dart

* add a wrapper around the obs. protocol

* use json-rpc in run

* consolidate obs. code; implement flutter run --benchmark

* review comments
This commit is contained in:
Devon Carew 2016-05-12 18:15:23 -07:00
parent ac4ad3bc98
commit 40c0d6ea12
15 changed files with 403 additions and 307 deletions

View file

@ -14,7 +14,7 @@ import '../build_info.dart';
import '../device.dart'; import '../device.dart';
import '../flx.dart' as flx; import '../flx.dart' as flx;
import '../globals.dart'; import '../globals.dart';
import '../service_protocol.dart'; import '../protocol_discovery.dart';
import 'adb.dart'; import 'adb.dart';
import 'android.dart'; import 'android.dart';
@ -243,14 +243,12 @@ class AndroidDevice extends Device {
runCheckedSync(adbCommandForDevice(<String>['push', bundlePath, _deviceBundlePath])); runCheckedSync(adbCommandForDevice(<String>['push', bundlePath, _deviceBundlePath]));
ServiceProtocolDiscovery observatoryDiscovery; ProtocolDiscovery observatoryDiscovery;
ServiceProtocolDiscovery diagnosticDiscovery; ProtocolDiscovery diagnosticDiscovery;
if (options.debuggingEnabled) { if (options.debuggingEnabled) {
observatoryDiscovery = new ServiceProtocolDiscovery( observatoryDiscovery = new ProtocolDiscovery(logReader, ProtocolDiscovery.kObservatoryService);
logReader, ServiceProtocolDiscovery.kObservatoryService); diagnosticDiscovery = new ProtocolDiscovery(logReader, ProtocolDiscovery.kDiagnosticService);
diagnosticDiscovery = new ServiceProtocolDiscovery(
logReader, ServiceProtocolDiscovery.kDiagnosticService);
} }
List<String> cmd = adbCommandForDevice(<String>[ List<String> cmd = adbCommandForDevice(<String>[
@ -295,13 +293,11 @@ class AndroidDevice extends Device {
int observatoryLocalPort = await options.findBestObservatoryPort(); int observatoryLocalPort = await options.findBestObservatoryPort();
// TODO(devoncarew): Remember the forwarding information (so we can later remove the // TODO(devoncarew): Remember the forwarding information (so we can later remove the
// port forwarding). // port forwarding).
await _forwardPort(ServiceProtocolDiscovery.kObservatoryService, await _forwardPort(ProtocolDiscovery.kObservatoryService, observatoryDevicePort, observatoryLocalPort);
observatoryDevicePort, observatoryLocalPort);
int diagnosticDevicePort = devicePorts[1]; int diagnosticDevicePort = devicePorts[1];
printTrace('diagnostic port = $diagnosticDevicePort'); printTrace('diagnostic port = $diagnosticDevicePort');
int diagnosticLocalPort = await options.findBestDiagnosticPort(); int diagnosticLocalPort = await options.findBestDiagnosticPort();
await _forwardPort(ServiceProtocolDiscovery.kDiagnosticService, await _forwardPort(ProtocolDiscovery.kDiagnosticService, diagnosticDevicePort, diagnosticLocalPort);
diagnosticDevicePort, diagnosticLocalPort);
return new LaunchResult.succeeded( return new LaunchResult.succeeded(
observatoryPort: observatoryLocalPort, observatoryPort: observatoryLocalPort,
diagnosticPort: diagnosticLocalPort diagnosticPort: diagnosticLocalPort

View file

@ -2,11 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
const int defaultObservatoryPort = 8100; const int kDefaultObservatoryPort = 8100;
const int defaultDiagnosticPort = 8101; const int kDefaultDiagnosticPort = 8101;
const int defaultDrivePort = 8183; const int kDefaultDrivePort = 8183;
// Names of some of the Timeline events we care about
const String flutterEngineMainEnterEventName = 'FlutterEngineMainEnter';
const String frameworkInitEventName = 'Framework initialization';
const String firstUsefulFrameEventName = 'Widgets completed first useful frame';

View file

@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
@ -60,6 +61,10 @@ File getUniqueFile(Directory dir, String baseName, String ext) {
} }
} }
String toPrettyJson(Object jsonable) {
return new JsonEncoder.withIndent(' ').convert(jsonable) + '\n';
}
/// A class to maintain a list of items, fire events when items are added or /// A class to maintain a list of items, fire events when items are added or
/// removed, and calculate a diff of changes when a new list of items is /// removed, and calculate a diff of changes when a new list of items is
/// available. /// available.

View file

@ -12,7 +12,7 @@ import 'base/logger.dart';
import 'base/os.dart'; import 'base/os.dart';
import 'globals.dart'; import 'globals.dart';
/// A warpper around the `bin/cache/` directory. /// A wrapper around the `bin/cache/` directory.
class Cache { class Cache {
/// [rootOverride] is configurable for testing. /// [rootOverride] is configurable for testing.
Cache({ Directory rootOverride }) { Cache({ Directory rootOverride }) {

View file

@ -376,9 +376,8 @@ class AnalyzeCommand extends FlutterCommand {
'time': (stopwatch.elapsedMilliseconds / 1000.0), 'time': (stopwatch.elapsedMilliseconds / 1000.0),
'issues': errorCount 'issues': errorCount
}; };
JsonEncoder encoder = new JsonEncoder.withIndent(' '); new File(benchmarkOut).writeAsStringSync(toPrettyJson(data));
new File(benchmarkOut).writeAsStringSync(encoder.convert(data) + '\n'); printStatus('Analysis benchmark written to $benchmarkOut ($data).');
printStatus('Analysis benchmark written to $benchmarkOut.');
} }
} }

View file

@ -62,7 +62,7 @@ class DriveCommand extends RunCommandBase {
); );
argParser.addOption('debug-port', argParser.addOption('debug-port',
defaultsTo: defaultDrivePort.toString(), defaultsTo: kDefaultDrivePort.toString(),
help: 'Listen to the given port for a debug connection.' help: 'Listen to the given port for a debug connection.'
); );
} }

View file

@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@ -11,12 +10,15 @@ import 'package:path/path.dart' as path;
import '../application_package.dart'; import '../application_package.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../device.dart'; import '../device.dart';
import '../globals.dart'; import '../globals.dart';
import '../observatory.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import 'build_apk.dart'; import 'build_apk.dart';
import 'install.dart'; import 'install.dart';
import 'trace.dart';
abstract class RunCommandBase extends FlutterCommand { abstract class RunCommandBase extends FlutterCommand {
RunCommandBase() { RunCommandBase() {
@ -62,7 +64,7 @@ class RunCommand extends RunCommandBase {
negatable: false, negatable: false,
help: 'Start in a paused mode and wait for a debugger to connect.'); help: 'Start in a paused mode and wait for a debugger to connect.');
argParser.addOption('debug-port', argParser.addOption('debug-port',
help: 'Listen to the given port for a debug connection (defaults to $defaultObservatoryPort).'); help: 'Listen to the given port for a debug connection (defaults to $kDefaultObservatoryPort).');
usesPubOption(); usesPubOption();
// A temporary, hidden flag to experiment with a different run style. // A temporary, hidden flag to experiment with a different run style.
@ -72,6 +74,12 @@ class RunCommand extends RunCommandBase {
negatable: false, negatable: false,
hide: true, hide: true,
help: 'Stay resident after running the app.'); help: 'Stay resident after running the app.');
// Hidden option to enable a benchmarking mode. This will run the given
// application, measure the startup time and the app restart time, write the
// results out to 'refresh_benchmark.json', and exit. This flag is intended
// for use in generating automated flutter benchmarks.
argParser.addFlag('benchmark', negatable: false, hide: true);
} }
@override @override
@ -107,7 +115,7 @@ class RunCommand extends RunCommandBase {
options = new DebuggingOptions.disabled(); options = new DebuggingOptions.disabled();
} else { } else {
options = new DebuggingOptions.enabled( options = new DebuggingOptions.enabled(
// TODO(devoncarew): Check this to 'getBuildMode() == BuildMode.debug'. // TODO(devoncarew): Change this to 'getBuildMode() == BuildMode.debug'.
checked: argResults['checked'], checked: argResults['checked'],
startPaused: argResults['start-paused'], startPaused: argResults['start-paused'],
observatoryPort: debugPort observatoryPort: debugPort
@ -119,11 +127,10 @@ class RunCommand extends RunCommandBase {
deviceForCommand, deviceForCommand,
target: target, target: target,
debuggingOptions: options, debuggingOptions: options,
traceStartup: traceStartup,
buildMode: getBuildMode() buildMode: getBuildMode()
); );
return runner.run(); return runner.run(traceStartup: traceStartup, benchmark: argResults['benchmark']);
} else { } else {
return startApp( return startApp(
deviceForCommand, deviceForCommand,
@ -132,6 +139,7 @@ class RunCommand extends RunCommandBase {
install: true, install: true,
debuggingOptions: options, debuggingOptions: options,
traceStartup: traceStartup, traceStartup: traceStartup,
benchmark: argResults['benchmark'],
route: route, route: route,
buildMode: getBuildMode() buildMode: getBuildMode()
); );
@ -146,6 +154,7 @@ Future<int> startApp(
bool install: true, bool install: true,
DebuggingOptions debuggingOptions, DebuggingOptions debuggingOptions,
bool traceStartup: false, bool traceStartup: false,
bool benchmark: false,
String route, String route,
BuildMode buildMode: BuildMode.debug BuildMode buildMode: BuildMode.debug
}) async { }) async {
@ -169,6 +178,8 @@ Future<int> startApp(
return 1; return 1;
} }
Stopwatch stopwatch = new Stopwatch()..start();
// TODO(devoncarew): We shouldn't have to do type checks here. // TODO(devoncarew): We shouldn't have to do type checks here.
if (install && device is AndroidDevice) { if (install && device is AndroidDevice) {
printTrace('Running build command.'); printTrace('Running build command.');
@ -221,10 +232,22 @@ Future<int> startApp(
platformArgs: platformArgs platformArgs: platformArgs
); );
if (!result.started) stopwatch.stop();
if (!result.started) {
printError('Error running application on ${device.name}.'); printError('Error running application on ${device.name}.');
else if (traceStartup) } else if (traceStartup) {
await _downloadStartupTrace(result.observatoryPort, device); try {
Observatory observatory = await Observatory.connect(result.observatoryPort);
await _downloadStartupTrace(observatory);
} catch (error) {
printError('Error connecting to observatory: $error');
return 1;
}
}
if (benchmark)
_writeBenchmark(stopwatch);
return result.started ? 0 : 2; return result.started ? 0 : 2;
} }
@ -241,87 +264,6 @@ String findMainDartFile([String target]) {
return targetPath; return targetPath;
} }
Future<Null> _downloadStartupTrace(int observatoryPort, Device device) async {
Map<String, dynamic> timeline = await device.stopTracingAndDownloadTimeline(
observatoryPort,
waitForFirstFrame: true
);
int extractInstantEventTimestamp(String eventName) {
List<Map<String, dynamic>> events = timeline['traceEvents'];
Map<String, dynamic> event = events
.firstWhere((Map<String, dynamic> event) => event['name'] == eventName, orElse: () => null);
if (event == null)
return null;
return event['ts'];
}
int engineEnterTimestampMicros = extractInstantEventTimestamp(flutterEngineMainEnterEventName);
int frameworkInitTimestampMicros = extractInstantEventTimestamp(frameworkInitEventName);
int firstFrameTimestampMicros = extractInstantEventTimestamp(firstUsefulFrameEventName);
if (engineEnterTimestampMicros == null) {
printError('Engine start event is missing in the timeline. Cannot compute startup time.');
return null;
}
if (firstFrameTimestampMicros == null) {
printError('First frame event is missing in the timeline. Cannot compute startup time.');
return null;
}
File traceInfoFile = new File('build/start_up_info.json');
int timeToFirstFrameMicros = firstFrameTimestampMicros - engineEnterTimestampMicros;
Map<String, dynamic> traceInfo = <String, dynamic>{
'engineEnterTimestampMicros': engineEnterTimestampMicros,
'timeToFirstFrameMicros': timeToFirstFrameMicros,
};
if (frameworkInitTimestampMicros != null) {
traceInfo['timeToFrameworkInitMicros'] = frameworkInitTimestampMicros - engineEnterTimestampMicros;
traceInfo['timeAfterFrameworkInitMicros'] = firstFrameTimestampMicros - frameworkInitTimestampMicros;
}
await traceInfoFile.writeAsString(JSON.encode(traceInfo));
String timeToFirstFrameMessage;
if (timeToFirstFrameMicros > 1000000) {
timeToFirstFrameMessage = '${(timeToFirstFrameMicros / 1000000).toStringAsFixed(2)} seconds';
} else {
timeToFirstFrameMessage = '${timeToFirstFrameMicros ~/ 1000} milliseconds';
}
printStatus('Time to first frame $timeToFirstFrameMessage');
printStatus('Saved startup trace info in ${traceInfoFile.path}');
}
/// Delay until the Observatory / service protocol is available.
///
/// This does not fail if we're unable to connect, and times out after the given
/// [timeout].
Future<Null> delayUntilObservatoryAvailable(String host, int port, {
Duration timeout: const Duration(seconds: 10)
}) async {
printTrace('Waiting until Observatory is available (port $port).');
final String url = 'ws://$host:$port/ws';
printTrace('Looking for the observatory at $url.');
Stopwatch stopwatch = new Stopwatch()..start();
while (stopwatch.elapsed <= timeout) {
try {
WebSocket ws = await WebSocket.connect(url);
printTrace('Connected to the observatory port.');
ws.close().catchError((dynamic error) => null);
return;
} catch (error) {
await new Future<Null>.delayed(new Duration(milliseconds: 250));
}
}
printTrace('Unable to connect to the observatory.');
}
String _getMissingPackageHintForPlatform(TargetPlatform platform) { String _getMissingPackageHintForPlatform(TargetPlatform platform) {
switch (platform) { switch (platform) {
case TargetPlatform.android_arm: case TargetPlatform.android_arm:
@ -346,25 +288,22 @@ class _RunAndStayResident {
this.device, { this.device, {
this.target, this.target,
this.debuggingOptions, this.debuggingOptions,
this.traceStartup : false,
this.buildMode : BuildMode.debug this.buildMode : BuildMode.debug
}); });
final Device device; final Device device;
final String target; final String target;
final DebuggingOptions debuggingOptions; final DebuggingOptions debuggingOptions;
final bool traceStartup;
final BuildMode buildMode; final BuildMode buildMode;
Completer<int> _exitCompleter; Completer<int> _exitCompleter;
StreamSubscription<String> _loggingSubscription; StreamSubscription<String> _loggingSubscription;
WebSocket _observatoryConnection; Observatory observatory;
String _isolateId; String _isolateId;
int _messageId = 0;
/// Start the app and keep the process running during its lifetime. /// Start the app and keep the process running during its lifetime.
Future<int> run() async { Future<int> run({ bool traceStartup: false, bool benchmark: false }) async {
String mainPath = findMainDartFile(target); String mainPath = findMainDartFile(target);
if (!FileSystemEntity.isFileSync(mainPath)) { if (!FileSystemEntity.isFileSync(mainPath)) {
String message = 'Tried to run $mainPath, but that file does not exist.'; String message = 'Tried to run $mainPath, but that file does not exist.';
@ -385,6 +324,8 @@ class _RunAndStayResident {
return 1; return 1;
} }
Stopwatch stopwatch = new Stopwatch()..start();
// TODO(devoncarew): We shouldn't have to do type checks here. // TODO(devoncarew): We shouldn't have to do type checks here.
if (device is AndroidDevice) { if (device is AndroidDevice) {
printTrace('Running build command.'); printTrace('Running build command.');
@ -440,89 +381,79 @@ class _RunAndStayResident {
return 2; return 2;
} }
stopwatch.stop();
_exitCompleter = new Completer<int>(); _exitCompleter = new Completer<int>();
// Connect to observatory. // Connect to observatory.
if (debuggingOptions.debuggingEnabled) { if (debuggingOptions.debuggingEnabled) {
final String localhost = InternetAddress.LOOPBACK_IP_V4.address; observatory = await Observatory.connect(result.observatoryPort);
final String url = 'ws://$localhost:${result.observatoryPort}/ws';
_observatoryConnection = await WebSocket.connect(url);
printTrace('Connected to observatory port: ${result.observatoryPort}.'); printTrace('Connected to observatory port: ${result.observatoryPort}.');
// Listen for observatory connection close. observatory.onIsolateEvent.listen((Event event) {
_observatoryConnection.listen((dynamic data) { if (event['isolate'] != null)
if (data is String) { _isolateId = event['isolate']['id'];
Map<String, dynamic> json = JSON.decode(data); });
observatory.streamListen('Isolate');
if (json['method'] == 'streamNotify') { // Listen for observatory connection close.
Map<String, dynamic> event = json['params']['event']; observatory.done.whenComplete(() {
if (event['isolate'] != null && _isolateId == null)
_isolateId = event['isolate']['id'];
} else if (json['result'] != null && json['result']['type'] == 'VM') {
// isolates: [{
// type: @Isolate, fixedId: true, id: isolates/724543296, name: dev.flx$main, number: 724543296
// }]
List<dynamic> isolates = json['result']['isolates'];
if (isolates.isNotEmpty)
_isolateId = isolates.first['id'];
} else if (json['error'] != null) {
printError('Error: ${json['error']['message']}.');
printTrace(data);
}
}
}, onDone: () {
_handleExit(); _handleExit();
}); });
_observatoryConnection.add(JSON.encode(<String, dynamic>{ observatory.getVM().then((VM vm) {
'method': 'streamListen', if (vm.isolates.isNotEmpty)
'params': <String, dynamic>{ 'streamId': 'Isolate' }, _isolateId = vm.isolates.first['id'];
'id': _messageId++ });
}));
_observatoryConnection.add(JSON.encode(<String, dynamic>{
'method': 'getVM',
'id': _messageId++
}));
} }
printStatus('Application running.'); printStatus('Application running.');
_printHelp();
terminal.singleCharMode = true; if (observatory != null && traceStartup) {
printStatus('Downloading startup trace info...');
terminal.onCharInput.listen((String code) { await _downloadStartupTrace(observatory);
String lower = code.toLowerCase();
if (lower == 'h' || code == AnsiTerminal.KEY_F1) { _handleExit();
// F1, help } else {
_printHelp(); _printHelp();
} else if (lower == 'r' || code == AnsiTerminal.KEY_F5) {
// F5, refresh terminal.singleCharMode = true;
_handleRefresh();
} else if (lower == 'q' || code == AnsiTerminal.KEY_F10) { terminal.onCharInput.listen((String code) {
// F10, exit String lower = code.toLowerCase();
if (lower == 'h' || code == AnsiTerminal.KEY_F1) {
// F1, help
_printHelp();
} else if (lower == 'r' || code == AnsiTerminal.KEY_F5) {
// F5, refresh
_handleRefresh();
} else if (lower == 'q' || code == AnsiTerminal.KEY_F10) {
// F10, exit
_handleExit();
}
});
ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) {
_handleExit(); _handleExit();
} });
}); ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) {
_handleExit();
});
}
ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) { if (benchmark) {
_handleExit(); _writeBenchmark(stopwatch);
}); new Future<Null>.delayed(new Duration(seconds: 2)).then((_) {
ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) { _handleExit();
_handleExit(); });
}); }
return _exitCompleter.future.then((int exitCode) async { return _exitCompleter.future.then((int exitCode) async {
if (_observatoryConnection != null && if (observatory != null && !observatory.isClosed && _isolateId != null) {
_observatoryConnection.readyState == WebSocket.OPEN && observatory.flutterExit(_isolateId);
_isolateId != null) {
_observatoryConnection.add(JSON.encode(<String, dynamic>{
'method': 'ext.flutter.exit',
'params': <String, dynamic>{ 'isolateId': _isolateId },
'id': _messageId++
}));
// WebSockets do not have a flush() method. // WebSockets do not have a flush() method.
await new Future<Null>.delayed(new Duration(milliseconds: 100)); await new Future<Null>.delayed(new Duration(milliseconds: 100));
} }
@ -536,27 +467,80 @@ class _RunAndStayResident {
} }
void _handleRefresh() { void _handleRefresh() {
if (_observatoryConnection == null) { if (observatory == null) {
printError('Debugging is not enabled.'); printError('Debugging is not enabled.');
} else { } else {
printStatus('Re-starting application...'); printStatus('Re-starting application...');
// TODO(devoncarew): Show an error if the isolate reload fails. observatory.isolateReload(_isolateId).catchError((dynamic error) {
_observatoryConnection.add(JSON.encode(<String, dynamic>{ printError('Error restarting app: $error');
'method': 'isolateReload', });
'params': <String, dynamic>{ 'isolateId': _isolateId },
'id': _messageId++
}));
} }
} }
void _handleExit() { void _handleExit() {
terminal.singleCharMode = false;
if (!_exitCompleter.isCompleted) { if (!_exitCompleter.isCompleted) {
_loggingSubscription?.cancel(); _loggingSubscription?.cancel();
printStatus('');
printStatus('Application finished.'); printStatus('Application finished.');
terminal.singleCharMode = false;
_exitCompleter.complete(0); _exitCompleter.complete(0);
} }
} }
} }
Future<Null> _downloadStartupTrace(Observatory observatory) async {
Tracing tracing = new Tracing(observatory);
Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline(
waitForFirstFrame: true
);
int extractInstantEventTimestamp(String eventName) {
List<Map<String, dynamic>> events = timeline['traceEvents'];
Map<String, dynamic> event = events.firstWhere(
(Map<String, dynamic> event) => event['name'] == eventName, orElse: () => null
);
return event == null ? null : event['ts'];
}
int engineEnterTimestampMicros = extractInstantEventTimestamp(kFlutterEngineMainEnterEventName);
int frameworkInitTimestampMicros = extractInstantEventTimestamp(kFrameworkInitEventName);
int firstFrameTimestampMicros = extractInstantEventTimestamp(kFirstUsefulFrameEventName);
if (engineEnterTimestampMicros == null) {
printError('Engine start event is missing in the timeline. Cannot compute startup time.');
return null;
}
if (firstFrameTimestampMicros == null) {
printError('First frame event is missing in the timeline. Cannot compute startup time.');
return null;
}
File traceInfoFile = new File('build/start_up_info.json');
int timeToFirstFrameMicros = firstFrameTimestampMicros - engineEnterTimestampMicros;
Map<String, dynamic> traceInfo = <String, dynamic>{
'engineEnterTimestampMicros': engineEnterTimestampMicros,
'timeToFirstFrameMicros': timeToFirstFrameMicros,
};
if (frameworkInitTimestampMicros != null) {
traceInfo['timeToFrameworkInitMicros'] = frameworkInitTimestampMicros - engineEnterTimestampMicros;
traceInfo['timeAfterFrameworkInitMicros'] = firstFrameTimestampMicros - frameworkInitTimestampMicros;
}
traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo));
printStatus('Time to first frame: ${timeToFirstFrameMicros ~/ 1000}ms.');
printStatus('Saved startup trace info in ${traceInfoFile.path}.');
}
void _writeBenchmark(Stopwatch stopwatch) {
final String benchmarkOut = 'refresh_benchmark.json';
Map<String, dynamic> data = <String, dynamic>{
'time': stopwatch.elapsedMilliseconds
};
new File(benchmarkOut).writeAsStringSync(toPrettyJson(data));
printStatus('Run benchmark written to $benchmarkOut ($data).');
}

View file

@ -16,7 +16,7 @@ class SkiaCommand extends FlutterCommand {
argParser.addOption('output-file', help: 'Write the Skia picture file to this path.'); argParser.addOption('output-file', help: 'Write the Skia picture file to this path.');
argParser.addOption('skiaserve', help: 'Post the picture to a skiaserve debugger at this URL.'); argParser.addOption('skiaserve', help: 'Post the picture to a skiaserve debugger at this URL.');
argParser.addOption('diagnostic-port', argParser.addOption('diagnostic-port',
defaultsTo: defaultDiagnosticPort.toString(), defaultsTo: kDefaultDiagnosticPort.toString(),
help: 'Local port where the diagnostic server is listening.'); help: 'Local port where the diagnostic server is listening.');
} }

View file

@ -8,10 +8,15 @@ import 'dart:io';
import '../base/common.dart'; import '../base/common.dart';
import '../base/utils.dart'; import '../base/utils.dart';
import '../device.dart';
import '../globals.dart'; import '../globals.dart';
import '../observatory.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
// Names of some of the Timeline events we care about.
const String kFlutterEngineMainEnterEventName = 'FlutterEngineMainEnter';
const String kFrameworkInitEventName = 'Framework initialization';
const String kFirstUsefulFrameEventName = 'Widgets completed first useful frame';
class TraceCommand extends FlutterCommand { class TraceCommand extends FlutterCommand {
TraceCommand() { TraceCommand() {
argParser.addFlag('start', negatable: false, help: 'Start tracing.'); argParser.addFlag('start', negatable: false, help: 'Start tracing.');
@ -20,7 +25,7 @@ class TraceCommand extends FlutterCommand {
argParser.addOption('duration', argParser.addOption('duration',
defaultsTo: '10', abbr: 'd', help: 'Duration in seconds to trace.'); defaultsTo: '10', abbr: 'd', help: 'Duration in seconds to trace.');
argParser.addOption('debug-port', argParser.addOption('debug-port',
defaultsTo: defaultObservatoryPort.toString(), defaultsTo: kDefaultObservatoryPort.toString(),
help: 'Local port where the observatory is listening.'); help: 'Local port where the observatory is listening.');
} }
@ -41,38 +46,104 @@ class TraceCommand extends FlutterCommand {
@override @override
Future<int> runInProject() async { Future<int> runInProject() async {
Device device = deviceForCommand;
int observatoryPort = int.parse(argResults['debug-port']); int observatoryPort = int.parse(argResults['debug-port']);
Tracing tracing;
try {
tracing = await Tracing.connect(observatoryPort);
} catch (error) {
printError('Error connecting to observatory: $error');
return 1;
}
if ((!argResults['start'] && !argResults['stop']) || if ((!argResults['start'] && !argResults['stop']) ||
(argResults['start'] && argResults['stop'])) { (argResults['start'] && argResults['stop'])) {
// Setting neither flags or both flags means do both commands and wait // Setting neither flags or both flags means do both commands and wait
// duration seconds in between. // duration seconds in between.
await device.startTracing(observatoryPort); await tracing.startTracing();
await new Future<Null>.delayed( await new Future<Null>.delayed(
new Duration(seconds: int.parse(argResults['duration'])), new Duration(seconds: int.parse(argResults['duration'])),
() => _stopTracing(device, observatoryPort) () => _stopTracing(tracing)
); );
} else if (argResults['stop']) { } else if (argResults['stop']) {
await _stopTracing(device, observatoryPort); await _stopTracing(tracing);
} else { } else {
await device.startTracing(observatoryPort); await tracing.startTracing();
} }
return 0; return 0;
} }
Future<Null> _stopTracing(Device device, int observatoryPort) async { Future<Null> _stopTracing(Tracing tracing) async {
Map<String, dynamic> timeline = await device.stopTracingAndDownloadTimeline(observatoryPort); Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline();
String outPath = argResults['out'];
File localFile; File localFile;
if (outPath != null) {
localFile = new File(outPath); if (argResults['out'] != null) {
localFile = new File(argResults['out']);
} else { } else {
localFile = getUniqueFile(Directory.current, 'trace', 'json'); localFile = getUniqueFile(Directory.current, 'trace', 'json');
} }
await localFile.writeAsString(JSON.encode(timeline)); await localFile.writeAsString(JSON.encode(timeline));
printStatus('Trace file saved to ${localFile.path}'); printStatus('Trace file saved to ${localFile.path}');
} }
} }
class Tracing {
Tracing(this.observatory);
static Future<Tracing> connect(int port) {
return Observatory.connect(port).then((Observatory observatory) => new Tracing(observatory));
}
final Observatory observatory;
Future<Null> startTracing() async {
await observatory.setVMTimelineFlags(<String>['Compiler', 'Dart', 'Embedder', 'GC']);
await observatory.clearVMTimeline();
}
/// Stops tracing; optionally wait for first frame.
Future<Map<String, dynamic>> stopTracingAndDownloadTimeline({
bool waitForFirstFrame: false
}) async {
Response timeline;
if (!waitForFirstFrame) {
// Stop tracing immediately and get the timeline
await observatory.setVMTimelineFlags(<String>[]);
timeline = await observatory.getVMTimeline();
} else {
Completer<Null> whenFirstFrameRendered = new Completer<Null>();
observatory.onTimelineEvent.listen((Event timelineEvent) {
List<Map<String, dynamic>> events = timelineEvent['timelineEvents'];
for (Map<String, dynamic> event in events) {
if (event['name'] == kFirstUsefulFrameEventName)
whenFirstFrameRendered.complete();
}
});
await observatory.streamListen('Timeline');
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;
}
);
timeline = await observatory.getVMTimeline();
await observatory.setVMTimelineFlags(<String>[]);
}
return timeline.response;
}
}

View file

@ -6,9 +6,6 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:web_socket_channel/io.dart';
import 'android/android_device.dart'; import 'android/android_device.dart';
import 'application_package.dart'; import 'application_package.dart';
import 'base/common.dart'; import 'base/common.dart';
@ -225,82 +222,6 @@ abstract class Device {
'${getNameForTargetPlatform(device.platform)}$supportIndicator'); '${getNameForTargetPlatform(device.platform)}$supportIndicator');
} }
} }
Future<rpc.Peer> _connectToObservatory(int observatoryPort) async {
Uri uri = new Uri(scheme: 'ws', host: '127.0.0.1', port: observatoryPort, path: 'ws');
WebSocket ws = await WebSocket.connect(uri.toString());
rpc.Peer peer = new rpc.Peer(new IOWebSocketChannel(ws));
peer.listen();
return peer;
}
Future<Null> startTracing(int observatoryPort) async {
rpc.Client client;
try {
client = await _connectToObservatory(observatoryPort);
} catch (e) {
printError('Error connecting to observatory: $e');
return;
}
await client.sendRequest('_setVMTimelineFlags',
<String, dynamic>{'recordedStreams': <String>['Compiler', 'Dart', 'Embedder', 'GC']}
);
await client.sendRequest('_clearVMTimeline');
}
/// Stops tracing, optionally waiting
Future<Map<String, dynamic>> stopTracingAndDownloadTimeline(int observatoryPort, {bool waitForFirstFrame: false}) async {
rpc.Peer peer;
try {
peer = await _connectToObservatory(observatoryPort);
} catch (e) {
printError('Error connecting to observatory: $e');
return null;
}
Future<Map<String, dynamic>> fetchTimeline() async {
return await peer.sendRequest('_getVMTimeline');
}
Map<String, dynamic> timeline;
if (!waitForFirstFrame) {
// Stop tracing immediately and get the timeline
await peer.sendRequest('_setVMTimelineFlags', <String, dynamic>{'recordedStreams': '[]'});
timeline = await fetchTimeline();
} else {
Completer<Null> whenFirstFrameRendered = new Completer<Null>();
peer.registerMethod('streamNotify', (rpc.Parameters params) {
Map<String, dynamic> data = params.asMap;
if (data['streamId'] == 'Timeline') {
List<Map<String, dynamic>> events = data['event']['timelineEvents'];
for (Map<String, dynamic> event in events) {
if (event['name'] == firstUsefulFrameEventName) {
whenFirstFrameRendered.complete();
}
}
}
});
await peer.sendRequest('streamListen', <String, dynamic>{'streamId': 'Timeline'});
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;
}
);
timeline = await fetchTimeline();
await peer.sendRequest('_setVMTimelineFlags', <String, dynamic>{'recordedStreams': '[]'});
}
return timeline;
}
} }
class DebuggingOptions { class DebuggingOptions {
@ -332,7 +253,7 @@ class DebuggingOptions {
Future<int> findBestObservatoryPort() { Future<int> findBestObservatoryPort() {
if (hasObservatoryPort) if (hasObservatoryPort)
return new Future<int>.value(observatoryPort); return new Future<int>.value(observatoryPort);
return findPreferredPort(observatoryPort ?? defaultObservatoryPort); return findPreferredPort(observatoryPort ?? kDefaultObservatoryPort);
} }
bool get hasDiagnosticPort => diagnosticPort != null; bool get hasDiagnosticPort => diagnosticPort != null;
@ -340,7 +261,7 @@ class DebuggingOptions {
/// Return the user specified diagnostic port. If that isn't available, /// Return the user specified diagnostic port. If that isn't available,
/// return [defaultObservatoryPort], or a port close to that one. /// return [defaultObservatoryPort], or a port close to that one.
Future<int> findBestDiagnosticPort() { Future<int> findBestDiagnosticPort() {
return findPreferredPort(diagnosticPort ?? defaultDiagnosticPort); return findPreferredPort(diagnosticPort ?? kDefaultDiagnosticPort);
} }
} }

View file

@ -15,7 +15,7 @@ import '../build_info.dart';
import '../device.dart'; import '../device.dart';
import '../flx.dart' as flx; import '../flx.dart' as flx;
import '../globals.dart'; import '../globals.dart';
import '../service_protocol.dart'; import '../protocol_discovery.dart';
import 'mac.dart'; import 'mac.dart';
const String _xcrunPath = '/usr/bin/xcrun'; const String _xcrunPath = '/usr/bin/xcrun';
@ -395,20 +395,17 @@ class IOSSimulator extends Device {
RegExp versionExp = new RegExp(r'iPhone ([0-9])+'); RegExp versionExp = new RegExp(r'iPhone ([0-9])+');
Match match = versionExp.firstMatch(name); Match match = versionExp.firstMatch(name);
if (match == null) { // Not an iPhone. All available non-iPhone simulators are compatible.
// Not an iPhone. All available non-iPhone simulators are compatible. if (match == null)
return true; return true;
}
if (int.parse(match.group(1)) > 5) { // iPhones 6 and above are always fine.
// iPhones 6 and above are always fine. if (int.parse(match.group(1)) > 5)
return true; return true;
}
// The 's' subtype of 5 is compatible. // The 's' subtype of 5 is compatible.
if (name.contains('iPhone 5s')) { if (name.contains('iPhone 5s'))
return true; return true;
}
_supportMessage = "The simulator version is too old. Choose an iPhone 5s or above."; _supportMessage = "The simulator version is too old. Choose an iPhone 5s or above.";
return false; return false;
@ -447,12 +444,10 @@ class IOSSimulator extends Device {
if (!(await _setupUpdatedApplicationBundle(app))) if (!(await _setupUpdatedApplicationBundle(app)))
return new LaunchResult.failed(); return new LaunchResult.failed();
ServiceProtocolDiscovery observatoryDiscovery; ProtocolDiscovery observatoryDiscovery;
if (debuggingOptions.debuggingEnabled) { if (debuggingOptions.debuggingEnabled)
observatoryDiscovery = new ServiceProtocolDiscovery( observatoryDiscovery = new ProtocolDiscovery(logReader, ProtocolDiscovery.kObservatoryService);
logReader, ServiceProtocolDiscovery.kObservatoryService);
}
// Prepare launch arguments. // Prepare launch arguments.
List<String> args = <String>[ List<String> args = <String>[

View file

@ -0,0 +1,130 @@
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:web_socket_channel/io.dart';
class Observatory {
Observatory._(this.peer, this.port) {
peer.registerMethod('streamNotify', (rpc.Parameters event) {
_handleStreamNotify(event.asMap);
});
}
static Future<Observatory> connect(int port) async {
Uri uri = new Uri(scheme: 'ws', host: '127.0.0.1', port: port, path: 'ws');
WebSocket ws = await WebSocket.connect(uri.toString());
rpc.Peer peer = new rpc.Peer(new IOWebSocketChannel(ws));
peer.listen();
return new Observatory._(peer, port);
}
final rpc.Peer peer;
final int port;
Map<String, StreamController<Event>> _eventControllers = <String, StreamController<Event>>{};
bool get isClosed => peer.isClosed;
Future<Null> get done => peer.done;
// Events
// IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, ServiceExtensionAdded
Stream<Event> get onIsolateEvent => _getEventController('Isolate').stream;
Stream<Event> get onTimelineEvent => _getEventController('Timeline').stream;
// Listen for a specific event name.
Stream<Event> onEvent(String streamName) => _getEventController(streamName).stream;
StreamController<Event> _getEventController(String eventName) {
StreamController<Event> controller = _eventControllers[eventName];
if (controller == null) {
controller = new StreamController<Event>.broadcast();
_eventControllers[eventName] = controller;
}
return controller;
}
void _handleStreamNotify(Map<String, dynamic> data) {
Event event = new Event(data['event']);
_getEventController(data['streamId']).add(event);
}
// Requests
Future<Response> sendRequest(String method, [Map<String, dynamic> args]) {
return peer.sendRequest(method, args).then((dynamic result) => new Response(result));
}
Future<Response> streamListen(String streamId) {
return sendRequest('streamListen', <String, dynamic>{
'streamId': streamId
});
}
Future<VM> getVM() {
return peer.sendRequest('getVM').then((dynamic result) {
return new VM(result);
});
}
Future<Response> isolateReload(String isolateId) {
return sendRequest('isolateReload', <String, dynamic>{
'isolateId': isolateId
});
}
Future<Response> clearVMTimeline() => sendRequest('_clearVMTimeline');
Future<Response> setVMTimelineFlags(List<String> recordedStreams) {
assert(recordedStreams != null);
return sendRequest('_setVMTimelineFlags', <String, dynamic> {
'recordedStreams': recordedStreams
});
}
Future<Response> getVMTimeline() => sendRequest('_getVMTimeline');
// Flutter extension methods.
Future<Response> flutterExit(String isolateId) {
return peer.sendRequest('ext.flutter.exit', <String, dynamic>{
'isolateId': isolateId
}).then((dynamic result) => new Response(result));
}
}
class Response {
Response(this.response);
final Map<String, dynamic> response;
dynamic operator[](String key) => response[key];
@override
String toString() => response.toString();
}
class VM extends Response {
VM(Map<String, dynamic> response) : super(response);
List<dynamic> get isolates => response['isolates'];
}
class Event {
Event(this.event);
final Map<String, dynamic> event;
String get kind => event['kind'];
dynamic operator[](String key) => event[key];
@override
String toString() => event.toString();
}

View file

@ -7,9 +7,9 @@ import 'dart:async';
import 'device.dart'; import 'device.dart';
/// Discover service protocol ports on devices. /// Discover service protocol ports on devices.
class ServiceProtocolDiscovery { class ProtocolDiscovery {
/// [logReader] - a [DeviceLogReader] to look for service messages in. /// [logReader] - a [DeviceLogReader] to look for service messages in.
ServiceProtocolDiscovery(DeviceLogReader logReader, String serviceName) ProtocolDiscovery(DeviceLogReader logReader, String serviceName)
: _logReader = logReader, _serviceName = serviceName { : _logReader = logReader, _serviceName = serviceName {
assert(_logReader != null); assert(_logReader != null);
_subscription = _logReader.logLines.listen(_onLine); _subscription = _logReader.logLines.listen(_onLine);

View file

@ -23,8 +23,8 @@ import 'install_test.dart' as install_test;
import 'listen_test.dart' as listen_test; import 'listen_test.dart' as listen_test;
import 'logs_test.dart' as logs_test; import 'logs_test.dart' as logs_test;
import 'os_utils_test.dart' as os_utils_test; import 'os_utils_test.dart' as os_utils_test;
import 'protocol_discovery_test.dart' as protocol_discovery_test;
import 'run_test.dart' as run_test; import 'run_test.dart' as run_test;
import 'service_protocol_test.dart' as service_protocol_test;
import 'stop_test.dart' as stop_test; import 'stop_test.dart' as stop_test;
import 'toolchain_test.dart' as toolchain_test; import 'toolchain_test.dart' as toolchain_test;
import 'trace_test.dart' as trace_test; import 'trace_test.dart' as trace_test;
@ -47,8 +47,8 @@ void main() {
listen_test.main(); listen_test.main();
logs_test.main(); logs_test.main();
os_utils_test.main(); os_utils_test.main();
protocol_discovery_test.main();
run_test.main(); run_test.main();
service_protocol_test.main();
stop_test.main(); stop_test.main();
toolchain_test.main(); toolchain_test.main();
trace_test.main(); trace_test.main();

View file

@ -3,9 +3,9 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'package:test/test.dart';
import 'package:flutter_tools/src/service_protocol.dart'; import 'package:flutter_tools/src/protocol_discovery.dart';
import 'package:test/test.dart';
import 'src/mocks.dart'; import 'src/mocks.dart';
@ -13,8 +13,8 @@ void main() {
group('service_protocol', () { group('service_protocol', () {
test('Discovery Heartbeat', () async { test('Discovery Heartbeat', () async {
MockDeviceLogReader logReader = new MockDeviceLogReader(); MockDeviceLogReader logReader = new MockDeviceLogReader();
ServiceProtocolDiscovery discoverer = ProtocolDiscovery discoverer =
new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kObservatoryService); new ProtocolDiscovery(logReader, ProtocolDiscovery.kObservatoryService);
// Get next port future. // Get next port future.
Future<int> nextPort = discoverer.nextPort(); Future<int> nextPort = discoverer.nextPort();