diff --git a/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart b/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart index 5aca2114c8..749c5e1829 100644 --- a/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart +++ b/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart @@ -3,8 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; @@ -22,7 +20,7 @@ void main() { }); test('measure', () async { - Map profileJson = await driver.traceAction(() async { + Timeline timeline = await driver.traceAction(() async { // Find the scrollable stock list ObjectRef stockList = await driver.findByValueKey('main-scroll'); expect(stockList, isNotNull); @@ -40,8 +38,9 @@ void main() { } }); - expect(profileJson, isNotNull); - await new File("build/profile.json").writeAsString(JSON.encode(profileJson)); + TimelineSummary summary = new TimelineSummary.summarize(timeline); + summary.writeSummaryToFile('complex_layout_scroll_perf', pretty: true); + summary.writeTimelineToFile('complex_layout_scroll_perf', pretty: true); }); }); } diff --git a/examples/stocks/test_driver/scroll_perf_test.dart b/examples/stocks/test_driver/scroll_perf_test.dart index 5308f6710f..5a61bf59b3 100644 --- a/examples/stocks/test_driver/scroll_perf_test.dart +++ b/examples/stocks/test_driver/scroll_perf_test.dart @@ -21,7 +21,7 @@ void main() { }); test('measure', () async { - Map timeline = await driver.traceAction(() async { + Timeline timeline = await driver.traceAction(() async { // Find the scrollable stock list ObjectRef stockList = await driver.findByValueKey('stock-list'); expect(stockList, isNotNull); @@ -39,8 +39,7 @@ void main() { } }); - expect(timeline, isNotNull); - TimelineSummary summary = summarizeTimeline(timeline); + TimelineSummary summary = new TimelineSummary.summarize(timeline); summary.writeSummaryToFile('stocks_scroll_perf', pretty: true); summary.writeTimelineToFile('stocks_scroll_perf', pretty: true); }); diff --git a/packages/flutter_driver/lib/flutter_driver.dart b/packages/flutter_driver/lib/flutter_driver.dart index b65be89719..7acd3ecda9 100644 --- a/packages/flutter_driver/lib/flutter_driver.dart +++ b/packages/flutter_driver/lib/flutter_driver.dart @@ -41,3 +41,7 @@ export 'src/timeline_summary.dart' show summarizeTimeline, EventTrace, TimelineSummary; + +export 'src/timeline.dart' show + Timeline, + TimelineEvent; diff --git a/packages/flutter_driver/lib/src/driver.dart b/packages/flutter_driver/lib/src/driver.dart index 4e733d3344..486fb4c9f6 100644 --- a/packages/flutter_driver/lib/src/driver.dart +++ b/packages/flutter_driver/lib/src/driver.dart @@ -15,6 +15,7 @@ import 'health.dart'; import 'matcher_util.dart'; import 'message.dart'; import 'retry.dart'; +import 'timeline.dart'; final Logger _log = new Logger('FlutterDriver'); @@ -229,16 +230,14 @@ class FlutterDriver { } } - /// 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> stopTracingAndDownloadProfile() async { + /// Stops recording performance traces and downloads the timeline. + Future stopTracingAndDownloadTimeline() async { try { await _peer.sendRequest(_kSetVMTimelineFlagsMethod, {'recordedStreams': '[]'}); - return _peer.sendRequest(_kGetVMTimelineMethod); + return new Timeline.fromJson(await _peer.sendRequest(_kGetVMTimelineMethod)); } catch(error, stackTrace) { throw new DriverError( - 'Failed to start tracing due to remote error', + 'Failed to stop tracing due to remote error', error, stackTrace ); @@ -251,11 +250,11 @@ class FlutterDriver { /// the trace. /// /// This is merely a convenience wrapper on top of [startTracing] and - /// [stopTracingAndDownloadProfile]. - Future> traceAction(Future action()) async { + /// [stopTracingAndDownloadTimeline]. + Future traceAction(Future action()) async { await startTracing(); await action(); - return stopTracingAndDownloadProfile(); + return stopTracingAndDownloadTimeline(); } /// Calls the [evaluator] repeatedly until the result of the evaluation diff --git a/packages/flutter_driver/lib/src/timeline.dart b/packages/flutter_driver/lib/src/timeline.dart new file mode 100644 index 0000000000..e8370ba68f --- /dev/null +++ b/packages/flutter_driver/lib/src/timeline.dart @@ -0,0 +1,119 @@ +// 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. + +/// Timeline data recorded by the Flutter runtime. +/// +/// The data is in the `chrome://tracing` format. It can be saved to a file +/// and loaded in Chrome for visual inspection. +/// +/// See https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview +class Timeline { + factory Timeline.fromJson(Map json) { + return new Timeline._(json, _parseEvents(json)); + } + + Timeline._(this.json, this.events); + + /// The original timeline JSON. + final Map json; + + /// List of all timeline events. + final List events; +} + +/// A single timeline event. +class TimelineEvent { + factory TimelineEvent(Map json) { + return new TimelineEvent._( + json, + json['name'], + json['cat'], + json['ph'], + json['pid'], + json['tid'], + json['dur'] != null + ? new Duration(microseconds: json['dur']) + : null, + json['ts'], + json['tts'], + json['args'] + ); + } + + TimelineEvent._( + this.json, + this.name, + this.category, + this.phase, + this.processId, + this.threadId, + this.duration, + this.timestampMicros, + this.threadTimestampMicros, + this.arguments + ); + + /// The original event JSON. + final Map json; + + /// The name of the event. + /// + /// Corresponds to the "name" field in the JSON event. + final String name; + + /// Event category. Events with different names may share the same category. + /// + /// Corresponds to the "cat" field in the JSON event. + final String category; + + /// For a given long lasting event, denotes the phase of the event, such as + /// "B" for "event began", and "E" for "event ended". + /// + /// Corresponds to the "ph" field in the JSON event. + final String phase; + + /// ID of process that emitted the event. + /// + /// Corresponds to the "pid" field in the JSON event. + final int processId; + + /// ID of thread that issues the event. + /// + /// Corresponds to the "tid" field in the JSON event. + final int threadId; + + /// The duration of the event. + /// + /// Note, some events are reported with duration. Others are reported as a + /// pair of begin/end events. + /// + /// Corresponds to the "dur" field in the JSON event. + final Duration duration; + + /// Time passed since tracing was enabled, in microseconds. + /// + /// Corresponds to the "ts" field in the JSON event. + final int timestampMicros; + + /// Thread clock time, in microseconds. + /// + /// Corresponds to the "tts" field in the JSON event. + final int threadTimestampMicros; + + /// Arbitrary data attached to the event. + /// + /// Corresponds to the "args" field in the JSON event. + final Map arguments; +} + +List _parseEvents(Map json) { + List> jsonEvents = json['traceEvents']; + + if (jsonEvents == null) + return null; + + return jsonEvents + .map((Map eventJson) => new TimelineEvent(eventJson)) + .toList(); +} diff --git a/packages/flutter_driver/lib/src/timeline_summary.dart b/packages/flutter_driver/lib/src/timeline_summary.dart index a1d53788fd..0dafcc341d 100644 --- a/packages/flutter_driver/lib/src/timeline_summary.dart +++ b/packages/flutter_driver/lib/src/timeline_summary.dart @@ -9,6 +9,7 @@ import 'package:file/file.dart'; import 'package:path/path.dart' as path; import 'common.dart'; +import 'timeline.dart'; const String _kDefaultDirectory = 'build'; final JsonEncoder _prettyEncoder = new JsonEncoder.withIndent(' '); @@ -18,14 +19,10 @@ final JsonEncoder _prettyEncoder = new JsonEncoder.withIndent(' '); const Duration kBuildBudget = const Duration(milliseconds: 8); /// Extracts statistics from the event loop timeline. -TimelineSummary summarizeTimeline(Map timeline) { - return new TimelineSummary(timeline); -} - class TimelineSummary { - TimelineSummary(this._timeline); + TimelineSummary.summarize(this._timeline); - final Map _timeline; + final Timeline _timeline; /// Average amount of time spent per frame in the framework building widgets, /// updating layout, painting and compositing. @@ -68,7 +65,7 @@ class TimelineSummary { {String destinationDirectory: _kDefaultDirectory, bool pretty: false}) async { await fs.directory(destinationDirectory).create(recursive: true); File file = fs.file(path.join(destinationDirectory, '$traceName.timeline.json')); - await file.writeAsString(_encodeJson(_timeline, pretty)); + await file.writeAsString(_encodeJson(_timeline.json, pretty)); } /// Writes [summaryJson] to a file. @@ -79,17 +76,15 @@ class TimelineSummary { await file.writeAsString(_encodeJson(summaryJson, pretty)); } - String _encodeJson(dynamic json, bool pretty) { + String _encodeJson(Map json, bool pretty) { return pretty ? _prettyEncoder.convert(json) : JSON.encode(json); } - List> get _traceEvents => _timeline['traceEvents']; - - List> _extractNamedEvents(String name) { - return _traceEvents - .where((Map event) => event['name'] == name) + List _extractNamedEvents(String name) { + return _timeline.events + .where((TimelineEvent event) => event.name == name) .toList(); } @@ -98,13 +93,16 @@ class TimelineSummary { List result = []; // Timeline does not guarantee that the first event is the "begin" event. - Iterator> events = _extractNamedEvents(name) - .skipWhile((Map evt) => evt['ph'] != 'B').iterator; + Iterator events = _extractNamedEvents(name) + .skipWhile((TimelineEvent evt) => evt.phase != 'B').iterator; while(events.moveNext()) { - Map beginEvent = events.current; + TimelineEvent beginEvent = events.current; if (events.moveNext()) { - Map endEvent = events.current; - result.add(new TimedEvent(beginEvent['ts'], endEvent['ts'])); + TimelineEvent endEvent = events.current; + result.add(new TimedEvent( + beginEvent.timestampMicros, + endEvent.timestampMicros + )); } } diff --git a/packages/flutter_driver/test/flutter_driver_test.dart b/packages/flutter_driver/test/flutter_driver_test.dart index 65b2513239..602092734c 100644 --- a/packages/flutter_driver/test/flutter_driver_test.dart +++ b/packages/flutter_driver/test/flutter_driver_test.dart @@ -8,6 +8,7 @@ import 'package:flutter_driver/src/driver.dart'; import 'package:flutter_driver/src/error.dart'; import 'package:flutter_driver/src/health.dart'; import 'package:flutter_driver/src/message.dart'; +import 'package:flutter_driver/src/timeline.dart'; import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:mockito/mockito.dart'; import 'package:quiver/testing/async.dart'; @@ -262,18 +263,22 @@ void main() { when(mockPeer.sendRequest('_getVMTimeline')).thenAnswer((_) async { return { - 'test': 'profile', + 'traceEvents': [ + { + 'name': 'test event' + } + ], }; }); - Map profile = await driver.traceAction(() { + Timeline timeline = await driver.traceAction(() { actionCalled = true; }); expect(actionCalled, isTrue); expect(startTracingCalled, isTrue); expect(stopTracingCalled, isTrue); - expect(profile['test'], 'profile'); + expect(timeline.events.single.name, 'test event'); }); }); }); diff --git a/packages/flutter_driver/test/src/timeline_summary_test.dart b/packages/flutter_driver/test/src/timeline_summary_test.dart index ec52636dee..bb564d1497 100644 --- a/packages/flutter_driver/test/src/timeline_summary_test.dart +++ b/packages/flutter_driver/test/src/timeline_summary_test.dart @@ -6,15 +6,15 @@ import 'dart:convert' show JSON; import 'package:test/test.dart'; import 'package:flutter_driver/src/common.dart'; -import 'package:flutter_driver/src/timeline_summary.dart'; +import 'package:flutter_driver/flutter_driver.dart'; void main() { group('TimelineSummary', () { TimelineSummary summarize(List> testEvents) { - return summarizeTimeline({ + return new TimelineSummary.summarize(new Timeline.fromJson({ 'traceEvents': testEvents, - }); + })); } Map begin(int timeStamp) => { diff --git a/packages/flutter_driver/test/src/timeline_test.dart b/packages/flutter_driver/test/src/timeline_test.dart new file mode 100644 index 0000000000..6c52caf583 --- /dev/null +++ b/packages/flutter_driver/test/src/timeline_test.dart @@ -0,0 +1,56 @@ +// 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 'package:test/test.dart'; +import 'package:flutter_driver/src/timeline.dart'; + +void main() { + group('Timeline', () { + test('parses JSON', () { + Timeline timeline = new Timeline.fromJson({ + 'traceEvents': [ + { + 'name': 'test event', + 'cat': 'test category', + 'ph': 'B', + 'pid': 123, + 'tid': 234, + 'dur': 345, + 'ts': 456, + 'tts': 567, + 'args': { + 'arg1': true, + } + }, + // Tests that we don't choke on missing data + {} + ] + }); + + expect(timeline.events, hasLength(2)); + + TimelineEvent e1 = timeline.events[0]; + expect(e1.name, 'test event'); + expect(e1.category, 'test category'); + expect(e1.phase, 'B'); + expect(e1.processId, 123); + expect(e1.threadId, 234); + expect(e1.duration, const Duration(microseconds: 345)); + expect(e1.timestampMicros, 456); + expect(e1.threadTimestampMicros, 567); + expect(e1.arguments, { 'arg1': true }); + + TimelineEvent e2 = timeline.events[1]; + expect(e2.name, isNull); + expect(e2.category, isNull); + expect(e2.phase, isNull); + expect(e2.processId, isNull); + expect(e2.threadId, isNull); + expect(e2.duration, isNull); + expect(e2.timestampMicros, isNull); + expect(e2.threadTimestampMicros, isNull); + expect(e2.arguments, isNull); + }); + }); +}