Commit 19e624cc authored by Yegor's avatar Yegor

[driver] give the timeline data some structure

Fixes https://github.com/flutter/flutter/issues/2713
parent 6ea7ab89
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -22,7 +20,7 @@ void main() { ...@@ -22,7 +20,7 @@ void main() {
}); });
test('measure', () async { test('measure', () async {
Map<String, dynamic> profileJson = await driver.traceAction(() async { Timeline timeline = await driver.traceAction(() async {
// Find the scrollable stock list // Find the scrollable stock list
ObjectRef stockList = await driver.findByValueKey('main-scroll'); ObjectRef stockList = await driver.findByValueKey('main-scroll');
expect(stockList, isNotNull); expect(stockList, isNotNull);
...@@ -40,8 +38,9 @@ void main() { ...@@ -40,8 +38,9 @@ void main() {
} }
}); });
expect(profileJson, isNotNull); TimelineSummary summary = new TimelineSummary.summarize(timeline);
await new File("build/profile.json").writeAsString(JSON.encode(profileJson)); summary.writeSummaryToFile('complex_layout_scroll_perf', pretty: true);
summary.writeTimelineToFile('complex_layout_scroll_perf', pretty: true);
}); });
}); });
} }
...@@ -21,7 +21,7 @@ void main() { ...@@ -21,7 +21,7 @@ void main() {
}); });
test('measure', () async { test('measure', () async {
Map<String, dynamic> timeline = await driver.traceAction(() async { Timeline timeline = await driver.traceAction(() async {
// Find the scrollable stock list // Find the scrollable stock list
ObjectRef stockList = await driver.findByValueKey('stock-list'); ObjectRef stockList = await driver.findByValueKey('stock-list');
expect(stockList, isNotNull); expect(stockList, isNotNull);
...@@ -39,8 +39,7 @@ void main() { ...@@ -39,8 +39,7 @@ void main() {
} }
}); });
expect(timeline, isNotNull); TimelineSummary summary = new TimelineSummary.summarize(timeline);
TimelineSummary summary = summarizeTimeline(timeline);
summary.writeSummaryToFile('stocks_scroll_perf', pretty: true); summary.writeSummaryToFile('stocks_scroll_perf', pretty: true);
summary.writeTimelineToFile('stocks_scroll_perf', pretty: true); summary.writeTimelineToFile('stocks_scroll_perf', pretty: true);
}); });
......
...@@ -41,3 +41,7 @@ export 'src/timeline_summary.dart' show ...@@ -41,3 +41,7 @@ export 'src/timeline_summary.dart' show
summarizeTimeline, summarizeTimeline,
EventTrace, EventTrace,
TimelineSummary; TimelineSummary;
export 'src/timeline.dart' show
Timeline,
TimelineEvent;
...@@ -15,6 +15,7 @@ import 'health.dart'; ...@@ -15,6 +15,7 @@ import 'health.dart';
import 'matcher_util.dart'; import 'matcher_util.dart';
import 'message.dart'; import 'message.dart';
import 'retry.dart'; import 'retry.dart';
import 'timeline.dart';
final Logger _log = new Logger('FlutterDriver'); final Logger _log = new Logger('FlutterDriver');
...@@ -229,16 +230,14 @@ class FlutterDriver { ...@@ -229,16 +230,14 @@ class FlutterDriver {
} }
} }
/// Stops recording performance traces and downloads the trace profile. /// Stops recording performance traces and downloads the timeline.
// TODO(yjbanov): return structured data rather than raw JSON once we have a Future<Timeline> stopTracingAndDownloadTimeline() async {
// stable protocol to talk to.
Future<Map<String, dynamic>> stopTracingAndDownloadProfile() async {
try { try {
await _peer.sendRequest(_kSetVMTimelineFlagsMethod, {'recordedStreams': '[]'}); await _peer.sendRequest(_kSetVMTimelineFlagsMethod, {'recordedStreams': '[]'});
return _peer.sendRequest(_kGetVMTimelineMethod); return new Timeline.fromJson(await _peer.sendRequest(_kGetVMTimelineMethod));
} catch(error, stackTrace) { } catch(error, stackTrace) {
throw new DriverError( throw new DriverError(
'Failed to start tracing due to remote error', 'Failed to stop tracing due to remote error',
error, error,
stackTrace stackTrace
); );
...@@ -251,11 +250,11 @@ class FlutterDriver { ...@@ -251,11 +250,11 @@ class FlutterDriver {
/// the trace. /// the trace.
/// ///
/// This is merely a convenience wrapper on top of [startTracing] and /// This is merely a convenience wrapper on top of [startTracing] and
/// [stopTracingAndDownloadProfile]. /// [stopTracingAndDownloadTimeline].
Future<Map<String, dynamic>> traceAction(Future<dynamic> action()) async { Future<Timeline> traceAction(Future<dynamic> action()) async {
await startTracing(); await startTracing();
await action(); await action();
return stopTracingAndDownloadProfile(); return stopTracingAndDownloadTimeline();
} }
/// Calls the [evaluator] repeatedly until the result of the evaluation /// Calls the [evaluator] repeatedly until the result of the evaluation
......
// 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<String, dynamic> json) {
return new Timeline._(json, _parseEvents(json));
}
Timeline._(this.json, this.events);
/// The original timeline JSON.
final Map<String, dynamic> json;
/// List of all timeline events.
final List<TimelineEvent> events;
}
/// A single timeline event.
class TimelineEvent {
factory TimelineEvent(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> arguments;
}
List<TimelineEvent> _parseEvents(Map<String, dynamic> json) {
List<Map<String, dynamic>> jsonEvents = json['traceEvents'];
if (jsonEvents == null)
return null;
return jsonEvents
.map((Map<String, dynamic> eventJson) => new TimelineEvent(eventJson))
.toList();
}
...@@ -9,6 +9,7 @@ import 'package:file/file.dart'; ...@@ -9,6 +9,7 @@ import 'package:file/file.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'common.dart'; import 'common.dart';
import 'timeline.dart';
const String _kDefaultDirectory = 'build'; const String _kDefaultDirectory = 'build';
final JsonEncoder _prettyEncoder = new JsonEncoder.withIndent(' '); final JsonEncoder _prettyEncoder = new JsonEncoder.withIndent(' ');
...@@ -18,14 +19,10 @@ final JsonEncoder _prettyEncoder = new JsonEncoder.withIndent(' '); ...@@ -18,14 +19,10 @@ final JsonEncoder _prettyEncoder = new JsonEncoder.withIndent(' ');
const Duration kBuildBudget = const Duration(milliseconds: 8); const Duration kBuildBudget = const Duration(milliseconds: 8);
/// Extracts statistics from the event loop timeline. /// Extracts statistics from the event loop timeline.
TimelineSummary summarizeTimeline(Map<String, dynamic> timeline) {
return new TimelineSummary(timeline);
}
class TimelineSummary { class TimelineSummary {
TimelineSummary(this._timeline); TimelineSummary.summarize(this._timeline);
final Map<String, dynamic> _timeline; final Timeline _timeline;
/// Average amount of time spent per frame in the framework building widgets, /// Average amount of time spent per frame in the framework building widgets,
/// updating layout, painting and compositing. /// updating layout, painting and compositing.
...@@ -68,7 +65,7 @@ class TimelineSummary { ...@@ -68,7 +65,7 @@ class TimelineSummary {
{String destinationDirectory: _kDefaultDirectory, bool pretty: false}) async { {String destinationDirectory: _kDefaultDirectory, bool pretty: false}) async {
await fs.directory(destinationDirectory).create(recursive: true); await fs.directory(destinationDirectory).create(recursive: true);
File file = fs.file(path.join(destinationDirectory, '$traceName.timeline.json')); 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. /// Writes [summaryJson] to a file.
...@@ -79,17 +76,15 @@ class TimelineSummary { ...@@ -79,17 +76,15 @@ class TimelineSummary {
await file.writeAsString(_encodeJson(summaryJson, pretty)); await file.writeAsString(_encodeJson(summaryJson, pretty));
} }
String _encodeJson(dynamic json, bool pretty) { String _encodeJson(Map<String, dynamic> json, bool pretty) {
return pretty return pretty
? _prettyEncoder.convert(json) ? _prettyEncoder.convert(json)
: JSON.encode(json); : JSON.encode(json);
} }
List<Map<String, dynamic>> get _traceEvents => _timeline['traceEvents']; List<TimelineEvent> _extractNamedEvents(String name) {
return _timeline.events
List<Map<String, dynamic>> _extractNamedEvents(String name) { .where((TimelineEvent event) => event.name == name)
return _traceEvents
.where((Map<String, dynamic> event) => event['name'] == name)
.toList(); .toList();
} }
...@@ -98,13 +93,16 @@ class TimelineSummary { ...@@ -98,13 +93,16 @@ class TimelineSummary {
List<TimedEvent> result = <TimedEvent>[]; List<TimedEvent> result = <TimedEvent>[];
// Timeline does not guarantee that the first event is the "begin" event. // Timeline does not guarantee that the first event is the "begin" event.
Iterator<Map<String, dynamic>> events = _extractNamedEvents(name) Iterator<TimelineEvent> events = _extractNamedEvents(name)
.skipWhile((Map<String, dynamic> evt) => evt['ph'] != 'B').iterator; .skipWhile((TimelineEvent evt) => evt.phase != 'B').iterator;
while(events.moveNext()) { while(events.moveNext()) {
Map<String, dynamic> beginEvent = events.current; TimelineEvent beginEvent = events.current;
if (events.moveNext()) { if (events.moveNext()) {
Map<String, dynamic> endEvent = events.current; TimelineEvent endEvent = events.current;
result.add(new TimedEvent(beginEvent['ts'], endEvent['ts'])); result.add(new TimedEvent(
beginEvent.timestampMicros,
endEvent.timestampMicros
));
} }
} }
......
...@@ -8,6 +8,7 @@ import 'package:flutter_driver/src/driver.dart'; ...@@ -8,6 +8,7 @@ import 'package:flutter_driver/src/driver.dart';
import 'package:flutter_driver/src/error.dart'; import 'package:flutter_driver/src/error.dart';
import 'package:flutter_driver/src/health.dart'; import 'package:flutter_driver/src/health.dart';
import 'package:flutter_driver/src/message.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:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:quiver/testing/async.dart'; import 'package:quiver/testing/async.dart';
...@@ -262,18 +263,22 @@ void main() { ...@@ -262,18 +263,22 @@ void main() {
when(mockPeer.sendRequest('_getVMTimeline')).thenAnswer((_) async { when(mockPeer.sendRequest('_getVMTimeline')).thenAnswer((_) async {
return <String, dynamic> { return <String, dynamic> {
'test': 'profile', 'traceEvents': [
{
'name': 'test event'
}
],
}; };
}); });
Map<String, dynamic> profile = await driver.traceAction(() { Timeline timeline = await driver.traceAction(() {
actionCalled = true; actionCalled = true;
}); });
expect(actionCalled, isTrue); expect(actionCalled, isTrue);
expect(startTracingCalled, isTrue); expect(startTracingCalled, isTrue);
expect(stopTracingCalled, isTrue); expect(stopTracingCalled, isTrue);
expect(profile['test'], 'profile'); expect(timeline.events.single.name, 'test event');
}); });
}); });
}); });
......
...@@ -6,15 +6,15 @@ import 'dart:convert' show JSON; ...@@ -6,15 +6,15 @@ import 'dart:convert' show JSON;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:flutter_driver/src/common.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() { void main() {
group('TimelineSummary', () { group('TimelineSummary', () {
TimelineSummary summarize(List<Map<String, dynamic>> testEvents) { TimelineSummary summarize(List<Map<String, dynamic>> testEvents) {
return summarizeTimeline(<String, dynamic>{ return new TimelineSummary.summarize(new Timeline.fromJson(<String, dynamic>{
'traceEvents': testEvents, 'traceEvents': testEvents,
}); }));
} }
Map<String, dynamic> begin(int timeStamp) => <String, dynamic>{ Map<String, dynamic> begin(int timeStamp) => <String, dynamic>{
......
// 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);
});
});
}
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