blob: 43938f5ac61563e495b9045104bd2d949f559c44 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart=2.12
import 'dart:convert';
import 'dart:io';
import 'time_delta.dart';
import 'time_point.dart';
import 'trace_model.dart';
/// Create a [Model] from a file path.
Future<Model> createModelFromFilePath(String path) async =>
createModelFromFile(File(path));
/// Create a [Model] from a [File].
Future<Model> createModelFromFile(File file) async =>
createModelFromJsonString(await file.readAsString());
/// Create a [Model] from a raw JSON string of trace data.
Model createModelFromJsonString(String jsonString) {
final jsonObject = json.decode(jsonString);
return createModelFromJson(jsonObject);
}
/// Create a [Model] from a root raw JSON trace object.
Model createModelFromJson(Map<String, dynamic> rootObject) =>
_createModelFromJson(rootObject);
/// Assert that expected fields in a JSON trace event are present and are of
/// the correct type. If any of these fields are missing or is of a different
/// type than what is asserted here, then the JSON trace event is considered to
/// be malformed.
void _checkTraceEvent(Map<String, dynamic> jsonTraceEvent) {
if (jsonTraceEvent['ph'] != 'M' &&
!(jsonTraceEvent.containsKey('cat') && jsonTraceEvent['cat'] is String)) {
throw FormatException(
'Expected $jsonTraceEvent to have field "cat" of type String');
}
if (!(jsonTraceEvent.containsKey('name') &&
jsonTraceEvent['name'] is String)) {
throw FormatException(
'Expected $jsonTraceEvent to have field "name" of type String');
}
if (jsonTraceEvent['ph'] != 'M' &&
!(jsonTraceEvent.containsKey('ts') &&
(jsonTraceEvent['ts'] is double || jsonTraceEvent['ts'] is int))) {
throw FormatException(
'Expected $jsonTraceEvent to have field "ts" of type double or int');
}
if (!(jsonTraceEvent.containsKey('pid') && jsonTraceEvent['pid'] is int)) {
throw FormatException(
'Expected $jsonTraceEvent to have field "pid" of type int');
}
if (!(jsonTraceEvent.containsKey('tid') &&
(jsonTraceEvent['tid'] is double || jsonTraceEvent['tid'] is int))) {
throw FormatException(
'Expected $jsonTraceEvent to have field "tid" of type double or int');
}
// It's legal for "args" to not be a key in the object.
if (jsonTraceEvent.containsKey('args')) {
if (!(jsonTraceEvent['args'] is Map<String, dynamic>)) {
throw FormatException(
'Expected $jsonTraceEvent with "args" field to have "args" field of type Map<String, dynamic>');
}
}
}
String _asStringId(dynamic object) {
if (object is String) {
return object;
} else if (object is int) {
return object.toString();
} else if (object is double) {
if (object.isNaN) {
throw FormatException('Got NaN double for id field value');
} else if (object.remainder(1.0) != 0.0) {
throw FormatException(
'Got float with non-zero decimal place ($object) for id field value');
} else {
return object.toInt().toString();
}
} else {
throw FormatException(
'Got unexpected type ${object.runtimeType} for id field value: $object');
}
}
/// A helper class to group events by their "track" ((pid, tid) pairs).
class _TrackKey {
int? pid;
int? tid;
_TrackKey(this.pid, this.tid);
@override
bool operator ==(Object other) {
if (other is! _TrackKey) {
return false;
}
_TrackKey tracekKey = other;
return pid == tracekKey.pid && tid == tracekKey.tid;
}
@override
int get hashCode {
// Hash combine logic inspired by https://dart.dev/guides/libraries/library-tour#implementing-map-keys.
int result = 17;
result = 37 * result + pid.hashCode;
result = 37 * result + tid.hashCode;
return result;
}
}
/// A helper struct to group flow events.
class _FlowKey {
String? category;
String? name;
String? id;
// Only used for 'local' flow ids.
int? pid;
_FlowKey.fromTraceEvent(Map<String, dynamic> traceEvent) {
name = traceEvent['name'];
category = traceEvent['cat'];
// Unlike _AsyncKey, _FlowKey is globally scoped unless specifically local.
pid = 0;
if (traceEvent.containsKey('id')) {
id = _asStringId(traceEvent['id']);
} else if (traceEvent.containsKey('id2')) {
final Map<String, dynamic> id2 = traceEvent['id2'];
if (id2.containsKey('local')) {
// 'local' id2 means scoped to the process.
pid = traceEvent['pid'];
id = _asStringId(id2['local']);
} else if (id2.containsKey('global')) {
id = _asStringId(id2['global']);
}
}
if (id == null) {
throw FormatException('Could not find id in $traceEvent');
}
}
@override
bool operator ==(Object other) {
if (other is! _FlowKey) {
return false;
}
_FlowKey flowKey = other;
return category == flowKey.category &&
name == flowKey.name &&
id == flowKey.id &&
pid == flowKey.pid;
}
@override
int get hashCode {
// Hash combine logic inspired by https://dart.dev/guides/libraries/library-tour#implementing-map-keys.
int result = 17;
result = 37 * result + category.hashCode;
result = 37 * result + name.hashCode;
result = 37 * result + id.hashCode;
result = 37 * result + pid.hashCode;
return result;
}
}
/// A helper struct to group async events.
class _AsyncKey {
int? pid;
String? category;
String? name;
int? id;
_AsyncKey.fromTraceEvent(Map<String, dynamic> traceEvent) {
pid = traceEvent['pid'];
name = traceEvent['name'];
category = traceEvent['cat'];
if (traceEvent.containsKey('id')) {
if (traceEvent['id'] is int) {
id = traceEvent['id'];
} else if (traceEvent['id'] is String) {
id = int.tryParse(traceEvent['id']);
}
} else if (traceEvent.containsKey('id2')) {
final Map<String, dynamic> id2 = traceEvent['id2'];
if (id2.containsKey('local')) {
// 'local' id2 means scoped to the process.
if (id2['local'] is int) {
id = id2['local'];
} else if (id2['local'] is String) {
id = int.tryParse(id2['local']);
}
} else if (id2.containsKey('global')) {
pid = 0;
if (id2['global'] is int) {
id = id2['global'];
} else if (id2['global'] is String) {
id = int.tryParse(id2['global']);
}
}
}
if (id == null) {
throw FormatException('Could not find id in $traceEvent');
}
}
@override
bool operator ==(Object other) {
if (other is! _AsyncKey) {
return false;
}
_AsyncKey asyncKey = other;
return pid == asyncKey.pid &&
category == asyncKey.category &&
name == asyncKey.name &&
id == asyncKey.id;
}
@override
int get hashCode {
// Hash combine logic inspired by https://dart.dev/guides/libraries/library-tour#implementing-map-keys.
int result = 17;
result = 37 * result + pid.hashCode;
result = 37 * result + category.hashCode;
result = 37 * result + name.hashCode;
result = 37 * result + id.hashCode;
return result;
}
}
Map<String, dynamic> _combineArgs(
Map<String, dynamic> args1, Map<String, dynamic>? args2) {
if (args2 == null) {
return args1;
}
return {...args1, ...args2};
}
Model _createModelFromJson(Map<String, dynamic> rootObject) {
final resultEvents = <Event>[];
// Maintains the current duration stack for each track.
final Map<_TrackKey, List<DurationEvent>> durationStacks = {};
// Maintains in progress async events.
final Map<_AsyncKey, AsyncEvent> liveAsyncEvents = {};
// Maintains in progress flow sequences.
final Map<_FlowKey, FlowEvent> liveFlows = {};
// Flows with "next slide" binding that are waiting for to be bound.
final Map<_TrackKey, List<FlowEvent>> unboundFlowEvents = {};
// A helper lambda to add duration events to the appropriate duration stack
// and do the appropriate duration/flow graph setup. It is used for both
// begin/end pairs and complete events.
void addToDurationStack(
DurationEvent durationEvent, List<DurationEvent> durationStack) {
durationStack.add(durationEvent);
if (durationStack.length > 1) {
final top = durationStack.last;
final topParent = durationStack[durationStack.length - 2];
top.parent = topParent;
topParent.childDurations.add(durationEvent);
}
}
if (!(rootObject.containsKey('traceEvents') &&
rootObject['traceEvents'] is List<dynamic>)) {
throw FormatException(
'Expected $rootObject to have field "traceEvents" of type List<dynamic>');
}
final List<dynamic> traceEvents = rootObject['traceEvents'];
// Add synthetic end events for each complete event in the trace data to
// assist with maintaining each thread's duration stack. This isn't
// strictly necessary, however it makes the duration stack bookkeeping simpler.
final extendedTraceEvents = List<Map<String, dynamic>>.from(traceEvents);
for (final traceEvent in traceEvents) {
if (traceEvent['ph'] == 'X') {
final syntheticEndEvent = Map<String, dynamic>.from(traceEvent);
syntheticEndEvent['ph'] = 'fuchsia_synthetic_end';
syntheticEndEvent['ts'] = traceEvent['ts'] + traceEvent['dur'];
extendedTraceEvents.add(syntheticEndEvent);
}
}
// Sort the events by their timestamp. We need to iterate through the
// events in sorted order to compute things such as duration stacks and flow
// sequences. Events without timestamps (e.g. Chrome's metadata events) are
// sorted to the beginning.
extendedTraceEvents.sort((a, b) => (a['ts'] ?? 0).compareTo(b['ts'] ?? 0));
int droppedFlowEventCounter = 0;
int droppedAsyncEventCounter = 0;
// TODO(fxbug.dev/41309): Support nested async events. In the meantime, just drop them.
int droppedNestedAsyncEventCounter = 0;
final Map<int?, String?> pidToName = {};
final Map<int?, String?> tidToName = {};
// Process the raw trace events into our model's [Event] representation.
for (final traceEvent in extendedTraceEvents) {
_checkTraceEvent(traceEvent);
final int? pid = traceEvent['pid'];
final int? tid = traceEvent['tid'].toInt();
final trackKey = _TrackKey(pid, tid);
final durationStack =
durationStacks.putIfAbsent(trackKey, () => <DurationEvent>[]);
final phase = traceEvent['ph'];
if (phase == 'X') {
final durationEvent = DurationEvent.fromJson(traceEvent);
if (unboundFlowEvents.containsKey(trackKey)) {
for (final flowEvent in unboundFlowEvents[trackKey]!) {
flowEvent.enclosingDuration = durationEvent;
}
unboundFlowEvents[trackKey]!.clear();
}
addToDurationStack(durationEvent, durationStack);
resultEvents.add(durationEvent);
} else if (phase == 'B') {
final durationEvent = DurationEvent.fromJson(traceEvent);
if (unboundFlowEvents.containsKey(trackKey)) {
for (final flowEvent in unboundFlowEvents[trackKey]!) {
flowEvent.enclosingDuration = durationEvent;
}
unboundFlowEvents[trackKey]!.clear();
}
addToDurationStack(durationEvent, durationStack);
} else if (phase == 'E') {
if (durationStack.isNotEmpty) {
final popped = durationStack.removeLast();
popped.duration ??= TimePoint.fromEpochDelta(
TimeDelta.fromMicroseconds(traceEvent['ts'])) -
popped.start;
popped.args = _combineArgs(popped.args, traceEvent['args']);
resultEvents.add(popped);
}
} else if (phase == 'fuchsia_synthetic_end') {
assert(durationStack.isNotEmpty);
durationStack.removeLast();
} else if (phase == 'b') {
final asyncKey = _AsyncKey.fromTraceEvent(traceEvent);
final asyncEvent = AsyncEvent.fromJson(asyncKey.id!, traceEvent);
liveAsyncEvents[asyncKey] = asyncEvent;
} else if (phase == 'e') {
final asyncKey = _AsyncKey.fromTraceEvent(traceEvent);
final asyncEvent = liveAsyncEvents.remove(asyncKey);
if (asyncEvent != null) {
asyncEvent
..duration = TimePoint.fromEpochDelta(
TimeDelta.fromMicroseconds(traceEvent['ts'])) -
asyncEvent.start
..args = _combineArgs(asyncEvent.args, traceEvent['args']);
} else {
droppedAsyncEventCounter++;
continue;
}
resultEvents.add(asyncEvent);
} else if (phase == 'i' || phase == 'I') {
resultEvents.add(InstantEvent.fromJson(traceEvent));
} else if (phase == 's' || phase == 't' || phase == 'f') {
String? bindingPoint;
if (traceEvent.containsKey('bp')) {
if (traceEvent['bp'] == 'e') {
bindingPoint = 'enclosing';
} else {
throw FormatException('Found unexpected value in bp field of '
'$traceEvent');
}
} else if (phase == 's' || phase == 't') {
bindingPoint = 'enclosing';
} else if (phase == 'f') {
bindingPoint = 'next';
}
final flowKey = _FlowKey.fromTraceEvent(traceEvent);
FlowEvent? previousFlow;
if (phase == 's') {
if (liveFlows.containsKey(flowKey)) {
droppedFlowEventCounter++;
continue;
}
} else if (phase == 't' || phase == 'f') {
previousFlow = liveFlows[flowKey];
if (previousFlow == null) {
droppedFlowEventCounter++;
continue;
}
}
if (durationStack.isEmpty) {
droppedFlowEventCounter++;
continue;
}
final enclosingDuration =
bindingPoint == 'enclosing' ? durationStack.last : null;
final flowEvent =
FlowEvent.fromJson(flowKey.id!, enclosingDuration, traceEvent);
if (bindingPoint == 'enclosing') {
enclosingDuration!.childFlows.add(flowEvent);
} else {
unboundFlowEvents
.putIfAbsent(trackKey, () => <FlowEvent>[])
.add(flowEvent);
}
if (previousFlow != null) {
previousFlow.nextFlow = flowEvent;
}
flowEvent.previousFlow = previousFlow;
if (phase == 's' || phase == 't') {
liveFlows[flowKey] = flowEvent;
} else {
liveFlows.remove(flowKey);
}
resultEvents.add(flowEvent);
} else if (phase == 'C') {
resultEvents.add(CounterEvent.fromJson(traceEvent));
} else if (phase == 'n') {
// TODO(fxbug.dev/41309): Support nested async events. In the meantime, just drop them.
droppedNestedAsyncEventCounter++;
} else if (phase == 'M') {
// Chrome metadata events. These define process and thread names, similar
// to the Fuchsia systemTraceEvents.
if (traceEvent['name'] == 'process_name') {
// If traceEvent contains args, those are verified to be of type
// Map<String, dynamic> in _checkTraceEvent.
if (!traceEvent.containsKey('args') ||
!traceEvent['args'].containsKey('name')) {
throw FormatException('$traceEvent is a process_name metadata event'
'but doesn\'t have a name argument');
}
pidToName[pid] = traceEvent['args']['name'];
}
if (traceEvent['name'] == 'thread_name') {
// If traceEvent contains args, those are verified to be of type
// Map<String, dynamic> in _checkTraceEvent.
if (!traceEvent.containsKey('args') ||
!traceEvent['args'].containsKey('name')) {
throw FormatException('$traceEvent is a thread_name metadata event'
'but doesn\'t have a name argument');
}
tidToName[tid] = traceEvent['args']['name'];
}
} else if (phase == 'R' ||
phase == '(' ||
phase == ')' ||
phase == 'O' ||
phase == 'N' ||
phase == 'D' ||
phase == 'S' ||
phase == 'T' ||
phase == 'p' ||
phase == 'F') {
// Ignore some phases that are in Chrome traces that we don't yet have use
// cases for.
//
// These are:
// * 'R' - Mark events, similar to instants created by the Navigation
// Timing API
// * '(', ')' - Context events
// * 'O', 'N', 'D' - Object events
// * 'S', 'T', 'p', 'F' - Legacy async events
} else {
throw FormatException(
'Encountered unknown phase $phase from $traceEvent');
}
}
final liveDurationEventsCount = durationStacks.values
.map((ds) => ds.length)
.fold(0, (int a, int b) => a + b);
if (liveDurationEventsCount > 0) {
print(
'Warning, finished processing trace events with $liveDurationEventsCount in progress duration events');
}
if (liveAsyncEvents.isNotEmpty) {
print(
'Warning, finished processing trace events with ${liveAsyncEvents.length} in progress async events');
}
if (liveFlows.isNotEmpty) {
print(
'Warning, finished processing trace events with ${liveFlows.length} in progress flow events');
}
if (droppedAsyncEventCounter > 0) {
print('Warning, dropped $droppedAsyncEventCounter async events');
}
if (droppedFlowEventCounter > 0) {
print('Warning, dropped $droppedFlowEventCounter flow events');
}
if (droppedNestedAsyncEventCounter > 0) {
print(
'Warning, dropped $droppedNestedAsyncEventCounter nested async events');
}
if (rootObject.containsKey('systemTraceEvents')) {
if (!(rootObject['systemTraceEvents'] is Map<String, dynamic>)) {
throw FormatException('Expected field "systemTraceEvents" to be of type '
'Map<String, dynamic>');
}
final systemTraceEvents = rootObject['systemTraceEvents'];
if (!(systemTraceEvents.containsKey('type') &&
systemTraceEvents['type'] is String)) {
throw FormatException(
'Expected $systemTraceEvents to have field "type" of type String');
}
if (systemTraceEvents['type'] != 'fuchsia') {
throw FormatException(
'Expected $systemTraceEvents to have field "type" equal to value "fuchsia"');
}
if (!(systemTraceEvents.containsKey('events') &&
systemTraceEvents['events'] is List<dynamic>)) {
throw FormatException(
'Expected $systemTraceEvents to have field "events" of type '
'List<dynamic>');
}
final systemTraceEventsEvents = systemTraceEvents['events'];
for (final systemTraceEvent in systemTraceEventsEvents) {
if (!(systemTraceEvent.containsKey('ph') &&
systemTraceEvent['ph'] is String)) {
throw FormatException(
'Expected $systemTraceEvent to have field "ph" of type String');
}
final String? phase = systemTraceEvent['ph'];
if (phase == 'p') {
if (!(systemTraceEvent.containsKey('pid') &&
systemTraceEvent['pid'] is int)) {
throw FormatException(
'Expected $systemTraceEvent to have field "pid" of type int');
}
final int? pid = systemTraceEvent['pid'];
if (!(systemTraceEvent.containsKey('name') &&
systemTraceEvent['name'] is String)) {
throw FormatException(
'Expected $systemTraceEvent to have field "name" of type String');
}
final String? name = systemTraceEvent['name'];
pidToName[pid] = name;
} else if (phase == 't') {
if (!(systemTraceEvent.containsKey('pid') &&
systemTraceEvent['pid'] is int)) {
throw FormatException(
'Expected $systemTraceEvent to have field "pid" of type int');
}
if (!(systemTraceEvent.containsKey('tid') &&
(systemTraceEvent['tid'] is int) ||
systemTraceEvent['tid'] is double)) {
throw FormatException(
'Expected $systemTraceEvent to have field "tid" of type int or double');
}
final int? tid = systemTraceEvent['tid'].toInt();
if (!(systemTraceEvent.containsKey('name') &&
systemTraceEvent['name'] is String)) {
throw FormatException(
'Expected $systemTraceEvent to have field "name" of type String');
}
final String? name = systemTraceEvent['name'];
tidToName[tid] = name;
} else if (phase == 'k') {
// CPU events are currently ignored. It would be interesting to support
// these in the future so we can track CPU durations in addition to wall
// durations.
} else {
print('Unknown phase $phase from $systemTraceEvent');
}
}
}
resultEvents.sort((a, b) => a.start.compareTo(b.start));
final Map<int, Process> processes = {};
for (final event in resultEvents) {
final process = processes.putIfAbsent(event.pid, () => Process(event.pid));
int threadIndex = process.threads.indexWhere((e) => e.tid == event.tid);
if (threadIndex == -1) {
final thread = Thread(event.tid);
if (tidToName.containsKey(event.tid)) {
thread.name = tidToName[event.tid]!;
}
process.threads.add(thread);
threadIndex = process.threads.length - 1;
}
final thread = process.threads[threadIndex];
thread.events.add(event);
}
final model = Model();
for (final pid in processes.keys.toList()..sort()) {
final process = processes[pid]!;
process.threads.sort((a, b) {
return a.tid.compareTo(b.tid);
});
if (pidToName.containsKey(process.pid)) {
process.name = pidToName[process.pid]!;
}
model.processes.add(process);
}
return model;
}