text: /**
  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.
  **/
  
  require("../../base/color_scheme.js");
  require("../../base/quad.js");
  require("../../base/range.js");
  require("../../base/units/units.js");
  require("../../base/utils.js");
  require("./trace_code_entry.js");
  require("./trace_code_map.js");
  require("./v8/codemap.js");
  require("../../importer/importer.js");
  require("../../model/attribute.js");
  require("../../model/comment_box_annotation.js");
  require("../../model/counter_series.js");
  require("../../model/flow_event.js");
  require("../../model/global_memory_dump.js");
  require("../../model/heap_dump.js");
  require("../../model/instant_event.js");
  require("../../model/memory_allocator_dump.js");
  require("../../model/model.js");
  require("../../model/process_memory_dump.js");
  require("../../model/rect_annotation.js");
  require("../../model/slice_group.js");
  require("../../model/x_marker_annotation.js");
  
  'use strict';
  
  /**
   * @fileoverview TraceEventImporter imports TraceEvent-formatted data
   * into the provided model.
   */
  global.tr.exportTo('tr.e.importer', function() {
    var deepCopy = tr.b.deepCopy;
    var ColorScheme = tr.b.ColorScheme;
  
    function getEventColor(event, opt_customName) {
      if (event.cname)
        return ColorScheme.getColorIdForReservedName(event.cname);
      else if (opt_customName || event.name) {
        return ColorScheme.getColorIdForGeneralPurposeString(
            opt_customName || event.name);
      }
    }
  
    var timestampFromUs = tr.b.u.Units.timestampFromUs;
    var maybeTimestampFromUs = tr.b.u.Units.maybeTimestampFromUs;
  
    var PRODUCER = 'producer';
    var CONSUMER = 'consumer';
    var STEP = 'step';
  
    var MEMORY_DUMP_LEVELS_OF_DETAIL = [undefined, 'light', 'detailed'];
    var GLOBAL_MEMORY_ALLOCATOR_DUMP_PREFIX = 'global/';
  
    function TraceEventImporter(model, eventData) {
      this.importPriority = 1;
      this.model_ = model;
      this.events_ = undefined;
      this.sampleEvents_ = undefined;
      this.stackFrameEvents_ = undefined;
      this.systemTraceEvents_ = undefined;
      this.battorData_ = undefined;
      this.eventsWereFromString_ = false;
      this.softwareMeasuredCpuCount_ = undefined;
  
      this.allAsyncEvents_ = [];
      this.allFlowEvents_ = [];
      this.allObjectEvents_ = [];
  
      this.traceEventSampleStackFramesByName_ = {};
  
      this.v8ProcessCodeMaps_ = {};
      this.v8ProcessRootStackFrame_ = {};
  
      // Dump ID -> PID -> [process memory dump events].
      this.allMemoryDumpEvents_ = {};
  
      if (typeof(eventData) === 'string' || eventData instanceof String) {
        eventData = eventData.trim();
        // 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] === '[') {
          eventData = eventData.replace(/\s*,\s*$/, '');
          if (eventData[eventData.length - 1] !== ']')
            eventData = eventData + ']';
        }
  
        this.events_ = JSON.parse(eventData);
        this.eventsWereFromString_ = true;
      } else {
        this.events_ = eventData;
      }
  
      this.traceAnnotations_ = this.events_.traceAnnotations;
  
      // 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) {
        var container = this.events_;
        this.events_ = this.events_.traceEvents;
  
        // Some trace_event implementations put ftrace_importer traces as a
        // huge string inside container.systemTraceEvents. If we see that, pull it
        // out. It will be picked up by extractSubtraces later on.
        this.systemTraceEvents_ = container.systemTraceEvents;
  
        // Some trace_event implementations put battor power traces as a
        // huge string inside container.battorLogAsString. If we see that, pull
        // it out. It will be picked up by extractSubtraces later on.
        this.battorData_ = container.battorLogAsString;
  
        // Sampling data.
        this.sampleEvents_ = container.samples;
        this.stackFrameEvents_ = container.stackFrames;
  
        // Some implementations specify displayTimeUnit
        if (container.displayTimeUnit) {
          var unitName = container.displayTimeUnit;
          var unit = tr.b.u.TimeDisplayModes[unitName];
          if (unit === undefined) {
            throw new Error('Unit ' + unitName + ' is not supported.');
          }
          this.model_.intrinsicTimeUnit = unit;
        }
  
        var knownFieldNames = {
          battorLogAsString: true,
          samples: true,
          stackFrames: true,
          systemTraceEvents: true,
          traceAnnotations: true,
          traceEvents: true
        };
        // Any other fields in the container should be treated as metadata.
        for (var fieldName in container) {
          if (fieldName in knownFieldNames)
            continue;
          this.model_.metadata.push({name: fieldName,
            value: container[fieldName]});
          if (fieldName === 'metadata' && container[fieldName]['highres-ticks']) {
            this.model_.isTimeHighResolution =
                container[fieldName]['highres-ticks'];
          }
        }
      }
    }
  
    /**
     * @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) {
        eventData = eventData.trim();
        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) {
        if (eventData.traceEvents instanceof Array) {
          if (eventData.traceEvents.length && eventData.traceEvents[0].ph)
            return true;
          if (eventData.samples.length && eventData.stackFrames !== undefined)
            return true;
        }
      }
  
      return false;
    };
  
    TraceEventImporter.prototype = {
      __proto__: tr.importer.Importer.prototype,
  
      extractSubtraces: function() {
        var systemEventsTmp = this.systemTraceEvents_;
        var battorDataTmp = this.battorData_;
        this.systemTraceEvents_ = undefined;
        this.battorData_ = undefined;
        var subTraces = systemEventsTmp ? [systemEventsTmp] : [];
        if (battorDataTmp)
          subTraces.push(battorDataTmp);
         return subTraces;
      },
  
      /**
       * Deep copying is only needed if the trace was given to us as events.
       */
      deepCopyIfNeeded_: function(obj) {
        if (obj === undefined)
          obj = {};
        if (this.eventsWereFromString_)
          return obj;
        return deepCopy(obj);
      },
  
      /**
       * Always perform deep copying.
       */
      deepCopyAlways_: function(obj) {
        if (obj === undefined)
          obj = {};
        return deepCopy(obj);
      },
  
      /**
       * Helper to process an async event.
       */
      processAsyncEvent: function(event) {
        var thread = this.model_.getOrCreateProcess(event.pid).
            getOrCreateThread(event.tid);
        this.allAsyncEvents_.push({
          sequenceNumber: this.allAsyncEvents_.length,
          event: event,
          thread: thread
        });
      },
  
      /**
       * Helper to process a flow event.
       */
      processFlowEvent: function(event, opt_slice) {
        var thread = this.model_.getOrCreateProcess(event.pid).
            getOrCreateThread(event.tid);
        this.allFlowEvents_.push({
          refGuid: tr.b.GUID.getLastGuid(),
          sequenceNumber: this.allFlowEvents_.length,
          event: event,
          slice: opt_slice,  // slice for events that have flow info
          thread: thread
        });
      },
  
      /**
       * Helper that creates and adds samples to a Counter 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);
        var reservedColorId = event.cname ? getEventColor(event) : undefined;
  
        // Initialize the counter's series fields if needed.
        if (ctr.numSeries === 0) {
          for (var seriesName in event.args) {
            var colorId = reservedColorId ||
                getEventColor(event, ctr.name + '.' + seriesName);
            ctr.addSeries(new tr.model.CounterSeries(seriesName, colorId));
          }
  
          if (ctr.numSeries === 0) {
            this.model_.importWarning({
              type: 'counter_parse_error',
              message: '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;
          }
        }
  
        var ts = timestampFromUs(event.ts);
        ctr.series.forEach(function(series) {
          var val = event.args[series.name] ? event.args[series.name] : 0;
          series.addCounterSample(ts, val);
        });
      },
  
      processObjectEvent: function(event) {
        var thread = this.model_.getOrCreateProcess(event.pid).
            getOrCreateThread(event.tid);
        this.allObjectEvents_.push({
          sequenceNumber: this.allObjectEvents_.length,
          event: event,
          thread: thread});
      },
  
      processDurationEvent: function(event) {
        var thread = this.model_.getOrCreateProcess(event.pid)
          .getOrCreateThread(event.tid);
        var ts = timestampFromUs(event.ts);
        if (!thread.sliceGroup.isTimestampValidForBeginOrEnd(ts)) {
          this.model_.importWarning({
            type: 'duration_parse_error',
            message: 'Timestamps are moving backward.'
          });
          return;
        }
  
        if (event.ph === 'B') {
          var slice = thread.sliceGroup.beginSlice(
              event.cat, event.name, timestampFromUs(event.ts),
              this.deepCopyIfNeeded_(event.args),
              timestampFromUs(event.tts), event.argsStripped,
              getEventColor(event));
          slice.startStackFrame = this.getStackFrameForEvent_(event);
        } else if (event.ph === 'I' || event.ph === 'i' || event.ph === 'R') {
          if (event.s !== undefined && event.s !== 't')
            throw new Error('This should never happen');
  
          thread.sliceGroup.beginSlice(event.cat, event.name,
                                       timestampFromUs(event.ts),
                                       this.deepCopyIfNeeded_(event.args),
                                       timestampFromUs(event.tts),
                                       event.argsStripped,
                                       getEventColor(event));
          var slice = thread.sliceGroup.endSlice(timestampFromUs(event.ts),
                                     timestampFromUs(event.tts));
          slice.startStackFrame = this.getStackFrameForEvent_(event);
          slice.endStackFrame = undefined;
        } else {
          if (!thread.sliceGroup.openSliceCount) {
            this.model_.importWarning({
              type: 'duration_parse_error',
              message: 'E phase event without a matching B phase event.'
            });
            return;
          }
  
          var slice = thread.sliceGroup.endSlice(timestampFromUs(event.ts),
                                                 timestampFromUs(event.tts),
                                                 getEventColor(event));
          if (event.name && slice.title != event.name) {
            this.model_.importWarning({
              type: 'title_match_error',
              message: 'Titles do not match. Title is ' +
                  slice.title + ' in openSlice, and is ' +
                  event.name + ' in endSlice'
            });
          }
          slice.endStackFrame = this.getStackFrameForEvent_(event);
  
          this.mergeArgsInto_(slice.args, event.args, slice.title);
        }
      },
  
      mergeArgsInto_: function(dstArgs, srcArgs, eventName) {
        for (var arg in srcArgs) {
          if (dstArgs[arg] !== undefined) {
            this.model_.importWarning({
              type: 'arg_merge_error',
              message: 'Different phases of ' + eventName +
                  ' provided values for argument ' + arg + '.' +
                  ' The last provided value will be used.'
            });
          }
          dstArgs[arg] = this.deepCopyIfNeeded_(srcArgs[arg]);
        }
      },
  
      processCompleteEvent: function(event) {
        // Preventing the overhead slices from making it into the model. This
        // only applies to legacy traces, as the overhead traces have been
        // removed from the chromium code.
        if (event.cat !== undefined &&
            event.cat.indexOf('trace_event_overhead') > -1)
          return undefined;
  
        var thread = this.model_.getOrCreateProcess(event.pid)
            .getOrCreateThread(event.tid);
  
        if (event.flow_out) {
          if (event.flow_in)
            event.flowPhase = STEP;
          else
            event.flowPhase = PRODUCER;
        } else if (event.flow_in) {
          event.flowPhase = CONSUMER;
        }
  
        var slice = thread.sliceGroup.pushCompleteSlice(event.cat, event.name,
            timestampFromUs(event.ts),
            maybeTimestampFromUs(event.dur),
            maybeTimestampFromUs(event.tts),
            maybeTimestampFromUs(event.tdur),
            this.deepCopyIfNeeded_(event.args),
            event.argsStripped,
            getEventColor(event),
            event.bind_id);
        slice.startStackFrame = this.getStackFrameForEvent_(event);
        slice.endStackFrame = this.getStackFrameForEvent_(event, true);
  
        return slice;
      },
  
      processMetadataEvent: function(event) {
        // The metadata events aren't useful without args.
        if (event.argsStripped)
          return;
  
        if (event.name === 'process_name') {
          var process = this.model_.getOrCreateProcess(event.pid);
          process.name = event.args.name;
        } else if (event.name === 'process_labels') {
          var process = this.model_.getOrCreateProcess(event.pid);
          var labels = event.args.labels.split(',');
          for (var i = 0; i < labels.length; i++)
            process.addLabelIfNeeded(labels[i]);
        } else if (event.name === 'process_sort_index') {
          var process = this.model_.getOrCreateProcess(event.pid);
          process.sortIndex = event.args.sort_index;
        } else if (event.name === 'thread_name') {
          var thread = this.model_.getOrCreateProcess(event.pid).
              getOrCreateThread(event.tid);
          thread.name = event.args.name;
        } else if (event.name === 'thread_sort_index') {
          var thread = this.model_.getOrCreateProcess(event.pid).
              getOrCreateThread(event.tid);
          thread.sortIndex = event.args.sort_index;
        } else if (event.name === 'num_cpus') {
          var n = event.args.number;
          // Not all render processes agree on the cpu count in trace_event. Some
          // processes will report 1, while others will report the actual cpu
          // count. To deal with this, take the max of what is reported.
          if (this.softwareMeasuredCpuCount_ !== undefined)
            n = Math.max(n, this.softwareMeasuredCpuCount_);
          this.softwareMeasuredCpuCount_ = n;
        } else if (event.name === 'stackFrames') {
          var stackFrames = event.args.stackFrames;
          if (stackFrames === undefined) {
            this.model_.importWarning({
              type: 'metadata_parse_error',
              message: 'No stack frames found in a \'' + event.name +
                  '\' metadata event'
            });
          } else {
            this.importStackFrames_(
                stackFrames, 'p' + event.pid + ':', true /* addRootFrame */);
          }
        } else {
          this.model_.importWarning({
            type: 'metadata_parse_error',
            message: 'Unrecognized metadata name: ' + event.name
          });
        }
      },
  
      processJitCodeEvent: function(event) {
        if (this.v8ProcessCodeMaps_[event.pid] === undefined)
          this.v8ProcessCodeMaps_[event.pid] = new tr.e.importer.TraceCodeMap();
        var map = this.v8ProcessCodeMaps_[event.pid];
  
        var data = event.args.data;
        if (event.name === 'JitCodeMoved')
          map.moveEntry(data.code_start, data.new_code_start, data.code_len);
        else
          map.addEntry(data.code_start, data.code_len, data.name, data.script_id);
      },
  
      processInstantEvent: function(event) {
        // V8 JIT events are logged as phase 'I' so we need to separate them out
        // and handle specially.
        //
        // TODO(dsinclair): There are _a lot_ of JitCode events so I'm skipping
        // the display for now. Can revisit later if we want to show them.
        if (event.name === 'JitCodeAdded' || event.name === 'JitCodeMoved') {
          this.processJitCodeEvent(event);
          return;
        }
  
        // Thread-level instant events are treated as zero-duration slices.
        if (event.s === 't' || event.s === undefined) {
          this.processDurationEvent(event);
          return;
        }
  
        var constructor;
        switch (event.s) {
          case 'g':
            constructor = tr.model.GlobalInstantEvent;
            break;
          case 'p':
            constructor = tr.model.ProcessInstantEvent;
            break;
          default:
            this.model_.importWarning({
              type: 'instant_parse_error',
              message: 'I phase event with unknown "s" field value.'
            });
            return;
        }
  
        var instantEvent = new constructor(event.cat, event.name,
            getEventColor(event), timestampFromUs(event.ts),
            this.deepCopyIfNeeded_(event.args));
  
        switch (instantEvent.type) {
          case tr.model.InstantEventType.GLOBAL:
            this.model_.pushInstantEvent(instantEvent);
            break;
  
          case tr.model.InstantEventType.PROCESS:
            var process = this.model_.getOrCreateProcess(event.pid);
            process.pushInstantEvent(instantEvent);
            break;
  
          default:
            throw new Error('Unknown instant event type: ' + event.s);
        }
      },
  
      processV8Sample: function(event) {
        var data = event.args.data;
  
        // As-per DevTools, the backend sometimes creates bogus samples. Skip it.
        if (data.vm_state === 'js' && !data.stack.length)
          return;
  
        var rootStackFrame = this.v8ProcessRootStackFrame_[event.pid];
        if (!rootStackFrame) {
          rootStackFrame = new tr.model.StackFrame(
              undefined /* parent */, 'v8-root-stack-frame' /* id */,
              'v8-root-stack-frame' /* title */, 0 /* colorId */);
          this.v8ProcessRootStackFrame_[event.pid] = rootStackFrame;
        }
  
        function findChildWithEntryID(stackFrame, entryID) {
          return tr.b.findFirstInArray(stackFrame.children, function(child) {
            return child.entryID === entryID;
          });
        }
  
        var model = this.model_;
        function addStackFrame(lastStackFrame, entry) {
          var childFrame = findChildWithEntryID(lastStackFrame, entry.id);
          if (childFrame)
            return childFrame;
  
          var frame = new tr.model.StackFrame(
              lastStackFrame, tr.b.GUID.allocate(), entry.name,
              ColorScheme.getColorIdForGeneralPurposeString(entry.name),
              entry.sourceInfo);
  
          frame.entryID = entry.id;
          model.addStackFrame(frame);
          return frame;
        }
  
        var lastStackFrame = rootStackFrame;
  
        // There are several types of v8 sample events, gc, native, compiler, etc.
        // Some of these types have stacks and some don't, we handle those two
        // cases differently. For types that don't have any stack frames attached
        // we synthesize one based on the type of thing that's happening so when
        // we view all the samples we'll see something like 'external' or 'gc'
        // as a fraction of the time spent.
        if (data.stack.length > 0 && this.v8ProcessCodeMaps_[event.pid]) {
          var map = this.v8ProcessCodeMaps_[event.pid];
  
          // Stacks have the leaf node first, flip them around so the root
          // comes first.
          data.stack.reverse();
  
          for (var i = 0; i < data.stack.length; i++) {
            var entry = map.lookupEntry(data.stack[i]);
            if (entry === undefined) {
              entry = {
                id: 'unknown',
                name: 'unknown',
                sourceInfo: undefined
              };
            }
  
            lastStackFrame = addStackFrame(lastStackFrame, entry);
          }
        } else {
          var entry = {
            id: data.vm_state,
            name: data.vm_state,
            sourceInfo: undefined
          };
          lastStackFrame = addStackFrame(lastStackFrame, entry);
        }
  
        var thread = this.model_.getOrCreateProcess(event.pid)
          .getOrCreateThread(event.tid);
  
        var sample = new tr.model.Sample(
            undefined /* cpu */, thread, 'V8 Sample',
            timestampFromUs(event.ts), lastStackFrame, 1 /* weight */,
            this.deepCopyIfNeeded_(event.args));
        this.model_.samples.push(sample);
      },
  
      processTraceSampleEvent: function(event) {
        if (event.name === 'V8Sample') {
          this.processV8Sample(event);
          return;
        }
  
        var stackFrame = this.getStackFrameForEvent_(event);
        if (stackFrame === undefined) {
          stackFrame = this.traceEventSampleStackFramesByName_[
              event.name];
        }
        if (stackFrame === undefined) {
          var id = 'te-' + tr.b.GUID.allocate();
          stackFrame = new tr.model.StackFrame(
              undefined, id, event.name,
              ColorScheme.getColorIdForGeneralPurposeString(event.name));
          this.model_.addStackFrame(stackFrame);
          this.traceEventSampleStackFramesByName_[event.name] = stackFrame;
        }
  
        var thread = this.model_.getOrCreateProcess(event.pid)
          .getOrCreateThread(event.tid);
  
        var sample = new tr.model.Sample(
            undefined, thread, 'Trace Event Sample',
            timestampFromUs(event.ts), stackFrame, 1,
            this.deepCopyIfNeeded_(event.args));
        this.model_.samples.push(sample);
      },
  
      processMemoryDumpEvent: function(event) {
        if (event.ph !== 'v')
          throw new Error('Invalid memory dump event phase "' + event.ph + '".');
  
        var dumpId = event.id;
        if (dumpId === undefined) {
          this.model_.importWarning({
            type: 'memory_dump_parse_error',
            message: 'Memory dump event (phase \'' + event.ph +
                '\') without a dump ID.'
          });
          return;
        }
  
        var pid = event.pid;
        if (pid === undefined) {
          this.model_.importWarning({
            type: 'memory_dump_parse_error',
            message: 'Memory dump event (phase\'' + event.ph + '\', dump ID \'' +
                dumpId + '\') without a PID.'
          });
          return;
        }
  
        // Dump ID -> PID -> [process memory dump events].
        var allEvents = this.allMemoryDumpEvents_;
  
        // PID -> [process memory dump events].
        var dumpIdEvents = allEvents[dumpId];
        if (dumpIdEvents === undefined)
          allEvents[dumpId] = dumpIdEvents = {};
  
        // [process memory dump events].
        var processEvents = dumpIdEvents[pid];
        if (processEvents === undefined)
          dumpIdEvents[pid] = processEvents = [];
  
        processEvents.push(event);
      },
  
      /**
       * Walks through the events_ list and outputs the structures discovered to
       * model_.
       */
      importEvents: function() {
        var csr = new tr.ClockSyncRecord('ftrace_importer', 0, {});
        this.model_.clockSyncRecords.push(csr);
        if (this.stackFrameEvents_) {
          this.importStackFrames_(
              this.stackFrameEvents_, 'g', false /* addRootFrame */);
        }
  
        if (this.traceAnnotations_)
          this.importAnnotations_();
  
        var events = this.events_;
        for (var eI = 0; eI < events.length; eI++) {
          var event = events[eI];
          if (event.args === '__stripped__') {
            event.argsStripped = true;
            event.args = undefined;
          }
  
          if (event.ph === 'B' || event.ph === 'E') {
            this.processDurationEvent(event);
  
          } else if (event.ph === 'X') {
            var slice = this.processCompleteEvent(event);
            // TODO(yuhaoz): If Chrome supports creating other events with flow,
            // we will need to call processFlowEvent for them also.
            // https://github.com/catapult-project/catapult/issues/1259
            if (slice !== undefined && event.bind_id !== undefined)
              this.processFlowEvent(event, slice);
  
          } else if (event.ph === 'b' || event.ph === 'e' || event.ph === 'n' ||
                     event.ph === 'S' || event.ph === 'F' || event.ph === 'T' ||
                     event.ph === 'p') {
            this.processAsyncEvent(event);
  
          // Note, I is historic. The instant event marker got changed, but we
          // want to support loading old trace files so we have both I and i.
          } else if (event.ph === 'I' || event.ph === 'i' || event.ph === 'R') {
            this.processInstantEvent(event);
  
          } else if (event.ph === 'P') {
            this.processTraceSampleEvent(event);
  
          } else if (event.ph === 'C') {
            this.processCounterEvent(event);
  
          } else if (event.ph === 'M') {
            this.processMetadataEvent(event);
  
          } else if (event.ph === 'N' || event.ph === 'D' || event.ph === 'O') {
            this.processObjectEvent(event);
  
          } else if (event.ph === 's' || event.ph === 't' || event.ph === 'f') {
            this.processFlowEvent(event);
  
          } else if (event.ph === 'v') {
            this.processMemoryDumpEvent(event);
  
          } else {
            this.model_.importWarning({
              type: 'parse_error',
              message: 'Unrecognized event phase: ' +
                  event.ph + ' (' + event.name + ')'
            });
          }
        }
  
        // Remove all the root stack frame children as they should
        // already be added.
        tr.b.iterItems(this.v8ProcessRootStackFrame_, function(name, frame) {
          frame.removeAllChildren();
        });
      },
  
      importStackFrames_: function(rawStackFrames, idPrefix, addRootFrame) {
        var model = this.model_;
  
        var rootStackFrame;
        if (addRootFrame) {
          // In certain cases (heap dumps), we need to be able to distinguish
          // between an empty and an undefined stack trace. To this end, we add
          // an auxiliary root stack frame which is common to all stack frames
          // in a process. An empty stack trace is then represented by setting
          // the root stack frame as the leaf stack frame (of the relevant model
          // object with an associated empty stack trace, e.g. HeapEntry in the
          // case of heap dumps).
          rootStackFrame = new tr.model.StackFrame(
              undefined /* parentFrame */, idPrefix, undefined /* title */,
              undefined /* colorId */);
          model.addStackFrame(rootStackFrame);
        } else {
          rootStackFrame = undefined;
        }
  
        for (var id in rawStackFrames) {
          var rawStackFrame = rawStackFrames[id];
          var fullId = idPrefix + id;
          var textForColor = rawStackFrame.category ?
              rawStackFrame.category : rawStackFrame.name;
          var stackFrame = new tr.model.StackFrame(
              undefined /* parentFrame */, fullId, rawStackFrame.name,
              ColorScheme.getColorIdForGeneralPurposeString(textForColor));
          model.addStackFrame(stackFrame);
        }
  
        for (var id in rawStackFrames) {
          var fullId = idPrefix + id;
          var stackFrame = model.stackFrames[fullId];
          if (stackFrame === undefined)
            throw new Error('Internal error');
  
          var rawStackFrame = rawStackFrames[id];
          var parentId = rawStackFrame.parent;
          var parentStackFrame;
          if (parentId === undefined) {
            parentStackFrame = rootStackFrame;
          } else {
            var parentFullId = idPrefix + parentId;
            parentStackFrame = model.stackFrames[parentFullId];
            if (parentStackFrame === undefined) {
              this.model_.importWarning({
                type: 'metadata_parse_error',
                message: 'Missing parent frame with ID ' + parentFullId +
                    ' for stack frame \'' + stackFrame.name + '\' (ID ' + fullId +
                    ').'
              });
              parentStackFrame = rootStackFrame;
            }
          }
          stackFrame.parentFrame = parentStackFrame;
        }
      },
  
      importAnnotations_: function() {
        for (var id in this.traceAnnotations_) {
          var annotation = tr.model.Annotation.fromDictIfPossible(
             this.traceAnnotations_[id]);
          if (!annotation) {
            this.model_.importWarning({
              type: 'annotation_warning',
              message: 'Unrecognized traceAnnotation typeName \"' +
                  this.traceAnnotations_[id].typeName + '\"'
            });
            continue;
          }
          this.model_.addAnnotation(annotation);
        }
      },
  
      /**
       * Called by the Model after all other importers have imported their
       * events.
       */
      finalizeImport: function() {
        if (this.softwareMeasuredCpuCount_ !== undefined) {
          this.model_.kernel.softwareMeasuredCpuCount =
              this.softwareMeasuredCpuCount_;
        }
        this.createAsyncSlices_();
        this.createFlowSlices_();
        this.createExplicitObjects_();
        this.createImplicitObjects_();
        this.createMemoryDumps_();
      },
  
      /* Events can have one or more stack frames associated with them, but
       * that frame might be encoded either as a stack trace of program counters,
       * or as a direct stack frame reference. This handles either case and
       * if found, returns the stackframe.
       */
      getStackFrameForEvent_: function(event, opt_lookForEndEvent) {
        var sf;
        var stack;
        if (opt_lookForEndEvent) {
          sf = event.esf;
          stack = event.estack;
        } else {
          sf = event.sf;
          stack = event.stack;
        }
        if (stack !== undefined && sf !== undefined) {
          this.model_.importWarning({
            type: 'stack_frame_and_stack_error',
            message: 'Event at ' + event.ts +
                ' cannot have both a stack and a stackframe.'
          });
          return undefined;
        }
  
        if (stack !== undefined)
          return this.model_.resolveStackToStackFrame_(event.pid, stack);
        if (sf === undefined)
          return undefined;
  
        var stackFrame = this.model_.stackFrames['g' + sf];
        if (stackFrame === undefined) {
          this.model_.importWarning({
            type: 'sample_import_error',
            message: 'No frame for ' + sf
          });
          return;
        }
        return stackFrame;
      },
  
      resolveStackToStackFrame_: function(pid, stack) {
        // TODO(alph,fmeawad): Add codemap resolution code here.
        return undefined;
      },
  
      importSampleData: function() {
        if (!this.sampleEvents_)
          return;
        var m = this.model_;
  
        // If this is the only importer, then fake-create the threads.
        var events = this.sampleEvents_;
        if (this.events_.length === 0) {
          for (var i = 0; i < events.length; i++) {
            var event = events[i];
            m.getOrCreateProcess(event.tid).getOrCreateThread(event.tid);
          }
        }
  
        var threadsByTid = {};
        m.getAllThreads().forEach(function(t) {
          threadsByTid[t.tid] = t;
        });
  
        for (var i = 0; i < events.length; i++) {
          var event = events[i];
          var thread = threadsByTid[event.tid];
          if (thread === undefined) {
            m.importWarning({
              type: 'sample_import_error',
              message: 'Thread ' + events.tid + 'not found'
            });
            continue;
          }
  
          var cpu;
          if (event.cpu !== undefined)
            cpu = m.kernel.getOrCreateCpu(event.cpu);
  
          var stackFrame = this.getStackFrameForEvent_(event);
  
          var sample = new tr.model.Sample(
              cpu, thread,
              event.name,
              timestampFromUs(event.ts),
              stackFrame,
              event.weight);
          m.samples.push(sample);
        }
      },
  
      /**
       * Called by the model to join references between objects, after final model
       * bounds have been computed.
       */
      joinRefs: function() {
        this.joinObjectRefs_();
      },
  
      createAsyncSlices_: function() {
        if (this.allAsyncEvents_.length === 0)
          return;
  
        this.allAsyncEvents_.sort(function(x, y) {
          var d = x.event.ts - y.event.ts;
          if (d !== 0)
            return d;
          return x.sequenceNumber - y.sequenceNumber;
        });
  
        var legacyEvents = [];
        // Group nestable async events by ID. Events with the same ID should
        // belong to the same parent async event.
        var nestableAsyncEventsByKey = {};
        for (var i = 0; i < this.allAsyncEvents_.length; i++) {
          var asyncEventState = this.allAsyncEvents_[i];
          var event = asyncEventState.event;
          if (event.ph === 'S' || event.ph === 'F' || event.ph === 'T' ||
              event.ph === 'p') {
            legacyEvents.push(asyncEventState);
            continue;
          }
          if (event.cat === undefined) {
            this.model_.importWarning({
              type: 'async_slice_parse_error',
              message: 'Nestable async events (ph: b, e, or n) require a ' +
                  'cat parameter.'
            });
            continue;
          }
  
          if (event.name === undefined) {
            this.model_.importWarning({
              type: 'async_slice_parse_error',
              message: 'Nestable async events (ph: b, e, or n) require a ' +
                  'name parameter.'
            });
            continue;
          }
  
          if (event.id === undefined) {
            this.model_.importWarning({
              type: 'async_slice_parse_error',
              message: 'Nestable async events (ph: b, e, or n) require an ' +
                  'id parameter.'
            });
            continue;
          }
          var key = event.cat + ':' + event.id;
          if (nestableAsyncEventsByKey[key] === undefined)
             nestableAsyncEventsByKey[key] = [];
          nestableAsyncEventsByKey[key].push(asyncEventState);
        }
        // Handle legacy async events.
        this.createLegacyAsyncSlices_(legacyEvents);
  
        // Parse nestable async events into AsyncSlices.
        for (var key in nestableAsyncEventsByKey) {
          var eventStateEntries = nestableAsyncEventsByKey[key];
          // Stack of enclosing BEGIN events.
          var parentStack = [];
          for (var i = 0; i < eventStateEntries.length; ++i) {
            var eventStateEntry = eventStateEntries[i];
            // If this is the end of an event, match it to the start.
            if (eventStateEntry.event.ph === 'e') {
              // Walk up the parent stack to find the corresponding BEGIN for
              // this END.
              var parentIndex = -1;
              for (var k = parentStack.length - 1; k >= 0; --k) {
                if (parentStack[k].event.name === eventStateEntry.event.name) {
                  parentIndex = k;
                  break;
                }
              }
              if (parentIndex === -1) {
                // Unmatched end.
                eventStateEntry.finished = false;
              } else {
                parentStack[parentIndex].end = eventStateEntry;
                // Pop off all enclosing unmatched BEGINs util parentIndex.
                while (parentIndex < parentStack.length) {
                  parentStack.pop();
                }
              }
            }
            // Inherit the current parent.
            if (parentStack.length > 0)
              eventStateEntry.parentEntry = parentStack[parentStack.length - 1];
            if (eventStateEntry.event.ph === 'b')
              parentStack.push(eventStateEntry);
          }
          var topLevelSlices = [];
          for (var i = 0; i < eventStateEntries.length; ++i) {
            var eventStateEntry = eventStateEntries[i];
            // Skip matched END, as its slice will be created when we
            // encounter its corresponding BEGIN.
            if (eventStateEntry.event.ph === 'e' &&
                eventStateEntry.finished === undefined) {
              continue;
            }
            var startState = undefined;
            var endState = undefined;
            var sliceArgs = eventStateEntry.event.args || {};
            var sliceError = undefined;
            if (eventStateEntry.event.ph === 'n') {
              startState = eventStateEntry;
              endState = eventStateEntry;
            } else if (eventStateEntry.event.ph === 'b') {
              if (eventStateEntry.end === undefined) {
                // Unmatched BEGIN. End it when last event with this ID ends.
                eventStateEntry.end =
                    eventStateEntries[eventStateEntries.length - 1];
                sliceError =
                    'Slice has no matching END. End time has been adjusted.';
                this.model_.importWarning({
                  type: 'async_slice_parse_error',
                  message: 'Nestable async BEGIN event at ' +
                      eventStateEntry.event.ts + ' with name=' +
                      eventStateEntry.event.name +
                      ' and id=' + eventStateEntry.event.id + ' was unmatched.'
                });
              } else {
                // Include args for both END and BEGIN for a matched pair.
                function concatenateArguments(args1, args2) {
                  if (args1.params === undefined || args2.params === undefined)
                    return tr.b.concatenateObjects(args1, args2);
                  // Make an argument object to hold the combined params.
                  var args3 = {};
                  args3.params = tr.b.concatenateObjects(args1.params,
                                                         args2.params);
                  return tr.b.concatenateObjects(args1, args2, args3);
                }
                var endArgs = eventStateEntry.end.event.args || {};
                sliceArgs = concatenateArguments(sliceArgs, endArgs);
              }
              startState = eventStateEntry;
              endState = eventStateEntry.end;
            } else {
              // Unmatched END. Start it at the first event with this ID starts.
              sliceError =
                  'Slice has no matching BEGIN. Start time has been adjusted.';
              this.model_.importWarning({
                type: 'async_slice_parse_error',
                message: 'Nestable async END event at ' +
                    eventStateEntry.event.ts + ' with name=' +
                    eventStateEntry.event.name +
                    ' and id=' + eventStateEntry.event.id + ' was unmatched.'
              });
              startState = eventStateEntries[0];
              endState = eventStateEntry;
            }
  
            var isTopLevel = (eventStateEntry.parentEntry === undefined);
            var asyncSliceConstructor =
               tr.model.AsyncSlice.getConstructor(
                  eventStateEntry.event.cat,
                  eventStateEntry.event.name);
  
            var thread_start = undefined;
            var thread_duration = undefined;
            if (startState.event.tts && startState.event.use_async_tts) {
              thread_start = timestampFromUs(startState.event.tts);
              if (endState.event.tts) {
                var thread_end = timestampFromUs(endState.event.tts);
                thread_duration = thread_end - thread_start;
              }
            }
  
            var slice = new asyncSliceConstructor(
                eventStateEntry.event.cat,
                eventStateEntry.event.name,
                getEventColor(endState.event),
                timestampFromUs(startState.event.ts),
                sliceArgs,
                timestampFromUs(endState.event.ts - startState.event.ts),
                isTopLevel,
                thread_start,
                thread_duration,
                startState.event.argsStripped);
  
            slice.startThread = startState.thread;
            slice.endThread = endState.thread;
  
            slice.startStackFrame = this.getStackFrameForEvent_(startState.event);
            slice.endStackFrame = this.getStackFrameForEvent_(endState.event);
  
            slice.id = key;
            if (sliceError !== undefined)
              slice.error = sliceError;
            eventStateEntry.slice = slice;
            // Add the slice to the topLevelSlices array if there is no parent.
            // Otherwise, add the slice to the subSlices of its parent.
            if (isTopLevel) {
              topLevelSlices.push(slice);
            } else if (eventStateEntry.parentEntry.slice !== undefined) {
              eventStateEntry.parentEntry.slice.subSlices.push(slice);
            }
          }
          for (var si = 0; si < topLevelSlices.length; si++) {
            topLevelSlices[si].startThread.asyncSliceGroup.push(
                topLevelSlices[si]);
          }
        }
      },
  
      createLegacyAsyncSlices_: function(legacyEvents) {
        if (legacyEvents.length === 0)
          return;
  
        legacyEvents.sort(function(x, y) {
          var d = x.event.ts - y.event.ts;
          if (d != 0)
            return d;
          return x.sequenceNumber - y.sequenceNumber;
        });
  
        var asyncEventStatesByNameThenID = {};
  
        for (var i = 0; i < legacyEvents.length; i++) {
          var asyncEventState = legacyEvents[i];
  
          var event = asyncEventState.event;
          var name = event.name;
          if (name === undefined) {
            this.model_.importWarning({
              type: 'async_slice_parse_error',
              message: 'Async events (ph: S, T, p, or F) require a name ' +
                  ' parameter.'
            });
            continue;
          }
  
          var id = event.id;
          if (id === undefined) {
            this.model_.importWarning({
              type: 'async_slice_parse_error',
              message: 'Async events (ph: S, T, p, 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_.importWarning({
                type: 'async_slice_parse_error',
                message: 'At ' + event.ts + ', a 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_.importWarning({
                type: 'async_slice_parse_error',
                message: 'At ' + event.ts + ', no slice named ' + name +
                    ' was open.'
              });
              continue;
            }
            if (asyncEventStatesByNameThenID[name][id] === undefined) {
              this.model_.importWarning({
                type: 'async_slice_parse_error',
                message: '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 asyncSliceConstructor =
                 tr.model.AsyncSlice.getConstructor(
                    events[0].event.cat,
                    name);
              var slice = new asyncSliceConstructor(
                  events[0].event.cat,
                  name,
                  getEventColor(events[0].event),
                  timestampFromUs(events[0].event.ts),
                  tr.b.concatenateObjects(events[0].event.args,
                                        events[events.length - 1].event.args),
                  timestampFromUs(event.ts - events[0].event.ts),
                  true, undefined, undefined, events[0].event.argsStripped);
              slice.startThread = events[0].thread;
              slice.endThread = asyncEventState.thread;
              slice.id = id;
  
              var stepType = events[1].event.ph;
              var isValid = true;
  
              // Create subSlices for each step. Skip the start and finish events,
              // which are always first and last respectively.
              for (var j = 1; j < events.length - 1; ++j) {
                if (events[j].event.ph === 'T' || events[j].event.ph === 'p') {
                  isValid = this.assertStepTypeMatches_(stepType, events[j]);
                  if (!isValid)
                    break;
                }
  
                if (events[j].event.ph === 'S') {
                  this.model_.importWarning({
                    type: 'async_slice_parse_error',
                    message: 'At ' + event.event.ts + ', a slice named ' +
                        event.event.name + ' with id=' + event.event.id +
                        ' had a step before the start event.'
                  });
                  continue;
                }
  
                if (events[j].event.ph === 'F') {
                  this.model_.importWarning({
                    type: 'async_slice_parse_error',
                    message: 'At ' + event.event.ts + ', a slice named ' +
                        event.event.name + ' with id=' + event.event.id +
                        ' had a step after the finish event.'
                  });
                  continue;
                }
  
                var startIndex = j + (stepType === 'T' ? 0 : -1);
                var endIndex = startIndex + 1;
  
                var subName = events[j].event.name;
                if (!events[j].event.argsStripped &&
                    (events[j].event.ph === 'T' || events[j].event.ph === 'p'))
                  subName = subName + ':' + events[j].event.args.step;
  
                var asyncSliceConstructor =
                   tr.model.AsyncSlice.getConstructor(
                      events[0].event.cat,
                      subName);
                var subSlice = new asyncSliceConstructor(
                    events[0].event.cat,
                    subName,
                    getEventColor(event, subName + j),
                    timestampFromUs(events[startIndex].event.ts),
                    this.deepCopyIfNeeded_(events[j].event.args),
                    timestampFromUs(
                      events[endIndex].event.ts - events[startIndex].event.ts),
                        undefined, undefined,
                        events[startIndex].event.argsStripped);
                subSlice.startThread = events[startIndex].thread;
                subSlice.endThread = events[endIndex].thread;
                subSlice.id = id;
  
                slice.subSlices.push(subSlice);
              }
  
              if (isValid) {
                // Add |slice| to the start-thread's asyncSlices.
                slice.startThread.asyncSliceGroup.push(slice);
              }
  
              delete asyncEventStatesByNameThenID[name][id];
            }
          }
        }
      },
  
      assertStepTypeMatches_: function(stepType, event) {
        if (stepType != event.event.ph) {
          this.model_.importWarning({
            type: 'async_slice_parse_error',
            message: 'At ' + event.event.ts + ', a slice named ' +
                event.event.name + ' with id=' + event.event.id +
                ' had both begin and end steps, which is not allowed.'
          });
          return false;
        }
        return true;
      },
  
      createFlowSlices_: function() {
        if (this.allFlowEvents_.length === 0)
          return;
  
        var that = this;
  
        function validateFlowEvent() {
          if (event.name === undefined) {
            that.model_.importWarning({
              type: 'flow_slice_parse_error',
              message: 'Flow events (ph: s, t or f) require a name parameter.'
            });
            return false;
          }
  
          // Support Flow API v1.
          if (event.ph === 's' || event.ph === 'f' || event.ph === 't') {
            if (event.id === undefined) {
              that.model_.importWarning({
                type: 'flow_slice_parse_error',
                message: 'Flow events (ph: s, t or f) require an id parameter.'
              });
              return false;
            }
            return true;
          }
  
          // Support Flow API v2.
          if (event.bind_id) {
            if (event.flow_in === undefined && event.flow_out === undefined) {
              that.model_.importWarning({
                type: 'flow_slice_parse_error',
                message: 'Flow producer or consumer require flow_in or flow_out.'
              });
              return false;
            }
            return true;
          }
  
          return false;
        }
  
        function createFlowEvent(thread, event, opt_slice) {
          var startSlice, flowId, flowStartTs;
  
          if (event.bind_id) {
            // Support Flow API v2.
            startSlice = opt_slice;
            flowId = event.bind_id;
            flowStartTs = timestampFromUs(event.ts + event.dur);
          } else {
            // Support Flow API v1.
            var ts = timestampFromUs(event.ts);
            startSlice = thread.sliceGroup.findSliceAtTs(ts);
            if (startSlice === undefined)
              return undefined;
            flowId = event.id;
            flowStartTs = ts;
          }
  
          var flowEvent = new tr.model.FlowEvent(
              event.cat,
              flowId,
              event.name,
              getEventColor(event),
              flowStartTs,
              that.deepCopyAlways_(event.args));
          flowEvent.startSlice = startSlice;
          flowEvent.startStackFrame = that.getStackFrameForEvent_(event);
          flowEvent.endStackFrame = undefined;
          startSlice.outFlowEvents.push(flowEvent);
          return flowEvent;
        }
  
        function finishFlowEventWith(flowEvent, thread, event,
                                     refGuid, bindToParent, opt_slice) {
          var endSlice;
  
          if (event.bind_id) {
            // Support Flow API v2.
            endSlice = opt_slice;
          } else {
            // Support Flow API v1.
            var ts = timestampFromUs(event.ts);
            if (bindToParent) {
              endSlice = thread.sliceGroup.findSliceAtTs(ts);
            } else {
              endSlice = thread.sliceGroup.findNextSliceAfter(ts, refGuid);
            }
            if (endSlice === undefined)
              return false;
          }
  
          endSlice.inFlowEvents.push(flowEvent);
          flowEvent.endSlice = endSlice;
          flowEvent.duration = timestampFromUs(event.ts) - flowEvent.start;
          flowEvent.endStackFrame = that.getStackFrameForEvent_(event);
          that.mergeArgsInto_(flowEvent.args, event.args, flowEvent.title);
          return true;
        }
  
        function processFlowConsumer(flowIdToEvent, sliceGuidToEvent, event,
            slice) {
          var flowEvent = flowIdToEvent[event.bind_id];
          if (flowEvent === undefined) {
            that.model_.importWarning({
                type: 'flow_slice_ordering_error',
                message: 'Flow consumer ' + event.bind_id + ' does not have ' +
                    'a flow producer'});
            return false;
          } else if (flowEvent.endSlice) {
            // One flow producer can have more than one flow consumers.
            // In this case, create a new flow event using the flow producer.
            var flowProducer = flowEvent.startSlice;
            flowEvent = createFlowEvent(undefined,
                sliceGuidToEvent[flowProducer.guid], flowProducer);
          }
  
          var ok = finishFlowEventWith(flowEvent, undefined, event,
                                       refGuid, undefined, slice);
          if (ok) {
            that.model_.flowEvents.push(flowEvent);
          } else {
            that.model_.importWarning({
                type: 'flow_slice_end_error',
                message: 'Flow consumer ' + event.bind_id + ' does not end ' +
                    'at an actual slice, so cannot be created.'});
            return false;
          }
  
          return true;
        }
  
        function processFlowProducer(flowIdToEvent, flowStatus, event, slice) {
          if (flowIdToEvent[event.bind_id] &&
              flowStatus[event.bind_id]) {
            // Can't open the same flow again while it's still open.
            // This is essentially the multi-producer case which we don't support
            that.model_.importWarning({
                type: 'flow_slice_start_error',
                message: 'Flow producer ' + event.bind_id + ' already seen'});
            return false;
          }
  
          var flowEvent = createFlowEvent(undefined, event, slice);
          if (!flowEvent) {
            that.model_.importWarning({
                type: 'flow_slice_start_error',
                message: 'Flow producer ' + event.bind_id + ' does not start' +
                    'a flow'});
            return false;
          }
          flowIdToEvent[event.bind_id] = flowEvent;
  
          return;
        }
  
        // Actual import.
        this.allFlowEvents_.sort(function(x, y) {
          var d = x.event.ts - y.event.ts;
          if (d != 0)
            return d;
          return x.sequenceNumber - y.sequenceNumber;
        });
  
        var flowIdToEvent = {};
        var sliceGuidToEvent = {};
        var flowStatus = {}; // true: open; false: closed.
        for (var i = 0; i < this.allFlowEvents_.length; ++i) {
          var data = this.allFlowEvents_[i];
          var refGuid = data.refGuid;
          var event = data.event;
          var thread = data.thread;
          if (!validateFlowEvent(event))
            continue;
  
          // Support for Flow API v2.
          if (event.bind_id) {
            var slice = data.slice;
            sliceGuidToEvent[slice.guid] = event;
  
            if (event.flowPhase === PRODUCER) {
              if (!processFlowProducer(flowIdToEvent, flowStatus, event, slice))
                continue;
              flowStatus[event.bind_id] = true; // open the flow.
            }
            else {
              if (!processFlowConsumer(flowIdToEvent, sliceGuidToEvent,
                  event, slice))
                continue;
              flowStatus[event.bind_id] = false; // close the flow.
  
              if (event.flowPhase === STEP) {
                if (!processFlowProducer(flowIdToEvent, flowStatus,
                    event, slice))
                  continue;
                flowStatus[event.bind_id] = true; // open the flow again.
              }
            }
            continue;
          }
  
          // Support for Flow API v1.
          var flowEvent;
          if (event.ph === 's') {
            if (flowIdToEvent[event.id]) {
              this.model_.importWarning({
                type: 'flow_slice_start_error',
                message: 'event id ' + event.id + ' already seen when ' +
                    'encountering start of flow event.'});
              continue;
            }
            flowEvent = createFlowEvent(thread, event);
            if (!flowEvent) {
              this.model_.importWarning({
                type: 'flow_slice_start_error',
                message: 'event id ' + event.id + ' does not start ' +
                    'at an actual slice, so cannot be created.'});
              continue;
            }
            flowIdToEvent[event.id] = flowEvent;
  
          } else if (event.ph === 't' || event.ph === 'f') {
            flowEvent = flowIdToEvent[event.id];
            if (flowEvent === undefined) {
              this.model_.importWarning({
                type: 'flow_slice_ordering_error',
                message: 'Found flow phase ' + event.ph + ' for id: ' + event.id +
                    ' but no flow start found.'
              });
              continue;
            }
  
            var bindToParent = event.ph === 't';
  
            if (event.ph === 'f') {
              if (event.bp === undefined) {
                // TODO(yuhaoz): In flow V2, there is no notion of binding point.
                // Removal of binding point is tracked in
                // https://github.com/google/trace-viewer/issues/991.
                if (event.cat.indexOf('input') > -1)
                  bindToParent = true;
                else if (event.cat.indexOf('ipc.flow') > -1)
                  bindToParent = true;
              } else {
                if (event.bp !== 'e') {
                  this.model_.importWarning({
                   type: 'flow_slice_bind_point_error',
                   message: 'Flow event with invalid binding point (event.bp).'
                  });
                  continue;
                }
                bindToParent = true;
              }
            }
  
            var ok = finishFlowEventWith(flowEvent, thread, event,
                                         refGuid, bindToParent);
            if (ok) {
              that.model_.flowEvents.push(flowEvent);
            } else {
              this.model_.importWarning({
                type: 'flow_slice_end_error',
                message: 'event id ' + event.id + ' does not end ' +
                    'at an actual slice, so cannot be created.'});
            }
            flowIdToEvent[event.id] = undefined;
  
            // If this is a step, then create another flow event.
            if (ok && event.ph === 't') {
              flowEvent = createFlowEvent(thread, event);
              flowIdToEvent[event.id] = flowEvent;
            }
          }
        }
      },
  
      /**
       * This function creates objects described via the N, D, and O phase
       * events.
       */
      createExplicitObjects_: function() {
        if (this.allObjectEvents_.length === 0)
          return;
  
        function processEvent(objectEventState) {
          var event = objectEventState.event;
          var thread = objectEventState.thread;
          if (event.name === undefined) {
            this.model_.importWarning({
              type: 'object_parse_error',
              message: 'While processing ' + JSON.stringify(event) + ': ' +
                  'Object events require an name parameter.'
            });
          }
  
          if (event.id === undefined) {
            this.model_.importWarning({
              type: 'object_parse_error',
              message: 'While processing ' + JSON.stringify(event) + ': ' +
                  'Object events require an id parameter.'
            });
          }
          var process = thread.parent;
          var ts = timestampFromUs(event.ts);
          var instance;
          if (event.ph === 'N') {
            try {
              instance = process.objects.idWasCreated(
                  event.id, event.cat, event.name, ts);
            } catch (e) {
              this.model_.importWarning({
                type: 'object_parse_error',
                message: 'While processing create of ' +
                    event.id + ' at ts=' + ts + ': ' + e
              });
              return;
            }
          } else if (event.ph === 'O') {
            if (event.args.snapshot === undefined) {
              this.model_.importWarning({
                type: 'object_parse_error',
                message: 'While processing ' + event.id + ' at ts=' + ts + ': ' +
                    'Snapshots must have args: {snapshot: ...}'
              });
              return;
            }
            var snapshot;
            try {
              var args = this.deepCopyIfNeeded_(event.args.snapshot);
              var cat;
              if (args.cat) {
                cat = args.cat;
                delete args.cat;
              } else {
                cat = event.cat;
              }
  
              var baseTypename;
              if (args.base_type) {
                baseTypename = args.base_type;
                delete args.base_type;
              } else {
                baseTypename = undefined;
              }
              snapshot = process.objects.addSnapshot(
                  event.id, cat, event.name, ts,
                  args, baseTypename);
              snapshot.snapshottedOnThread = thread;
            } catch (e) {
              this.model_.importWarning({
                type: 'object_parse_error',
                message: 'While processing snapshot of ' +
                    event.id + ' at ts=' + ts + ': ' + e
              });
              return;
            }
            instance = snapshot.objectInstance;
          } else if (event.ph === 'D') {
            try {
              process.objects.idWasDeleted(event.id, event.cat, event.name, ts);
              var instanceMap = process.objects.getOrCreateInstanceMap_(event.id);
              instance = instanceMap.lastInstance;
            } catch (e) {
              this.model_.importWarning({
                type: 'object_parse_error',
                message: 'While processing delete of ' +
                    event.id + ' at ts=' + ts + ': ' + e
              });
              return;
            }
          }
  
          if (instance)
            instance.colorId = getEventColor(event, instance.typeName);
        }
  
        this.allObjectEvents_.sort(function(x, y) {
          var d = x.event.ts - y.event.ts;
          if (d != 0)
            return d;
          return x.sequenceNumber - y.sequenceNumber;
        });
  
        var allObjectEvents = this.allObjectEvents_;
        for (var i = 0; i < allObjectEvents.length; i++) {
          var objectEventState = allObjectEvents[i];
          try {
            processEvent.call(this, objectEventState);
          } catch (e) {
            this.model_.importWarning({
              type: 'object_parse_error',
              message: e.message
            });
          }
        }
      },
  
      createImplicitObjects_: function() {
        tr.b.iterItems(this.model_.processes, function(pid, process) {
          this.createImplicitObjectsForProcess_(process);
        }, this);
      },
  
      // Here, we collect all the snapshots that internally contain a
      // Javascript-level object inside their args list that has an "id" field,
      // and turn that into a snapshot of the instance referred to by id.
      createImplicitObjectsForProcess_: function(process) {
  
        function processField(referencingObject,
                              referencingObjectFieldName,
                              referencingObjectFieldValue,
                              containingSnapshot) {
          if (!referencingObjectFieldValue)
            return;
  
          if (referencingObjectFieldValue instanceof
              tr.model.ObjectSnapshot)
            return null;
          if (referencingObjectFieldValue.id === undefined)
            return;
  
          var implicitSnapshot = referencingObjectFieldValue;
  
          var rawId = implicitSnapshot.id;
          var m = /(.+)\/(.+)/.exec(rawId);
          if (!m)
            throw new Error('Implicit snapshots must have names.');
          delete implicitSnapshot.id;
          var name = m[1];
          var id = m[2];
          var res;
  
          var cat;
          if (implicitSnapshot.cat !== undefined)
            cat = implicitSnapshot.cat;
          else
            cat = containingSnapshot.objectInstance.category;
  
          var baseTypename;
          if (implicitSnapshot.base_type)
            baseTypename = implicitSnapshot.base_type;
          else
            baseTypename = undefined;
  
          try {
            res = process.objects.addSnapshot(
                id, cat,
                name, containingSnapshot.ts,
                implicitSnapshot, baseTypename);
          } catch (e) {
            this.model_.importWarning({
              type: 'object_snapshot_parse_error',
              message: 'While processing implicit snapshot of ' +
                  rawId + ' at ts=' + containingSnapshot.ts + ': ' + e
            });
            return;
          }
          res.objectInstance.hasImplicitSnapshots = true;
          res.containingSnapshot = containingSnapshot;
          res.snapshottedOnThread = containingSnapshot.snapshottedOnThread;
          referencingObject[referencingObjectFieldName] = res;
          if (!(res instanceof tr.model.ObjectSnapshot))
            throw new Error('Created object must be instanceof snapshot');
          return res.args;
        }
  
        /**
         * Iterates over the fields in the object, calling func for every
         * field/value found.
         *
         * @return {object} If the function does not want the field's value to be
         * iterated, return null. If iteration of the field value is desired, then
         * return either undefined (if the field value did not change) or the new
         * field value if it was changed.
         */
        function iterObject(object, func, containingSnapshot, thisArg) {
          if (!(object instanceof Object))
            return;
  
          if (object instanceof Array) {
            for (var i = 0; i < object.length; i++) {
              var res = func.call(thisArg, object, i, object[i],
                                  containingSnapshot);
              if (res === null)
                continue;
              if (res)
                iterObject(res, func, containingSnapshot, thisArg);
              else
                iterObject(object[i], func, containingSnapshot, thisArg);
            }
            return;
          }
  
          for (var key in object) {
            var res = func.call(thisArg, object, key, object[key],
                                containingSnapshot);
            if (res === null)
              continue;
            if (res)
              iterObject(res, func, containingSnapshot, thisArg);
            else
              iterObject(object[key], func, containingSnapshot, thisArg);
          }
        }
  
        // TODO(nduca): We may need to iterate the instances in sorted order by
        // creationTs.
        process.objects.iterObjectInstances(function(instance) {
          instance.snapshots.forEach(function(snapshot) {
            if (snapshot.args.id !== undefined)
              throw new Error('args cannot have an id field inside it');
            iterObject(snapshot.args, processField, snapshot, this);
          }, this);
        }, this);
      },
  
      createMemoryDumps_: function() {
        for (var dumpId in this.allMemoryDumpEvents_)
          this.createGlobalMemoryDump_(this.allMemoryDumpEvents_[dumpId], dumpId);
      },
  
      createGlobalMemoryDump_: function(dumpIdEvents, dumpId) {
        // 1. Create a GlobalMemoryDump for the provided process memory dump
        // the events, all of which have the same dump ID.
  
        // Calculate the range of the global memory dump.
        var globalRange = new tr.b.Range();
        for (var pid in dumpIdEvents) {
          var processEvents = dumpIdEvents[pid];
          for (var i = 0; i < processEvents.length; i++)
            globalRange.addValue(timestampFromUs(processEvents[i].ts));
        }
        if (globalRange.isEmpty)
          throw new Error('Internal error: Global memory dump without events');
  
        // Create the global memory dump.
        var globalMemoryDump = new tr.model.GlobalMemoryDump(
            this.model_, globalRange.min);
        globalMemoryDump.duration = globalRange.range;
        this.model_.globalMemoryDumps.push(globalMemoryDump);
  
        var globalMemoryAllocatorDumpsByFullName = {};
        var levelOfDetailIndices = {};
        var allMemoryAllocatorDumpsByGuid = {};
  
        // 2. Create a ProcessMemoryDump for each PID in the provided process
        // memory dump events. Everything except for edges between memory
        // allocator dumps is parsed from the process memory dump trace events at
        // this step.
        for (var pid in dumpIdEvents) {
          this.createProcessMemoryDump_(globalMemoryDump,
              globalMemoryAllocatorDumpsByFullName, levelOfDetailIndices,
              allMemoryAllocatorDumpsByGuid, dumpIdEvents[pid], pid, dumpId);
        }
  
        // 3. Set the level of detail and memory allocator dumps of the
        // GlobalMemoryDump, which come from the process memory dump trace
        // events parsed in the prebvious step.
        globalMemoryDump.levelOfDetail =
            MEMORY_DUMP_LEVELS_OF_DETAIL[levelOfDetailIndices.global];
  
        // Find the root allocator dumps and establish the parent links of
        // the global memory dump.
        globalMemoryDump.memoryAllocatorDumps =
            this.inferMemoryAllocatorDumpTree_(
                globalMemoryAllocatorDumpsByFullName);
  
        // 4. Finally, parse the edges between all memory allocator dumps within
        // the GlobalMemoryDump. This can only be done once all memory allocator
        // dumps have been parsed (i.e. it is necessary to iterate over the
        // process memory dump trace events once more).
        this.parseMemoryDumpAllocatorEdges_(allMemoryAllocatorDumpsByGuid,
            dumpIdEvents, dumpId);
      },
  
      createProcessMemoryDump_: function(globalMemoryDump,
          globalMemoryAllocatorDumpsByFullName, levelOfDetailIndices,
          allMemoryAllocatorDumpsByGuid, processEvents, pid, dumpId) {
        // Calculate the range of the process memory dump.
        var processRange = new tr.b.Range();
        for (var i = 0; i < processEvents.length; i++)
          processRange.addValue(timestampFromUs(processEvents[i].ts));
        if (processRange.isEmpty)
          throw new Error('Internal error: Process memory dump without events');
  
        // Create the process memory dump.
        var process = this.model_.getOrCreateProcess(pid);
        var processMemoryDump = new tr.model.ProcessMemoryDump(
            globalMemoryDump, process, processRange.min);
        processMemoryDump.duration = processRange.range;
        process.memoryDumps.push(processMemoryDump);
        globalMemoryDump.processMemoryDumps[pid] = processMemoryDump;
  
        var processMemoryAllocatorDumpsByFullName = {};
  
        // Parse all process memory dump trace events for the newly created
        // ProcessMemoryDump.
        for (var i = 0; i < processEvents.length; i++) {
          var processEvent = processEvents[i];
  
          var dumps = processEvent.args.dumps;
          if (dumps === undefined) {
            this.model_.importWarning({
              type: 'memory_dump_parse_error',
              message: '\'dumps\' field not found in a process memory dump' +
                  ' event for PID=' + pid + ' and dump ID=' + dumpId + '.'
            });
            continue;
          }
  
          // Totals, VM regions, and heap dumps for the newly created
          // ProcessMemoryDump should be present in at most one event, so they
          // can be added to the ProcessMemoryDump immediately.
          this.parseMemoryDumpTotals_(processMemoryDump, dumps, pid, dumpId);
          this.parseMemoryDumpVmRegions_(processMemoryDump, dumps, pid, dumpId);
          this.parseMemoryDumpHeapDumps_(processMemoryDump, dumps, pid, dumpId);
  
          // All process memory dump trace events for the newly created
          // ProcessMemoryDump must be processed before level of detail and
          // allocator dumps can be added to it.
          this.parseMemoryDumpLevelOfDetail_(levelOfDetailIndices, dumps, pid,
              dumpId);
          this.parseMemoryDumpAllocatorDumps_(processMemoryDump, globalMemoryDump,
              processMemoryAllocatorDumpsByFullName,
              globalMemoryAllocatorDumpsByFullName,
              allMemoryAllocatorDumpsByGuid, dumps, pid, dumpId);
        }
  
        processMemoryDump.levelOfDetail =
            MEMORY_DUMP_LEVELS_OF_DETAIL[levelOfDetailIndices.process];
        delete levelOfDetailIndices.process;  // Reused for all process dumps.
  
        // Find the root allocator dumps and establish the parent links of
        // the process memory dump.
        processMemoryDump.memoryAllocatorDumps =
            this.inferMemoryAllocatorDumpTree_(
                processMemoryAllocatorDumpsByFullName);
      },
  
      parseMemoryDumpTotals_: function(processMemoryDump, dumps, pid, dumpId) {
        var rawTotals = dumps.process_totals;
        if (rawTotals === undefined)
          return;
  
        if (processMemoryDump.totals !== undefined) {
          this.model_.importWarning({
            type: 'memory_dump_parse_error',
            message: 'Process totals provided multiple times for' +
                ' process memory dump for PID=' + pid +
                ' and dump ID=' + dumpId + '.'
          });
          return;
        }
  
        var totals = {};
        var platformSpecificTotals = undefined;
  
        for (var rawTotalName in rawTotals) {
          var rawTotalValue = rawTotals[rawTotalName];
          if (rawTotalValue === undefined)
            continue;
  
          // Total resident bytes.
          if (rawTotalName === 'resident_set_bytes') {
            totals.residentBytes = parseInt(rawTotalValue, 16);
            continue;
          }
  
          // Peak resident bytes.
          if (rawTotalName === 'peak_resident_set_bytes') {
            totals.peakResidentBytes = parseInt(rawTotalValue, 16);
            continue;
          }
          if (rawTotalName === 'is_peak_rss_resetable') {
            totals.arePeakResidentBytesResettable = !!rawTotalValue;
            continue;
          }
  
          // OS-specific totals (e.g. private resident on Mac).
          if (platformSpecificTotals === undefined) {
            platformSpecificTotals = {};
            totals.platformSpecific = platformSpecificTotals;
          }
          platformSpecificTotals[rawTotalName] = parseInt(rawTotalValue, 16);
        }
  
        // Either both peak_resident_set_bytes and is_peak_rss_resetable should
        // be present in the trace, or neither.
        if (totals.peakResidentBytes === undefined &&
            totals.arePeakResidentBytesResettable !== undefined) {
          this.model_.importWarning({
              type: 'memory_dump_parse_error',
              message: 'Optional field peak_resident_set_bytes found' +
                  ' but is_peak_rss_resetable not found in' +
                  ' process memory dump for PID=' + pid +
                  ' and dump ID=' + dumpId + '.'
          });
        }
        if (totals.arePeakResidentBytesResettable !== undefined &&
            totals.peakResidentBytes === undefined) {
          this.model_.importWarning({
              type: 'memory_dump_parse_error',
              message: 'Optional field is_peak_rss_resetable found' +
                  ' but peak_resident_set_bytes not found in' +
                  ' process memory dump for PID=' + pid +
                  ' and dump ID=' + dumpId + '.'
          });
        }
  
        processMemoryDump.totals = totals;
      },
  
      parseMemoryDumpVmRegions_: function(processMemoryDump, dumps, pid, dumpId) {
        var rawProcessMmaps = dumps.process_mmaps;
        if (rawProcessMmaps === undefined)
          return;
  
        var rawVmRegions = rawProcessMmaps.vm_regions;
        if (rawVmRegions === undefined)
          return;
  
        if (processMemoryDump.vmRegions !== undefined) {
          this.model_.importWarning({
            type: 'memory_dump_parse_error',
            message: 'VM regions provided multiple times for' +
                ' process memory dump for PID=' + pid +
                ' and dump ID=' + dumpId + '.'
          });
          return;
        }
  
        function parseByteStat(rawValue) {
          if (rawValue === undefined)
            return undefined;
          return parseInt(rawValue, 16);
        }
  
        var vmRegions = new Array(rawVmRegions.length);
  
        for (var i = 0; i < rawVmRegions.length; i++) {
          var rawVmRegion = rawVmRegions[i];
  
          // See //base/trace_event/process_memory_maps.cc in Chromium.
          var byteStats = new tr.model.VMRegionByteStats(
              parseByteStat(rawVmRegion.bs.pc),
              parseByteStat(rawVmRegion.bs.pd),
              parseByteStat(rawVmRegion.bs.sc),
              parseByteStat(rawVmRegion.bs.sd),
              parseByteStat(rawVmRegion.bs.pss),
              parseByteStat(rawVmRegion.bs.sw));
  
          vmRegions[i] = new tr.model.VMRegion(
              parseInt(rawVmRegion.sa, 16),  // startAddress
              parseInt(rawVmRegion.sz, 16),  // sizeInBytes
              rawVmRegion.pf,  // protectionFlags
              rawVmRegion.mf,  // mappedFile
              byteStats);
        }
  
        processMemoryDump.vmRegions = vmRegions;
      },
  
      parseMemoryDumpHeapDumps_: function(processMemoryDump, dumps, pid, dumpId) {
        var rawHeapDumps = dumps.heaps;
        if (rawHeapDumps === undefined)
          return;
  
        if (processMemoryDump.heapDumps !== undefined) {
          this.model_.importWarning({
            type: 'memory_dump_parse_error',
            message: 'Heap dumps provided multiple times for' +
                ' process memory dump for PID=' + pid +
                ' and dump ID=' + dumpId + '.'
          });
          return;
        }
  
        var model = this.model_;
        var idPrefix = 'p' + pid + ':';
        var heapDumps = {};
  
        for (var allocatorName in rawHeapDumps) {
          var entries = rawHeapDumps[allocatorName].entries;
          if (entries === undefined) {
            this.model_.importWarning({
              type: 'memory_dump_parse_error',
              message: 'Missing heap entries in a ' + allocatorName +
                  ' heap dump for PID=' + pid + ' and dump ID=' + dumpId + '.'
            });
            continue;
          }
  
          var heapDump = new tr.model.HeapDump(processMemoryDump, allocatorName);
  
          for (var i = 0; i < entries.length; i++) {
            var entry = entries[i];
            var type = entry.type;
            if (type !== undefined) {
              // Heap dump entries with specified object type ID need to be
              // ignored for the time being because Chrome doesn't dump the
              // mapping from object type IDs to object type names yet. See
              // crbug.com/524631.
              continue;
            }
            var size = parseInt(entry.size, 16);
            var leafStackFrameIndex = entry.bt;
            var leafStackFrame;
            if (leafStackFrameIndex === undefined) {
              leafStackFrame = undefined;
            } else {
              var leafStackFrameId = idPrefix + leafStackFrameIndex;
              leafStackFrame = model.stackFrames[leafStackFrameId];
              if (leafStackFrame === undefined) {
                this.model_.importWarning({
                  type: 'memory_dump_parse_error',
                  message: 'Missing leaf stack frame (ID ' + leafStackFrameId +
                      ') of heap entry ' + i + ' (size ' + size + ') in a ' +
                      allocatorName + ' heap dump for PID=' + pid + '.'
                });
                continue;
              }
            }
            heapDump.addEntry(leafStackFrame, size);
          }
          heapDumps[allocatorName] = heapDump;
        }
  
        processMemoryDump.heapDumps = heapDumps;
      },
  
      parseMemoryDumpLevelOfDetail_: function(levelOfDetailIndices, dumps, pid,
          dumpId) {
        var rawLevelOfDetail = dumps.level_of_detail;
  
        // Determine the index of the level of detail of the dump.
        var index = MEMORY_DUMP_LEVELS_OF_DETAIL.indexOf(rawLevelOfDetail);
        if (index < 0) {
          this.model_.importWarning({
            type: 'memory_dump_parse_error',
            message: 'unknown level of detail \'' + rawLevelOfDetail +
                '\' of process memory dump for PID=' + pid +
                ' and dump ID=' + dumpId + '.'
          });
          return;
        }
  
        // If the process memory dump events have different levels of detail
        // (for the particular process and/or globally), show a warning and use
        // the highest level.
  
        if (levelOfDetailIndices.process === undefined) {
          levelOfDetailIndices.process = index;
        } else if (index !== levelOfDetailIndices.process) {
          this.model_.importWarning({
            type: 'memory_dump_parse_error',
            message: 'diffent levels of detail provided for process memory' +
                ' dump for PID=' + pid + ' (dump ID=' + dumpId + ').'
          });
          levelOfDetailIndices.process =
              Math.max(levelOfDetailIndices.process, index);
        }
  
        if (levelOfDetailIndices.global === undefined) {
          levelOfDetailIndices.global = index;
        } else if (index !== levelOfDetailIndices.global) {
          this.model_.importWarning({
            type: 'memory_dump_parse_error',
            message: 'diffent levels of detail provided for global memory' +
                ' dump (dump ID=' + dumpId + ').'
          });
          levelOfDetailIndices.global =
              Math.max(levelOfDetailIndices.global, index);
        }
      },
  
      parseMemoryDumpAllocatorDumps_: function(processMemoryDump,
          globalMemoryDump, processMemoryAllocatorDumpsByFullName,
          globalMemoryAllocatorDumpsByFullName, allMemoryAllocatorDumpsByGuid,
          dumps, pid, dumpId) {
        var rawAllocatorDumps = dumps.allocators;
        if (rawAllocatorDumps === undefined)
          return;
  
        // Construct the MemoryAllocatorDump objects without parent links
        // and add them to the processMemoryAllocatorDumpsByName and
        // globalMemoryAllocatorDumpsByName indices appropriately.
        for (var fullName in rawAllocatorDumps) {
          var rawAllocatorDump = rawAllocatorDumps[fullName];
  
          // Every memory allocator dump should have a GUID. If not, then
          // it cannot be associated with any edges.
          var guid = rawAllocatorDump.guid;
          if (guid === undefined) {
            this.model_.importWarning({
              type: 'memory_dump_parse_error',
              message: 'Memory allocator dump ' + fullName + ' for PID=' + pid +
                  ' and dump ID=' + dumpId + ' does not have a GUID.'
            });
          }
  
          // Determine if this is a global memory allocator dump (check if
          // it's prefixed with 'global/').
          var containerMemoryDump;
          var dstIndex;
          if (fullName.startsWith(GLOBAL_MEMORY_ALLOCATOR_DUMP_PREFIX)) {
            // Global memory allocator dump.
            fullName = fullName.substring(
                GLOBAL_MEMORY_ALLOCATOR_DUMP_PREFIX.length);
            containerMemoryDump = globalMemoryDump;
            dstIndex = globalMemoryAllocatorDumpsByFullName;
          } else {
            // Process memory allocator dump.
            containerMemoryDump = processMemoryDump;
            dstIndex = processMemoryAllocatorDumpsByFullName;
          }
  
          // Construct or retrieve a memory allocator dump with the provided
          // GUID.
          var allocatorDump = allMemoryAllocatorDumpsByGuid[guid];
          if (allocatorDump === undefined) {
            if (fullName in dstIndex) {
              this.model_.importWarning({
                type: 'memory_dump_parse_error',
                message: 'Multiple GUIDs provided for' +
                    ' memory allocator dump ' + fullName + ': ' +
                    dstIndex[fullName].guid + ', ' + guid + ' (ignored) for' +
                    ' PID=' + pid + ' and dump ID=' + dumpId + '.'
              });
              continue;
            }
            allocatorDump = new tr.model.MemoryAllocatorDump(
                containerMemoryDump, fullName, guid);
            dstIndex[fullName] = allocatorDump;
            if (guid !== undefined)
              allMemoryAllocatorDumpsByGuid[guid] = allocatorDump;
          } else {
            // A memory allocator dump with this GUID has already been
            // dumped (so we will only add new attributes). Check that it
            // belonged to the same process or was also global.
            if (allocatorDump.containerMemoryDump !== containerMemoryDump) {
              this.model_.importWarning({
              type: 'memory_dump_parse_error',
              message: 'Memory allocator dump ' + fullName +
                  ' (GUID=' + guid + ') for PID=' + pid + ' and dump ID=' +
                  dumpId + ' dumped in different contexts.'
              });
              continue;
            }
            // Check that the names of the memory allocator dumps match.
            if (allocatorDump.fullName !== fullName) {
              this.model_.importWarning({
              type: 'memory_dump_parse_error',
              message: 'Memory allocator dump with GUID=' + guid + ' for PID=' +
                  pid + ' and dump ID=' + dumpId + ' has multiple names: ' +
                  allocatorDump.fullName + ', ' + fullName + ' (ignored).'
              });
              continue;
            }
          }
  
          // Add all new attributes to the memory allocator dump.
          var attributes = rawAllocatorDump.attrs;
          if (attributes === undefined) {
            this.model_.importWarning({
              type: 'memory_dump_parse_error',
              message: 'Memory allocator dump ' + fullName + ' (GUID=' + guid +
                  ') for PID=' + pid + ' and dump ID=' + dumpId +
                  ' does not have attributes.'
            });
            attributes = {};
          }
  
          for (var attrName in attributes) {
            if (attrName in allocatorDump.attributes) {
              // Skip existing attributes of the memory allocator dump.
              this.model_.importWarning({
              type: 'memory_dump_parse_error',
              message: 'Multiple values provided for attribute ' + attrName +
                  ' of memory allocator dump ' + fullName + ' (GUID=' + guid +
                  ') for PID=' + pid + ' and dump ID=' + dumpId + '.'
              });
              continue;
            }
  
            var attrArgs = attributes[attrName];
            var attrValue = tr.model.Attribute.fromDictIfPossible(attrArgs);
            allocatorDump.addAttribute(attrName, attrValue);
          }
        }
      },
  
      inferMemoryAllocatorDumpTree_: function(memoryAllocatorDumpsByFullName) {
        var rootAllocatorDumps = [];
  
        var fullNames = Object.keys(memoryAllocatorDumpsByFullName);
        fullNames.sort();
        for (var i = 0; i < fullNames.length; i++) {
          var fullName = fullNames[i];
          var allocatorDump = memoryAllocatorDumpsByFullName[fullName];
  
          // This is a loop because we might need to build implicit
          // ancestors in case they were not present in the trace.
          while (true) {
            var lastSlashIndex = fullName.lastIndexOf('/');
            if (lastSlashIndex === -1) {
              // If the dump is a root, add it to the top-level
              // rootAllocatorDumps list.
              rootAllocatorDumps.push(allocatorDump);
              break;
            }
  
            // If the dump is not a root, find its parent.
            var parentFullName = fullName.substring(0, lastSlashIndex);
            var parentAllocatorDump =
                memoryAllocatorDumpsByFullName[parentFullName];
  
            // If the parent dump does not exist yet, we build an implicit
            // one and continue up the ancestor chain.
            var parentAlreadyExisted = true;
            if (parentAllocatorDump === undefined) {
              parentAlreadyExisted = false;
              parentAllocatorDump = new tr.model.MemoryAllocatorDump(
                  allocatorDump.containerMemoryDump, parentFullName);
              memoryAllocatorDumpsByFullName[parentFullName] =
                  parentAllocatorDump;
            }
  
            // Setup the parent <-> children relationships
            allocatorDump.parent = parentAllocatorDump;
            parentAllocatorDump.children.push(allocatorDump);
  
            // If the parent already existed, then its ancestors were/will be
            // constructed in another iteration of the forEach loop.
            if (parentAlreadyExisted)
              break;
  
            fullName = parentFullName;
            allocatorDump = parentAllocatorDump;
          }
        }
  
        return rootAllocatorDumps;
      },
  
      parseMemoryDumpAllocatorEdges_: function(allMemoryAllocatorDumpsByGuid,
          dumpIdEvents, dumpId) {
        for (var pid in dumpIdEvents) {
          var processEvents = dumpIdEvents[pid];
  
          for (var i = 0; i < processEvents.length; i++) {
            var processEvent = processEvents[i];
  
            var dumps = processEvent.args.dumps;
            if (dumps === undefined)
              continue;
  
            var rawEdges = dumps.allocators_graph;
            if (rawEdges === undefined)
              continue;
  
            for (var j = 0; j < rawEdges.length; j++) {
              var rawEdge = rawEdges[j];
  
              var sourceGuid = rawEdge.source;
              var sourceDump = allMemoryAllocatorDumpsByGuid[sourceGuid];
              if (sourceDump === undefined) {
                this.model_.importWarning({
                  type: 'memory_dump_parse_error',
                  message: 'Edge for PID=' + pid + ' and dump ID=' + dumpId +
                      ' is missing source memory allocator dump (GUID=' +
                      sourceGuid + ').'
                });
                continue;
              }
  
              var targetGuid = rawEdge.target;
              var targetDump = allMemoryAllocatorDumpsByGuid[targetGuid];
              if (targetDump === undefined) {
                this.model_.importWarning({
                  type: 'memory_dump_parse_error',
                  message: 'Edge for PID=' + pid + ' and dump ID=' + dumpId +
                      ' is missing target memory allocator dump (GUID=' +
                      targetGuid + ').'
                });
                continue;
              }
  
              var importance = rawEdge.importance;
              var edge = new tr.model.MemoryAllocatorDumpLink(
                  sourceDump, targetDump, importance);
  
              switch (rawEdge.type) {
                case 'ownership':
                  if (sourceDump.owns !== undefined) {
                    this.model_.importWarning({
                      type: 'memory_dump_parse_error',
                      message: 'Memory allocator dump ' + sourceDump.fullName +
                          ' (GUID=' + sourceGuid + ') already owns a memory' +
                          ' allocator dump (' +
                          sourceDump.owns.target.fullName + ').'
                    });
                  } else {
                    sourceDump.owns = edge;
                    targetDump.ownedBy.push(edge);
                  }
                  break;
  
                case 'retention':
                  sourceDump.retains.push(edge);
                  targetDump.retainedBy.push(edge);
                  break;
  
                default:
                  this.model_.importWarning({
                    type: 'memory_dump_parse_error',
                    message: 'Invalid edge type: ' + rawEdge.type +
                        ' (PID=' + pid + ', dump ID=' + dumpId +
                        ', source=' + sourceGuid + ', target=' + targetGuid +
                        ', importance=' + importance + ').'
                  });
              }
            }
          }
        }
      },
  
      joinObjectRefs_: function() {
        tr.b.iterItems(this.model_.processes, function(pid, process) {
          this.joinObjectRefsForProcess_(process);
        }, this);
      },
  
      joinObjectRefsForProcess_: function(process) {
        // Iterate the world, looking for id_refs
        var patchupsToApply = [];
        tr.b.iterItems(process.threads, function(tid, thread) {
          thread.asyncSliceGroup.slices.forEach(function(item) {
            this.searchItemForIDRefs_(
                patchupsToApply, process.objects, 'start', item);
          }, this);
          thread.sliceGroup.slices.forEach(function(item) {
            this.searchItemForIDRefs_(
                patchupsToApply, process.objects, 'start', item);
          }, this);
        }, this);
        process.objects.iterObjectInstances(function(instance) {
          instance.snapshots.forEach(function(item) {
            this.searchItemForIDRefs_(
                patchupsToApply, process.objects, 'ts', item);
          }, this);
        }, this);
  
        // Change all the fields pointing at id_refs to their real values.
        patchupsToApply.forEach(function(patchup) {
          patchup.object[patchup.field] = patchup.value;
        });
      },
  
      searchItemForIDRefs_: function(patchupsToApply, objectCollection,
                                     itemTimestampField, item) {
        if (!item.args)
          throw new Error('item is missing its args');
  
        function handleField(object, fieldName, fieldValue) {
          if (!fieldValue || (!fieldValue.id_ref && !fieldValue.idRef))
            return;
  
          var id = fieldValue.id_ref || fieldValue.idRef;
          var ts = item[itemTimestampField];
          var snapshot = objectCollection.getSnapshotAt(id, ts);
          if (!snapshot)
            return;
  
          // We have to delay the actual change to the new value until after all
          // refs have been located. Otherwise, we could end up recursing in
          // ways we definitely didn't intend.
          patchupsToApply.push({object: object,
            field: fieldName,
            value: snapshot});
        }
        function iterObjectFieldsRecursively(object) {
          if (!(object instanceof Object))
            return;
  
          if ((object instanceof tr.model.ObjectSnapshot) ||
              (object instanceof Float32Array) ||
              (object instanceof tr.b.Quad))
            return;
  
          if (object instanceof Array) {
            for (var i = 0; i < object.length; i++) {
              handleField(object, i, object[i]);
              iterObjectFieldsRecursively(object[i]);
            }
            return;
          }
  
          for (var key in object) {
            var value = object[key];
            handleField(object, key, value);
            iterObjectFieldsRecursively(value);
          }
        }
  
        iterObjectFieldsRecursively(item.args);
      }
    };
  
    tr.importer.Importer.register(TraceEventImporter);
  
    return {
      TraceEventImporter: TraceEventImporter
    };
  });
  