From 40c0d6ea1240366690f6800c91312dfceda77517 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Thu, 12 May 2016 18:15:23 -0700 Subject: [PATCH] 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 --- .../lib/src/android/android_device.dart | 18 +- .../flutter_tools/lib/src/base/common.dart | 11 +- .../flutter_tools/lib/src/base/utils.dart | 5 + packages/flutter_tools/lib/src/cache.dart | 2 +- .../lib/src/commands/analyze.dart | 5 +- .../flutter_tools/lib/src/commands/drive.dart | 2 +- .../flutter_tools/lib/src/commands/run.dart | 316 +++++++++--------- .../flutter_tools/lib/src/commands/skia.dart | 2 +- .../flutter_tools/lib/src/commands/trace.dart | 97 +++++- packages/flutter_tools/lib/src/device.dart | 83 +---- .../flutter_tools/lib/src/ios/simulators.dart | 23 +- .../flutter_tools/lib/src/observatory.dart | 130 +++++++ ..._protocol.dart => protocol_discovery.dart} | 4 +- packages/flutter_tools/test/all.dart | 4 +- ...test.dart => protocol_discovery_test.dart} | 8 +- 15 files changed, 403 insertions(+), 307 deletions(-) create mode 100644 packages/flutter_tools/lib/src/observatory.dart rename packages/flutter_tools/lib/src/{service_protocol.dart => protocol_discovery.dart} (93%) rename packages/flutter_tools/test/{service_protocol_test.dart => protocol_discovery_test.dart} (89%) diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index 80860247084..3304e8f317d 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -14,7 +14,7 @@ import '../build_info.dart'; import '../device.dart'; import '../flx.dart' as flx; import '../globals.dart'; -import '../service_protocol.dart'; +import '../protocol_discovery.dart'; import 'adb.dart'; import 'android.dart'; @@ -243,14 +243,12 @@ class AndroidDevice extends Device { runCheckedSync(adbCommandForDevice(['push', bundlePath, _deviceBundlePath])); - ServiceProtocolDiscovery observatoryDiscovery; - ServiceProtocolDiscovery diagnosticDiscovery; + ProtocolDiscovery observatoryDiscovery; + ProtocolDiscovery diagnosticDiscovery; if (options.debuggingEnabled) { - observatoryDiscovery = new ServiceProtocolDiscovery( - logReader, ServiceProtocolDiscovery.kObservatoryService); - diagnosticDiscovery = new ServiceProtocolDiscovery( - logReader, ServiceProtocolDiscovery.kDiagnosticService); + observatoryDiscovery = new ProtocolDiscovery(logReader, ProtocolDiscovery.kObservatoryService); + diagnosticDiscovery = new ProtocolDiscovery(logReader, ProtocolDiscovery.kDiagnosticService); } List cmd = adbCommandForDevice([ @@ -295,13 +293,11 @@ class AndroidDevice extends Device { int observatoryLocalPort = await options.findBestObservatoryPort(); // TODO(devoncarew): Remember the forwarding information (so we can later remove the // port forwarding). - await _forwardPort(ServiceProtocolDiscovery.kObservatoryService, - observatoryDevicePort, observatoryLocalPort); + await _forwardPort(ProtocolDiscovery.kObservatoryService, observatoryDevicePort, observatoryLocalPort); int diagnosticDevicePort = devicePorts[1]; printTrace('diagnostic port = $diagnosticDevicePort'); int diagnosticLocalPort = await options.findBestDiagnosticPort(); - await _forwardPort(ServiceProtocolDiscovery.kDiagnosticService, - diagnosticDevicePort, diagnosticLocalPort); + await _forwardPort(ProtocolDiscovery.kDiagnosticService, diagnosticDevicePort, diagnosticLocalPort); return new LaunchResult.succeeded( observatoryPort: observatoryLocalPort, diagnosticPort: diagnosticLocalPort diff --git a/packages/flutter_tools/lib/src/base/common.dart b/packages/flutter_tools/lib/src/base/common.dart index d512ca26dda..2725d23f55e 100644 --- a/packages/flutter_tools/lib/src/base/common.dart +++ b/packages/flutter_tools/lib/src/base/common.dart @@ -2,11 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -const int defaultObservatoryPort = 8100; -const int defaultDiagnosticPort = 8101; -const int defaultDrivePort = 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'; +const int kDefaultObservatoryPort = 8100; +const int kDefaultDiagnosticPort = 8101; +const int kDefaultDrivePort = 8183; diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart index a550f9cbbf4..312739a85dd 100644 --- a/packages/flutter_tools/lib/src/base/utils.dart +++ b/packages/flutter_tools/lib/src/base/utils.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io'; 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 /// removed, and calculate a diff of changes when a new list of items is /// available. diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart index 4ec348ff232..791c1eaa748 100644 --- a/packages/flutter_tools/lib/src/cache.dart +++ b/packages/flutter_tools/lib/src/cache.dart @@ -12,7 +12,7 @@ import 'base/logger.dart'; import 'base/os.dart'; import 'globals.dart'; -/// A warpper around the `bin/cache/` directory. +/// A wrapper around the `bin/cache/` directory. class Cache { /// [rootOverride] is configurable for testing. Cache({ Directory rootOverride }) { diff --git a/packages/flutter_tools/lib/src/commands/analyze.dart b/packages/flutter_tools/lib/src/commands/analyze.dart index ed72d523781..c0c4c630b5d 100644 --- a/packages/flutter_tools/lib/src/commands/analyze.dart +++ b/packages/flutter_tools/lib/src/commands/analyze.dart @@ -376,9 +376,8 @@ class AnalyzeCommand extends FlutterCommand { 'time': (stopwatch.elapsedMilliseconds / 1000.0), 'issues': errorCount }; - JsonEncoder encoder = new JsonEncoder.withIndent(' '); - new File(benchmarkOut).writeAsStringSync(encoder.convert(data) + '\n'); - printStatus('Analysis benchmark written to $benchmarkOut.'); + new File(benchmarkOut).writeAsStringSync(toPrettyJson(data)); + printStatus('Analysis benchmark written to $benchmarkOut ($data).'); } } diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index 37745acd5cd..143235a5363 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -62,7 +62,7 @@ class DriveCommand extends RunCommandBase { ); argParser.addOption('debug-port', - defaultsTo: defaultDrivePort.toString(), + defaultsTo: kDefaultDrivePort.toString(), help: 'Listen to the given port for a debug connection.' ); } diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index a1fe71bb8a6..def7580ed94 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as path; @@ -11,12 +10,15 @@ import 'package:path/path.dart' as path; import '../application_package.dart'; import '../base/common.dart'; import '../base/logger.dart'; +import '../base/utils.dart'; import '../build_info.dart'; import '../device.dart'; import '../globals.dart'; +import '../observatory.dart'; import '../runner/flutter_command.dart'; import 'build_apk.dart'; import 'install.dart'; +import 'trace.dart'; abstract class RunCommandBase extends FlutterCommand { RunCommandBase() { @@ -62,7 +64,7 @@ class RunCommand extends RunCommandBase { negatable: false, help: 'Start in a paused mode and wait for a debugger to connect.'); 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(); // A temporary, hidden flag to experiment with a different run style. @@ -72,6 +74,12 @@ class RunCommand extends RunCommandBase { negatable: false, hide: true, 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 @@ -107,7 +115,7 @@ class RunCommand extends RunCommandBase { options = new DebuggingOptions.disabled(); } else { options = new DebuggingOptions.enabled( - // TODO(devoncarew): Check this to 'getBuildMode() == BuildMode.debug'. + // TODO(devoncarew): Change this to 'getBuildMode() == BuildMode.debug'. checked: argResults['checked'], startPaused: argResults['start-paused'], observatoryPort: debugPort @@ -119,11 +127,10 @@ class RunCommand extends RunCommandBase { deviceForCommand, target: target, debuggingOptions: options, - traceStartup: traceStartup, buildMode: getBuildMode() ); - return runner.run(); + return runner.run(traceStartup: traceStartup, benchmark: argResults['benchmark']); } else { return startApp( deviceForCommand, @@ -132,6 +139,7 @@ class RunCommand extends RunCommandBase { install: true, debuggingOptions: options, traceStartup: traceStartup, + benchmark: argResults['benchmark'], route: route, buildMode: getBuildMode() ); @@ -146,6 +154,7 @@ Future startApp( bool install: true, DebuggingOptions debuggingOptions, bool traceStartup: false, + bool benchmark: false, String route, BuildMode buildMode: BuildMode.debug }) async { @@ -169,6 +178,8 @@ Future startApp( return 1; } + Stopwatch stopwatch = new Stopwatch()..start(); + // TODO(devoncarew): We shouldn't have to do type checks here. if (install && device is AndroidDevice) { printTrace('Running build command.'); @@ -221,10 +232,22 @@ Future startApp( platformArgs: platformArgs ); - if (!result.started) + stopwatch.stop(); + + if (!result.started) { printError('Error running application on ${device.name}.'); - else if (traceStartup) - await _downloadStartupTrace(result.observatoryPort, device); + } else if (traceStartup) { + 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; } @@ -241,87 +264,6 @@ String findMainDartFile([String target]) { return targetPath; } -Future _downloadStartupTrace(int observatoryPort, Device device) async { - Map timeline = await device.stopTracingAndDownloadTimeline( - observatoryPort, - waitForFirstFrame: true - ); - - int extractInstantEventTimestamp(String eventName) { - List> events = timeline['traceEvents']; - Map event = events - .firstWhere((Map 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 traceInfo = { - '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 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.delayed(new Duration(milliseconds: 250)); - } - } - - printTrace('Unable to connect to the observatory.'); -} - String _getMissingPackageHintForPlatform(TargetPlatform platform) { switch (platform) { case TargetPlatform.android_arm: @@ -346,25 +288,22 @@ class _RunAndStayResident { this.device, { this.target, this.debuggingOptions, - this.traceStartup : false, this.buildMode : BuildMode.debug }); final Device device; final String target; final DebuggingOptions debuggingOptions; - final bool traceStartup; final BuildMode buildMode; Completer _exitCompleter; StreamSubscription _loggingSubscription; - WebSocket _observatoryConnection; + Observatory observatory; String _isolateId; - int _messageId = 0; /// Start the app and keep the process running during its lifetime. - Future run() async { + Future run({ bool traceStartup: false, bool benchmark: false }) async { String mainPath = findMainDartFile(target); if (!FileSystemEntity.isFileSync(mainPath)) { String message = 'Tried to run $mainPath, but that file does not exist.'; @@ -385,6 +324,8 @@ class _RunAndStayResident { return 1; } + Stopwatch stopwatch = new Stopwatch()..start(); + // TODO(devoncarew): We shouldn't have to do type checks here. if (device is AndroidDevice) { printTrace('Running build command.'); @@ -440,89 +381,79 @@ class _RunAndStayResident { return 2; } + stopwatch.stop(); + _exitCompleter = new Completer(); // Connect to observatory. if (debuggingOptions.debuggingEnabled) { - final String localhost = InternetAddress.LOOPBACK_IP_V4.address; - final String url = 'ws://$localhost:${result.observatoryPort}/ws'; - - _observatoryConnection = await WebSocket.connect(url); + observatory = await Observatory.connect(result.observatoryPort); printTrace('Connected to observatory port: ${result.observatoryPort}.'); - // Listen for observatory connection close. - _observatoryConnection.listen((dynamic data) { - if (data is String) { - Map json = JSON.decode(data); + observatory.onIsolateEvent.listen((Event event) { + if (event['isolate'] != null) + _isolateId = event['isolate']['id']; + }); + observatory.streamListen('Isolate'); - if (json['method'] == 'streamNotify') { - Map event = json['params']['event']; - 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 isolates = json['result']['isolates']; - if (isolates.isNotEmpty) - _isolateId = isolates.first['id']; - } else if (json['error'] != null) { - printError('Error: ${json['error']['message']}.'); - printTrace(data); - } - } - }, onDone: () { + // Listen for observatory connection close. + observatory.done.whenComplete(() { _handleExit(); }); - _observatoryConnection.add(JSON.encode({ - 'method': 'streamListen', - 'params': { 'streamId': 'Isolate' }, - 'id': _messageId++ - })); - - _observatoryConnection.add(JSON.encode({ - 'method': 'getVM', - 'id': _messageId++ - })); + observatory.getVM().then((VM vm) { + if (vm.isolates.isNotEmpty) + _isolateId = vm.isolates.first['id']; + }); } printStatus('Application running.'); - _printHelp(); - terminal.singleCharMode = true; + if (observatory != null && traceStartup) { + printStatus('Downloading startup trace info...'); - terminal.onCharInput.listen((String code) { - String lower = code.toLowerCase(); + await _downloadStartupTrace(observatory); - 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(); + } else { + _printHelp(); + + terminal.singleCharMode = true; + + terminal.onCharInput.listen((String code) { + 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(); - } - }); + }); + ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) { + _handleExit(); + }); + } - ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) { - _handleExit(); - }); - ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) { - _handleExit(); - }); + if (benchmark) { + _writeBenchmark(stopwatch); + new Future.delayed(new Duration(seconds: 2)).then((_) { + _handleExit(); + }); + } return _exitCompleter.future.then((int exitCode) async { - if (_observatoryConnection != null && - _observatoryConnection.readyState == WebSocket.OPEN && - _isolateId != null) { - _observatoryConnection.add(JSON.encode({ - 'method': 'ext.flutter.exit', - 'params': { 'isolateId': _isolateId }, - 'id': _messageId++ - })); + if (observatory != null && !observatory.isClosed && _isolateId != null) { + observatory.flutterExit(_isolateId); + // WebSockets do not have a flush() method. await new Future.delayed(new Duration(milliseconds: 100)); } @@ -536,27 +467,80 @@ class _RunAndStayResident { } void _handleRefresh() { - if (_observatoryConnection == null) { + if (observatory == null) { printError('Debugging is not enabled.'); } else { printStatus('Re-starting application...'); - // TODO(devoncarew): Show an error if the isolate reload fails. - _observatoryConnection.add(JSON.encode({ - 'method': 'isolateReload', - 'params': { 'isolateId': _isolateId }, - 'id': _messageId++ - })); + observatory.isolateReload(_isolateId).catchError((dynamic error) { + printError('Error restarting app: $error'); + }); } } void _handleExit() { + terminal.singleCharMode = false; + if (!_exitCompleter.isCompleted) { _loggingSubscription?.cancel(); - printStatus(''); printStatus('Application finished.'); - terminal.singleCharMode = false; _exitCompleter.complete(0); } } } + +Future _downloadStartupTrace(Observatory observatory) async { + Tracing tracing = new Tracing(observatory); + + Map timeline = await tracing.stopTracingAndDownloadTimeline( + waitForFirstFrame: true + ); + + int extractInstantEventTimestamp(String eventName) { + List> events = timeline['traceEvents']; + Map event = events.firstWhere( + (Map 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 traceInfo = { + '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 data = { + 'time': stopwatch.elapsedMilliseconds + }; + new File(benchmarkOut).writeAsStringSync(toPrettyJson(data)); + printStatus('Run benchmark written to $benchmarkOut ($data).'); +} diff --git a/packages/flutter_tools/lib/src/commands/skia.dart b/packages/flutter_tools/lib/src/commands/skia.dart index e5a64312d41..4ff7f0666c6 100644 --- a/packages/flutter_tools/lib/src/commands/skia.dart +++ b/packages/flutter_tools/lib/src/commands/skia.dart @@ -16,7 +16,7 @@ class SkiaCommand extends FlutterCommand { 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('diagnostic-port', - defaultsTo: defaultDiagnosticPort.toString(), + defaultsTo: kDefaultDiagnosticPort.toString(), help: 'Local port where the diagnostic server is listening.'); } diff --git a/packages/flutter_tools/lib/src/commands/trace.dart b/packages/flutter_tools/lib/src/commands/trace.dart index 6e4e437fd10..bed77a924ff 100644 --- a/packages/flutter_tools/lib/src/commands/trace.dart +++ b/packages/flutter_tools/lib/src/commands/trace.dart @@ -8,10 +8,15 @@ import 'dart:io'; import '../base/common.dart'; import '../base/utils.dart'; -import '../device.dart'; import '../globals.dart'; +import '../observatory.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 { TraceCommand() { argParser.addFlag('start', negatable: false, help: 'Start tracing.'); @@ -20,7 +25,7 @@ class TraceCommand extends FlutterCommand { argParser.addOption('duration', defaultsTo: '10', abbr: 'd', help: 'Duration in seconds to trace.'); argParser.addOption('debug-port', - defaultsTo: defaultObservatoryPort.toString(), + defaultsTo: kDefaultObservatoryPort.toString(), help: 'Local port where the observatory is listening.'); } @@ -41,38 +46,104 @@ class TraceCommand extends FlutterCommand { @override Future runInProject() async { - Device device = deviceForCommand; 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']) || (argResults['start'] && argResults['stop'])) { // Setting neither flags or both flags means do both commands and wait // duration seconds in between. - await device.startTracing(observatoryPort); + await tracing.startTracing(); await new Future.delayed( new Duration(seconds: int.parse(argResults['duration'])), - () => _stopTracing(device, observatoryPort) + () => _stopTracing(tracing) ); } else if (argResults['stop']) { - await _stopTracing(device, observatoryPort); + await _stopTracing(tracing); } else { - await device.startTracing(observatoryPort); + await tracing.startTracing(); } + return 0; } - Future _stopTracing(Device device, int observatoryPort) async { - Map timeline = await device.stopTracingAndDownloadTimeline(observatoryPort); - - String outPath = argResults['out']; + Future _stopTracing(Tracing tracing) async { + Map timeline = await tracing.stopTracingAndDownloadTimeline(); File localFile; - if (outPath != null) { - localFile = new File(outPath); + + if (argResults['out'] != null) { + localFile = new File(argResults['out']); } else { localFile = getUniqueFile(Directory.current, 'trace', 'json'); } await localFile.writeAsString(JSON.encode(timeline)); + printStatus('Trace file saved to ${localFile.path}'); } } + +class Tracing { + Tracing(this.observatory); + + static Future connect(int port) { + return Observatory.connect(port).then((Observatory observatory) => new Tracing(observatory)); + } + + final Observatory observatory; + + Future startTracing() async { + await observatory.setVMTimelineFlags(['Compiler', 'Dart', 'Embedder', 'GC']); + await observatory.clearVMTimeline(); + } + + /// Stops tracing; optionally wait for first frame. + Future> stopTracingAndDownloadTimeline({ + bool waitForFirstFrame: false + }) async { + Response timeline; + + if (!waitForFirstFrame) { + // Stop tracing immediately and get the timeline + await observatory.setVMTimelineFlags([]); + timeline = await observatory.getVMTimeline(); + } else { + Completer whenFirstFrameRendered = new Completer(); + + observatory.onTimelineEvent.listen((Event timelineEvent) { + List> events = timelineEvent['timelineEvents']; + for (Map 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([]); + } + + return timeline.response; + } +} diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index ce443cafd82..519cd81b1a1 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -6,9 +6,6 @@ import 'dart:async'; import 'dart:io'; 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 'application_package.dart'; import 'base/common.dart'; @@ -225,82 +222,6 @@ abstract class Device { '${getNameForTargetPlatform(device.platform)}$supportIndicator'); } } - - - Future _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 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', - {'recordedStreams': ['Compiler', 'Dart', 'Embedder', 'GC']} - ); - await client.sendRequest('_clearVMTimeline'); - } - - /// Stops tracing, optionally waiting - Future> 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> fetchTimeline() async { - return await peer.sendRequest('_getVMTimeline'); - } - - Map timeline; - - if (!waitForFirstFrame) { - // Stop tracing immediately and get the timeline - await peer.sendRequest('_setVMTimelineFlags', {'recordedStreams': '[]'}); - timeline = await fetchTimeline(); - } else { - Completer whenFirstFrameRendered = new Completer(); - peer.registerMethod('streamNotify', (rpc.Parameters params) { - Map data = params.asMap; - if (data['streamId'] == 'Timeline') { - List> events = data['event']['timelineEvents']; - for (Map event in events) { - if (event['name'] == firstUsefulFrameEventName) { - whenFirstFrameRendered.complete(); - } - } - } - }); - await peer.sendRequest('streamListen', {'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', {'recordedStreams': '[]'}); - } - - return timeline; - } } class DebuggingOptions { @@ -332,7 +253,7 @@ class DebuggingOptions { Future findBestObservatoryPort() { if (hasObservatoryPort) return new Future.value(observatoryPort); - return findPreferredPort(observatoryPort ?? defaultObservatoryPort); + return findPreferredPort(observatoryPort ?? kDefaultObservatoryPort); } bool get hasDiagnosticPort => diagnosticPort != null; @@ -340,7 +261,7 @@ class DebuggingOptions { /// Return the user specified diagnostic port. If that isn't available, /// return [defaultObservatoryPort], or a port close to that one. Future findBestDiagnosticPort() { - return findPreferredPort(diagnosticPort ?? defaultDiagnosticPort); + return findPreferredPort(diagnosticPort ?? kDefaultDiagnosticPort); } } diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 2494cc972ef..02e47b11b68 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -15,7 +15,7 @@ import '../build_info.dart'; import '../device.dart'; import '../flx.dart' as flx; import '../globals.dart'; -import '../service_protocol.dart'; +import '../protocol_discovery.dart'; import 'mac.dart'; const String _xcrunPath = '/usr/bin/xcrun'; @@ -395,20 +395,17 @@ class IOSSimulator extends Device { RegExp versionExp = new RegExp(r'iPhone ([0-9])+'); 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; - } - 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; - } // The 's' subtype of 5 is compatible. - if (name.contains('iPhone 5s')) { + if (name.contains('iPhone 5s')) return true; - } _supportMessage = "The simulator version is too old. Choose an iPhone 5s or above."; return false; @@ -447,12 +444,10 @@ class IOSSimulator extends Device { if (!(await _setupUpdatedApplicationBundle(app))) return new LaunchResult.failed(); - ServiceProtocolDiscovery observatoryDiscovery; + ProtocolDiscovery observatoryDiscovery; - if (debuggingOptions.debuggingEnabled) { - observatoryDiscovery = new ServiceProtocolDiscovery( - logReader, ServiceProtocolDiscovery.kObservatoryService); - } + if (debuggingOptions.debuggingEnabled) + observatoryDiscovery = new ProtocolDiscovery(logReader, ProtocolDiscovery.kObservatoryService); // Prepare launch arguments. List args = [ diff --git a/packages/flutter_tools/lib/src/observatory.dart b/packages/flutter_tools/lib/src/observatory.dart new file mode 100644 index 00000000000..27b83a46b5e --- /dev/null +++ b/packages/flutter_tools/lib/src/observatory.dart @@ -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 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> _eventControllers = >{}; + + bool get isClosed => peer.isClosed; + Future get done => peer.done; + + // Events + + // IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, ServiceExtensionAdded + Stream get onIsolateEvent => _getEventController('Isolate').stream; + Stream get onTimelineEvent => _getEventController('Timeline').stream; + + // Listen for a specific event name. + Stream onEvent(String streamName) => _getEventController(streamName).stream; + + StreamController _getEventController(String eventName) { + StreamController controller = _eventControllers[eventName]; + if (controller == null) { + controller = new StreamController.broadcast(); + _eventControllers[eventName] = controller; + } + return controller; + } + + void _handleStreamNotify(Map data) { + Event event = new Event(data['event']); + _getEventController(data['streamId']).add(event); + } + + // Requests + + Future sendRequest(String method, [Map args]) { + return peer.sendRequest(method, args).then((dynamic result) => new Response(result)); + } + + Future streamListen(String streamId) { + return sendRequest('streamListen', { + 'streamId': streamId + }); + } + + Future getVM() { + return peer.sendRequest('getVM').then((dynamic result) { + return new VM(result); + }); + } + + Future isolateReload(String isolateId) { + return sendRequest('isolateReload', { + 'isolateId': isolateId + }); + } + + Future clearVMTimeline() => sendRequest('_clearVMTimeline'); + + Future setVMTimelineFlags(List recordedStreams) { + assert(recordedStreams != null); + + return sendRequest('_setVMTimelineFlags', { + 'recordedStreams': recordedStreams + }); + } + + Future getVMTimeline() => sendRequest('_getVMTimeline'); + + // Flutter extension methods. + + Future flutterExit(String isolateId) { + return peer.sendRequest('ext.flutter.exit', { + 'isolateId': isolateId + }).then((dynamic result) => new Response(result)); + } +} + +class Response { + Response(this.response); + + final Map response; + + dynamic operator[](String key) => response[key]; + + @override + String toString() => response.toString(); +} + +class VM extends Response { + VM(Map response) : super(response); + + List get isolates => response['isolates']; +} + +class Event { + Event(this.event); + + final Map event; + + String get kind => event['kind']; + + dynamic operator[](String key) => event[key]; + + @override + String toString() => event.toString(); +} diff --git a/packages/flutter_tools/lib/src/service_protocol.dart b/packages/flutter_tools/lib/src/protocol_discovery.dart similarity index 93% rename from packages/flutter_tools/lib/src/service_protocol.dart rename to packages/flutter_tools/lib/src/protocol_discovery.dart index 1ce825a6959..7c27b137e5d 100644 --- a/packages/flutter_tools/lib/src/service_protocol.dart +++ b/packages/flutter_tools/lib/src/protocol_discovery.dart @@ -7,9 +7,9 @@ import 'dart:async'; import 'device.dart'; /// Discover service protocol ports on devices. -class ServiceProtocolDiscovery { +class ProtocolDiscovery { /// [logReader] - a [DeviceLogReader] to look for service messages in. - ServiceProtocolDiscovery(DeviceLogReader logReader, String serviceName) + ProtocolDiscovery(DeviceLogReader logReader, String serviceName) : _logReader = logReader, _serviceName = serviceName { assert(_logReader != null); _subscription = _logReader.logLines.listen(_onLine); diff --git a/packages/flutter_tools/test/all.dart b/packages/flutter_tools/test/all.dart index 1d1afebb6ec..d929af8fb33 100644 --- a/packages/flutter_tools/test/all.dart +++ b/packages/flutter_tools/test/all.dart @@ -23,8 +23,8 @@ import 'install_test.dart' as install_test; import 'listen_test.dart' as listen_test; import 'logs_test.dart' as logs_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 'service_protocol_test.dart' as service_protocol_test; import 'stop_test.dart' as stop_test; import 'toolchain_test.dart' as toolchain_test; import 'trace_test.dart' as trace_test; @@ -47,8 +47,8 @@ void main() { listen_test.main(); logs_test.main(); os_utils_test.main(); + protocol_discovery_test.main(); run_test.main(); - service_protocol_test.main(); stop_test.main(); toolchain_test.main(); trace_test.main(); diff --git a/packages/flutter_tools/test/service_protocol_test.dart b/packages/flutter_tools/test/protocol_discovery_test.dart similarity index 89% rename from packages/flutter_tools/test/service_protocol_test.dart rename to packages/flutter_tools/test/protocol_discovery_test.dart index 567517bb31a..bd0317d2ac3 100644 --- a/packages/flutter_tools/test/service_protocol_test.dart +++ b/packages/flutter_tools/test/protocol_discovery_test.dart @@ -3,9 +3,9 @@ // found in the LICENSE file. 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'; @@ -13,8 +13,8 @@ void main() { group('service_protocol', () { test('Discovery Heartbeat', () async { MockDeviceLogReader logReader = new MockDeviceLogReader(); - ServiceProtocolDiscovery discoverer = - new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kObservatoryService); + ProtocolDiscovery discoverer = + new ProtocolDiscovery(logReader, ProtocolDiscovery.kObservatoryService); // Get next port future. Future nextPort = discoverer.nextPort();