Bug 1154115 - Make the performance devtool handle the new profiler JSON format. (r=jsantell,vporof)
authorShu-yu Guo <shu@rfrn.org>
Mon, 11 May 2015 14:16:44 -0700
changeset 243415 f98738059b8e477f9d884b66b02d60334330bf1d
parent 243414 78b5f6585c910dff5e280bb80f89cf32756dc30b
child 243416 4756cdcc1f2fc28cc1ae14596d9baaf41675353b
push id28738
push usercbook@mozilla.com
push dateTue, 12 May 2015 14:11:31 +0000
treeherdermozilla-central@bedce1b405a3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell, vporof
bugs1154115
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1154115 - Make the performance devtool handle the new profiler JSON format. (r=jsantell,vporof)
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;
   }