[driver] API for getting performance traces
diff --git a/examples/stocks/test_driver/scroll_perf.dart b/examples/stocks/test_driver/scroll_perf.dart
index 623f7b7..8188a55 100644
--- a/examples/stocks/test_driver/scroll_perf.dart
+++ b/examples/stocks/test_driver/scroll_perf.dart
@@ -3,11 +3,9 @@
 // found in the LICENSE file.
 
 import 'package:flutter_driver/driver_extension.dart';
-import 'package:flutter_driver/src/error.dart';
 import 'package:stocks/main.dart' as app;
 
 void main() {
-  flutterDriverLog.listen(print);
   enableFlutterDriverExtension();
   app.main();
 }
diff --git a/examples/stocks/test_driver/scroll_perf_test.dart b/examples/stocks/test_driver/scroll_perf_test.dart
index 6165417..a6b3a30 100644
--- a/examples/stocks/test_driver/scroll_perf_test.dart
+++ b/examples/stocks/test_driver/scroll_perf_test.dart
@@ -19,22 +19,29 @@
         driver.close();
     });
 
-    test('tap on the floating action button; verify counter', () async {
-      // Find the scrollable stock list
-      ObjectRef stockList = await driver.findByValueKey('stock-list');
-      expect(stockList, isNotNull);
+    test('measure', () async {
+      Map<String, dynamic> profileJson = await driver.traceAction(() async {
+        // Find the scrollable stock list
+        ObjectRef stockList = await driver.findByValueKey('stock-list');
+        expect(stockList, isNotNull);
 
-      // Scroll down 5 times
-      for (int i = 0; i < 5; i++) {
-        await driver.scroll(stockList, 0.0, -300.0, new Duration(milliseconds: 300));
-        await new Future<Null>.delayed(new Duration(milliseconds: 500));
-      }
+        // Scroll down
+        for (int i = 0; i < 5; i++) {
+          await driver.scroll(stockList, 0.0, -300.0, new Duration(milliseconds: 300));
+          await new Future<Null>.delayed(new Duration(milliseconds: 500));
+        }
 
-      // Scroll up 5 times
-      for (int i = 0; i < 5; i++) {
-        await driver.scroll(stockList, 0.0, 300.0, new Duration(milliseconds: 300));
-        await new Future<Null>.delayed(new Duration(milliseconds: 500));
-      }
+        // Scroll up
+        for (int i = 0; i < 5; i++) {
+          await driver.scroll(stockList, 0.0, 300.0, new Duration(milliseconds: 300));
+          await new Future<Null>.delayed(new Duration(milliseconds: 500));
+        }
+      });
+
+      // Usually the profile is saved to a file and then analyzed using
+      // chrom://tracing or a script. Both are out of scope for this little
+      // test, so all we do is check that we received something.
+      expect(profileJson, isNotNull);
     });
   });
 }
diff --git a/packages/flutter_driver/lib/src/driver.dart b/packages/flutter_driver/lib/src/driver.dart
index b58aeba..59fd3b3 100644
--- a/packages/flutter_driver/lib/src/driver.dart
+++ b/packages/flutter_driver/lib/src/driver.dart
@@ -29,6 +29,9 @@
   FlutterDriver.connectedTo(this._serviceClient, this._appIsolate);
 
   static const String _kFlutterExtensionMethod = 'ext.flutter_driver';
+  static const String _kStartTracingMethod = 'ext.flutter_startTracing';
+  static const String _kStopTracingMethod = 'ext.flutter_stopTracing';
+  static const String _kDownloadTraceDataMethod = 'ext.flutter_downloadTraceData';
   static const Duration _kDefaultTimeout = const Duration(seconds: 5);
   static const Duration _kDefaultPauseBetweenRetries = const Duration(milliseconds: 160);
 
@@ -205,6 +208,63 @@
     return result.text;
   }
 
+  /// Starts recording performance traces.
+  Future<Null> startTracing() async {
+    try {
+      await _appIsolate.invokeExtension(_kStartTracingMethod);
+      return null;
+    } catch(error, stackTrace) {
+      throw new DriverError(
+        'Failed to start tracing due to remote error',
+        error,
+        stackTrace
+      );
+    }
+  }
+
+  /// Stops recording performance traces and downloads the trace profile.
+  // TODO(yjbanov): return structured data rather than raw JSON once we have a
+  //                stable protocol to talk to.
+  Future<Map<String, dynamic>> stopTracingAndDownloadProfile() async {
+    Map<String, dynamic> stopResult =
+        await _appIsolate.invokeExtension(_kStopTracingMethod);
+    String traceFilePath = stopResult['trace_file_path'];
+
+    // Tracing data isn't available immediately as some of it is queued up in
+    // the event loop.
+    Stopwatch sw = new Stopwatch()..start();
+    while(sw.elapsed < const Duration(seconds: 30)) {
+      Map<String, dynamic> downloadResult =
+          await _appIsolate.invokeExtension(_kDownloadTraceDataMethod, <String, String>{
+            'trace_file_path': traceFilePath,
+          });
+
+      if (downloadResult['success'] == false)
+        throw new DriverError('Failed to download trace file: $traceFilePath');
+      else if (downloadResult['status'] != 'not ready')
+        return downloadResult;
+
+      // Give the event loop a chance to flush the trace log
+      await new Future<Null>.delayed(const Duration(milliseconds: 200));
+    }
+    throw new DriverError(
+      'Timed out waiting for tracing profile to become ready for download.'
+    );
+  }
+
+  /// Runs [action] and outputs a performance trace for it.
+  ///
+  /// Waits for the `Future` returned by [action] to complete prior to stopping
+  /// the trace.
+  ///
+  /// This is merely a convenience wrapper on top of [startTracing] and
+  /// [stopTracingAndDownloadProfile].
+  Future<Map<String, dynamic>> traceAction(Future<dynamic> action()) async {
+    await startTracing();
+    await action();
+    return stopTracingAndDownloadProfile();
+  }
+
   /// Calls the [evaluator] repeatedly until the result of the evaluation
   /// satisfies the [matcher].
   ///