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 @@
// 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<String, dynamic> 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);
});
});
}
......@@ -21,7 +21,7 @@ void main() {
});
test('measure', () async {
Map<String, dynamic> 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);
});
......
......@@ -41,3 +41,7 @@ export 'src/timeline_summary.dart' show
summarizeTimeline,
EventTrace,
TimelineSummary;
export 'src/timeline.dart' show
Timeline,
TimelineEvent;
......@@ -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<Map<String, dynamic>> stopTracingAndDownloadProfile() async {
/// Stops recording performance traces and downloads the timeline.
Future<Timeline> 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<Map<String, dynamic>> traceAction(Future<dynamic> action()) async {
/// [stopTracingAndDownloadTimeline].
Future<Timeline> traceAction(Future<dynamic> action()) async {
await startTracing();
await action();
return stopTracingAndDownloadProfile();
return stopTracingAndDownloadTimeline();
}
/// 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';
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<String, dynamic> timeline) {
return new TimelineSummary(timeline);
}
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,
/// 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<String, dynamic> json, bool pretty) {
return pretty
? _prettyEncoder.convert(json)
: JSON.encode(json);
}
List<Map<String, dynamic>> get _traceEvents => _timeline['traceEvents'];
List<Map<String, dynamic>> _extractNamedEvents(String name) {
return _traceEvents
.where((Map<String, dynamic> event) => event['name'] == name)
List<TimelineEvent> _extractNamedEvents(String name) {
return _timeline.events
.where((TimelineEvent event) => event.name == name)
.toList();
}
......@@ -98,13 +93,16 @@ class TimelineSummary {
List<TimedEvent> result = <TimedEvent>[];
// Timeline does not guarantee that the first event is the "begin" event.
Iterator<Map<String, dynamic>> events = _extractNamedEvents(name)
.skipWhile((Map<String, dynamic> evt) => evt['ph'] != 'B').iterator;
Iterator<TimelineEvent> events = _extractNamedEvents(name)
.skipWhile((TimelineEvent evt) => evt.phase != 'B').iterator;
while(events.moveNext()) {
Map<String, dynamic> beginEvent = events.current;
TimelineEvent beginEvent = events.current;
if (events.moveNext()) {
Map<String, dynamic> endEvent = events.current;
result.add(new TimedEvent(beginEvent['ts'], endEvent['ts']));
TimelineEvent endEvent = events.current;
result.add(new TimedEvent(
beginEvent.timestampMicros,
endEvent.timestampMicros
));
}
}
......
......@@ -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 <String, dynamic> {
'test': 'profile',
'traceEvents': [
{
'name': 'test event'
}
],
};
});
Map<String, dynamic> 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');
});
});
});
......
......@@ -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<Map<String, dynamic>> testEvents) {
return summarizeTimeline(<String, dynamic>{
return new TimelineSummary.summarize(new Timeline.fromJson(<String, dynamic>{
'traceEvents': testEvents,
});
}));
}
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