// 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 }; });