Commit 8451b669 authored by Jason Simmons's avatar Jason Simmons

Collect trace data through the observatory HTTP interface (#3393)

parent 61605a9d
...@@ -6,13 +6,16 @@ import 'dart:async'; ...@@ -6,13 +6,16 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:web_socket_channel/io.dart';
import '../android/android_sdk.dart'; import '../android/android_sdk.dart';
import '../application_package.dart'; import '../application_package.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/os.dart'; import '../base/os.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../base/utils.dart';
import '../build_configuration.dart'; import '../build_configuration.dart';
import '../device.dart'; import '../device.dart';
import '../flx.dart' as flx; import '../flx.dart' as flx;
...@@ -348,16 +351,6 @@ class AndroidDevice extends Device { ...@@ -348,16 +351,6 @@ class AndroidDevice extends Device {
return _portForwarder; return _portForwarder;
} }
void startTracing(AndroidApk apk) {
runCheckedSync(adbCommandForDevice(<String>[
'shell',
'am',
'broadcast',
'-a',
'${apk.id}.TRACING_START'
]));
}
/// Return the most recent timestamp in the Android log or `null` if there is /// Return the most recent timestamp in the Android log or `null` if there is
/// no available timestamp. The format can be passed to logcat's -T option. /// no available timestamp. The format can be passed to logcat's -T option.
String get lastLogcatTimestamp { String get lastLogcatTimestamp {
...@@ -370,59 +363,57 @@ class AndroidDevice extends Device { ...@@ -370,59 +363,57 @@ class AndroidDevice extends Device {
return timeMatch?.group(0); return timeMatch?.group(0);
} }
Future<String> stopTracing(AndroidApk apk, { String outPath }) async { Future<rpc.Client> _connectToObservatory(int observatoryPort) async {
// Workaround for logcat -c not always working: Uri uri = new Uri(scheme: 'ws', host: '127.0.0.1', port: observatoryPort, path: 'ws');
// http://stackoverflow.com/questions/25645012/logcat-on-android-l-not-clearing-after-unplugging-and-reconnecting WebSocket ws = await WebSocket.connect(uri.toString());
String beforeStop = lastLogcatTimestamp; rpc.Client client = new rpc.Client(new IOWebSocketChannel(ws));
runCheckedSync(adbCommandForDevice(<String>[ client.listen();
'shell', return client;
'am', }
'broadcast',
'-a',
'${apk.id}.TRACING_STOP'
]));
RegExp traceRegExp = new RegExp(r'Saving trace to (\S+)', multiLine: true); Future<Null> startTracing(AndroidApk apk, int observatoryPort) async {
RegExp completeRegExp = new RegExp(r'Trace complete', multiLine: true); rpc.Client client;
try {
client = await _connectToObservatory(observatoryPort);
} catch (e) {
printError('Error connecting to observatory: $e');
return;
}
String tracePath; await client.sendRequest('_setVMTimelineFlags',
bool isComplete = false; {'recordedStreams': ['Compiler', 'Dart', 'Embedder', 'GC']}
while (!isComplete) { );
List<String> args = <String>['logcat', '-d']; await client.sendRequest('_clearVMTimeline');
if (beforeStop != null)
args.addAll(<String>['-T', beforeStop]);
String logs = runCheckedSync(adbCommandForDevice(args));
Match fileMatch = traceRegExp.firstMatch(logs);
if (fileMatch != null && fileMatch[1] != null) {
tracePath = fileMatch[1];
} }
isComplete = completeRegExp.hasMatch(logs);
Future<String> stopTracing(AndroidApk apk, int observatoryPort, String outPath) async {
rpc.Client client;
try {
client = await _connectToObservatory(observatoryPort);
} catch (e) {
printError('Error connecting to observatory: $e');
return null;
} }
if (tracePath != null) { await client.sendRequest('_setVMTimelineFlags', {'recordedStreams': '[]'});
String localPath = (outPath != null) ? outPath : path.basename(tracePath);
// Run cat via ADB to print the captured trace file. (adb pull will be unable File localFile;
// to access the file if it does not have root permissions) if (outPath != null) {
IOSink catOutput = new File(localPath).openWrite(); localFile = new File(outPath);
List<String> catCommand = adbCommandForDevice( } else {
<String>['shell', 'run-as', apk.id, 'cat', tracePath] localFile = getUniqueFile(Directory.current, 'trace', 'json');
);
Process catProcess = await Process.start(catCommand[0],
catCommand.getRange(1, catCommand.length).toList());
catProcess.stdout.pipe(catOutput);
int exitCode = await catProcess.exitCode;
if (exitCode != 0)
throw 'Error code $exitCode returned when running ${catCommand.join(" ")}';
runSync(adbCommandForDevice(
<String>['shell', 'run-as', apk.id, 'rm', tracePath]
));
return localPath;
} }
printError('No trace file detected. '
'Did you remember to start the trace before stopping it?'); Map<String, dynamic> response = await client.sendRequest('_getVMTimeline');
return null; List<dynamic> traceEvents = response['traceEvents'];
IOSink sink = localFile.openWrite();
Stream<Object> streamIn = new Stream<Object>.fromIterable(<Object>[traceEvents]);
Stream<List<int>> streamOut = new JsonUtf8Encoder().bind(streamIn);
await sink.addStream(streamOut);
await sink.close();
return path.basename(localFile.path);
} }
@override @override
......
...@@ -6,6 +6,7 @@ import 'dart:async'; ...@@ -6,6 +6,7 @@ import 'dart:async';
import '../android/android_device.dart'; import '../android/android_device.dart';
import '../application_package.dart'; import '../application_package.dart';
import '../base/common.dart';
import '../globals.dart'; import '../globals.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
...@@ -16,6 +17,9 @@ class TraceCommand extends FlutterCommand { ...@@ -16,6 +17,9 @@ class TraceCommand extends FlutterCommand {
argParser.addOption('out', help: 'Specify the path of the saved trace file.'); argParser.addOption('out', help: 'Specify the path of the saved trace file.');
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',
defaultsTo: observatoryDefaultPort.toString(),
help: 'Local port where the observatory is listening.');
} }
@override @override
...@@ -40,26 +44,27 @@ class TraceCommand extends FlutterCommand { ...@@ -40,26 +44,27 @@ class TraceCommand extends FlutterCommand {
Future<int> runInProject() async { Future<int> runInProject() async {
ApplicationPackage androidApp = applicationPackages.android; ApplicationPackage androidApp = applicationPackages.android;
AndroidDevice device = deviceForCommand; AndroidDevice device = deviceForCommand;
int observatoryPort = int.parse(argResults['debug-port']);
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.
device.startTracing(androidApp); await device.startTracing(androidApp, observatoryPort);
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, androidApp) () => _stopTracing(device, androidApp, observatoryPort)
); );
} else if (argResults['stop']) { } else if (argResults['stop']) {
await _stopTracing(device, androidApp); await _stopTracing(device, androidApp, observatoryPort);
} else { } else {
device.startTracing(androidApp); await device.startTracing(androidApp, observatoryPort);
} }
return 0; return 0;
} }
Future<Null> _stopTracing(AndroidDevice android, AndroidApk androidApp) async { Future<Null> _stopTracing(AndroidDevice android, AndroidApk androidApp, int observatoryPort) async {
String tracePath = await android.stopTracing(androidApp, outPath: argResults['out']); String tracePath = await android.stopTracing(androidApp, observatoryPort, argResults['out']);
if (tracePath == null) { if (tracePath == null) {
printError('No trace file saved.'); printError('No trace file saved.');
} else { } else {
......
...@@ -13,14 +13,16 @@ dependencies: ...@@ -13,14 +13,16 @@ dependencies:
crypto: 0.9.2 crypto: 0.9.2
file: ^0.1.0 file: ^0.1.0
http: ^0.11.3 http: ^0.11.3
json_rpc_2: ^2.0.0
json_schema: ^1.0.3 json_schema: ^1.0.3
mustache4dart: ^1.0.0 mustache4dart: ^1.0.0
package_config: ^0.1.3 package_config: ^0.1.3
path: ^1.3.0 path: ^1.3.0
pub_semver: ^1.0.0 pub_semver: ^1.0.0
stack_trace: ^1.4.0 stack_trace: ^1.4.0
yaml: ^2.1.3 web_socket_channel: ^1.0.0
xml: ^2.4.1 xml: ^2.4.1
yaml: ^2.1.3
flx: flx:
path: ../flx path: ../flx
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment