Bug 1154115 - Make the performance devtool handle the new profiler JSON format. (r=jsantell,vporof, a=sledru)
authorShu-yu Guo <shu@rfrn.org>
Mon, 11 May 2015 14:16:44 -0700
changeset 273293 b7ba8798ffc76799f97004bbd0fc9cfd435fffb7
parent 273292 b78b5fac8645243dc08097675a86e0b89290c84c
child 273294 4c67c1f6531bcb69908fd1dedb206121f41cb687
push id4830
push userjlund@mozilla.com
push dateMon, 29 Jun 2015 20:18:48 +0000
treeherdermozilla-beta@4c2175bb0420 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell, vporof, sledru
bugs1154115
milestone40.0a2
Bug 1154115 - Make the performance devtool handle the new profiler JSON format. (r=jsantell,vporof, a=sledru)
browser/devtools/performance/modules/recording-utils.js
browser/devtools/performance/views/details-js-call-tree.js
browser/devtools/performance/views/details-js-flamegraph.js
browser/devtools/performance/views/details-memory-flamegraph.js
browser/devtools/performance/views/jit-optimizations.js
browser/devtools/shared/profiler/frame-utils.js
browser/devtools/shared/profiler/jit.js
browser/devtools/shared/profiler/tree-model.js
browser/devtools/shared/profiler/tree-view.js
browser/devtools/shared/widgets/FlameGraph.js
--- a/browser/devtools/performance/modules/recording-utils.js
+++ b/browser/devtools/performance/modules/recording-utils.js
@@ -18,35 +18,36 @@ exports.RecordingUtils = {};
  *
  * @param object profile
  *        The profiler data received from the backend.
  * @param number profilerStartTime
  *        The earliest acceptable sample time (in milliseconds).
  */
 exports.RecordingUtils.filterSamples = function(profile, profilerStartTime) {
   let firstThread = profile.threads[0];
-
-  firstThread.samples = firstThread.samples.filter(e => {
-    return e.time >= profilerStartTime;
+  const TIME_SLOT = firstThread.samples.schema.time;
+  firstThread.samples.data = firstThread.samples.data.filter(e => {
+    return e[TIME_SLOT] >= profilerStartTime;
   });
 }
 
 /**
  * Offsets all the samples in the provided profiler data by the specified time.
  *
  * @param object profile
  *        The profiler data received from the backend.
  * @param number timeOffset
  *        The amount of time to offset by (in milliseconds).
  */
 exports.RecordingUtils.offsetSampleTimes = function(profile, timeOffset) {
   let firstThread = profile.threads[0];
-
-  for (let sample of firstThread.samples) {
-    sample.time -= timeOffset;
+  const TIME_SLOT = firstThread.samples.schema.time;
+  let samplesData = firstThread.samples.data;
+  for (let i = 0; i < samplesData.length; i++) {
+    samplesData[i][TIME_SLOT] -= timeOffset;
   }
 }
 
 /**
  * Offsets all the markers in the provided timeline data by the specified time.
  *
  * @param array markers
  *        The markers array received from the backend.
--- a/browser/devtools/performance/views/details-js-call-tree.js
+++ b/browser/devtools/performance/views/details-js-call-tree.js
@@ -5,17 +5,18 @@
 
 /**
  * CallTree view containing profiler call tree, controlled by DetailsView.
  */
 let JsCallTreeView = Heritage.extend(DetailsSubview, {
 
   rerenderPrefs: [
     "invert-call-tree",
-    "show-platform-data"
+    "show-platform-data",
+    "flatten-tree-recursion"
   ],
 
   rangeChangeDebounceTime: 75, // ms
 
   /**
    * Sets up the view with event binding.
    */
   initialize: function () {
@@ -42,17 +43,18 @@ let JsCallTreeView = Heritage.extend(Det
    * Method for handling all the set up for rendering a new call tree.
    *
    * @param object interval [optional]
    *        The { startTime, endTime }, in milliseconds.
    */
   render: function (interval={}) {
     let options = {
       contentOnly: !PerformanceController.getOption("show-platform-data"),
-      invertTree: PerformanceController.getOption("invert-call-tree")
+      invertTree: PerformanceController.getOption("invert-call-tree"),
+      flattenRecursion: PerformanceController.getOption("flatten-tree-recursion")
     };
     let recording = PerformanceController.getCurrentRecording();
     let profile = recording.getProfile();
     let threadNode = this._prepareCallTree(profile, interval, options);
     this._populateCallTree(threadNode, options);
     this.emit(EVENTS.JS_CALL_TREE_RENDERED);
   },
 
@@ -70,39 +72,44 @@ let JsCallTreeView = Heritage.extend(Det
     });
   },
 
   /**
    * Called when the recording is stopped and prepares data to
    * populate the call tree.
    */
   _prepareCallTree: function (profile, { startTime, endTime }, options) {
-    let threadSamples = profile.threads[0].samples;
-    let optimizations = profile.threads[0].optimizations;
-    let { contentOnly, invertTree } = options;
+    let thread = profile.threads[0];
+    let { contentOnly, invertTree, flattenRecursion } = options;
+    let threadNode = new ThreadNode(thread, { startTime, endTime, contentOnly, invertTree, flattenRecursion });
 
-    let threadNode = new ThreadNode(threadSamples,
-      { startTime, endTime, contentOnly, invertTree, optimizations });
+    // Real profiles from nsProfiler (i.e. not synthesized from allocation
+    // logs) always have a (root) node. Go down one level in the uninverted
+    // view to avoid displaying both the synthesized root node and the (root)
+    // node from the profiler.
+    if (!invertTree) {
+      threadNode.calls = threadNode.calls[0].calls;
+    }
 
     return threadNode;
   },
 
   /**
    * Renders the call tree.
    */
   _populateCallTree: function (frameNode, options={}) {
     // If we have an empty profile (no samples), then don't invert the tree, as
     // it would hide the root node and a completely blank call tree space can be
     // mis-interpreted as an error.
     let inverted = options.invertTree && frameNode.samples > 0;
 
     let root = new CallView({
       frame: frameNode,
       inverted: inverted,
-      // Root nodes are hidden in inverted call trees.
+      // The synthesized root node is hidden in inverted call trees.
       hidden: inverted,
       // Call trees should only auto-expand when not inverted. Passing undefined
       // will default to the CALL_TREE_AUTO_EXPAND depth.
       autoExpandDepth: inverted ? 0 : undefined
     });
 
     // Bind events.
     root.on("link", this._onLink);
--- a/browser/devtools/performance/views/details-js-flamegraph.js
+++ b/browser/devtools/performance/views/details-js-flamegraph.js
@@ -53,22 +53,22 @@ let JsFlameGraphView = Heritage.extend(D
    *
    * @param object interval [optional]
    *        The { startTime, endTime }, in milliseconds.
    */
   render: function (interval={}) {
     let recording = PerformanceController.getCurrentRecording();
     let duration = recording.getDuration();
     let profile = recording.getProfile();
-    let samples = profile.threads[0].samples;
+    let thread = profile.threads[0];
 
-    let data = FlameGraphUtils.createFlameGraphDataFromSamples(samples, {
-      invertStack: PerformanceController.getOption("invert-flame-graph"),
+    let data = FlameGraphUtils.createFlameGraphDataFromThread(thread, {
+      invertTree: PerformanceController.getOption("invert-flame-graph"),
       flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"),
-      filterFrames: !PerformanceController.getOption("show-platform-data") && FrameNode.isContent,
+      contentOnly: !PerformanceController.getOption("show-platform-data"),
       showIdleBlocks: PerformanceController.getOption("show-idle-blocks") && L10N.getStr("table.idle")
     });
 
     this.graph.setData({ data,
       bounds: {
         startTime: 0,
         endTime: duration
       },
@@ -90,18 +90,18 @@ let JsFlameGraphView = Heritage.extend(D
   },
 
   /**
    * Called whenever a pref is changed and this view needs to be rerendered.
    */
   _onRerenderPrefChanged: function() {
     let recording = PerformanceController.getCurrentRecording();
     let profile = recording.getProfile();
-    let samples = profile.threads[0].samples;
-    FlameGraphUtils.removeFromCache(samples);
+    let thread = profile.threads[0];
+    FlameGraphUtils.removeFromCache(thread);
   },
 
   /**
    * Called when `devtools.theme` changes.
    */
   _onThemeChanged: function (_, theme) {
     this.graph.setTheme(theme);
     this.graph.refresh({ force: true });
--- a/browser/devtools/performance/views/details-memory-flamegraph.js
+++ b/browser/devtools/performance/views/details-memory-flamegraph.js
@@ -88,18 +88,18 @@ let MemoryFlameGraphView = Heritage.exte
   },
 
   /**
    * Called whenever a pref is changed and this view needs to be rerendered.
    */
   _onRerenderPrefChanged: function() {
     let recording = PerformanceController.getCurrentRecording();
     let allocations = recording.getAllocations();
-    let samples = RecordingUtils.getSamplesFromAllocations(allocations);
-    FlameGraphUtils.removeFromCache(samples);
+    let thread = RecordingUtils.getProfileThreadFromAllocations(allocations);
+    FlameGraphUtils.removeFromCache(thread);
   },
 
   /**
    * Called when `devtools.theme` changes.
    */
   _onThemeChanged: function (_, theme) {
     this.graph.setTheme(theme);
     this.graph.refresh({ force: true });
--- a/browser/devtools/performance/views/jit-optimizations.js
+++ b/browser/devtools/performance/views/jit-optimizations.js
@@ -134,17 +134,17 @@ let JITOptimizationsView = {
     // case of only showing content, reset the view.
     if (!frameNode.hasOptimizations() || frameNode.isMetaCategory) {
       this.reset();
       return;
     }
     this.el.classList.remove("empty");
 
     // An array of sorted OptimizationSites.
-    let sites = frameNode.getOptimizations().getOptimizationSites();
+    let sites = frameNode.getOptimizations().optimizationSites;
 
     for (let site of sites) {
       this._renderSite(view, site, frameData);
     }
 
     this.emit(EVENTS.OPTIMIZATIONS_RENDERED, this.getCurrentFrame());
   },
 
@@ -182,17 +182,17 @@ let JITOptimizationsView = {
     let { id, data: { types }} = site;
     // Cast `id` to a string so TreeWidget doesn't think it does not exist
     id = id + "";
     for (let i = 0; i < types.length; i++) {
       let ionType = types[i];
 
       let ionNode = this._createIonNode(ionType);
       view.add([id, `${id}-types`, { id: `${id}-types-${i}`, node: ionNode }]);
-      for (let observedType of (ionType.types || [])) {
+      for (let observedType of (ionType.typeset || [])) {
         let node = this._createObservedTypeNode(observedType);
         view.add([id, `${id}-types`, `${id}-types-${i}`, { node }]);
       }
     }
   },
 
   /**
    * Creates an element for insertion in the raw view for an OptimizationSite.
--- a/browser/devtools/shared/profiler/frame-utils.js
+++ b/browser/devtools/shared/profiler/frame-utils.js
@@ -30,16 +30,19 @@ const CHAR_CODE_9 = "9".charCodeAt(0);
 
 const CHAR_CODE_LPAREN = "(".charCodeAt(0);
 const CHAR_CODE_COLON = ":".charCodeAt(0);
 const CHAR_CODE_SLASH = "/".charCodeAt(0);
 
 // The cache used in the `nsIURL` function.
 const gNSURLStore = new Map();
 
+// The cache used to store inflated frames.
+const gInflatedFrameStore = new WeakMap();
+
 /**
  * Parses the raw location of this function call to retrieve the actual
  * function name, source url, host name, line and column.
  */
 exports.parseLocation = function parseLocation(location, fallbackLine, fallbackColumn) {
   // Parse the `location` for the function name, source url, line, column etc.
 
   let line, column, url;
@@ -93,20 +96,27 @@ exports.parseLocation = function parseLo
         let start = ++i;
         let length = 1;
         while (isNumeric(location.charCodeAt(++i))) {
           length++;
         }
 
         if (!line) {
           line = location.substr(start, length);
-        } else if (!column) {
-          column = location.substr(start, length);
+
+          // Unwind a character due to the isNumeric loop above.
+          --i;
+
+          // There still might be a column number, continue looking.
+          continue;
         }
 
+        column = location.substr(start, length);
+
+        // We've gotten both a line and a column, stop looking.
         break;
       }
     }
   }
 
   let uri;
   if (lineAndColumnIndex > 0) {
     let resource = location.substring(firstParenIndex + 1, lineAndColumnIndex);
@@ -128,18 +138,18 @@ exports.parseLocation = function parseLo
     url = null;
   }
 
   return {
     functionName: functionName,
     fileName: fileName,
     hostName: hostName,
     url: url,
-    line: line,
-    column: column
+    line: line || fallbackLine,
+    column: column || fallbackColumn
   };
 };
 
 /**
  * Checks if the specified function represents a chrome or content frame.
  *
  * @param string location
  *        The location of the frame.
@@ -165,58 +175,113 @@ function isContent({ location, category 
   }
 
   // If there was no left parenthesis, try matching from the start.
   return isContentScheme(location, 0);
 }
 exports.isContent = isContent;
 
 /**
- * This filters out platform data frames in a sample. With latest performance
- * tool in Fx40, when displaying only content, we still filter out all platform data,
- * except we generalize platform data that are leaves. We do this because of two
- * observations:
- *
- * 1. The leaf is where time is _actually_ being spent, so we _need_ to show it
- * to developers in some way to give them accurate profiling data. We decide to
- * split the platform into various category buckets and just show time spent in
- * each bucket.
- *
- * 2. The calls leading to the leaf _aren't_ where we are spending time, but
- * _do_ give the developer context for how they got to the leaf where they _are_
- * spending time. For non-platform hackers, the non-leaf platform frames don't
- * give any meaningful context, and so we can safely filter them out.
- *
- * Example transformations:
- * Before: PlatformA -> PlatformB -> ContentA -> ContentB
- * After:  ContentA -> ContentB
+ * Get caches to cache inflated frames and computed frame keys of a frame
+ * table.
  *
- * Before: PlatformA -> ContentA -> PlatformB -> PlatformC
- * After:  ContentA -> Category(PlatformC)
+ * @param object framesTable
+ * @return object
  */
-exports.filterPlatformData = function filterPlatformData (frames) {
-  let result = [];
-  let last = frames.length - 1;
-  let frame;
-
-  for (let i = 0; i < frames.length; i++) {
-    frame = frames[i];
-    if (exports.isContent(frame)) {
-      result.push(frame);
-    } else if (last === i) {
-      // Extend here so we're not destructively editing
-      // the original profiler data. Set isMetaCategory `true`,
-      // and ensure we have a category set by default, because that's how
-      // the generalized frame nodes are organized.
-      result.push(extend({ isMetaCategory: true, category: CATEGORY_OTHER }, frame));
-    }
+exports.getInflatedFrameCache = function getInflatedFrameCache(frameTable) {
+  let inflatedCache = gInflatedFrameStore.get(frameTable);
+  if (inflatedCache !== undefined) {
+    return inflatedCache;
   }
 
-  return result;
-}
+  // Fill with nulls to ensure no holes.
+  inflatedCache = Array.from({ length: frameTable.data.length }, () => null);
+  gInflatedFrameStore.set(frameTable, inflatedCache);
+  return inflatedCache;
+};
+
+/**
+ * Get or add an inflated frame to a cache.
+ *
+ * @param object cache
+ * @param number index
+ * @param object frameTable
+ * @param object stringTable
+ */
+exports.getOrAddInflatedFrame = function getOrAddInflatedFrame(cache, index, frameTable, stringTable) {
+  let inflatedFrame = cache[index];
+  if (inflatedFrame === null) {
+    inflatedFrame = cache[index] = new InflatedFrame(index, frameTable, stringTable);
+  }
+  return inflatedFrame;
+};
+
+/**
+ * An intermediate data structured used to hold inflated frames.
+ *
+ * @param number index
+ * @param object frameTable
+ * @param object stringTable
+ */
+function InflatedFrame(index, frameTable, stringTable) {
+  const LOCATION_SLOT = frameTable.schema.location;
+  const OPTIMIZATIONS_SLOT = frameTable.schema.optimizations;
+  const LINE_SLOT = frameTable.schema.line;
+  const CATEGORY_SLOT = frameTable.schema.category;
+
+  let frame = frameTable.data[index];
+  let category = frame[CATEGORY_SLOT];
+  this.location = stringTable[frame[LOCATION_SLOT]];
+  this.optimizations = frame[OPTIMIZATIONS_SLOT];
+  this.line = frame[LINE_SLOT];
+  this.column = undefined;
+  this.category = category;
+  this.metaCategory = category || CATEGORY_OTHER;
+  this.isContent = isContent(this);
+};
+
+/**
+ * Gets the frame key (i.e., equivalence group) according to options. Content
+ * frames are always identified by location. Chrome frames are identified by
+ * location if content-only filtering is off. If content-filtering is on, they
+ * are identified by their category.
+ *
+ * @param object options
+ * @return string
+ */
+InflatedFrame.prototype.getFrameKey = function getFrameKey(options) {
+  if (this.isContent || !options.contentOnly || options.isRoot) {
+    options.isMetaCategoryOut = false;
+    return this.location;
+  }
+
+  if (options.isLeaf) {
+    // We only care about leaf platform frames if we are displaying content
+    // only. If no category is present, give the default category of
+    // CATEGORY_OTHER.
+    //
+    // 1. The leaf is where time is _actually_ being spent, so we _need_ to
+    // show it to developers in some way to give them accurate profiling
+    // data. We decide to split the platform into various category buckets
+    // and just show time spent in each bucket.
+    //
+    // 2. The calls leading to the leaf _aren't_ where we are spending time,
+    // but _do_ give the developer context for how they got to the leaf
+    // where they _are_ spending time. For non-platform hackers, the
+    // non-leaf platform frames don't give any meaningful context, and so we
+    // can safely filter them out.
+    options.isMetaCategoryOut = true;
+    return this.metaCategory;
+  }
+
+  // Return an empty string denoting that this frame should be skipped.
+  return "";
+};
+
+exports.InflatedFrame = InflatedFrame;
 
 /**
  * Helper for getting an nsIURL instance out of a string.
  */
 function nsIURL(url) {
   let cached = gNSURLStore.get(url);
   // If we cached a valid URI, or `null` in the case
   // of a failure, return it.
@@ -237,17 +302,17 @@ function nsIURL(url) {
   return uri;
 };
 
 /**
  * Takes a `host` string from an nsIURL instance and
  * returns the same string, or null, if it's an invalid host.
  */
 function getHost (url, hostName) {
-  return isChromeScheme(url) ? null : hostName;
+  return isChromeScheme(url, 0) ? null : hostName;
 }
 
 // For the functions below, we assume that we will never access the location
 // argument out of bounds, which is indeed the vast majority of cases.
 //
 // They are written this way because they are hot. Each frame is checked for
 // being content or chrome when processing the profile.
 
@@ -260,17 +325,17 @@ function isColonSlashSlash(location, i) 
 function isContentScheme(location, i) {
   let firstChar = location.charCodeAt(i);
 
   switch (firstChar) {
   case CHAR_CODE_H: // "http://" or "https://"
     if (location.charCodeAt(++i) === CHAR_CODE_T &&
         location.charCodeAt(++i) === CHAR_CODE_T &&
         location.charCodeAt(++i) === CHAR_CODE_P) {
-      if (location.charCodeAt(i) === CHAR_CODE_S) {
+      if (location.charCodeAt(i + 1) === CHAR_CODE_S) {
         ++i;
       }
       return isColonSlashSlash(location, i);
     }
     return false;
 
   case CHAR_CODE_F: // "file://"
     if (location.charCodeAt(++i) === CHAR_CODE_I &&
--- a/browser/devtools/shared/profiler/jit.js
+++ b/browser/devtools/shared/profiler/jit.js
@@ -8,55 +8,58 @@ const SUCCESSFUL_OUTCOMES = [
   "GenericSuccess", "Inlined", "DOM", "Monomorphic", "Polymorphic"
 ];
 
 /**
  * Model representing JIT optimization sites from the profiler
  * for a frame (represented by a FrameNode). Requires optimization data from
  * a profile, which is an array of RawOptimizationSites.
  *
- * When the ThreadNode for the profile iterates over the samples' frames, a JITOptimization
- * model is attached to each frame node, with each sample of the frame, usually with each
- * sample containing different optimization information for the same frame (one sample may
- * pick up optimization X on line Y in the frame, with the next sample containing optimization Z
- * on line W in the same frame, as each frame is only function.
+ * When the ThreadNode for the profile iterates over the samples' frames, each
+ * frame's optimizations are accumulated in their respective FrameNodes. Each
+ * FrameNode may contain many different optimization sites. One sample may
+ * pick up optimization X on line Y in the frame, with the next sample
+ * containing optimization Z on line W in the same frame, as each frame is
+ * only function.
  *
- * Each RawOptimizationSite can be sampled multiple times, which multiple calls to
- * JITOptimizations#addOptimizationSite handles. An OptimizationSite contains
- * a record of how many times the RawOptimizationSite was sampled, as well as the unique id
- * based off of the original profiler array, and the RawOptimizationSite itself as a reference.
+ * An OptimizationSite contains a record of how many times the
+ * RawOptimizationSite was sampled, as well as the unique id based off of the
+ * original profiler array, and the RawOptimizationSite itself as a reference.
  * @see browser/devtools/shared/profiler/tree-model.js
  *
- *
  * @struct RawOptimizationSite
  * A structure describing a location in a script that was attempted to be optimized.
  * Contains all the IonTypes observed, and the sequence of OptimizationAttempts that
  * were attempted, and the line and column in the script. This is retrieved from the
  * profiler after a recording, and our base data structure. Should always be referenced,
  * and unmodified.
  *
+ * Note that propertyName is an index into a string table, which needs to be
+ * provided in order for the raw optimization site to be inflated.
+ *
  * @type {Array<IonType>} types
  * @type {Array<OptimizationAttempt>} attempts
+ * @type {?number} propertyName
  * @type {number} line
  * @type {number} column
  *
  *
  * @struct IonType
  * IonMonkey attempts to classify each value in an optimization site by some type.
  * Based off of the observed types for a value (like a variable that could be a
  * string or an instance of an object), it determines what kind of type it should be classified
  * as. Each IonType here contains an array of all ObservedTypes under `types`,
  * the Ion type that IonMonkey decided this value should be (Int32, Object, etc.) as `mirType`,
  * and the component of this optimization type that this value refers to -- like
  * a "getter" optimization, `a[b]`, has site `a` (the "Receiver") and `b` (the "Index").
  *
  * Generally the more ObservedTypes, the more deoptimized this OptimizationSite is.
  * There could be no ObservedTypes, in which case `types` is undefined.
  *
- * @type {?Array<ObservedType>} types
+ * @type {?Array<ObservedType>} typeset
  * @type {string} site
  * @type {string} mirType
  *
  *
  * @struct ObservedType
  * When IonMonkey attempts to determine what type a value is, it checks on each sample.
  * The ObservedType can be thought of in more of JavaScripty-terms, rather than C++.
  * The `keyedBy` property is a high level description of the type, like "primitive",
@@ -106,26 +109,27 @@ const SUCCESSFUL_OUTCOMES = [
  * @param {Array<RawOptimizationSite>} optimizations
  * @param {number} optsIndex
  *
  * @type {RawOptimizationSite} data
  * @type {number} samples
  * @type {number} id
  */
 
-const OptimizationSite = exports.OptimizationSite = function (optimizations, optsIndex) {
-  this.id = optsIndex;
-  this.data = optimizations[optsIndex];
-  this.samples = 0;
+const OptimizationSite = exports.OptimizationSite = function (id, opts) {
+  this.id = id;
+  this.data = opts;
+  this.samples = 1;
 };
 
 /**
  * Returns a boolean indicating if the passed in OptimizationSite
  * has a "good" outcome at the end of its attempted strategies.
  *
+ * @param {Array<string>} stringTable
  * @return {boolean}
  */
 
 OptimizationSite.prototype.hasSuccessfulOutcome = function () {
   let attempts = this.getAttempts();
   let lastOutcome = attempts[attempts.length - 1].outcome;
   return OptimizationSite.isSuccessfulOutcome(lastOutcome);
 };
@@ -150,56 +154,87 @@ OptimizationSite.prototype.getIonTypes =
   return this.data.types;
 };
 
 
 /**
  * Constructor for JITOptimizations. A collection of OptimizationSites for a frame.
  *
  * @constructor
- * @param {Array<RawOptimizationSite>} optimizations
- *        Array of RawOptimizationSites from the profiler. Do not modify this!
+ * @param {Array<RawOptimizationSite>} rawSites
+ *                                     Array of raw optimization sites.
+ * @param {Array<string>} stringTable
+ *                        Array of strings from the profiler used to inflate
+ *                        JIT optimizations. Do not modify this!
  */
 
-const JITOptimizations = exports.JITOptimizations = function (optimizations) {
-  this._opts = optimizations;
-  // Hash of OptimizationSites observed for this frame.
-  this._optSites = {};
-};
+const JITOptimizations = exports.JITOptimizations = function (rawSites, stringTable) {
+  // Build a histogram of optimization sites.
+  let sites = [];
 
-/**
- * Called when a sample detects an optimization on this frame. Takes an `optsIndex`,
- * referring to an optimization in the stored `this._opts` array. Creates a histogram
- * of optimization site data by creating or incrementing an OptimizationSite
- * for each observed optimization.
- *
- * @param {Number} optsIndex
- */
+  for (let rawSite of rawSites) {
+    let existingSite = sites.find((site) => site.data === rawSite);
+    if (existingSite) {
+      existingSite.samples++;
+    } else {
+      sites.push(new OptimizationSite(sites.length, rawSite));
+    }
+  }
+
+  // Inflate the optimization information.
+  for (let site of sites) {
+    let data = site.data;
+    let STRATEGY_SLOT = data.attempts.schema.strategy;
+    let OUTCOME_SLOT = data.attempts.schema.outcome;
 
-JITOptimizations.prototype.addOptimizationSite = function (optsIndex) {
-  let op = this._optSites[optsIndex] || (this._optSites[optsIndex] = new OptimizationSite(this._opts, optsIndex));
-  op.samples++;
-};
+    site.data = {
+      attempts: data.attempts.data.map((a) => {
+        return {
+          strategy: stringTable[a[STRATEGY_SLOT]],
+          outcome: stringTable[a[OUTCOME_SLOT]]
+        }
+      }),
 
-/**
- * Returns an array of OptimizationSites, sorted from most to least times sampled.
- *
- * @return {Array<OptimizationSite>}
- */
+      types: data.types.map((t) => {
+        return {
+          typeset: maybeTypeset(t.typeset, stringTable),
+          site: stringTable[t.site],
+          mirType: stringTable[t.mirType]
+        };
+      }),
 
-JITOptimizations.prototype.getOptimizationSites = function () {
-  let opts = [];
-  for (let opt of Object.keys(this._optSites)) {
-    opts.push(this._optSites[opt]);
+      propertyName: maybeString(stringTable, data.propertyName),
+      line: data.line,
+      column: data.column
+    };
   }
-  return opts.sort((a, b) => b.samples - a.samples);
+
+  this.optimizationSites = sites.sort((a, b) => b.samples - a.samples);;
 };
 
 /**
  * Takes an "outcome" string from an OptimizationAttempt and returns
  * a boolean indicating whether or not its a successful outcome.
  *
  * @return {boolean}
  */
 
 OptimizationSite.isSuccessfulOutcome = JITOptimizations.isSuccessfulOutcome = function (outcome) {
   return !!~SUCCESSFUL_OUTCOMES.indexOf(outcome);
 };
+
+function maybeString(stringTable, index) {
+  return index ? stringTable[index] : undefined;
+}
+
+function maybeTypeset(typeset, stringTable) {
+  if (!typeset) {
+    return undefined;
+  }
+  return typeset.map((ty) => {
+    return {
+      keyedBy: maybeString(stringTable, ty.keyedBy),
+      name: maybeString(stringTable, ty.name),
+      location: maybeString(stringTable, ty.location),
+      line: ty.line
+    };
+  });
+}
--- a/browser/devtools/shared/profiler/tree-model.js
+++ b/browser/devtools/shared/profiler/tree-model.js
@@ -8,127 +8,320 @@ const {Cc, Ci, Cu, Cr} = require("chrome
 loader.lazyRequireGetter(this, "L10N",
   "devtools/shared/profiler/global", true);
 loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
   "devtools/shared/profiler/global", true);
 loader.lazyRequireGetter(this, "CATEGORIES",
   "devtools/shared/profiler/global", true);
 loader.lazyRequireGetter(this, "CATEGORY_JIT",
   "devtools/shared/profiler/global", true);
+loader.lazyRequireGetter(this, "CATEGORY_OTHER",
+  "devtools/shared/profiler/global", true);
 loader.lazyRequireGetter(this, "JITOptimizations",
   "devtools/shared/profiler/jit", true);
 loader.lazyRequireGetter(this, "FrameUtils",
   "devtools/shared/profiler/frame-utils");
 
 exports.ThreadNode = ThreadNode;
 exports.FrameNode = FrameNode;
 exports.FrameNode.isContent = FrameUtils.isContent;
 
 /**
  * A call tree for a thread. This is essentially a linkage between all frames
  * of all samples into a single tree structure, with additional information
  * on each node, like the time spent (in milliseconds) and samples count.
  *
- * Example:
- * {
- *   duration: number,
- *   calls: {
- *     "FunctionName (url:line)": {
- *       line: number,
- *       category: number,
- *       samples: number,
- *       duration: number,
- *       calls: {
- *         ...
- *       }
- *     }, // FrameNode
- *     ...
- *   }
- * } // ThreadNode
- *
- * @param object threadSamples
- *        The raw samples array received from the backend.
+ * @param object thread
+ *        The raw thread object received from the backend. Contains samples,
+ *        stackTable, frameTable, and stringTable.
  * @param object options
  *        Additional supported options, @see ThreadNode.prototype.insert
  *          - number startTime [optional]
  *          - number endTime [optional]
  *          - boolean contentOnly [optional]
  *          - boolean invertTree [optional]
- *          - object optimizations [optional]
- *            The raw tracked optimizations array received from the backend.
+ *          - boolean flattenRecursion [optional]
  */
-function ThreadNode(threadSamples, options = {}) {
+function ThreadNode(thread, options = {}) {
   this.samples = 0;
   this.duration = 0;
-  this.calls = {};
-  this._previousSampleTime = 0;
+  this.calls = [];
+
+  // Maps of frame to their self counts and duration.
+  this.selfCount = Object.create(null);
+  this.selfDuration = Object.create(null);
+
+  let { samples, stackTable, frameTable, stringTable } = thread;
 
-  for (let sample of threadSamples) {
-    this.insert(sample, options);
+  // Nothing to do if there are no samples.
+  if (samples.data.length === 0) {
+    return;
+  }
+
+  this._buildInverted(samples, stackTable, frameTable, stringTable, options);
+  if (!options.invertTree) {
+    this._uninvert();
   }
 }
 
 ThreadNode.prototype = {
   /**
-   * Adds function calls in the tree from a sample's frames.
+   * Build an inverted call tree from profile samples. The format of the
+   * samples is described in tools/profiler/ProfileEntry.h, under the heading
+   * "ThreadProfile JSON Format".
+   *
+   * The profile data is naturally presented inverted. Inverting the call tree
+   * is also the default in the Performance tool.
    *
-   * @param object sample
-   *        The { frames, time } sample, containing an array of frames and
-   *        the time the sample was taken. This sample is assumed to be older
-   *        than the most recently inserted one.
-   * @param object options [optional]
-   *        Additional supported options:
-   *          - number startTime: the earliest sample to start at (in milliseconds)
-   *          - number endTime: the latest sample to end at (in milliseconds)
-   *          - boolean contentOnly: if platform frames shouldn't be used
-   *          - boolean invertTree: if the call tree should be inverted
-   *          - object optimizations: The array of all indexable optimizations from the backend.
+   * @param object samples
+   *        The raw samples array received from the backend.
+   * @param object stackTable
+   *        The table of deduplicated stacks from the backend.
+   * @param object frameTable
+   *        The table of deduplicated frames from the backend.
+   * @param object stringTable
+   *        The table of deduplicated strings from the backend.
+   * @param object options
+   *        Additional supported options
+   *          - number startTime [optional]
+   *          - number endTime [optional]
+   *          - boolean contentOnly [optional]
+   *          - boolean invertTree [optional]
    */
-  insert: function(sample, options = {}) {
-    let startTime = options.startTime || 0;
-    let endTime = options.endTime || Infinity;
-    let optimizations = options.optimizations;
-    let sampleTime = sample.time;
-    if (!sampleTime || sampleTime < startTime || sampleTime > endTime) {
-      return;
+  _buildInverted: function buildInverted(samples, stackTable, frameTable, stringTable, options) {
+    function getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame, isMetaCategory, leafTable) {
+      // Insert the inflated frame into the call tree at the current level.
+      let frameNode;
+
+      // Leaf nodes have fan out much greater than non-leaf nodes, thus the
+      // use of a hash table. Otherwise, do linear search.
+      //
+      // Note that this method is very hot, thus the manual looping over
+      // Array.prototype.find.
+      if (isLeaf) {
+        frameNode = leafTable[frameKey];
+      } else {
+        for (let i = 0; i < calls.length; i++) {
+          if (calls[i].key === frameKey) {
+            frameNode = calls[i];
+            break;
+          }
+        }
+      }
+
+      if (!frameNode) {
+        frameNode = new FrameNode(frameKey, inflatedFrame, isMetaCategory);
+        if (isLeaf) {
+          leafTable[frameKey] = frameNode;
+        }
+        calls.push(frameNode);
+      }
+
+      return frameNode;
     }
 
-    let sampleFrames = sample.frames;
+    const SAMPLE_STACK_SLOT = samples.schema.stack;
+    const SAMPLE_TIME_SLOT = samples.schema.time;
+
+    const STACK_PREFIX_SLOT = stackTable.schema.prefix;
+    const STACK_FRAME_SLOT = stackTable.schema.frame;
+
+    const InflatedFrame = FrameUtils.InflatedFrame;
+    const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame;
+
+    let selfCount = this.selfCount;
+    let selfDuration = this.selfDuration;
+
+    let samplesData = samples.data;
+    let stacksData = stackTable.data;
+
+    // Caches.
+    let inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable);
+    let leafTable = Object.create(null);
+
+    let startTime = options.startTime || 0
+    let endTime = options.endTime || Infinity;
+    let flattenRecursion = options.flattenRecursion;
+
+    // Take the timestamp of the first sample as prevSampleTime. 0 is
+    // incorrect due to circular buffer wraparound. If wraparound happens,
+    // then the first sample will have an incorrect, large duration.
+    let prevSampleTime = samplesData[0][SAMPLE_TIME_SLOT];
+
+    // Reused options object passed to InflatedFrame.prototype.getFrameKey.
+    let mutableFrameKeyOptions = {
+      contentOnly: options.contentOnly,
+      isRoot: false,
+      isLeaf: false,
+      isMetaCategoryOut: false
+    };
+
+    // Start iteration at the second sample, as we use the first sample to
+    // compute prevSampleTime.
+    for (let i = 1; i < samplesData.length; i++) {
+      let sample = samplesData[i];
+      let sampleTime = sample[SAMPLE_TIME_SLOT];
+
+      // A sample's end time is considered to be its time of sampling. Its
+      // start time is the sampling time of the previous sample.
+      //
+      // Thus, we compare sampleTime <= start instead of < to filter out
+      // samples that end exactly at the start time.
+      if (!sampleTime || sampleTime <= startTime || sampleTime > endTime) {
+        prevSampleTime = sampleTime;
+        continue;
+      }
+
+      let sampleDuration = sampleTime - prevSampleTime;
+      let stackIndex = sample[SAMPLE_STACK_SLOT];
+      let calls = this.calls;
+      let prevCalls = this.calls;
+      let prevFrameKey;
+      let isLeaf = mutableFrameKeyOptions.isLeaf = true;
 
-    if (!options.invertTree) {
-      // Remove the (root) node if the tree is not inverted: we will synthesize
-      // our own root in the view. However, for inverted trees, we wish to be
-      // able to differentiate between (root)->A->B->C and (root)->B->C stacks,
-      // so we need the root node in that case.
-      sampleFrames = sampleFrames.slice(1);
+      // Inflate the stack and build the FrameNode call tree directly.
+      //
+      // In the profiler data, each frame's stack is referenced by an index
+      // into stackTable.
+      //
+      // Each entry in stackTable is a pair [ prefixIndex, frameIndex ]. The
+      // prefixIndex is itself an index into stackTable, referencing the
+      // prefix of the current stack (that is, the younger frames). In other
+      // words, the stackTable is encoded as a trie of the inverted
+      // callstack. The frameIndex is an index into frameTable, describing the
+      // frame at the current depth.
+      //
+      // This algorithm inflates each frame in the frame table while walking
+      // the stack trie as described above.
+      //
+      // The frame key is then computed from the inflated frame /and/ the
+      // current depth in the FrameNode call tree.  That is, the frame key is
+      // not wholly determinable from just the inflated frame.
+      //
+      // For content frames, the frame key is just its location. For chrome
+      // frames, the key may be a metacategory or its location, depending on
+      // rendering options and its position in the FrameNode call tree.
+      //
+      // The frame key is then used to build up the inverted FrameNode call
+      // tree.
+      //
+      // Note that various filtering functions, such as filtering for content
+      // frames or flattening recursion, are inlined into the stack inflation
+      // loop. This is important for performance as it avoids intermediate
+      // structures and multiple passes.
+      while (stackIndex !== null) {
+        let stackEntry = stacksData[stackIndex];
+        let frameIndex = stackEntry[STACK_FRAME_SLOT];
+
+        // Fetch the stack prefix (i.e. older frames) index.
+        stackIndex = stackEntry[STACK_PREFIX_SLOT];
+
+        // Inflate the frame.
+        let inflatedFrame = getOrAddInflatedFrame(inflatedFrameCache, frameIndex,
+                                                  frameTable, stringTable);
+
+        // Compute the frame key.
+        mutableFrameKeyOptions.isRoot = stackIndex === null;
+        let frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
+
+        // Leaf frames are never skipped and require self count and duration
+        // bookkeeping.
+        if (isLeaf) {
+          // Tabulate self count and duration for the leaf frame. The frameKey
+          // is never empty for a leaf frame.
+          if (selfCount[frameKey] === undefined) {
+            selfCount[frameKey] = 0;
+            selfDuration[frameKey] = 0;
+          }
+          selfCount[frameKey]++;
+          selfDuration[frameKey] += sampleDuration;
+        } else {
+          // An empty frame key means this frame should be skipped.
+          if (frameKey === "") {
+            continue;
+          }
+        }
+
+        // If we shouldn't flatten the current frame into the previous one, advance a
+        // level in the call tree.
+        if (!flattenRecursion || frameKey !== prevFrameKey) {
+          calls = prevCalls;
+        }
+
+        let frameNode = getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame,
+                                          mutableFrameKeyOptions.isMetaCategoryOut,
+                                          leafTable);
+
+        frameNode._countSample(prevSampleTime, sampleTime, inflatedFrame.optimizations,
+                               stringTable);
+
+        prevFrameKey = frameKey;
+        prevCalls = frameNode.calls;
+        isLeaf = mutableFrameKeyOptions.isLeaf = false;
+      }
+
+      this.duration += sampleDuration;
+      this.samples++;
+      prevSampleTime = sampleTime;
     }
+  },
 
-    // Filter out platform frames if only content-related function calls
-    // should be taken into consideration.
-    if (options.contentOnly) {
-      sampleFrames = FrameUtils.filterPlatformData(sampleFrames);
+  /**
+   * Uninverts the call tree after its having been built.
+   */
+  _uninvert: function uninvert() {
+    function mergeOrAddFrameNode(calls, node) {
+      // Unlike the inverted call tree, we don't use a root table for the top
+      // level, as in general, there are many fewer entry points than
+      // leaves. Instead, linear search is used regardless of level.
+      for (let i = 0; i < calls.length; i++) {
+        if (calls[i].key === node.key) {
+          let foundNode = calls[i];
+          foundNode._merge(node);
+          return foundNode.calls;
+        }
+      }
+      let copy = node._clone();
+      calls.push(copy);
+      return copy.calls;
     }
 
-    // If no frames remain after filtering, then this is a leaf node, no need
-    // to continue.
-    if (!sampleFrames.length) {
-      return;
-    }
-    // Invert the tree after filtering, if preferred.
-    if (options.invertTree) {
-      sampleFrames.reverse();
+    let workstack = [{ node: this, level: 0 }];
+    let spine = [];
+    let entry;
+
+    // The new root.
+    let rootCalls = [];
+
+    // Walk depth-first and keep the current spine (e.g., callstack).
+    while (entry = workstack.pop()) {
+      spine[entry.level] = entry;
+
+      let node = entry.node;
+      let calls = node.calls;
+
+      if (calls.length === 0) {
+        // We've bottomed out. Reverse the spine and add them to the
+        // uninverted call tree.
+        let uninvertedCalls = rootCalls;
+        for (let level = entry.level; level > 0; level--) {
+          let callee = spine[level];
+          uninvertedCalls = mergeOrAddFrameNode(uninvertedCalls, callee.node);
+        }
+      } else {
+        // We still have children. Continue the depth-first walk.
+        for (let i = 0; i < calls.length; i++) {
+          workstack.push({ node: calls[i], level: entry.level + 1 });
+        }
+      }
     }
 
-    let sampleDuration = sampleTime - this._previousSampleTime;
-    this._previousSampleTime = sampleTime;
-    this.samples++;
-    this.duration += sampleDuration;
-
-    FrameNode.prototype.insert(
-      sampleFrames, optimizations, 0, sampleTime, sampleDuration, this.calls);
+    // Replace the toplevel calls with rootCalls, which now contains the
+    // uninverted roots.
+    this.calls = rootCalls;
   },
 
   /**
    * Gets additional details about this node.
    * @return object
    */
   getInfo: function() {
     return {
@@ -150,86 +343,104 @@ ThreadNode.prototype = {
   hasOptimizations: function () {
     return null;
   }
 };
 
 /**
  * A function call node in a tree.
  *
+ * @param string frameKey
+ *        The key associated with this frame. The key determines identity of
+ *        the node.
  * @param string location
  *        The location of this function call. Note that this isn't sanitized,
  *        so it may very well (not?) include the function name, url, etc.
  * @param number line
  *        The line number inside the source containing this function call.
- * @param number column
- *        The column number inside the source containing this function call.
  * @param number category
  *        The category type of this function call ("js", "graphics" etc.).
  * @param number allocations
  *        The number of memory allocations performed in this frame.
+ * @param number isContent
+ *        Whether this frame is content.
  * @param boolean isMetaCategory
  *        Whether or not this is a platform node that should appear as a
  *        generalized meta category or not.
  */
-function FrameNode({ location, line, column, category, allocations, isMetaCategory }) {
+function FrameNode(frameKey, { location, line, category, allocations, isContent }, isMetaCategory) {
+  this.key = frameKey;
   this.location = location;
   this.line = line;
-  this.column = column;
   this.category = category;
-  this.allocations = allocations || 0;
-  this.sampleTimes = [];
+  this.allocations = 0;
   this.samples = 0;
   this.duration = 0;
-  this.calls = {};
+  this.calls = [];
+  this.isContent = isContent;
   this._optimizations = null;
+  this._stringTable = null;
   this.isMetaCategory = isMetaCategory;
 }
 
 FrameNode.prototype = {
   /**
-   * Adds function calls in the tree from a sample's frames. For example, given
-   * the the frames below (which would account for three calls to `insert` on
-   * the root frame), the following tree structure is created:
+   * Count a sample as associated with this node.
    *
-   *                          A
-   *   A -> B -> C           / \
-   *   A -> B -> D    ~>    B   E
-   *   A -> E -> F         / \   \
-   *                      C   D   F
-   * @param frames
-   *        The sample call stack.
-   * @param optimizations
-   *        The array of indexable optimizations.
-   * @param index
-   *        The index of the call in the stack representing this node.
-   * @param number time
-   *        The delta time (in milliseconds) when the frame was sampled.
-   * @param number duration
-   *        The amount of time spent executing all functions on the stack.
+   * @param number prevSampleTime
+   *               The time when the immediate previous sample was sampled.
+   * @param number sampleTime
+   *               The time when the current sample was sampled.
+   * @param object optimizationSite
+   *               Any JIT optimization information attached to the current
+   *               sample. Lazily inflated via stringTable.
+   * @param object stringTable
+   *               The string table used to inflate the optimizationSite.
    */
-  insert: function(frames, optimizations, index, time, duration, _store = this.calls) {
-    let frame = frames[index];
-    if (!frame) {
+  _countSample: function (prevSampleTime, sampleTime, optimizationSite, stringTable) {
+    this.samples++;
+    this.duration += sampleTime - prevSampleTime;
+
+    // Simply accumulate optimization sites for now. Processing is done lazily
+    // by JITOptimizations, if optimization information is actually displayed.
+    if (optimizationSite) {
+      let opts = this._optimizations;
+      if (opts === null) {
+        opts = this._optimizations = [];
+        this._stringTable = stringTable;
+      }
+      opts.push(optimizationSite);
+    }
+  },
+
+  _clone: function () {
+    let newNode = new FrameNode(this.key, this, this.isMetaCategory);
+    newNode._merge(this);
+    return newNode;
+  },
+
+  _merge: function (otherNode) {
+    if (this === otherNode) {
       return;
     }
-    // If we are only displaying content, then platform data will have
-    // a `isMetaCategory` property. Group by category (GC, Graphics, etc.)
-    // to group together frames so they're displayed only once, since we don't
-    // need the location anyway.
-    let key = frame.isMetaCategory ? frame.category : frame.location;
-    let child = _store[key] || (_store[key] = new FrameNode(frame));
-    child.sampleTimes.push({ start: time, end: time + duration });
-    child.samples++;
-    child.duration += duration;
-    if (optimizations && frame.optsIndex != null) {
-      let opts = child._optimizations || (child._optimizations = new JITOptimizations(optimizations));
-      opts.addOptimizationSite(frame.optsIndex);
+
+    this.samples += otherNode.samples;
+    this.duration += otherNode.duration;
+
+    if (otherNode._optimizations) {
+      let opts = this._optimizations;
+      if (opts === null) {
+        opts = this._optimizations = [];
+        this._stringTable = otherNode._stringTable;
+      }
+      let otherOpts = otherNode._optimizations;
+      for (let i = 0; i < otherOpts.length; i++) {
+        opts.push(otherOpts[i]);
+      }
     }
-    child.insert(frames, optimizations, index + 1, time, duration);
   },
 
   /**
    * Returns the parsed location and additional data describing
    * this frame. Uses cached data if possible.
    *
    * @return object
    *         The computed { name, file, url, line } properties for this
@@ -244,24 +455,28 @@ FrameNode.prototype = {
    * function name and source url.
    */
   _computeInfo: function() {
     // "EnterJIT" pseudoframes are special, not actually on the stack.
     if (this.location == "EnterJIT") {
       this.category = CATEGORY_JIT;
     }
 
+    if (this.isMetaCategory && !this.category) {
+      this.category = CATEGORY_OTHER;
+    }
+
     // Since only C++ stack frames have associated category information,
     // default to an "unknown" category otherwise.
     let categoryData = CATEGORY_MAPPINGS[this.category] || {};
 
-    let parsedData = FrameUtils.parseLocation(this);
+    let parsedData = FrameUtils.parseLocation(this.location, this.line, this.column);
     parsedData.nodeType = "Frame";
     parsedData.categoryData = categoryData;
-    parsedData.isContent = FrameUtils.isContent(this);
+    parsedData.isContent = this.isContent;
     parsedData.isMetaCategory = this.isMetaCategory;
 
     return this._data = parsedData;
   },
 
   /**
    * Returns whether or not the frame node has an JITOptimizations model.
    *
@@ -273,11 +488,14 @@ FrameNode.prototype = {
 
   /**
    * Returns the underlying JITOptimizations model representing
    * the optimization attempts occuring in this frame.
    *
    * @return {JITOptimizations|null}
    */
   getOptimizations: function () {
-    return this._optimizations;
+    if (!this._optimizations) {
+      return null;
+    }
+    return new JITOptimizations(this._optimizations, this._stringTable);
   }
 };
--- a/browser/devtools/shared/profiler/tree-view.js
+++ b/browser/devtools/shared/profiler/tree-view.js
@@ -120,45 +120,33 @@ CallView.prototype = Heritage.extend(Abs
 
     let frameInfo = this.frame.getInfo();
     let framePercentage = this._getPercentage(this.frame.samples);
 
     let selfPercentage;
     let selfDuration;
     let totalAllocations;
 
-    if (!this._getChildCalls().length) {
-      if (this.visibleCells.selfPercentage) {
-        selfPercentage = framePercentage;
-      }
-      if (this.visibleCells.selfDuration) {
-        selfDuration = this.frame.duration;
-      }
+    let frameKey = this.frame.key;
+    if (this.visibleCells.selfPercentage) {
+      selfPercentage = this._getPercentage(this.root.frame.selfCount[frameKey]);
+    }
+    if (this.visibleCells.selfDuration) {
+      selfDuration = this.root.frame.selfDuration[frameKey];
+    }
+
+    if (!this.frame.calls.length) {
       if (this.visibleCells.allocations) {
         totalAllocations = this.frame.allocations;
       }
     } else {
-      // Avoid performing costly computations if the respective columns
-      // won't be shown anyway.
-      if (this.visibleCells.selfPercentage) {
-        let childrenPercentage = sum([this._getPercentage(c.samples) for (c of this._getChildCalls())]);
-        selfPercentage = clamp(framePercentage - childrenPercentage, 0, 100);
-      }
-      if (this.visibleCells.selfDuration) {
-        let childrenDuration = sum([c.duration for (c of this._getChildCalls())]);
-        selfDuration = this.frame.duration - childrenDuration;
-      }
       if (this.visibleCells.allocations) {
-        let childrenAllocations = sum([c.allocations for (c of this._getChildCalls())]);
+        let childrenAllocations = this.frame.calls.reduce((acc, node) => acc + node.allocations, 0);
         totalAllocations = this.frame.allocations + childrenAllocations;
       }
-      if (this.inverted) {
-        selfPercentage = framePercentage - selfPercentage;
-        selfDuration = this.frame.duration - selfDuration;
-      }
     }
 
     if (this.visibleCells.duration) {
       var durationCell = this._createTimeCell(this.frame.duration);
     }
     if (this.visibleCells.selfDuration) {
       var selfDurationCell = this._createTimeCell(selfDuration, true);
     }
@@ -227,31 +215,24 @@ CallView.prototype = Heritage.extend(Abs
   /**
    * Calculate what percentage of all samples the given number of samples is.
    */
   _getPercentage: function(samples) {
     return samples / this.root.frame.samples * 100;
   },
 
   /**
-   * Return an array of this frame's child calls.
-   */
-  _getChildCalls: function() {
-    return Object.keys(this.frame.calls).map(k => this.frame.calls[k]);
-  },
-
-  /**
    * Populates this node in the call tree with the corresponding "callees".
    * These are defined in the `frame` data source for this call view.
    * @param array:AbstractTreeItem children
    */
   _populateSelf: function(children) {
     let newLevel = this.level + 1;
 
-    for (let newFrame of this._getChildCalls()) {
+    for (let newFrame of this.frame.calls) {
       children.push(new CallView({
         caller: this,
         frame: newFrame,
         level: newLevel,
         inverted: this.inverted
       }));
     }
 
--- a/browser/devtools/shared/widgets/FlameGraph.js
+++ b/browser/devtools/shared/widgets/FlameGraph.js
@@ -6,16 +6,19 @@
 const { ViewHelpers } = require("resource:///modules/devtools/ViewHelpers.jsm");
 const { AbstractCanvasGraph, GraphArea, GraphAreaDragger } = require("resource:///modules/devtools/Graphs.jsm");
 const { Promise } = require("resource://gre/modules/Promise.jsm");
 const { Task } = require("resource://gre/modules/Task.jsm");
 const { getColor } = require("devtools/shared/theme");
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const FrameUtils = require("devtools/shared/profiler/frame-utils");
 
+loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
+  "devtools/shared/profiler/global", true);
+
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
 const L10N = new ViewHelpers.L10N();
 
 const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
 
 const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00035;
 const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.5;
@@ -45,17 +48,17 @@ const FLAME_GRAPH_BLOCK_TEXT_PADDING_RIG
 /**
  * A flamegraph visualization. This implementation is responsable only with
  * drawing the graph, using a data source consisting of rectangles and
  * their corresponding widths.
  *
  * Example usage:
  *   let graph = new FlameGraph(node);
  *   graph.once("ready", () => {
- *     let data = FlameGraphUtils.createFlameGraphDataFromSamples(samples);
+ *     let data = FlameGraphUtils.createFlameGraphDataFromThread(thread);
  *     let bounds = { startTime, endTime };
  *     graph.setData({ data, bounds });
  *   });
  *
  * Data source format:
  *   [
  *     {
  *       color: "string",
@@ -987,157 +990,218 @@ const COLOR_PALLETTE = Array.from(Array(
 /**
  * A collection of utility functions converting various data sources
  * into a format drawable by the FlameGraph.
  */
 let FlameGraphUtils = {
   _cache: new WeakMap(),
 
   /**
-   * Converts a list of samples from the profiler data to something that's
-   * drawable by a FlameGraph widget.
-   *
-   * The outputted data will be cached, so the next time this method is called
-   * the previous output is returned. If this is undesirable, or should the
-   * options change, use `removeFromCache`.
+   * Create data suitable for use with FlameGraph from a profile's samples.
+   * Iterate the profile's samples and keep a moving window of stack traces.
    *
-   * @param array samples
-   *        A list of { time, frames: [{ location }] } objects.
-   * @param object options [optional]
-   *        Additional options supported by this operation:
-   *          - invertStack: specifies if the frames array in every sample
-   *                         should be reversed
-   *          - flattenRecursion: specifies if identical consecutive frames
-   *                              should be omitted from the output
-   *          - filterFrames: predicate used for filtering all frames, passing
-   *                          in each frame, its index and the sample array
-   *          - showIdleBlocks: adds "idle" blocks when no frames are available
-   *                            using the provided localized text
-   * @param array out [optional]
-   *        An output storage to reuse for storing the flame graph data.
-   * @return array
-   *         The flame graph data.
+   * @param object thread
+   *               The raw thread object received from the backend.
+   * @param object options
+   *               Additional supported options,
+   *                 - boolean contentOnly [optional]
+   *                 - boolean invertTree [optional]
+   *                 - boolean flattenRecursion [optional]
+   *                 - string showIdleBlocks [optional]
+   * @return object
+   *         Data source usable by FlameGraph.
    */
-  createFlameGraphDataFromSamples: function(samples, options = {}, out = []) {
-    let cached = this._cache.get(samples);
+  createFlameGraphDataFromThread: function(thread, options = {}, out = []) {
+    let cached = this._cache.get(thread);
     if (cached) {
       return cached;
     }
 
     // 1. Create a map of colors to arrays, representing buckets of
     // blocks inside the flame graph pyramid sharing the same style.
 
-    let buckets = new Map();
-
-    for (let color of COLOR_PALLETTE) {
-      buckets.set(color, []);
-    }
+    let buckets = Array.from({ length: PALLETTE_SIZE }, () => []);
 
     // 2. Populate the buckets by iterating over every frame in every sample.
 
-    let prevTime = 0;
+    let { samples, stackTable, frameTable, stringTable } = thread;
+
+    const SAMPLE_STACK_SLOT = samples.schema.stack;
+    const SAMPLE_TIME_SLOT = samples.schema.time;
+
+    const STACK_PREFIX_SLOT = stackTable.schema.prefix;
+    const STACK_FRAME_SLOT = stackTable.schema.frame;
+
+    const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame;
+
+    let inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable);
+    let labelCache = Object.create(null);
+
+    let samplesData = samples.data;
+    let stacksData = stackTable.data;
+
+    let flattenRecursion = options.flattenRecursion;
+
+    // Reused objects.
+    let mutableFrameKeyOptions = {
+      contentOnly: options.contentOnly,
+      isRoot: false,
+      isLeaf: false,
+      isMetaCategoryOut: false
+    };
+
+    // Take the timestamp of the first sample as prevTime. 0 is incorrect due
+    // to circular buffer wraparound. If wraparound happens, then the first
+    // sample will have an incorrect, large duration.
+    let prevTime = samplesData.length > 0 ? samplesData[0][SAMPLE_TIME_SLOT] : 0;
     let prevFrames = [];
+    let sampleFrames = [];
+    let sampleFrameKeys = [];
+
+    for (let i = 1; i < samplesData.length; i++) {
+      let sample = samplesData[i];
+      let time = sample[SAMPLE_TIME_SLOT];
+
+      let stackIndex = sample[SAMPLE_STACK_SLOT];
+      let prevFrameKey;
+
+      let stackDepth = 0;
 
-    for (let { frames, time } of samples) {
-      let frameIndex = 0;
+      // Inflate the stack and keep a moving window of call stacks.
+      //
+      // For reference, see the similar block comment in
+      // ThreadNode.prototype._buildInverted.
+      //
+      // In a similar fashion to _buildInverted, frames are inflated on the
+      // fly while stackwalking the stackTable trie. The exact same frame key
+      // is computed in both _buildInverted and here.
+      //
+      // Unlike _buildInverted, which builds a call tree directly, the flame
+      // graph inflates the stack into an array, as it maintains a moving
+      // window of stacks over time.
+      //
+      // Like _buildInverted, the various filtering functions are also inlined
+      // into stack inflation loop.
+      while (stackIndex !== null) {
+        let stackEntry = stacksData[stackIndex];
+        let frameIndex = stackEntry[STACK_FRAME_SLOT];
+
+        // Fetch the stack prefix (i.e. older frames) index.
+        stackIndex = stackEntry[STACK_PREFIX_SLOT];
 
-      // Flatten recursion if preferred, by removing consecutive frames
-      // sharing the same location.
-      if (options.flattenRecursion) {
-        frames = frames.filter(this._isConsecutiveDuplicate);
+        // Inflate the frame.
+        let inflatedFrame = getOrAddInflatedFrame(inflatedFrameCache, frameIndex,
+                                                  frameTable, stringTable);
+
+        mutableFrameKeyOptions.isRoot = stackIndex === null;
+        mutableFrameKeyOptions.isLeaf = stackDepth === 0;
+        let frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
+
+        // If not skipping the frame, add it to the current level. The (root)
+        // node isn't useful for flame graphs.
+        if (frameKey !== "" && frameKey !== "(root)") {
+          // If the frame is a meta category, use the category label.
+          if (mutableFrameKeyOptions.isMetaCategoryOut) {
+            frameKey = CATEGORY_MAPPINGS[frameKey].label;
+          }
+
+          sampleFrames[stackDepth] = inflatedFrame;
+          sampleFrameKeys[stackDepth] = frameKey;
+
+          // If we shouldn't flatten the current frame into the previous one,
+          // increment the stack depth.
+          if (!flattenRecursion || frameKey !== prevFrameKey) {
+            stackDepth++;
+          }
+
+          prevFrameKey = frameKey;
+        }
       }
 
-      // Apply a provided filter function. This can be used, for example, to
-      // filter out platform frames if only content-related function calls
-      // should be taken into consideration.
-      if (options.filterFrames) {
-        frames = frames.filter(options.filterFrames);
-      }
-
-      // Invert the stack if preferred, reversing the frames array in place.
-      if (options.invertStack) {
-        frames.reverse();
+      // Uninvert frames in place if needed.
+      if (!options.invertTree) {
+        sampleFrames.length = stackDepth;
+        sampleFrames.reverse();
+        sampleFrameKeys.length = stackDepth;
+        sampleFrameKeys.reverse();
       }
 
       // If no frames are available, add a pseudo "idle" block in between.
-      if (options.showIdleBlocks && frames.length == 0) {
-        frames = [{ location: options.showIdleBlocks || "", idle: true }];
+      let isIdleFrame = false;
+      if (options.showIdleBlocks && stackDepth === 0) {
+        sampleFrames[0] = null;
+        sampleFrameKeys[0] = options.showIdleBlocks;
+        stackDepth = 1;
+        isIdleFrame = true;
       }
 
-      for (let frame of frames) {
-        let { location } = frame;
+      // Put each frame in a bucket.
+      for (let frameIndex = 0; frameIndex < stackDepth; frameIndex++) {
+        let key = sampleFrameKeys[frameIndex];
         let prevFrame = prevFrames[frameIndex];
 
         // Frames at the same location and the same depth will be reused.
         // If there is a block already created, change its width.
-        if (prevFrame && prevFrame.srcData.rawLocation == location) {
-          prevFrame.width = (time - prevFrame.srcData.startTime);
+        if (prevFrame && prevFrame.frameKey === key) {
+          prevFrame.width = (time - prevFrame.startTime);
         }
         // Otherwise, create a new block for this frame at this depth,
         // using a simple location based salt for picking a color.
         else {
-          let hash = this._getStringHash(location);
-          let color = COLOR_PALLETTE[hash % PALLETTE_SIZE];
-          let bucket = buckets.get(color);
+          let hash = this._getStringHash(key);
+          let bucket = buckets[hash % PALLETTE_SIZE];
+
+          let label;
+          if (isIdleFrame) {
+            label = key;
+          } else {
+            label = labelCache[key];
+            if (!label) {
+              label = labelCache[key] = this._formatLabel(key, sampleFrames[frameIndex]);
+            }
+          }
 
           bucket.push(prevFrames[frameIndex] = {
-            srcData: { startTime: prevTime, rawLocation: location },
+            startTime: prevTime,
+            frameKey: key,
             x: prevTime,
             y: frameIndex * FLAME_GRAPH_BLOCK_HEIGHT,
             width: time - prevTime,
             height: FLAME_GRAPH_BLOCK_HEIGHT,
-            text: this._formatLabel(frame)
+            text: label
           });
         }
-
-        frameIndex++;
       }
 
       // Previous frames at stack depths greater than the current sample's
       // maximum need to be nullified. It's nonsensical to reuse them.
-      prevFrames.length = frameIndex;
+      prevFrames.length = stackDepth;
       prevTime = time;
     }
 
     // 3. Convert the buckets into a data source usable by the FlameGraph.
     // This is a simple conversion from a Map to an Array.
 
-    for (let [color, blocks] of buckets) {
-      out.push({ color, blocks });
+    for (let i = 0; i < buckets.length; i++) {
+      out.push({ color: COLOR_PALLETTE[i], blocks: buckets[i] });
     }
 
-    this._cache.set(samples, out);
+    this._cache.set(thread, out);
     return out;
   },
 
   /**
    * Clears the cached flame graph data created for the given source.
    * @param any source
    */
   removeFromCache: function(source) {
     this._cache.delete(source);
   },
 
   /**
-   * Checks if the provided frame is the same as the next one in a sample.
-   *
-   * @param object e
-   *        An object containing a { location } property.
-   * @param number index
-   *        The index of the object in the parent array.
-   * @param array array
-   *        The parent array.
-   * @return boolean
-   *         True if the next frame shares the same location, false otherwise.
-   */
-  _isConsecutiveDuplicate: function(e, index, array) {
-    return index < array.length - 1 && e.location != array[index + 1].location;
-  },
-
-  /**
    * Very dumb hashing of a string. Used to pick colors from a pallette.
    *
    * @param string input
    * @return number
    */
   _getStringHash: function(input) {
     const STRING_HASH_PRIME1 = 7;
     const STRING_HASH_PRIME2 = 31;
@@ -1152,30 +1216,25 @@ let FlameGraphUtils = {
         return hash;
       }
     }
 
     return hash;
   },
 
   /**
-   * Takes a FrameNode and returns a string that should be displayed
-   * in its flame block.
+   * Takes a frame key and a frame, and returns a string that should be
+   * displayed in its flame block.
    *
-   * @param FrameNode frame
+   * @param string key
+   * @param object frame
    * @return string
    */
-  _formatLabel: function (frame) {
-    // If an idle block, just return the location which will just be "(idle)" text
-    // anyway.
-    if (frame.idle) {
-      return frame.location;
-    }
-
-    let { functionName, fileName, line } = FrameUtils.parseLocation(frame);
+  _formatLabel: function (key, frame) {
+    let { functionName, fileName, line } = FrameUtils.parseLocation(key, frame.line);
     let label = functionName;
 
     if (fileName) {
       label += ` (${fileName}${line != null ? (":" + line) : ""})`;
     }
 
     return label;
   }