1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @fileoverview TraceEventImporter imports TraceEvent-formatted data
7 * into the provided timeline model.
8 */
9cr.define('tracing', function() {
10  function ThreadState(tid) {
11    this.openSlices = [];
12  }
13
14  function TraceEventImporter(model, eventData) {
15    this.model_ = model;
16
17    if (typeof(eventData) === 'string' || eventData instanceof String) {
18      // If the event data begins with a [, then we know it should end with a ].
19      // The reason we check for this is because some tracing implementations
20      // cannot guarantee that a ']' gets written to the trace file. So, we are
21      // forgiving and if this is obviously the case, we fix it up before
22      // throwing the string at JSON.parse.
23      if (eventData[0] == '[') {
24        n = eventData.length;
25        if (eventData[n - 1] != ']' && eventData[n - 1] != '\n') {
26          eventData = eventData + ']';
27        } else if (eventData[n - 2] != ']' && eventData[n - 1] == '\n') {
28          eventData = eventData + ']';
29        } else if (eventData[n - 3] != ']' && eventData[n - 2] == '\r' &&
30            eventData[n - 1] == '\n') {
31          eventData = eventData + ']';
32        }
33      }
34      this.events_ = JSON.parse(eventData);
35
36    } else {
37      this.events_ = eventData;
38    }
39
40    // Some trace_event implementations put the actual trace events
41    // inside a container. E.g { ... , traceEvents: [ ] }
42    //
43    // If we see that, just pull out the trace events.
44    if (this.events_.traceEvents)
45      this.events_ = this.events_.traceEvents;
46
47    // To allow simple indexing of threads, we store all the threads by a
48    // PTID. A ptid is a pid and tid joined together x:y fashion, eg
49    // 1024:130. The ptid is a unique key for a thread in the trace.
50    this.threadStateByPTID_ = {};
51
52    // Async events need to be processed durign finalizeEvents
53    this.allAsyncEvents_ = [];
54  }
55
56  /**
57   * @return {boolean} Whether obj is a TraceEvent array.
58   */
59  TraceEventImporter.canImport = function(eventData) {
60    // May be encoded JSON. But we dont want to parse it fully yet.
61    // Use a simple heuristic:
62    //   - eventData that starts with [ are probably trace_event
63    //   - eventData that starts with { are probably trace_event
64    // May be encoded JSON. Treat files that start with { as importable by us.
65    if (typeof(eventData) === 'string' || eventData instanceof String) {
66      return eventData[0] == '{' || eventData[0] == '[';
67    }
68
69    // Might just be an array of events
70    if (eventData instanceof Array && eventData.length && eventData[0].ph)
71      return true;
72
73    // Might be an object with a traceEvents field in it.
74    if (eventData.traceEvents)
75      return eventData.traceEvents instanceof Array &&
76          eventData.traceEvents[0].ph;
77
78    return false;
79  };
80
81  TraceEventImporter.prototype = {
82
83    __proto__: Object.prototype,
84
85    /**
86     * Helper to process a 'begin' event (e.g. initiate a slice).
87     * @param {ThreadState} state Thread state (holds slices).
88     * @param {Object} event The current trace event.
89     */
90    processBeginEvent: function(index, state, event) {
91      var colorId = tracing.getStringColorId(event.name);
92      var slice =
93          { index: index,
94            slice: new tracing.TimelineThreadSlice(event.name, colorId,
95                                                   event.ts / 1000,
96                                                   event.args) };
97
98      if (event.uts)
99        slice.slice.startInUserTime = event.uts / 1000;
100
101      if (event.args['ui-nest'] === '0') {
102        this.model_.importErrors.push('ui-nest no longer supported.');
103        return;
104      }
105
106      state.openSlices.push(slice);
107    },
108
109    /**
110     * Helper to process an 'end' event (e.g. close a slice).
111     * @param {ThreadState} state Thread state (holds slices).
112     * @param {Object} event The current trace event.
113     */
114    processEndEvent: function(state, event) {
115      if (event.args['ui-nest'] === '0') {
116        this.model_.importErrors.push('ui-nest no longer supported.');
117        return;
118      }
119      if (state.openSlices.length == 0) {
120        // Ignore E events that are unmatched.
121        return;
122      }
123      var slice = state.openSlices.pop().slice;
124      slice.duration = (event.ts / 1000) - slice.start;
125      if (event.uts)
126        slice.durationInUserTime = (event.uts / 1000) - slice.startInUserTime;
127      for (var arg in event.args)
128        slice.args[arg] = event.args[arg];
129
130      // Store the slice on the correct subrow.
131      var thread = this.model_.getOrCreateProcess(event.pid).
132          getOrCreateThread(event.tid);
133      var subRowIndex = state.openSlices.length;
134      thread.getSubrow(subRowIndex).push(slice);
135
136      // Add the slice to the subSlices array of its parent.
137      if (state.openSlices.length) {
138        var parentSlice = state.openSlices[state.openSlices.length - 1];
139        parentSlice.slice.subSlices.push(slice);
140      }
141    },
142
143    /**
144     * Helper to process an 'async finish' event, which will close an open slice
145     * on a TimelineAsyncSliceGroup object.
146     **/
147    processAsyncEvent: function(index, state, event) {
148      var thread = this.model_.getOrCreateProcess(event.pid).
149          getOrCreateThread(event.tid);
150      this.allAsyncEvents_.push({
151        event: event,
152        thread: thread});
153    },
154
155    /**
156     * Helper function that closes any open slices. This happens when a trace
157     * ends before an 'E' phase event can get posted. When that happens, this
158     * closes the slice at the highest timestamp we recorded and sets the
159     * didNotFinish flag to true.
160     */
161    autoCloseOpenSlices: function() {
162      // We need to know the model bounds in order to assign an end-time to
163      // the open slices.
164      this.model_.updateBounds();
165
166      // The model's max value in the trace is wrong at this point if there are
167      // un-closed events. To close those events, we need the true global max
168      // value. To compute this, build a list of timestamps that weren't
169      // included in the max calculation, then compute the real maximum based on
170      // that.
171      var openTimestamps = [];
172      for (var ptid in this.threadStateByPTID_) {
173        var state = this.threadStateByPTID_[ptid];
174        for (var i = 0; i < state.openSlices.length; i++) {
175          var slice = state.openSlices[i];
176          openTimestamps.push(slice.slice.start);
177          for (var s = 0; s < slice.slice.subSlices.length; s++) {
178            var subSlice = slice.slice.subSlices[s];
179            openTimestamps.push(subSlice.start);
180            if (subSlice.duration)
181              openTimestamps.push(subSlice.end);
182          }
183        }
184      }
185
186      // Figure out the maximum value of model.maxTimestamp and
187      // Math.max(openTimestamps). Made complicated by the fact that the model
188      // timestamps might be undefined.
189      var realMaxTimestamp;
190      if (this.model_.maxTimestamp) {
191        realMaxTimestamp = Math.max(this.model_.maxTimestamp,
192                                    Math.max.apply(Math, openTimestamps));
193      } else {
194        realMaxTimestamp = Math.max.apply(Math, openTimestamps);
195      }
196
197      // Automatically close any slices are still open. These occur in a number
198      // of reasonable situations, e.g. deadlock. This pass ensures the open
199      // slices make it into the final model.
200      for (var ptid in this.threadStateByPTID_) {
201        var state = this.threadStateByPTID_[ptid];
202        while (state.openSlices.length > 0) {
203          var slice = state.openSlices.pop();
204          slice.slice.duration = realMaxTimestamp - slice.slice.start;
205          slice.slice.didNotFinish = true;
206          var event = this.events_[slice.index];
207
208          // Store the slice on the correct subrow.
209          var thread = this.model_.getOrCreateProcess(event.pid)
210                           .getOrCreateThread(event.tid);
211          var subRowIndex = state.openSlices.length;
212          thread.getSubrow(subRowIndex).push(slice.slice);
213
214          // Add the slice to the subSlices array of its parent.
215          if (state.openSlices.length) {
216            var parentSlice = state.openSlices[state.openSlices.length - 1];
217            parentSlice.slice.subSlices.push(slice.slice);
218          }
219        }
220      }
221    },
222
223    /**
224     * Helper that creates and adds samples to a TimelineCounter object based on
225     * 'C' phase events.
226     */
227    processCounterEvent: function(event) {
228      var ctr_name;
229      if (event.id !== undefined)
230        ctr_name = event.name + '[' + event.id + ']';
231      else
232        ctr_name = event.name;
233
234      var ctr = this.model_.getOrCreateProcess(event.pid)
235          .getOrCreateCounter(event.cat, ctr_name);
236      // Initialize the counter's series fields if needed.
237      if (ctr.numSeries == 0) {
238        for (var seriesName in event.args) {
239          ctr.seriesNames.push(seriesName);
240          ctr.seriesColors.push(
241              tracing.getStringColorId(ctr.name + '.' + seriesName));
242        }
243        if (ctr.numSeries == 0) {
244          this.model_.importErrors.push('Expected counter ' + event.name +
245              ' to have at least one argument to use as a value.');
246          // Drop the counter.
247          delete ctr.parent.counters[ctr.name];
248          return;
249        }
250      }
251
252      // Add the sample values.
253      ctr.timestamps.push(event.ts / 1000);
254      for (var i = 0; i < ctr.numSeries; i++) {
255        var seriesName = ctr.seriesNames[i];
256        if (event.args[seriesName] === undefined) {
257          ctr.samples.push(0);
258          continue;
259        }
260        ctr.samples.push(event.args[seriesName]);
261      }
262    },
263
264    /**
265     * Walks through the events_ list and outputs the structures discovered to
266     * model_.
267     */
268    importEvents: function() {
269      // Walk through events
270      var events = this.events_;
271      // Some events cannot be handled until we have done a first pass over the
272      // data set.  So, accumulate them into a temporary data structure.
273      var second_pass_events = [];
274      for (var eI = 0; eI < events.length; eI++) {
275        var event = events[eI];
276        var ptid = tracing.TimelineThread.getPTIDFromPidAndTid(
277            event.pid, event.tid);
278
279        if (!(ptid in this.threadStateByPTID_))
280          this.threadStateByPTID_[ptid] = new ThreadState();
281        var state = this.threadStateByPTID_[ptid];
282
283        if (event.ph == 'B') {
284          this.processBeginEvent(eI, state, event);
285        } else if (event.ph == 'E') {
286          this.processEndEvent(state, event);
287        } else if (event.ph == 'S') {
288          this.processAsyncEvent(eI, state, event);
289        } else if (event.ph == 'F') {
290          this.processAsyncEvent(eI, state, event);
291        } else if (event.ph == 'T') {
292          this.processAsyncEvent(eI, state, event);
293        } else if (event.ph == 'I') {
294          // Treat an Instant event as a duration 0 slice.
295          // TimelineSliceTrack's redraw() knows how to handle this.
296          this.processBeginEvent(eI, state, event);
297          this.processEndEvent(state, event);
298        } else if (event.ph == 'C') {
299          this.processCounterEvent(event);
300        } else if (event.ph == 'M') {
301          if (event.name == 'thread_name') {
302            var thread = this.model_.getOrCreateProcess(event.pid)
303                             .getOrCreateThread(event.tid);
304            thread.name = event.args.name;
305          } else {
306            this.model_.importErrors.push(
307                'Unrecognized metadata name: ' + event.name);
308          }
309        } else {
310          this.model_.importErrors.push(
311              'Unrecognized event phase: ' + event.ph +
312              '(' + event.name + ')');
313        }
314      }
315
316      // Autoclose any open slices.
317      var hasOpenSlices = false;
318      for (var ptid in this.threadStateByPTID_) {
319        var state = this.threadStateByPTID_[ptid];
320        hasOpenSlices |= state.openSlices.length > 0;
321      }
322      if (hasOpenSlices)
323        this.autoCloseOpenSlices();
324    },
325
326    /**
327     * Called by the TimelineModel after all other importers have imported their
328     * events. This function creates async slices for any async events we saw.
329     */
330    finalizeImport: function() {
331      if (this.allAsyncEvents_.length == 0)
332        return;
333
334      this.allAsyncEvents_.sort(function(x, y) {
335        return x.event.ts - y.event.ts;
336      });
337
338      var asyncEventStatesByNameThenID = {};
339
340      var allAsyncEvents = this.allAsyncEvents_;
341      for (var i = 0; i < allAsyncEvents.length; i++) {
342        var asyncEventState = allAsyncEvents[i];
343
344        var event = asyncEventState.event;
345        var name = event.name;
346        if (name === undefined) {
347          this.model_.importErrors.push(
348              'Async events (ph: S, T or F) require an name parameter.');
349          continue;
350        }
351
352        var id = event.id;
353        if (id === undefined) {
354          this.model_.importErrors.push(
355              'Async events (ph: S, T or F) require an id parameter.');
356          continue;
357        }
358
359        // TODO(simonjam): Add a synchronous tick on the appropriate thread.
360
361        if (event.ph == 'S') {
362          if (asyncEventStatesByNameThenID[name] === undefined)
363            asyncEventStatesByNameThenID[name] = {};
364          if (asyncEventStatesByNameThenID[name][id]) {
365            this.model_.importErrors.push(
366                'At ' + event.ts + ', an slice of the same id ' + id +
367                ' was alrady open.');
368            continue;
369          }
370          asyncEventStatesByNameThenID[name][id] = [];
371          asyncEventStatesByNameThenID[name][id].push(asyncEventState);
372        } else {
373          if (asyncEventStatesByNameThenID[name] === undefined) {
374            this.model_.importErrors.push(
375                'At ' + event.ts + ', no slice named ' + name +
376                ' was open.');
377            continue;
378          }
379          if (asyncEventStatesByNameThenID[name][id] === undefined) {
380            this.model_.importErrors.push(
381                'At ' + event.ts + ', no slice named ' + name +
382                ' with id=' + id + ' was open.');
383            continue;
384          }
385          var events = asyncEventStatesByNameThenID[name][id];
386          events.push(asyncEventState);
387
388          if (event.ph == 'F') {
389            // Create a slice from start to end.
390            var slice = new tracing.TimelineAsyncSlice(
391                name,
392                tracing.getStringColorId(name),
393                events[0].event.ts / 1000);
394
395            slice.duration = (event.ts / 1000) - (events[0].event.ts / 1000);
396
397            slice.startThread = events[0].thread;
398            slice.endThread = asyncEventState.thread;
399            slice.id = id;
400            slice.args = events[0].event.args;
401            slice.subSlices = [];
402
403            // Create subSlices for each step.
404            for (var j = 1; j < events.length; ++j) {
405              var subName = name;
406              if (events[j - 1].event.ph == 'T')
407                subName = name + ':' + events[j - 1].event.args.step;
408              var subSlice = new tracing.TimelineAsyncSlice(
409                  subName,
410                  tracing.getStringColorId(name + j),
411                  events[j - 1].event.ts / 1000);
412
413              subSlice.duration =
414                  (events[j].event.ts / 1000) - (events[j - 1].event.ts / 1000);
415
416              subSlice.startThread = events[j - 1].thread;
417              subSlice.endThread = events[j].thread;
418              subSlice.id = id;
419              subSlice.args = events[j - 1].event.args;
420
421              slice.subSlices.push(subSlice);
422            }
423
424            // The args for the finish event go in the last subSlice.
425            var lastSlice = slice.subSlices[slice.subSlices.length - 1];
426            for (var arg in event.args)
427              lastSlice.args[arg] = event.args[arg];
428
429            // Add |slice| to the start-thread's asyncSlices.
430            slice.startThread.asyncSlices.push(slice);
431            delete asyncEventStatesByNameThenID[name][id];
432          }
433        }
434      }
435    }
436  };
437
438  tracing.TimelineModel.registerImporter(TraceEventImporter);
439
440  return {
441    TraceEventImporter: TraceEventImporter
442  };
443});
444