blob: a4ddf774200da135bdd73ecc7f954bc04561e946 [file] [log] [blame]
// Copyright 2019 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:meta/meta.dart';
import '../profiler/cpu_profile_model.dart';
import '../trees.dart';
import '../utils.dart';
import 'timeline_controller.dart';
/// Data model for DevTools Timeline.
class TimelineData {
TimelineData({
List<Map<String, dynamic>> traceEvents,
List<TimelineFrame> frames,
this.selectedFrame,
this.selectedEvent,
this.cpuProfileData,
}) : traceEvents = traceEvents ?? [],
frames = frames ?? [];
static const traceEventsKey = 'traceEvents';
static const cpuProfileKey = 'cpuProfile';
static const selectedEventKey = 'selectedEvent';
static const devToolsScreenKey = 'dartDevToolsScreen';
/// List that will store trace events in the order we process them.
///
/// These events are scrubbed so that bad data from the engine does not hinder
/// event processing or trace viewing. When the export timeline button is
/// clicked, this will be part of the output.
List<Map<String, dynamic>> traceEvents = [];
/// All frames currently visible in the timeline.
List<TimelineFrame> frames = [];
TimelineFrame selectedFrame;
TimelineEvent selectedEvent;
CpuProfileData cpuProfileData;
Map<String, dynamic> get json => {
traceEventsKey: traceEvents,
cpuProfileKey: cpuProfileData?.json ?? {},
selectedEventKey: selectedEvent?.json ?? {},
devToolsScreenKey: timelineScreenId,
};
void clear() {
traceEvents.clear();
frames.clear();
selectedFrame = null;
selectedEvent = null;
cpuProfileData = null;
}
}
class OfflineTimelineData extends TimelineData {
OfflineTimelineData._({
List<Map<String, dynamic>> traceEvents,
List<TimelineFrame> frames,
TimelineFrame selectedFrame,
TimelineEvent selectedEvent,
CpuProfileData cpuProfileData,
}) : super(
traceEvents: traceEvents,
frames: frames,
selectedFrame: selectedFrame,
selectedEvent: selectedEvent,
cpuProfileData: cpuProfileData,
);
static OfflineTimelineData parse(Map<String, dynamic> json) {
final List<dynamic> traceEvents =
(json[TimelineData.traceEventsKey] ?? []).cast<Map<String, dynamic>>();
final Map<String, dynamic> cpuProfileJson =
json[TimelineData.cpuProfileKey] ?? {};
final CpuProfileData cpuProfileData =
cpuProfileJson.isNotEmpty ? CpuProfileData.parse(cpuProfileJson) : null;
final Map<String, dynamic> selectedEventJson =
json[TimelineData.selectedEventKey] ?? {};
final OfflineTimelineEvent selectedEvent = selectedEventJson.isNotEmpty
? OfflineTimelineEvent(
selectedEventJson[TimelineEvent.eventNameKey],
selectedEventJson[TimelineEvent.eventTypeKey],
selectedEventJson[TimelineEvent.eventStartTimeKey],
selectedEventJson[TimelineEvent.eventDurationKey],
)
: null;
return OfflineTimelineData._(
traceEvents: traceEvents,
selectedEvent: selectedEvent,
cpuProfileData: cpuProfileData,
);
}
bool get isEmpty => traceEvents.isEmpty;
/// Creates a new instance of [OfflineTimelineData] with references to the
/// same objects contained in this instance ([traceEvents], [frames],
/// [selectedFrame], [selectedEvent], [cpuProfileData]).
///
/// This is not a deep copy. We are not modifying the before-mentioned
/// objects, only pointing our reference variables at different objects.
/// Therefore, we do not need to store a copy of all these objects (and the
/// objects they contain) in memory.
OfflineTimelineData copy() {
return OfflineTimelineData._(
traceEvents: traceEvents,
frames: frames,
selectedFrame: selectedFrame,
selectedEvent: selectedEvent,
cpuProfileData: cpuProfileData,
);
}
}
/// Wrapper class for [TimelineEvent] that only includes information we need for
/// importing and exporting snapshots.
///
/// * name
/// * start time
/// * duration
///
/// We extend TimelineEvent so that our CPU profiler code requiring a selected
/// timeline event will work as it does when we are not loading from offline.
class OfflineTimelineEvent extends TimelineEvent {
OfflineTimelineEvent(
String name, String eventType, int startMicros, int durationMicros)
: super(TraceEventWrapper(
TraceEvent({
TraceEvent.nameKey: name,
TraceEvent.timestampKey: startMicros,
TraceEvent.durationKey: durationMicros,
TraceEvent.argsKey: {TraceEvent.typeKey: 'ui'},
}),
0, // 0 is an arbitrary value for [TraceEventWrapper.timeReceived].
)) {
time.end = Duration(microseconds: startMicros + durationMicros);
type = eventType == TimelineEventType.ui.toString()
? TimelineEventType.ui
: TimelineEventType.gpu;
}
}
/// Data describing a single frame.
///
/// Each TimelineFrame should have 2 distinct pieces of data:
/// * [uiEventFlow] : flow of events showing the UI work for the frame.
/// * [gpuEventFlow] : flow of events showing the GPU work for the frame.
class TimelineFrame {
TimelineFrame(this.id);
// TODO(kenzie): we should query the device for targetFps at some point.
static const targetFps = 60.0;
static const targetMaxDuration = 1000.0 / targetFps;
final String id;
/// Marks whether this frame has been added to the timeline.
///
/// This should only be set once.
bool get addedToTimeline => _addedToTimeline;
bool _addedToTimeline;
set addedToTimeline(bool v) {
assert(_addedToTimeline == null);
_addedToTimeline = v;
}
/// Event flows for the UI and GPU work for the frame.
final List<TimelineEvent> eventFlows = List.generate(2, (_) => null);
/// Flow of events describing the UI work for the frame.
TimelineEvent get uiEventFlow => eventFlows[TimelineEventType.ui.index];
/// Flow of events describing the GPU work for the frame.
TimelineEvent get gpuEventFlow => eventFlows[TimelineEventType.gpu.index];
/// Whether the frame is ready for the timeline.
///
/// A frame is ready once it has both required event flows as well as
/// [_pipelineItemStartTime] and [_pipelineItemEndTime].
bool get isReadyForTimeline {
return uiEventFlow != null &&
gpuEventFlow != null &&
pipelineItemTime.start?.inMicroseconds != null &&
pipelineItemTime.end?.inMicroseconds != null;
}
// Stores frame start time, end time, and duration.
final time = TimeRange();
/// Pipeline item time range in micros.
///
/// This stores the start and end times for the pipeline item event for this
/// frame. We use this value to determine whether a TimelineEvent fits within
/// the frame's time boundaries.
final pipelineItemTime = TimeRange(singleAssignment: false);
TraceEvent pipelineItemStartTrace;
TraceEvent pipelineItemEndTrace;
bool get isWellFormed =>
pipelineItemTime.start?.inMicroseconds != null &&
pipelineItemTime.end?.inMicroseconds != null;
int get uiDuration =>
uiEventFlow != null ? uiEventFlow.time.duration.inMicroseconds : null;
double get uiDurationMs => uiDuration != null ? uiDuration / 1000 : null;
int get gpuDuration =>
gpuEventFlow != null ? gpuEventFlow.time.duration.inMicroseconds : null;
double get gpuDurationMs => gpuDuration != null ? gpuDuration / 1000 : null;
CpuProfileData cpuProfileData;
void setEventFlow(TimelineEvent event, {TimelineEventType type}) {
type ??= event?.type;
if (type == TimelineEventType.ui) {
time.start = event?.time?.start;
}
if (type == TimelineEventType.gpu) {
time.end = event?.time?.end;
}
eventFlows[type.index] = event;
event?.frameId = id;
}
@override
String toString() {
return 'Frame $id - $time, ui: ${uiEventFlow.time}, '
'gpu: ${gpuEventFlow.time}';
}
}
enum TimelineEventType {
ui,
gpu,
unknown,
}
class TimelineEvent extends TreeNode<TimelineEvent> {
TimelineEvent(TraceEventWrapper firstTraceEvent)
: traceEvents = [firstTraceEvent],
type = firstTraceEvent.event.type {
time.start = Duration(microseconds: firstTraceEvent.event.timestampMicros);
}
static const eventNameKey = 'name';
static const eventTypeKey = 'type';
static const eventStartTimeKey = 'startMicros';
static const eventDurationKey = 'durationMicros';
/// Trace events associated with this [TimelineEvent].
///
/// There will either be one entry in the list (for DurationComplete events)
/// or two (one for the associated DurationBegin event and one for the
/// associated DurationEnd event).
final List<TraceEventWrapper> traceEvents;
TimelineEventType type;
TimeRange time = TimeRange();
String get frameId => _frameId ?? root._frameId;
String _frameId;
set frameId(String id) => _frameId = id;
String get name => traceEvents.first.event.name;
Map<String, dynamic> get beginTraceEventJson => traceEvents.first.json;
Map<String, dynamic> get endTraceEventJson =>
traceEvents.length > 1 ? traceEvents.last.json : null;
bool get isUiEvent => type == TimelineEventType.ui;
bool get isGpuEvent => type == TimelineEventType.gpu;
bool get isUiEventFlow => containsChildWithCondition(
(TimelineEvent event) => event.name.contains('Engine::BeginFrame'));
bool get isGpuEventFlow => containsChildWithCondition(
(TimelineEvent event) => event.name.contains('PipelineConsume'));
void maybeRemoveDuplicate() {
void _maybeRemoveDuplicate({@required TimelineEvent parent}) {
if (parent.children.length == 1 &&
// [parent]'s DurationBegin trace is equal to that of its only child.
collectionEquals(
parent.beginTraceEventJson,
parent.children.first.beginTraceEventJson,
) &&
// [parent]'s DurationEnd trace is equal to that of its only child.
collectionEquals(
parent.endTraceEventJson,
parent.children.first.endTraceEventJson,
)) {
parent.removeChild(children.first);
}
}
// Remove [this] event's child if it is a duplicate of [this].
if (children.isNotEmpty) {
_maybeRemoveDuplicate(parent: this);
}
// Remove [this] event if it is a duplicate of [parent].
if (parent != null) {
_maybeRemoveDuplicate(parent: parent);
}
}
void removeChild(TimelineEvent childToRemove) {
assert(children.contains(childToRemove));
final List<TimelineEvent> newChildren = List.from(childToRemove.children);
newChildren.forEach(_addChild);
children.remove(childToRemove);
}
@override
void addChild(TimelineEvent child) {
// Places the child in it's correct position amongst the other children.
void _putChildInTree(TimelineEvent root) {
// [root] is a leaf. Add child here.
if (root.children.isEmpty) {
root._addChild(child);
return;
}
final _children = root.children.toList();
// If [child] is the parent of some or all of the members in [_children],
// those members will need to be reordered in the tree.
final childrenToReorder = [];
for (TimelineEvent otherChild in _children) {
if (child.couldBeParentOf(otherChild)) {
childrenToReorder.add(otherChild);
}
}
if (childrenToReorder.isNotEmpty) {
root._addChild(child);
for (TimelineEvent otherChild in childrenToReorder) {
// Link [otherChild] with its correct parent [child].
child._addChild(otherChild);
// Unlink [otherChild] from its incorrect parent [root].
root.children.remove(otherChild);
}
return;
}
// Check if a member of [_children] is the parent of [child]. If multiple
// children in [_children] share a timestamp, they both could be the
// parent of [child]. We reverse [_children] so that we will pick the last
// received candidate as the new parent of [child].
for (TimelineEvent otherChild in _children.reversed) {
if (otherChild.couldBeParentOf(child)) {
// Recurse on [otherChild]'s subtree.
_putChildInTree(otherChild);
return;
}
}
// If we have not returned at this point, [child] belongs in
// [root.children].
root._addChild(child);
}
_putChildInTree(this);
}
void _addChild(TimelineEvent child) {
assert(!children.contains(child));
children.add(child);
child.parent = this;
}
bool couldBeParentOf(TimelineEvent e) {
final startTime = time.start.inMicroseconds;
final endTime = time.end?.inMicroseconds;
final eStartTime = e.time.start.inMicroseconds;
final eEndTime = e.time.end?.inMicroseconds;
if (endTime != null && eEndTime != null) {
if (startTime == eStartTime && endTime == eEndTime) {
return traceEvents.first.id < e.traceEvents.first.id;
}
return startTime <= eStartTime && endTime >= eEndTime;
} else if (endTime != null) {
// We don't use >= to compare [endTime] and [e.startTime] here because we
// don't want to falsely make [this] the parent of [e]. We do not know
// [e.endTime], meaning [e] could start at [endTime] and end later than
// [endTime] (unless e has a duration of 0). In this case, [this] would
// not be the parent of [e].
return startTime <= eStartTime && endTime > eStartTime;
} else if (startTime == eStartTime) {
return traceEvents.first.id < e.traceEvents.first.id;
} else {
return startTime < eStartTime;
}
}
void format(StringBuffer buf, String indent) {
buf.writeln('$indent$name $time');
for (TimelineEvent child in children) {
child.format(buf, ' $indent');
}
}
void formatFromRoot(StringBuffer buf, String indent) {
root.format(buf, indent);
}
void writeTraceToBuffer(StringBuffer buf) {
buf.writeln(beginTraceEventJson);
for (TimelineEvent child in children) {
child.writeTraceToBuffer(buf);
}
if (endTraceEventJson != null) {
buf.writeln(endTraceEventJson);
}
}
Map<String, dynamic> get json {
return {
eventNameKey: name,
eventTypeKey: type.toString(),
eventStartTimeKey: time.start.inMicroseconds,
eventDurationKey: time.duration.inMicroseconds,
};
}
@visibleForTesting
TimelineEvent deepCopy() {
final copy = TimelineEvent(traceEvents.first);
copy.time.end = time.end;
copy.parent = parent;
for (TimelineEvent child in children) {
copy._addChild(child.deepCopy());
}
return copy;
}
// TODO(kenzie): use DiagnosticableTreeMixin instead.
@override
String toString() {
final buf = StringBuffer();
format(buf, ' ');
return buf.toString();
}
}
// TODO(devoncarew): Upstream this class to the service protocol library.
/// A single timeline event.
class TraceEvent {
/// Creates a timeline event given JSON-encoded event data.
TraceEvent(this.json)
: name = json[nameKey],
category = json[categoryKey],
phase = json[phaseKey],
processId = json[processIdKey],
threadId = json[threadIdKey],
duration = json[durationKey],
timestampMicros = json[timestampKey],
args = json[argsKey];
static const nameKey = 'name';
static const categoryKey = 'cat';
static const phaseKey = 'ph';
static const processIdKey = 'pid';
static const threadIdKey = 'tid';
static const durationKey = 'dur';
static const timestampKey = 'ts';
static const argsKey = 'args';
static const typeKey = 'type';
static const idKey = 'id';
static const scopeKey = 'scope';
/// 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;
/// Each async event has an additional required parameter id. We consider the
/// events with the same category and id as events from the same event tree.
dynamic get id => json[idKey];
/// An optional scope string can be specified to avoid id conflicts, in which
/// case we consider events with the same category, scope, and id as events
/// from the same event tree.
String get scope => json[scopeKey];
/// The duration of the event, in microseconds.
///
/// 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 int duration;
/// Time passed since tracing was enabled, in microseconds.
final int timestampMicros;
/// Arbitrary data attached to the event.
final Map<String, dynamic> args;
String get asyncUID {
if (scope == null) {
return '$category:$id';
} else {
return '$category:$scope:$id';
}
}
TimelineEventType _type;
TimelineEventType get type {
if (_type == null) {
if (args[typeKey] == 'ui') {
_type = TimelineEventType.ui;
} else if (args[typeKey] == 'gpu') {
_type = TimelineEventType.gpu;
} else {
_type = TimelineEventType.unknown;
}
}
return _type;
}
set type(TimelineEventType t) => _type = t;
bool get isUiEvent => type == TimelineEventType.ui;
bool get isGpuEvent => type == TimelineEventType.gpu;
@override
String toString() => '$type event [$idKey: $id] [$phaseKey: $phase] '
'$name - [$timestampKey: $timestampMicros] [$durationKey: $duration]';
}
int _traceEventWrapperId = 0;
class TraceEventWrapper implements Comparable<TraceEventWrapper> {
TraceEventWrapper(this.event, this.timeReceived)
: id = _traceEventWrapperId++;
final TraceEvent event;
final num timeReceived;
final int id;
Map<String, dynamic> get json => event.json;
bool processed = false;
@override
int compareTo(TraceEventWrapper other) {
// Order events based on their timestamps. If the events share a timestamp,
// order them in the order we received them.
final compare =
event.timestampMicros.compareTo(other.event.timestampMicros);
return compare != 0 ? compare : id.compareTo(other.id);
}
}