// Copyright (c) 2012 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.
/**
* @fileoverview TraceEventImporter imports TraceEvent-formatted data
* into the provided timeline model.
*/
cr.define('tracing', function() {
function ThreadState(tid) {
this.openSlices = [];
}
function TraceEventImporter(model, eventData) {
this.model_ = model;
if (typeof(eventData) === 'string' || eventData instanceof String) {
// If the event data begins with a [, then we know it should end with a ].
// The reason we check for this is because some tracing implementations
// cannot guarantee that a ']' gets written to the trace file. So, we are
// forgiving and if this is obviously the case, we fix it up before
// throwing the string at JSON.parse.
if (eventData[0] == '[') {
n = eventData.length;
if (eventData[n - 1] != ']' && eventData[n - 1] != '\n') {
eventData = eventData + ']';
} else if (eventData[n - 2] != ']' && eventData[n - 1] == '\n') {
eventData = eventData + ']';
} else if (eventData[n - 3] != ']' && eventData[n - 2] == '\r' &&
eventData[n - 1] == '\n') {
eventData = eventData + ']';
}
}
this.events_ = JSON.parse(eventData);
} else {
this.events_ = eventData;
}
// Some trace_event implementations put the actual trace events
// inside a container. E.g { ... , traceEvents: [ ] }
//
// If we see that, just pull out the trace events.
if (this.events_.traceEvents)
this.events_ = this.events_.traceEvents;
// To allow simple indexing of threads, we store all the threads by a
// PTID. A ptid is a pid and tid joined together x:y fashion, eg
// 1024:130. The ptid is a unique key for a thread in the trace.
this.threadStateByPTID_ = {};
// Async events need to be processed durign finalizeEvents
this.allAsyncEvents_ = [];
}
/**
* @return {boolean} Whether obj is a TraceEvent array.
*/
TraceEventImporter.canImport = function(eventData) {
// May be encoded JSON. But we dont want to parse it fully yet.
// Use a simple heuristic:
// - eventData that starts with [ are probably trace_event
// - eventData that starts with { are probably trace_event
// May be encoded JSON. Treat files that start with { as importable by us.
if (typeof(eventData) === 'string' || eventData instanceof String) {
return eventData[0] == '{' || eventData[0] == '[';
}
// Might just be an array of events
if (eventData instanceof Array && eventData.length && eventData[0].ph)
return true;
// Might be an object with a traceEvents field in it.
if (eventData.traceEvents)
return eventData.traceEvents instanceof Array &&
eventData.traceEvents[0].ph;
return false;
};
TraceEventImporter.prototype = {
__proto__: Object.prototype,
/**
* Helper to process a 'begin' event (e.g. initiate a slice).
* @param {ThreadState} state Thread state (holds slices).
* @param {Object} event The current trace event.
*/
processBeginEvent: function(index, state, event) {
var colorId = tracing.getStringColorId(event.name);
var slice =
{ index: index,
slice: new tracing.TimelineThreadSlice(event.name, colorId,
event.ts / 1000,
event.args) };
if (event.uts)
slice.slice.startInUserTime = event.uts / 1000;
if (event.args['ui-nest'] === '0') {
this.model_.importErrors.push('ui-nest no longer supported.');
return;
}
state.openSlices.push(slice);
},
/**
* Helper to process an 'end' event (e.g. close a slice).
* @param {ThreadState} state Thread state (holds slices).
* @param {Object} event The current trace event.
*/
processEndEvent: function(state, event) {
if (event.args['ui-nest'] === '0') {
this.model_.importErrors.push('ui-nest no longer supported.');
return;
}
if (state.openSlices.length == 0) {
// Ignore E events that are unmatched.
return;
}
var slice = state.openSlices.pop().slice;
slice.duration = (event.ts / 1000) - slice.start;
if (event.uts)
slice.durationInUserTime = (event.uts / 1000) - slice.startInUserTime;
for (var arg in event.args)
slice.args[arg] = event.args[arg];
// Store the slice on the correct subrow.
var thread = this.model_.getOrCreateProcess(event.pid).
getOrCreateThread(event.tid);
var subRowIndex = state.openSlices.length;
thread.getSubrow(subRowIndex).push(slice);
// Add the slice to the subSlices array of its parent.
if (state.openSlices.length) {
var parentSlice = state.openSlices[state.openSlices.length - 1];
parentSlice.slice.subSlices.push(slice);
}
},
/**
* Helper to process an 'async finish' event, which will close an open slice
* on a TimelineAsyncSliceGroup object.
**/
processAsyncEvent: function(index, state, event) {
var thread = this.model_.getOrCreateProcess(event.pid).
getOrCreateThread(event.tid);
this.allAsyncEvents_.push({
event: event,
thread: thread});
},
/**
* Helper function that closes any open slices. This happens when a trace
* ends before an 'E' phase event can get posted. When that happens, this
* closes the slice at the highest timestamp we recorded and sets the
* didNotFinish flag to true.
*/
autoCloseOpenSlices: function() {
// We need to know the model bounds in order to assign an end-time to
// the open slices.
this.model_.updateBounds();
// The model's max value in the trace is wrong at this point if there are
// un-closed events. To close those events, we need the true global max
// value. To compute this, build a list of timestamps that weren't
// included in the max calculation, then compute the real maximum based on
// that.
var openTimestamps = [];
for (var ptid in this.threadStateByPTID_) {
var state = this.threadStateByPTID_[ptid];
for (var i = 0; i < state.openSlices.length; i++) {
var slice = state.openSlices[i];
openTimestamps.push(slice.slice.start);
for (var s = 0; s < slice.slice.subSlices.length; s++) {
var subSlice = slice.slice.subSlices[s];
openTimestamps.push(subSlice.start);
if (subSlice.duration)
openTimestamps.push(subSlice.end);
}
}
}
// Figure out the maximum value of model.maxTimestamp and
// Math.max(openTimestamps). Made complicated by the fact that the model
// timestamps might be undefined.
var realMaxTimestamp;
if (this.model_.maxTimestamp) {
realMaxTimestamp = Math.max(this.model_.maxTimestamp,
Math.max.apply(Math, openTimestamps));
} else {
realMaxTimestamp = Math.max.apply(Math, openTimestamps);
}
// Automatically close any slices are still open. These occur in a number
// of reasonable situations, e.g. deadlock. This pass ensures the open
// slices make it into the final model.
for (var ptid in this.threadStateByPTID_) {
var state = this.threadStateByPTID_[ptid];
while (state.openSlices.length > 0) {
var slice = state.openSlices.pop();
slice.slice.duration = realMaxTimestamp - slice.slice.start;
slice.slice.didNotFinish = true;
var event = this.events_[slice.index];
// Store the slice on the correct subrow.
var thread = this.model_.getOrCreateProcess(event.pid)
.getOrCreateThread(event.tid);
var subRowIndex = state.openSlices.length;
thread.getSubrow(subRowIndex).push(slice.slice);
// Add the slice to the subSlices array of its parent.
if (state.openSlices.length) {
var parentSlice = state.openSlices[state.openSlices.length - 1];
parentSlice.slice.subSlices.push(slice.slice);
}
}
}
},
/**
* Helper that creates and adds samples to a TimelineCounter object based on
* 'C' phase events.
*/
processCounterEvent: function(event) {
var ctr_name;
if (event.id !== undefined)
ctr_name = event.name + '[' + event.id + ']';
else
ctr_name = event.name;
var ctr = this.model_.getOrCreateProcess(event.pid)
.getOrCreateCounter(event.cat, ctr_name);
// Initialize the counter's series fields if needed.
if (ctr.numSeries == 0) {
for (var seriesName in event.args) {
ctr.seriesNames.push(seriesName);
ctr.seriesColors.push(
tracing.getStringColorId(ctr.name + '.' + seriesName));
}
if (ctr.numSeries == 0) {
this.model_.importErrors.push('Expected counter ' + event.name +
' to have at least one argument to use as a value.');
// Drop the counter.
delete ctr.parent.counters[ctr.name];
return;
}
}
// Add the sample values.
ctr.timestamps.push(event.ts / 1000);
for (var i = 0; i < ctr.numSeries; i++) {
var seriesName = ctr.seriesNames[i];
if (event.args[seriesName] === undefined) {
ctr.samples.push(0);
continue;
}
ctr.samples.push(event.args[seriesName]);
}
},
/**
* Walks through the events_ list and outputs the structures discovered to
* model_.
*/
importEvents: function() {
// Walk through events
var events = this.events_;
// Some events cannot be handled until we have done a first pass over the
// data set. So, accumulate them into a temporary data structure.
var second_pass_events = [];
for (var eI = 0; eI < events.length; eI++) {
var event = events[eI];
var ptid = tracing.TimelineThread.getPTIDFromPidAndTid(
event.pid, event.tid);
if (!(ptid in this.threadStateByPTID_))
this.threadStateByPTID_[ptid] = new ThreadState();
var state = this.threadStateByPTID_[ptid];
if (event.ph == 'B') {
this.processBeginEvent(eI, state, event);
} else if (event.ph == 'E') {
this.processEndEvent(state, event);
} else if (event.ph == 'S') {
this.processAsyncEvent(eI, state, event);
} else if (event.ph == 'F') {
this.processAsyncEvent(eI, state, event);
} else if (event.ph == 'T') {
this.processAsyncEvent(eI, state, event);
} else if (event.ph == 'I') {
// Treat an Instant event as a duration 0 slice.
// TimelineSliceTrack's redraw() knows how to handle this.
this.processBeginEvent(eI, state, event);
this.processEndEvent(state, event);
} else if (event.ph == 'C') {
this.processCounterEvent(event);
} else if (event.ph == 'M') {
if (event.name == 'thread_name') {
var thread = this.model_.getOrCreateProcess(event.pid)
.getOrCreateThread(event.tid);
thread.name = event.args.name;
} else {
this.model_.importErrors.push(
'Unrecognized metadata name: ' + event.name);
}
} else {
this.model_.importErrors.push(
'Unrecognized event phase: ' + event.ph +
'(' + event.name + ')');
}
}
// Autoclose any open slices.
var hasOpenSlices = false;
for (var ptid in this.threadStateByPTID_) {
var state = this.threadStateByPTID_[ptid];
hasOpenSlices |= state.openSlices.length > 0;
}
if (hasOpenSlices)
this.autoCloseOpenSlices();
},
/**
* Called by the TimelineModel after all other importers have imported their
* events. This function creates async slices for any async events we saw.
*/
finalizeImport: function() {
if (this.allAsyncEvents_.length == 0)
return;
this.allAsyncEvents_.sort(function(x, y) {
return x.event.ts - y.event.ts;
});
var asyncEventStatesByNameThenID = {};
var allAsyncEvents = this.allAsyncEvents_;
for (var i = 0; i < allAsyncEvents.length; i++) {
var asyncEventState = allAsyncEvents[i];
var event = asyncEventState.event;
var name = event.name;
if (name === undefined) {
this.model_.importErrors.push(
'Async events (ph: S, T or F) require an name parameter.');
continue;
}
var id = event.id;
if (id === undefined) {
this.model_.importErrors.push(
'Async events (ph: S, T or F) require an id parameter.');
continue;
}
// TODO(simonjam): Add a synchronous tick on the appropriate thread.
if (event.ph == 'S') {
if (asyncEventStatesByNameThenID[name] === undefined)
asyncEventStatesByNameThenID[name] = {};
if (asyncEventStatesByNameThenID[name][id]) {
this.model_.importErrors.push(
'At ' + event.ts + ', an slice of the same id ' + id +
' was alrady open.');
continue;
}
asyncEventStatesByNameThenID[name][id] = [];
asyncEventStatesByNameThenID[name][id].push(asyncEventState);
} else {
if (asyncEventStatesByNameThenID[name] === undefined) {
this.model_.importErrors.push(
'At ' + event.ts + ', no slice named ' + name +
' was open.');
continue;
}
if (asyncEventStatesByNameThenID[name][id] === undefined) {
this.model_.importErrors.push(
'At ' + event.ts + ', no slice named ' + name +
' with id=' + id + ' was open.');
continue;
}
var events = asyncEventStatesByNameThenID[name][id];
events.push(asyncEventState);
if (event.ph == 'F') {
// Create a slice from start to end.
var slice = new tracing.TimelineAsyncSlice(
name,
tracing.getStringColorId(name),
events[0].event.ts / 1000);
slice.duration = (event.ts / 1000) - (events[0].event.ts / 1000);
slice.startThread = events[0].thread;
slice.endThread = asyncEventState.thread;
slice.id = id;
slice.args = events[0].event.args;
slice.subSlices = [];
// Create subSlices for each step.
for (var j = 1; j < events.length; ++j) {
var subName = name;
if (events[j - 1].event.ph == 'T')
subName = name + ':' + events[j - 1].event.args.step;
var subSlice = new tracing.TimelineAsyncSlice(
subName,
tracing.getStringColorId(name + j),
events[j - 1].event.ts / 1000);
subSlice.duration =
(events[j].event.ts / 1000) - (events[j - 1].event.ts / 1000);
subSlice.startThread = events[j - 1].thread;
subSlice.endThread = events[j].thread;
subSlice.id = id;
subSlice.args = events[j - 1].event.args;
slice.subSlices.push(subSlice);
}
// The args for the finish event go in the last subSlice.
var lastSlice = slice.subSlices[slice.subSlices.length - 1];
for (var arg in event.args)
lastSlice.args[arg] = event.args[arg];
// Add |slice| to the start-thread's asyncSlices.
slice.startThread.asyncSlices.push(slice);
delete asyncEventStatesByNameThenID[name][id];
}
}
}
}
};
tracing.TimelineModel.registerImporter(TraceEventImporter);
return {
TraceEventImporter: TraceEventImporter
};
});