• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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'use strict';
6
7/**
8 * @fileoverview TraceEventImporter imports TraceEvent-formatted data
9 * into the provided model.
10 */
11base.require('base.quad');
12base.require('tracing.trace_model');
13base.require('tracing.color_scheme');
14base.require('tracing.trace_model.instant_event');
15base.require('tracing.trace_model.counter_series');
16
17base.exportTo('tracing.importer', function() {
18
19  function deepCopy(value) {
20    if (!(value instanceof Object)) {
21      if (value === undefined || value === null)
22        return value;
23      if (typeof value == 'string')
24        return value.substring();
25      if (typeof value == 'boolean')
26        return value;
27      if (typeof value == 'number')
28        return value;
29      throw new Error('Unrecognized: ' + typeof value);
30    }
31
32    var object = value;
33    if (object instanceof Array) {
34      var res = new Array(object.length);
35      for (var i = 0; i < object.length; i++)
36        res[i] = deepCopy(object[i]);
37      return res;
38    }
39
40    if (object.__proto__ != Object.prototype)
41      throw new Error('Can only clone simple types');
42    var res = {};
43    for (var key in object) {
44      res[key] = deepCopy(object[key]);
45    }
46    return res;
47  }
48
49  function TraceEventImporter(model, eventData) {
50    this.importPriority = 1;
51    this.model_ = model;
52    this.events_ = undefined;
53    this.systemTraceEvents_ = undefined;
54    this.eventsWereFromString_ = false;
55    this.allAsyncEvents_ = [];
56    this.allObjectEvents_ = [];
57
58    if (typeof(eventData) === 'string' || eventData instanceof String) {
59      // If the event data begins with a [, then we know it should end with a ].
60      // The reason we check for this is because some tracing implementations
61      // cannot guarantee that a ']' gets written to the trace file. So, we are
62      // forgiving and if this is obviously the case, we fix it up before
63      // throwing the string at JSON.parse.
64      if (eventData[0] === '[') {
65        eventData = eventData.replace(/[\r|\n]*$/, '')
66                             .replace(/\s*,\s*$/, '');
67        if (eventData[eventData.length - 1] !== ']')
68          eventData = eventData + ']';
69      }
70      this.events_ = JSON.parse(eventData);
71      this.eventsWereFromString_ = true;
72    } else {
73      this.events_ = eventData;
74    }
75
76    // Some trace_event implementations put the actual trace events
77    // inside a container. E.g { ... , traceEvents: [ ] }
78    // If we see that, just pull out the trace events.
79    if (this.events_.traceEvents) {
80      var container = this.events_;
81      this.events_ = this.events_.traceEvents;
82
83      // Some trace_event implementations put linux_perf_importer traces as a
84      // huge string inside container.systemTraceEvents. If we see that, pull it
85      // out. It will be picked up by extractSubtrace later on.
86      this.systemTraceEvents_ = container.systemTraceEvents;
87
88      // Any other fields in the container should be treated as metadata.
89      for (var fieldName in container) {
90        if (fieldName === 'traceEvents' || fieldName === 'systemTraceEvents')
91          continue;
92        this.model_.metadata.push({name: fieldName,
93          value: container[fieldName]});
94      }
95    }
96  }
97
98  /**
99   * @return {boolean} Whether obj is a TraceEvent array.
100   */
101  TraceEventImporter.canImport = function(eventData) {
102    // May be encoded JSON. But we dont want to parse it fully yet.
103    // Use a simple heuristic:
104    //   - eventData that starts with [ are probably trace_event
105    //   - eventData that starts with { are probably trace_event
106    // May be encoded JSON. Treat files that start with { as importable by us.
107    if (typeof(eventData) === 'string' || eventData instanceof String) {
108      return eventData[0] == '{' || eventData[0] == '[';
109    }
110
111    // Might just be an array of events
112    if (eventData instanceof Array && eventData.length && eventData[0].ph)
113      return true;
114
115    // Might be an object with a traceEvents field in it.
116    if (eventData.traceEvents)
117      return eventData.traceEvents instanceof Array &&
118          eventData.traceEvents[0].ph;
119
120    return false;
121  };
122
123  TraceEventImporter.prototype = {
124
125    __proto__: Object.prototype,
126
127    extractSubtrace: function() {
128      var tmp = this.systemTraceEvents_;
129      this.systemTraceEvents_ = undefined;
130      return tmp;
131    },
132
133    /**
134     * Deep copying is only needed if the trace was given to us as events.
135     */
136    deepCopyIfNeeded_: function(obj) {
137      if (this.eventsWereFromString_)
138        return obj;
139      return deepCopy(obj);
140    },
141
142    /**
143     * Helper to process an 'async finish' event, which will close an open slice
144     * on a AsyncSliceGroup object.
145     */
146    processAsyncEvent: function(event) {
147      var thread = this.model_.getOrCreateProcess(event.pid).
148          getOrCreateThread(event.tid);
149      this.allAsyncEvents_.push({
150        event: event,
151        thread: thread});
152    },
153
154    /**
155     * Helper that creates and adds samples to a Counter object based on
156     * 'C' phase events.
157     */
158    processCounterEvent: function(event) {
159      var ctr_name;
160      if (event.id !== undefined)
161        ctr_name = event.name + '[' + event.id + ']';
162      else
163        ctr_name = event.name;
164
165      var ctr = this.model_.getOrCreateProcess(event.pid)
166          .getOrCreateCounter(event.cat, ctr_name);
167
168      // Initialize the counter's series fields if needed.
169      if (ctr.numSeries === 0) {
170        for (var seriesName in event.args) {
171          ctr.addSeries(new tracing.trace_model.CounterSeries(seriesName,
172              tracing.getStringColorId(ctr.name + '.' + seriesName)));
173        }
174
175        if (ctr.numSeries === 0) {
176          this.model_.importErrors.push('Expected counter ' + event.name +
177              ' to have at least one argument to use as a value.');
178
179          // Drop the counter.
180          delete ctr.parent.counters[ctr.name];
181          return;
182        }
183      }
184
185      var ts = event.ts / 1000;
186      ctr.series.forEach(function(series) {
187        var val = event.args[series.name] ? event.args[series.name] : 0;
188        series.addSample(ts, val);
189      });
190    },
191
192    processObjectEvent: function(event) {
193      var thread = this.model_.getOrCreateProcess(event.pid).
194          getOrCreateThread(event.tid);
195      this.allObjectEvents_.push({
196        event: event,
197        thread: thread});
198    },
199
200    processDurationEvent: function(event) {
201      var thread = this.model_.getOrCreateProcess(event.pid)
202        .getOrCreateThread(event.tid);
203      if (!thread.sliceGroup.isTimestampValidForBeginOrEnd(event.ts / 1000)) {
204        this.model_.importErrors.push(
205            'Timestamps are moving backward.');
206        return;
207      }
208
209      if (event.ph == 'B') {
210        thread.sliceGroup.beginSlice(event.cat, event.name, event.ts / 1000,
211                                     this.deepCopyIfNeeded_(event.args));
212      } else {
213        if (!thread.sliceGroup.openSliceCount) {
214          this.model_.importErrors.push(
215              'E phase event without a matching B phase event.');
216          return;
217        }
218
219        var slice = thread.sliceGroup.endSlice(event.ts / 1000);
220        for (var arg in event.args) {
221          if (slice.args[arg] !== undefined) {
222            this.model_.importErrors.push(
223                'Both the B and E phases of ' + slice.name +
224                'provided values for argument ' + arg + '. ' +
225                'The value of the E phase event will be used.');
226          }
227          slice.args[arg] = this.deepCopyIfNeeded_(event.args[arg]);
228        }
229      }
230    },
231
232    processMetadataEvent: function(event) {
233      if (event.name == 'process_name') {
234        var process = this.model_.getOrCreateProcess(event.pid);
235        process.name = event.args.name;
236      } else if (event.name == 'process_labels') {
237        var process = this.model_.getOrCreateProcess(event.pid);
238        process.labels.push.apply(
239            process.labels, event.args.labels.split(','));
240      } else if (event.name == 'process_sort_index') {
241        var process = this.model_.getOrCreateProcess(event.pid);
242        process.sortIndex = event.args.sort_index;
243      } else if (event.name == 'thread_name') {
244        var thread = this.model_.getOrCreateProcess(event.pid).
245            getOrCreateThread(event.tid);
246        thread.name = event.args.name;
247      } else if (event.name == 'thread_sort_index') {
248        var thread = this.model_.getOrCreateProcess(event.pid).
249            getOrCreateThread(event.tid);
250        thread.sortIndex = event.args.sort_index;
251      } else {
252        this.model_.importErrors.push(
253            'Unrecognized metadata name: ' + event.name);
254      }
255    },
256
257    // Treat an Instant event as a duration 0 slice.
258    // SliceTrack's redraw() knows how to handle this.
259    processInstantEvent: function(event) {
260      var constructor;
261      switch (event.s) {
262        case 'g':
263          constructor = tracing.trace_model.GlobalInstantEvent;
264          break;
265        case 'p':
266          constructor = tracing.trace_model.ProcessInstantEvent;
267          break;
268        case 't':
269          // fall through
270        default:
271          // Default to thread to support old style input files.
272          constructor = tracing.trace_model.ThreadInstantEvent;
273          break;
274      }
275
276      var colorId = tracing.getStringColorId(event.name);
277      var instantEvent = new constructor(event.cat, event.name,
278          colorId, event.ts / 1000, this.deepCopyIfNeeded_(event.args));
279
280      switch (instantEvent.type) {
281        case tracing.trace_model.InstantEventType.GLOBAL:
282          this.model_.pushInstantEvent(instantEvent);
283          break;
284
285        case tracing.trace_model.InstantEventType.PROCESS:
286          var process = this.model_.getOrCreateProcess(event.pid);
287          process.pushInstantEvent(instantEvent);
288          break;
289
290        case tracing.trace_model.InstantEventType.THREAD:
291          var thread = this.model_.getOrCreateProcess(event.pid)
292              .getOrCreateThread(event.tid);
293          thread.sliceGroup.pushInstantEvent(instantEvent);
294          break;
295        default:
296          throw new Error('Unknown instant event type: ' + event.s);
297      }
298    },
299
300    processSampleEvent: function(event) {
301      var thread = this.model_.getOrCreateProcess(event.pid)
302        .getOrCreateThread(event.tid);
303      thread.addSample(event.cat, event.name, event.ts / 1000,
304                       this.deepCopyIfNeeded_(event.args));
305    },
306
307    /**
308     * Walks through the events_ list and outputs the structures discovered to
309     * model_.
310     */
311    importEvents: function() {
312      var events = this.events_;
313      for (var eI = 0; eI < events.length; eI++) {
314        var event = events[eI];
315        if (event.ph === 'B' || event.ph === 'E') {
316          this.processDurationEvent(event);
317
318        } else if (event.ph === 'S' || event.ph === 'F' || event.ph === 'T') {
319          this.processAsyncEvent(event);
320
321        // Note, I is historic. The instant event marker got changed, but we
322        // want to support loading load trace files so we have both I and i.
323        } else if (event.ph == 'I' || event.ph == 'i') {
324          this.processInstantEvent(event);
325
326        } else if (event.ph == 'P') {
327          this.processSampleEvent(event);
328
329        } else if (event.ph == 'C') {
330          this.processCounterEvent(event);
331
332        } else if (event.ph == 'M') {
333          this.processMetadataEvent(event);
334
335        } else if (event.ph === 'N' || event.ph === 'D' || event.ph === 'O') {
336          this.processObjectEvent(event);
337
338        } else if (event.ph === 's' || event.ph === 't' || event.ph === 'f') {
339          // NB: toss flow events until there's proper support
340
341        } else {
342          this.model_.importErrors.push('Unrecognized event phase: ' +
343              event.ph + ' (' + event.name + ')');
344        }
345      }
346    },
347
348    /**
349     * Called by the Model after all other importers have imported their
350     * events.
351     */
352    finalizeImport: function() {
353      this.createAsyncSlices_();
354      this.createExplicitObjects_();
355      this.createImplicitObjects_();
356    },
357
358    /**
359     * Called by the model to join references between objects, after final model
360     * bounds have been computed.
361     */
362    joinRefs: function() {
363      this.joinObjectRefs_();
364    },
365
366    createAsyncSlices_: function() {
367      if (this.allAsyncEvents_.length == 0)
368        return;
369
370      this.allAsyncEvents_.sort(function(x, y) {
371        return x.event.ts - y.event.ts;
372      });
373
374      var asyncEventStatesByNameThenID = {};
375
376      var allAsyncEvents = this.allAsyncEvents_;
377      for (var i = 0; i < allAsyncEvents.length; i++) {
378        var asyncEventState = allAsyncEvents[i];
379
380        var event = asyncEventState.event;
381        var name = event.name;
382        if (name === undefined) {
383          this.model_.importErrors.push(
384              'Async events (ph: S, T or F) require an name parameter.');
385          continue;
386        }
387
388        var id = event.id;
389        if (id === undefined) {
390          this.model_.importErrors.push(
391              'Async events (ph: S, T or F) require an id parameter.');
392          continue;
393        }
394
395        // TODO(simonjam): Add a synchronous tick on the appropriate thread.
396
397        if (event.ph == 'S') {
398          if (asyncEventStatesByNameThenID[name] === undefined)
399            asyncEventStatesByNameThenID[name] = {};
400          if (asyncEventStatesByNameThenID[name][id]) {
401            this.model_.importErrors.push(
402                'At ' + event.ts + ', a slice of the same id ' + id +
403                ' was alrady open.');
404            continue;
405          }
406          asyncEventStatesByNameThenID[name][id] = [];
407          asyncEventStatesByNameThenID[name][id].push(asyncEventState);
408        } else {
409          if (asyncEventStatesByNameThenID[name] === undefined) {
410            this.model_.importErrors.push(
411                'At ' + event.ts + ', no slice named ' + name +
412                ' was open.');
413            continue;
414          }
415          if (asyncEventStatesByNameThenID[name][id] === undefined) {
416            this.model_.importErrors.push(
417                'At ' + event.ts + ', no slice named ' + name +
418                ' with id=' + id + ' was open.');
419            continue;
420          }
421          var events = asyncEventStatesByNameThenID[name][id];
422          events.push(asyncEventState);
423
424          if (event.ph == 'F') {
425            // Create a slice from start to end.
426            var slice = new tracing.trace_model.AsyncSlice(
427                events[0].event.cat,
428                name,
429                tracing.getStringColorId(name),
430                events[0].event.ts / 1000);
431
432            slice.duration = (event.ts / 1000) - (events[0].event.ts / 1000);
433
434            slice.startThread = events[0].thread;
435            slice.endThread = asyncEventState.thread;
436            slice.id = id;
437            slice.args = this.deepCopyIfNeeded_(events[0].event.args);
438            slice.subSlices = [];
439
440            // Create subSlices for each step.
441            for (var j = 1; j < events.length; ++j) {
442              var subName = name;
443              if (events[j - 1].event.ph == 'T')
444                subName = name + ':' + events[j - 1].event.args.step;
445              var subSlice = new tracing.trace_model.AsyncSlice(
446                  events[0].event.cat,
447                  subName,
448                  tracing.getStringColorId(name + j),
449                  events[j - 1].event.ts / 1000);
450
451              subSlice.duration =
452                  (events[j].event.ts / 1000) - (events[j - 1].event.ts / 1000);
453
454              subSlice.startThread = events[j - 1].thread;
455              subSlice.endThread = events[j].thread;
456              subSlice.id = id;
457              subSlice.args = this.deepCopyIfNeeded_(events[j - 1].event.args);
458
459              slice.subSlices.push(subSlice);
460            }
461
462            // The args for the finish event go in the last subSlice.
463            var lastSlice = slice.subSlices[slice.subSlices.length - 1];
464            for (var arg in event.args)
465              lastSlice.args[arg] = this.deepCopyIfNeeded_(event.args[arg]);
466
467            // Add |slice| to the start-thread's asyncSlices.
468            slice.startThread.asyncSliceGroup.push(slice);
469            delete asyncEventStatesByNameThenID[name][id];
470          }
471        }
472      }
473    },
474
475    /**
476     * This function creates objects described via the N, D, and O phase
477     * events.
478     */
479    createExplicitObjects_: function() {
480      if (this.allObjectEvents_.length == 0)
481        return;
482
483      function processEvent(objectEventState) {
484        var event = objectEventState.event;
485        var thread = objectEventState.thread;
486        if (event.name === undefined) {
487          this.model_.importErrors.push(
488              'While processing ' + JSON.stringify(event) + ': ' +
489              'Object events require an name parameter.');
490        }
491
492        if (event.id === undefined) {
493          this.model_.importErrors.push(
494              'While processing ' + JSON.stringify(event) + ': ' +
495              'Object events require an id parameter.');
496        }
497        var process = thread.parent;
498        var ts = event.ts / 1000;
499        var instance;
500        if (event.ph == 'N') {
501          try {
502            instance = process.objects.idWasCreated(
503                event.id, event.cat, event.name, ts);
504          } catch (e) {
505            this.model_.importErrors.push(
506                'While processing create of ' +
507                event.id + ' at ts=' + ts + ': ' + e);
508            return;
509          }
510        } else if (event.ph == 'O') {
511          if (event.args.snapshot === undefined) {
512            this.model_.importErrors.push(
513                'While processing ' + event.id + ' at ts=' + ts + ': ' +
514                'Snapshots must have args: {snapshot: ...}');
515            return;
516          }
517          var snapshot;
518          try {
519            snapshot = process.objects.addSnapshot(
520                event.id, event.cat, event.name, ts,
521                this.deepCopyIfNeeded_(event.args.snapshot));
522          } catch (e) {
523            this.model_.importErrors.push(
524                'While processing snapshot of ' +
525                event.id + ' at ts=' + ts + ': ' + e);
526            return;
527          }
528          instance = snapshot.objectInstance;
529        } else if (event.ph == 'D') {
530          try {
531            instance = process.objects.idWasDeleted(
532                event.id, event.cat, event.name, ts);
533          } catch (e) {
534            this.model_.importErrors.push(
535                'While processing delete of ' +
536                event.id + ' at ts=' + ts + ': ' + e);
537            return;
538          }
539        }
540
541        if (instance)
542          instance.colorId = tracing.getStringColorId(instance.typeName);
543      }
544
545      this.allObjectEvents_.sort(function(x, y) {
546        return x.event.ts - y.event.ts;
547      });
548
549      var allObjectEvents = this.allObjectEvents_;
550      for (var i = 0; i < allObjectEvents.length; i++) {
551        var objectEventState = allObjectEvents[i];
552        try {
553          processEvent.call(this, objectEventState);
554        } catch (e) {
555          this.model_.importErrors.push(e.message);
556        }
557      }
558    },
559
560    createImplicitObjects_: function() {
561      base.iterItems(this.model_.processes, function(pid, process) {
562        this.createImplicitObjectsForProcess_(process);
563      }, this);
564    },
565
566    // Here, we collect all the snapshots that internally contain a
567    // Javascript-level object inside their args list that has an "id" field,
568    // and turn that into a snapshot of the instance referred to by id.
569    createImplicitObjectsForProcess_: function(process) {
570
571      function processField(referencingObject,
572                            referencingObjectFieldName,
573                            referencingObjectFieldValue,
574                            containingSnapshot) {
575        if (!referencingObjectFieldValue)
576          return;
577
578        if (referencingObjectFieldValue.id === undefined)
579          return;
580        if (referencingObjectFieldValue instanceof
581            tracing.trace_model.ObjectSnapshot)
582          return;
583
584        var implicitSnapshot = referencingObjectFieldValue;
585
586        var rawId = implicitSnapshot.id;
587        var m = /(.+)\/(.+)/.exec(rawId);
588        if (!m)
589          throw new Error('Implicit snapshots must have names.');
590        delete implicitSnapshot.id;
591        var name = m[1];
592        var id = m[2];
593        var res;
594        try {
595          res = process.objects.addSnapshot(
596              id, containingSnapshot.objectInstance.category,
597              name, containingSnapshot.ts,
598              implicitSnapshot);
599        } catch (e) {
600          this.model_.importErrors.push(
601              'While processing implicit snapshot of ' +
602              rawId + ' at ts=' + containingSnapshot.ts + ': ' + e);
603          return;
604        }
605        res.objectInstance.hasImplicitSnapshots = true;
606        res.containingSnapshot = containingSnapshot;
607        referencingObject[referencingObjectFieldName] = res;
608        if (!(res instanceof tracing.trace_model.ObjectSnapshot))
609          throw new Error('Created object must be instanceof snapshot');
610        return res.args;
611      }
612
613      function iterObject(object, func, containingSnapshot, thisArg) {
614        if (!(object instanceof Object))
615          return;
616
617        if (object instanceof Array) {
618          for (var i = 0; i < object.length; i++) {
619            var res = func.call(thisArg, object, i, object[i],
620                                containingSnapshot);
621            if (res)
622              iterObject(res, func, containingSnapshot, thisArg);
623            else
624              iterObject(object[i], func, containingSnapshot, thisArg);
625          }
626          return;
627        }
628
629        for (var key in object) {
630          var res = func.call(thisArg, object, key, object[key],
631                              containingSnapshot);
632          if (res)
633            iterObject(res, func, containingSnapshot, thisArg);
634          else
635            iterObject(object[key], func, containingSnapshot, thisArg);
636        }
637      }
638
639      // TODO(nduca): We may need to iterate the instances in sorted order by
640      // creationTs.
641      process.objects.iterObjectInstances(function(instance) {
642        instance.snapshots.forEach(function(snapshot) {
643          if (snapshot.args.id !== undefined)
644            throw new Error('args cannot have an id field inside it');
645          iterObject(snapshot.args, processField, snapshot, this);
646        }, this);
647      }, this);
648    },
649
650    joinObjectRefs_: function() {
651      base.iterItems(this.model_.processes, function(pid, process) {
652        this.joinObjectRefsForProcess_(process);
653      }, this);
654    },
655
656    joinObjectRefsForProcess_: function(process) {
657      // Iterate the world, looking for id_refs
658      var patchupsToApply = [];
659      base.iterItems(process.threads, function(tid, thread) {
660        thread.asyncSliceGroup.slices.forEach(function(item) {
661          this.searchItemForIDRefs_(
662              patchupsToApply, process.objects, 'start', item);
663        }, this);
664        thread.sliceGroup.slices.forEach(function(item) {
665          this.searchItemForIDRefs_(
666              patchupsToApply, process.objects, 'start', item);
667        }, this);
668      }, this);
669      process.objects.iterObjectInstances(function(instance) {
670        instance.snapshots.forEach(function(item) {
671          this.searchItemForIDRefs_(
672              patchupsToApply, process.objects, 'ts', item);
673        }, this);
674      }, this);
675
676      // Change all the fields pointing at id_refs to their real values.
677      patchupsToApply.forEach(function(patchup) {
678        patchup.object[patchup.field] = patchup.value;
679      });
680    },
681
682    searchItemForIDRefs_: function(patchupsToApply, objectCollection,
683                                   itemTimestampField, item) {
684      if (!item.args)
685        throw new Error('');
686
687      function handleField(object, fieldName, fieldValue) {
688        if (fieldValue === undefined ||
689            (!fieldValue.id_ref && !fieldValue.idRef))
690          return;
691
692        var id = fieldValue.id_ref || fieldValue.idRef;
693        var ts = item[itemTimestampField];
694        var snapshot = objectCollection.getSnapshotAt(id, ts);
695        if (!snapshot)
696          return;
697
698        // We have to delay the actual change to the new value until after all
699        // refs have been located. Otherwise, we could end up recursing in
700        // ways we definitely didn't intend.
701        patchupsToApply.push({object: object,
702          field: fieldName,
703          value: snapshot});
704      }
705      function iterObjectFieldsRecursively(object) {
706        if (!(object instanceof Object))
707          return;
708
709        if ((object instanceof tracing.trace_model.ObjectSnapshot) ||
710            (object instanceof Float32Array) ||
711            (object instanceof base.Quad))
712          return;
713
714        if (object instanceof Array) {
715          for (var i = 0; i < object.length; i++) {
716            handleField(object, i, object[i]);
717            iterObjectFieldsRecursively(object[i]);
718          }
719          return;
720        }
721
722        for (var key in object) {
723          var value = object[key];
724          handleField(object, key, value);
725          iterObjectFieldsRecursively(value);
726        }
727      }
728
729      iterObjectFieldsRecursively(item.args);
730    }
731  };
732
733  tracing.TraceModel.registerImporter(TraceEventImporter);
734
735  return {
736    TraceEventImporter: TraceEventImporter
737  };
738});
739