Home | History | Annotate | Download | only in importer
      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  */
     11 base.require('base.quad');
     12 base.require('tracing.trace_model');
     13 base.require('tracing.color_scheme');
     14 base.require('tracing.trace_model.instant_event');
     15 base.require('tracing.trace_model.counter_series');
     16 
     17 base.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