Collect trace data through the observatory HTTP interface (#3393)
This commit is contained in:
@@ -6,13 +6,16 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
|
||||
import '../android/android_sdk.dart';
|
||||
import '../application_package.dart';
|
||||
import '../base/common.dart';
|
||||
import '../base/os.dart';
|
||||
import '../base/process.dart';
|
||||
import '../base/utils.dart';
|
||||
import '../build_configuration.dart';
|
||||
import '../device.dart';
|
||||
import '../flx.dart' as flx;
|
||||
@@ -348,16 +351,6 @@ class AndroidDevice extends Device {
|
||||
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
|
||||
/// no available timestamp. The format can be passed to logcat's -T option.
|
||||
String get lastLogcatTimestamp {
|
||||
@@ -370,59 +363,57 @@ class AndroidDevice extends Device {
|
||||
return timeMatch?.group(0);
|
||||
}
|
||||
|
||||
Future<String> stopTracing(AndroidApk apk, { String outPath }) async {
|
||||
// Workaround for logcat -c not always working:
|
||||
// http://stackoverflow.com/questions/25645012/logcat-on-android-l-not-clearing-after-unplugging-and-reconnecting
|
||||
String beforeStop = lastLogcatTimestamp;
|
||||
runCheckedSync(adbCommandForDevice(<String>[
|
||||
'shell',
|
||||
'am',
|
||||
'broadcast',
|
||||
'-a',
|
||||
'${apk.id}.TRACING_STOP'
|
||||
]));
|
||||
Future<rpc.Client> _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.Client client = new rpc.Client(new IOWebSocketChannel(ws));
|
||||
client.listen();
|
||||
return client;
|
||||
}
|
||||
|
||||
RegExp traceRegExp = new RegExp(r'Saving trace to (\S+)', multiLine: true);
|
||||
RegExp completeRegExp = new RegExp(r'Trace complete', multiLine: true);
|
||||
|
||||
String tracePath;
|
||||
bool isComplete = false;
|
||||
while (!isComplete) {
|
||||
List<String> args = <String>['logcat', '-d'];
|
||||
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<Null> startTracing(AndroidApk apk, int observatoryPort) async {
|
||||
rpc.Client client;
|
||||
try {
|
||||
client = await _connectToObservatory(observatoryPort);
|
||||
} catch (e) {
|
||||
printError('Error connecting to observatory: $e');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tracePath != null) {
|
||||
String localPath = (outPath != null) ? outPath : path.basename(tracePath);
|
||||
await client.sendRequest('_setVMTimelineFlags',
|
||||
{'recordedStreams': ['Compiler', 'Dart', 'Embedder', 'GC']}
|
||||
);
|
||||
await client.sendRequest('_clearVMTimeline');
|
||||
}
|
||||
|
||||
// Run cat via ADB to print the captured trace file. (adb pull will be unable
|
||||
// to access the file if it does not have root permissions)
|
||||
IOSink catOutput = new File(localPath).openWrite();
|
||||
List<String> catCommand = adbCommandForDevice(
|
||||
<String>['shell', 'run-as', apk.id, 'cat', tracePath]
|
||||
);
|
||||
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;
|
||||
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;
|
||||
}
|
||||
printError('No trace file detected. '
|
||||
'Did you remember to start the trace before stopping it?');
|
||||
return null;
|
||||
|
||||
await client.sendRequest('_setVMTimelineFlags', {'recordedStreams': '[]'});
|
||||
|
||||
File localFile;
|
||||
if (outPath != null) {
|
||||
localFile = new File(outPath);
|
||||
} else {
|
||||
localFile = getUniqueFile(Directory.current, 'trace', 'json');
|
||||
}
|
||||
|
||||
Map<String, dynamic> response = await client.sendRequest('_getVMTimeline');
|
||||
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
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'dart:async';
|
||||
|
||||
import '../android/android_device.dart';
|
||||
import '../application_package.dart';
|
||||
import '../base/common.dart';
|
||||
import '../globals.dart';
|
||||
import '../runner/flutter_command.dart';
|
||||
|
||||
@@ -16,6 +17,9 @@ class TraceCommand extends FlutterCommand {
|
||||
argParser.addOption('out', help: 'Specify the path of the saved trace file.');
|
||||
argParser.addOption('duration',
|
||||
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
|
||||
@@ -40,26 +44,27 @@ class TraceCommand extends FlutterCommand {
|
||||
Future<int> runInProject() async {
|
||||
ApplicationPackage androidApp = applicationPackages.android;
|
||||
AndroidDevice device = deviceForCommand;
|
||||
int observatoryPort = int.parse(argResults['debug-port']);
|
||||
|
||||
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.
|
||||
device.startTracing(androidApp);
|
||||
await device.startTracing(androidApp, observatoryPort);
|
||||
await new Future<Null>.delayed(
|
||||
new Duration(seconds: int.parse(argResults['duration'])),
|
||||
() => _stopTracing(device, androidApp)
|
||||
() => _stopTracing(device, androidApp, observatoryPort)
|
||||
);
|
||||
} else if (argResults['stop']) {
|
||||
await _stopTracing(device, androidApp);
|
||||
await _stopTracing(device, androidApp, observatoryPort);
|
||||
} else {
|
||||
device.startTracing(androidApp);
|
||||
await device.startTracing(androidApp, observatoryPort);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Future<Null> _stopTracing(AndroidDevice android, AndroidApk androidApp) async {
|
||||
String tracePath = await android.stopTracing(androidApp, outPath: argResults['out']);
|
||||
Future<Null> _stopTracing(AndroidDevice android, AndroidApk androidApp, int observatoryPort) async {
|
||||
String tracePath = await android.stopTracing(androidApp, observatoryPort, argResults['out']);
|
||||
if (tracePath == null) {
|
||||
printError('No trace file saved.');
|
||||
} else {
|
||||
|
||||
@@ -13,14 +13,16 @@ dependencies:
|
||||
crypto: 0.9.2
|
||||
file: ^0.1.0
|
||||
http: ^0.11.3
|
||||
json_rpc_2: ^2.0.0
|
||||
json_schema: ^1.0.3
|
||||
mustache4dart: ^1.0.0
|
||||
package_config: ^0.1.3
|
||||
path: ^1.3.0
|
||||
pub_semver: ^1.0.0
|
||||
stack_trace: ^1.4.0
|
||||
yaml: ^2.1.3
|
||||
web_socket_channel: ^1.0.0
|
||||
xml: ^2.4.1
|
||||
yaml: ^2.1.3
|
||||
|
||||
flx:
|
||||
path: ../flx
|
||||
|
||||
Reference in New Issue
Block a user