Bug 1150299 - Show optimization graph in the call tree sidebar. r=shu,vp
authorJordan Santell <jsantell@mozilla.com>
Mon, 31 Aug 2015 15:26:02 -0700
changeset 262459 d81b35757962722421c4cd36dc88cf2a0eee0d7d
parent 262458 e543c88468c04cb43c926a6bb2631a59b47e7e04
child 262460 e6f7943f32fca2cd6b332d3c717d54488d973e32
push id15181
push userjsantell@mozilla.com
push dateTue, 15 Sep 2015 00:30:33 +0000
treeherderfx-team@d81b35757962 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersshu, vp
bugs1150299
milestone43.0a1
Bug 1150299 - Show optimization graph in the call tree sidebar. r=shu,vp
browser/devtools/performance/modules/logic/frame-utils.js
browser/devtools/performance/modules/logic/jit.js
browser/devtools/performance/modules/logic/tree-model.js
browser/devtools/performance/modules/widgets/graphs.js
browser/devtools/performance/performance-controller.js
browser/devtools/performance/performance.xul
browser/devtools/performance/test/unit/test_jit-graph-data.js
browser/devtools/performance/views/details-js-call-tree.js
browser/devtools/performance/views/optimizations-list.js
browser/devtools/shared/widgets/MountainGraphWidget.js
browser/themes/shared/devtools/performance.css
--- a/browser/devtools/performance/modules/logic/frame-utils.js
+++ b/browser/devtools/performance/modules/logic/frame-utils.js
@@ -549,13 +549,37 @@ function getFrameInfo (node, options) {
     data.totalSizePercentage = node.byteSize / totalBytes * 100;
     data.ALLOCATION_DATA_CALCULATED = true;
   }
 
   return data;
 }
 
 exports.getFrameInfo = getFrameInfo;
+
+/**
+ * Takes an inverted ThreadNode and searches its youngest frames for
+ * a FrameNode with matching location.
+ *
+ * @param {ThreadNode} threadNode
+ * @param {string} location
+ * @return {?FrameNode}
+ */
+function findFrameByLocation (threadNode, location) {
+  if (!threadNode.inverted) {
+    throw new Error("FrameUtils.findFrameByLocation only supports leaf nodes in an inverted tree.");
+  }
+
+  let calls = threadNode.calls;
+  for (let i = 0; i < calls.length; i++) {
+    if (calls[i].location === location) {
+      return calls[i];
+    }
+  }
+  return null;
+}
+
+exports.findFrameByLocation = findFrameByLocation;
 exports.computeIsContentAndCategory = computeIsContentAndCategory;
 exports.parseLocation = parseLocation;
 exports.getInflatedFrameCache = getInflatedFrameCache;
 exports.getOrAddInflatedFrame = getOrAddInflatedFrame;
 exports.InflatedFrame = InflatedFrame;
--- a/browser/devtools/performance/modules/logic/jit.js
+++ b/browser/devtools/performance/modules/logic/jit.js
@@ -264,70 +264,72 @@ const IMPLEMENTATION_NAMES = Object.keys
  * Takes data from a FrameNode and computes rendering positions for
  * a stacked mountain graph, to visualize JIT optimization tiers over time.
  *
  * @param {FrameNode} frameNode
  *                    The FrameNode who's optimizations we're iterating.
  * @param {Array<number>} sampleTimes
  *                        An array of every sample time within the range we're counting.
  *                        From a ThreadNode's `sampleTimes` property.
- * @param {number} op.startTime
- *                 The start time of the first sample.
- * @param {number} op.endTime
- *                 The end time of the last sample.
- * @param {number} op.resolution
- *                 The maximum amount of possible data points returned.
- *                 Also determines the size in milliseconds of each bucket
- *                 via `(endTime - startTime) / resolution`
+ * @param {number} bucketSize
+ *                 Size of each bucket in milliseconds.
+ *                 `duration / resolution = bucketSize` in OptimizationsGraph.
  * @return {?Array<object>}
  */
-function createTierGraphDataFromFrameNode (frameNode, sampleTimes, { startTime, endTime, resolution }) {
-  if (!frameNode.hasOptimizations()) {
-    return;
-  }
-
-  let tierData = frameNode.getOptimizationTierData();
-  let duration = endTime - startTime;
+function createTierGraphDataFromFrameNode (frameNode, sampleTimes, bucketSize) {
+  let tierData = frameNode.getTierData();
   let stringTable = frameNode._stringTable;
   let output = [];
   let implEnum;
 
   let tierDataIndex = 0;
   let nextOptSample = tierData[tierDataIndex];
 
   // Bucket data
   let samplesInCurrentBucket = 0;
   let currentBucketStartTime = sampleTimes[0];
   let bucket = [];
-  // Size of each bucket in milliseconds
-  let bucketSize = Math.ceil(duration / resolution);
+
+  // Store previous data point so we can have straight vertical lines
+  let previousValues;
 
   // Iterate one after the samples, so we can finalize the last bucket
   for (let i = 0; i <= sampleTimes.length; i++) {
     let sampleTime = sampleTimes[i];
 
     // If this sample is in the next bucket, or we're done
     // checking sampleTimes and on the last iteration, finalize previous bucket
     if (sampleTime >= (currentBucketStartTime + bucketSize) ||
         i >= sampleTimes.length) {
 
       let dataPoint = {};
-      dataPoint.ys = [];
-      dataPoint.x = currentBucketStartTime;
+      dataPoint.values = [];
+      dataPoint.delta = currentBucketStartTime;
 
       // Map the opt site counts as a normalized percentage (0-1)
       // of its count in context of total samples this bucket
       for (let j = 0; j < IMPLEMENTATION_NAMES.length; j++) {
-        dataPoint.ys[j] = (bucket[j] || 0) / (samplesInCurrentBucket || 1);
+        dataPoint.values[j] = (bucket[j] || 0) / (samplesInCurrentBucket || 1);
       }
+
+      // Push the values from the previous bucket to the same time
+      // as the current bucket so we get a straight vertical line.
+      if (previousValues) {
+        let data = Object.create(null);
+        data.values = previousValues;
+        data.delta = currentBucketStartTime;
+        output.push(data);
+      }
+
       output.push(dataPoint);
 
       // Set the new start time of this bucket and reset its count
       currentBucketStartTime += bucketSize;
       samplesInCurrentBucket = 0;
+      previousValues = dataPoint.values;
       bucket = [];
     }
 
     // If this sample observed an optimization in this frame, record it
     if (nextOptSample && nextOptSample.time === sampleTime) {
       // If no implementation defined, it was the "interpreter".
       implEnum = IMPLEMENTATION_MAP[stringTable[nextOptSample.implementation] || "interpreter"];
       bucket[implEnum] = (bucket[implEnum] || 0) + 1;
--- a/browser/devtools/performance/modules/logic/tree-model.js
+++ b/browser/devtools/performance/modules/logic/tree-model.js
@@ -31,16 +31,18 @@ function ThreadNode(thread, options = {}
     throw new Error("ThreadNode requires both `startTime` and `endTime`.");
   }
   this.samples = 0;
   this.sampleTimes = [];
   this.youngestFrameSamples = 0;
   this.calls = [];
   this.duration = options.endTime - options.startTime;
   this.nodeType = "Thread";
+  this.inverted = options.invertTree;
+
   // Total bytesize of all allocations if enabled
   this.byteSize = 0;
   this.youngestFrameByteSize = 0;
 
   let { samples, stackTable, frameTable, stringTable } = thread;
 
   // Nothing to do if there are no samples.
   if (samples.data.length === 0) {
@@ -227,20 +229,18 @@ ThreadNode.prototype = {
           calls = prevCalls;
         }
 
         let frameNode = getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame,
                                           mutableFrameKeyOptions.isMetaCategoryOut,
                                           leafTable);
         if (isLeaf) {
           frameNode.youngestFrameSamples++;
-          if (inflatedFrame.optimizations) {
-            frameNode._addOptimizations(inflatedFrame.optimizations, inflatedFrame.implementation,
-                                        sampleTime, stringTable);
-          }
+          frameNode._addOptimizations(inflatedFrame.optimizations, inflatedFrame.implementation,
+                                      sampleTime, stringTable);
 
           if (byteSize) {
             frameNode.youngestFrameByteSize += byteSize;
           }
         }
 
         // Don't overcount flattened recursive frames.
         if (!shouldFlatten) {
@@ -405,57 +405,57 @@ function FrameNode(frameKey, { location,
   this.key = frameKey;
   this.location = location;
   this.line = line;
   this.youngestFrameSamples = 0;
   this.samples = 0;
   this.calls = [];
   this.isContent = !!isContent;
   this._optimizations = null;
-  this._tierData = null;
+  this._tierData = [];
   this._stringTable = null;
   this.isMetaCategory = !!isMetaCategory;
   this.category = category;
   this.nodeType = "Frame";
   this.byteSize = 0;
   this.youngestFrameByteSize = 0;
 }
 
 FrameNode.prototype = {
   /**
    * Take optimization data observed for this frame.
    *
    * @param object optimizationSite
    *               Any JIT optimization information attached to the current
    *               sample. Lazily inflated via stringTable.
    * @param number implementation
-   *               JIT implementation used for this observed frame (interpreter,
-   *               baseline, ion);
+   *               JIT implementation used for this observed frame (baseline, ion);
+   *               can be null indicating "interpreter"
    * @param number time
    *               The time this optimization occurred.
    * @param object stringTable
    *               The string table used to inflate the optimizationSite.
    */
   _addOptimizations: function (site, implementation, time, stringTable) {
     // Simply accumulate optimization sites for now. Processing is done lazily
     // by JITOptimizations, if optimization information is actually displayed.
     if (site) {
       let opts = this._optimizations;
       if (opts === null) {
         opts = this._optimizations = [];
-        this._stringTable = stringTable;
       }
       opts.push(site);
+    }
 
-      if (this._tierData === null) {
-        this._tierData = [];
-      }
-      // Record type of implementation used and the sample time
-      this._tierData.push({ implementation, time });
+    if (!this._stringTable) {
+      this._stringTable = stringTable;
     }
+
+    // Record type of implementation used and the sample time
+    this._tierData.push({ implementation, time });
   },
 
   _clone: function (samples, size) {
     let newNode = new FrameNode(this.key, this, this.isMetaCategory);
     newNode._merge(this, samples, size);
     return newNode;
   },
 
@@ -469,27 +469,39 @@ FrameNode.prototype = {
     if (otherNode.youngestFrameSamples > 0) {
       this.youngestFrameSamples += samples;
     }
 
     if (otherNode.youngestFrameByteSize > 0) {
       this.youngestFrameByteSize += otherNode.youngestFrameByteSize;
     }
 
+    if (this._stringTable === null) {
+      this._stringTable = otherNode._stringTable;
+    }
+
     if (otherNode._optimizations) {
+      if (!this._optimizations) {
+        this._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]);
+       opts.push(otherOpts[i]);
       }
     }
+
+    if (otherNode._tierData.length) {
+      let tierData = this._tierData;
+      let otherTierData = otherNode._tierData;
+      for (let i = 0; i < otherTierData.length; i++) {
+        tierData.push(otherTierData[i]);
+      }
+      tierData.sort((a, b) => a.time - b.time);
+    }
   },
 
   /**
    * Returns the parsed location and additional data describing
    * this frame. Uses cached data if possible. Takes the following
    * options:
    *
    * @param {ThreadNode} options.root
@@ -524,22 +536,19 @@ FrameNode.prototype = {
   getOptimizations: function () {
     if (!this._optimizations) {
       return null;
     }
     return new JITOptimizations(this._optimizations, this._stringTable);
   },
 
   /**
-   * Returns the optimization tiers used overtime.
+   * Returns the tiers used overtime.
    *
-   * @return {?Array<object>}
+   * @return {Array<object>}
    */
-  getOptimizationTierData: function () {
-    if (!this._tierData) {
-      return null;
-    }
+  getTierData: function () {
     return this._tierData;
   }
 };
 
 exports.ThreadNode = ThreadNode;
 exports.FrameNode = FrameNode;
--- a/browser/devtools/performance/modules/widgets/graphs.js
+++ b/browser/devtools/performance/modules/widgets/graphs.js
@@ -7,53 +7,61 @@
  * This file contains the base line graph that all Performance line graphs use.
  */
 
 const { Cc, Ci, Cu, Cr } = require("chrome");
 const { Task } = require("resource://gre/modules/Task.jsm");
 const { Heritage } = require("resource:///modules/devtools/ViewHelpers.jsm");
 const LineGraphWidget = require("devtools/shared/widgets/LineGraphWidget");
 const BarGraphWidget = require("devtools/shared/widgets/BarGraphWidget");
+const MountainGraphWidget = require("devtools/shared/widgets/MountainGraphWidget");
 const { CanvasGraphUtils } = require("devtools/shared/widgets/Graphs");
 
 loader.lazyRequireGetter(this, "promise");
 loader.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 
 loader.lazyRequireGetter(this, "colorUtils",
   "devtools/css-color", true);
 loader.lazyRequireGetter(this, "getColor",
   "devtools/shared/theme", true);
 loader.lazyRequireGetter(this, "ProfilerGlobal",
   "devtools/performance/global");
 loader.lazyRequireGetter(this, "L10N",
   "devtools/performance/global", true);
 loader.lazyRequireGetter(this, "MarkersOverview",
   "devtools/performance/markers-overview", true);
+loader.lazyRequireGetter(this, "createTierGraphDataFromFrameNode",
+  "devtools/performance/jit", true);
 
 /**
  * For line graphs
  */
 const HEIGHT = 35; // px
 const STROKE_WIDTH = 1; // px
 const DAMPEN_VALUES = 0.95;
 const CLIPHEAD_LINE_COLOR = "#666";
 const SELECTION_LINE_COLOR = "#555";
-const SELECTION_BACKGROUND_COLOR_NAME = "highlight-blue";
-const FRAMERATE_GRAPH_COLOR_NAME = "highlight-green";
-const MEMORY_GRAPH_COLOR_NAME = "highlight-blue";
+const SELECTION_BACKGROUND_COLOR_NAME = "graphs-blue";
+const FRAMERATE_GRAPH_COLOR_NAME = "graphs-green";
+const MEMORY_GRAPH_COLOR_NAME = "graphs-blue";
 
 /**
  * For timeline overview
  */
 const MARKERS_GRAPH_HEADER_HEIGHT = 14; // px
 const MARKERS_GRAPH_ROW_HEIGHT = 10; // px
 const MARKERS_GROUP_VERTICAL_PADDING = 4; // px
 
 /**
+ * For optimization graph
+ */
+const OPTIMIZATIONS_GRAPH_RESOLUTION = 100;
+
+/**
  * A base class for performance graphs to inherit from.
  *
  * @param nsIDOMNode parent
  *        The parent node holding the overview.
  * @param string metric
  *        The unit of measurement for this graph.
  */
 function PerformanceGraph(parent, metric) {
@@ -81,17 +89,17 @@ PerformanceGraph.prototype = Heritage.ex
 
   /**
    * Sets the theme via `theme` to either "light" or "dark",
    * and updates the internal styling to match. Requires a redraw
    * to see the effects.
    */
   setTheme: function (theme) {
     theme = theme || "light";
-    let mainColor = getColor(this.mainColor || "highlight-blue", theme);
+    let mainColor = getColor(this.mainColor || "graphs-blue", theme);
     this.backgroundColor = getColor("body-background", theme);
     this.strokeColor = mainColor;
     this.backgroundGradientStart = colorUtils.setAlpha(mainColor, 0.2);
     this.backgroundGradientEnd = colorUtils.setAlpha(mainColor, 0.2);
     this.selectionBackgroundColor = colorUtils.setAlpha(getColor(SELECTION_BACKGROUND_COLOR_NAME, theme), 0.25);
     this.selectionStripesColor = "rgba(255, 255, 255, 0.1)";
     this.maximumLineColor = colorUtils.setAlpha(mainColor, 0.4);
     this.averageLineColor = colorUtils.setAlpha(mainColor, 0.7);
@@ -420,12 +428,88 @@ GraphsController.prototype = {
       if (graph = yield this.isAvailable(graphName)) {
         enabled.push(graph);
       }
     }
     return this._enabledGraphs = enabled;
   }),
 };
 
+/**
+ * A base class for performance graphs to inherit from.
+ *
+ * @param nsIDOMNode parent
+ *        The parent node holding the overview.
+ * @param string metric
+ *        The unit of measurement for this graph.
+ */
+function OptimizationsGraph(parent) {
+  MountainGraphWidget.call(this, parent);
+  this.setTheme();
+}
+
+OptimizationsGraph.prototype = Heritage.extend(MountainGraphWidget.prototype, {
+
+  render: Task.async(function *(threadNode, frameNode) {
+    // Regardless if we draw or clear the graph, wait
+    // until it's ready.
+    yield this.ready();
+
+    if (!threadNode || !frameNode) {
+      this.setData([]);
+      return;
+    }
+
+    let { sampleTimes } = threadNode;
+
+    if (!sampleTimes.length) {
+      this.setData([]);
+      return;
+    }
+
+    // Take startTime/endTime from samples recorded, rather than
+    // using duration directly from threadNode, as the first sample that
+    // equals the startTime does not get recorded.
+    let startTime = sampleTimes[0];
+    let endTime = sampleTimes[sampleTimes.length - 1];
+
+    let bucketSize = (endTime - startTime) / OPTIMIZATIONS_GRAPH_RESOLUTION;
+    let data = createTierGraphDataFromFrameNode(frameNode, sampleTimes, bucketSize);
+
+    // If for some reason we don't have data (like the frameNode doesn't
+    // have optimizations, but it shouldn't be at this point if it doesn't),
+    // log an error.
+    if (!data) {
+      Cu.reportError(`FrameNode#${frameNode.location} does not have optimizations data to render.`);
+      return;
+    }
+
+    this.dataOffsetX = startTime;
+    yield this.setData(data);
+  }),
+
+  /**
+   * Sets the theme via `theme` to either "light" or "dark",
+   * and updates the internal styling to match. Requires a redraw
+   * to see the effects.
+   */
+  setTheme: function (theme) {
+    theme = theme || "light";
+
+    let interpreterColor = getColor("graphs-red", theme);
+    let baselineColor = getColor("graphs-blue", theme);
+    let ionColor = getColor("graphs-green", theme);
+
+    this.format = [
+      { color: interpreterColor },
+      { color: baselineColor },
+      { color: ionColor },
+    ];
+
+    this.backgroundColor = getColor("sidebar-background", theme);
+  }
+});
+
+exports.OptimizationsGraph = OptimizationsGraph;
 exports.FramerateGraph = FramerateGraph;
 exports.MemoryGraph = MemoryGraph;
 exports.TimelineGraph = TimelineGraph;
 exports.GraphsController = GraphsController;
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -28,26 +28,30 @@ loader.lazyRequireGetter(this, "L10N",
 loader.lazyRequireGetter(this, "PerformanceTelemetry",
   "devtools/performance/telemetry", true);
 loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
   "devtools/performance/markers", true);
 loader.lazyRequireGetter(this, "RecordingUtils",
   "devtools/toolkit/performance/utils");
 loader.lazyRequireGetter(this, "GraphsController",
   "devtools/performance/graphs", true);
+loader.lazyRequireGetter(this, "OptimizationsGraph",
+  "devtools/performance/graphs", true);
 loader.lazyRequireGetter(this, "WaterfallHeader",
   "devtools/performance/waterfall-ticks", true);
 loader.lazyRequireGetter(this, "MarkerView",
   "devtools/performance/marker-view", true);
 loader.lazyRequireGetter(this, "MarkerDetails",
   "devtools/performance/marker-details", true);
 loader.lazyRequireGetter(this, "MarkerUtils",
   "devtools/performance/marker-utils");
 loader.lazyRequireGetter(this, "WaterfallUtils",
   "devtools/performance/waterfall-utils");
+loader.lazyRequireGetter(this, "FrameUtils",
+  "devtools/performance/frame-utils");
 loader.lazyRequireGetter(this, "CallView",
   "devtools/performance/tree-view", true);
 loader.lazyRequireGetter(this, "ThreadNode",
   "devtools/performance/tree-model", true);
 loader.lazyRequireGetter(this, "FrameNode",
   "devtools/performance/tree-model", true);
 loader.lazyRequireGetter(this, "JITOptimizations",
   "devtools/performance/jit", true);
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -72,17 +72,17 @@
                 class="experimental-option"
                 type="checkbox"
                 data-pref="enable-jit-optimizations"
                 label="&performanceUI.enableJITOptimizations;"
                 tooltiptext="&performanceUI.enableJITOptimizations.tooltiptext;"/>
     </menupopup>
   </popupset>
 
-  <hbox class="theme-body" flex="1">
+  <hbox id="body" class="theme-body" flex="1">
 
     <!-- Sidebar: controls and recording list -->
     <vbox id="recordings-pane">
       <toolbar id="recordings-toolbar"
                class="devtools-toolbar">
         <hbox id="recordings-controls"
               class="devtools-toolbarbutton-group">
           <toolbarbutton id="main-record-button"
@@ -299,16 +299,17 @@
                   <toolbar id="jit-optimizations-toolbar" class="devtools-toolbar">
                     <hbox id="jit-optimizations-header">
                       <span class="jit-optimizations-title">&performanceUI.JITOptimizationsTitle;</span>
                       <span class="header-function-name" />
                       <span class="header-file opt-url debugger-link" />
                       <span class="header-line opt-line" />
                     </hbox>
                   </toolbar>
+                  <hbox id="optimizations-graph"></hbox>
                   <vbox id="jit-optimizations-raw-view"></vbox>
                 </vbox>
               </hbox>
 
               <!-- JS FlameChart -->
               <hbox id="js-flamegraph-view" flex="1">
               </hbox>
 
@@ -359,17 +360,16 @@
                          type="function"
                          crop="end"
                          value="&performanceUI.table.function;"/>
                 </hbox>
                 <vbox class="call-tree-cells-container" flex="1"/>
               </vbox>
 
               <!-- Memory FlameChart -->
-              <hbox id="memory-flamegraph-view" flex="1">
-              </hbox>
+              <hbox id="memory-flamegraph-view" flex="1"></hbox>
             </deck>
           </deck>
         </vbox>
       </deck>
     </vbox>
   </hbox>
 </window>
--- a/browser/devtools/performance/test/unit/test_jit-graph-data.js
+++ b/browser/devtools/performance/test/unit/test_jit-graph-data.js
@@ -31,75 +31,88 @@ add_task(function test() {
 
   equal(root.samples, SAMPLE_COUNT / 2, "root has correct amount of samples");
   equal(root.sampleTimes.length, SAMPLE_COUNT / 2, "root has correct amount of sample times");
   // Add time offset since the first sample begins TIME_OFFSET after startTime
   equal(root.sampleTimes[0], startTime + TIME_OFFSET, "root recorded first sample time in scope");
   equal(root.sampleTimes[root.sampleTimes.length - 1], endTime, "root recorded last sample time in scope");
 
   let frame = getFrameNodePath(root, "X");
-  let data = createTierGraphDataFromFrameNode(frame, root.sampleTimes, { startTime, endTime, resolution: RESOLUTION });
+  let data = createTierGraphDataFromFrameNode(frame, root.sampleTimes, (endTime-startTime)/RESOLUTION);
 
   let TIME_PER_WINDOW = SAMPLE_COUNT / 2 / RESOLUTION * TIME_PER_SAMPLE;
 
-  for (let i = 0; i < 10; i++) {
-    equal(data[i].x, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "first window has correct x");
-    equal(data[i].ys[0], 0.2, "first window has 2 frames in interpreter");
-    equal(data[i].ys[1], 0.2, "first window has 2 frames in baseline");
-    equal(data[i].ys[2], 0.2, "first window has 2 frames in ion");
+  // Filter out the dupes created with the same delta so the graph
+  // can render correctly.
+  let filteredData = [];
+  for (let i = 0; i < data.length; i++) {
+    if (!i || data[i].delta !== data[i-1].delta) {
+      filteredData.push(data[i]);
+    }
+  }
+  data = filteredData;
+
+  for (let i = 0; i < 11; i++) {
+    equal(data[i].delta, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "first window has correct x");
+    equal(data[i].values[0], 0.2, "first window has 2 frames in interpreter");
+    equal(data[i].values[1], 0.2, "first window has 2 frames in baseline");
+    equal(data[i].values[2], 0.2, "first window has 2 frames in ion");
   }
-  for (let i = 10; i < 20; i++) {
-    equal(data[i].x, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "second window has correct x");
-    equal(data[i].ys[0], 0, "second window observed no optimizations");
-    equal(data[i].ys[1], 0, "second window observed no optimizations");
-    equal(data[i].ys[2], 0, "second window observed no optimizations");
+  // Start on 11, since i===10 is where the values change, and the new value (0,0,0)
+  // is removed in `filteredData`
+  for (let i = 11; i < 20; i++) {
+    equal(data[i].delta, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "second window has correct x");
+    equal(data[i].values[0], 0, "second window observed no optimizations");
+    equal(data[i].values[1], 0, "second window observed no optimizations");
+    equal(data[i].values[2], 0, "second window observed no optimizations");
   }
-  for (let i = 20; i < 30; i++) {
-    equal(data[i].x, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "third window has correct x");
-    equal(data[i].ys[0], 0.3, "third window has 3 frames in interpreter");
-    equal(data[i].ys[1], 0, "third window has 0 frames in baseline");
-    equal(data[i].ys[2], 0, "third window has 0 frames in ion");
+  // Start on 21, since i===20 is where the values change, and the new value (0.3,0,0)
+  // is removed in `filteredData`
+  for (let i = 21; i < 30; i++) {
+    equal(data[i].delta, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "third window has correct x");
+    equal(data[i].values[0], 0.3, "third window has 3 frames in interpreter");
+    equal(data[i].values[1], 0, "third window has 0 frames in baseline");
+    equal(data[i].values[2], 0, "third window has 0 frames in ion");
   }
 });
 
 let gUniqueStacks = new RecordingUtils.UniqueStacks();
 
 function uniqStr(s) {
   return gUniqueStacks.getOrAddStringIndex(s);
 }
 
 const TIER_PATTERNS = [
   // 0-99
-  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+  ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
   // 100-199
-  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+  ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
   // 200-299
-  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+  ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
   // 300-399
-  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+  ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
   // 400-499
-  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+  ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
 
   // 500-599
-  // Test current frames in all opts, including that
-  // the same frame with no opts does not get counted
-  ["X", "X", "A", "A", "X_1", "X_2", "X_1", "X_2", "X_0", "X_0"],
+  // Test current frames in all opts
+  ["A", "A", "A", "A", "X_1", "X_2", "X_1", "X_2", "X_0", "X_0"],
 
   // 600-699
   // Nothing for current frame
   ["A", "B", "A", "B", "A", "B", "A", "B", "A", "B"],
 
   // 700-799
   // A few frames where the frame is not the leaf node
   ["X_2 -> Y", "X_2 -> Y", "X_2 -> Y", "X_0", "X_0", "X_0", "A", "A", "A", "A"],
 
   // 800-899
-  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+  ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
   // 900-999
-  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+  ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
 ];
 
 function createSample (i, frames) {
   let sample = {};
   sample.time = i * TIME_PER_SAMPLE;
   sample.frames = [{ location: "(root)" }];
   if (i === 0) {
     return sample;
--- a/browser/devtools/performance/views/details-js-call-tree.js
+++ b/browser/devtools/performance/views/details-js-call-tree.js
@@ -31,16 +31,17 @@ let JsCallTreeView = Heritage.extend(Det
   },
 
   /**
    * Unbinds events.
    */
   destroy: function () {
     OptimizationsListView.destroy();
     this.container = null;
+    this.threadNode = null;
     DetailsSubview.destroy.call(this);
   },
 
   /**
    * Method for handling all the set up for rendering a new call tree.
    *
    * @param object interval [optional]
    *        The { startTime, endTime }, in milliseconds.
@@ -51,17 +52,17 @@ let JsCallTreeView = Heritage.extend(Det
     let optimizations = recording.getConfiguration().withJITOptimizations;
 
     let options = {
       contentOnly: !PerformanceController.getOption("show-platform-data"),
       invertTree: PerformanceController.getOption("invert-call-tree"),
       flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"),
       showOptimizationHint: optimizations
     };
-    let threadNode = this._prepareCallTree(profile, interval, options);
+    let threadNode = this.threadNode = this._prepareCallTree(profile, interval, options);
     this._populateCallTree(threadNode, options);
 
     if (optimizations) {
       this.showOptimizations();
     } else {
       this.hideOptimizations();
     }
     OptimizationsListView.reset();
@@ -74,17 +75,17 @@ let JsCallTreeView = Heritage.extend(Det
   },
 
   hideOptimizations: function () {
     $("#jit-optimizations-view").classList.add("hidden");
   },
 
   _onFocus: function (_, treeItem) {
     if (PerformanceController.getCurrentRecording().getConfiguration().withJITOptimizations) {
-      OptimizationsListView.setCurrentFrame(treeItem.frame);
+      OptimizationsListView.setCurrentFrame(this.threadNode, treeItem.frame);
       OptimizationsListView.render();
     }
 
     this.emit("focus", treeItem);
   },
 
   /**
    * Fired on the "link" event for the call tree in this container.
--- a/browser/devtools/performance/views/optimizations-list.js
+++ b/browser/devtools/performance/views/optimizations-list.js
@@ -19,66 +19,84 @@ let OptimizationsListView = {
 
   _currentFrame: null,
 
   /**
    * Initialization function called when the tool starts up.
    */
   initialize: function () {
     this.reset = this.reset.bind(this);
+    this._onThemeChanged = this._onThemeChanged.bind(this);
 
     this.el = $("#jit-optimizations-view");
     this.$headerName = $("#jit-optimizations-header .header-function-name");
     this.$headerFile = $("#jit-optimizations-header .header-file");
     this.$headerLine = $("#jit-optimizations-header .header-line");
 
     this.tree = new TreeWidget($("#jit-optimizations-raw-view"), {
       sorted: false,
       emptyText: JIT_EMPTY_TEXT
     });
+    this.graph = new OptimizationsGraph($("#optimizations-graph"));
+    this.graph.setTheme(PerformanceController.getTheme());
 
     // Start the tree by resetting.
     this.reset();
+
+    PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
   },
 
   /**
    * Destruction function called when the tool cleans up.
    */
   destroy: function () {
+    PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
     this.tree = null;
     this.$headerName = this.$headerFile = this.$headerLine = this.el = null;
   },
 
   /**
    * Takes a FrameNode, with corresponding optimization data to be displayed
    * in the view.
    *
    * @param {FrameNode} frameNode
    */
-  setCurrentFrame: function (frameNode) {
+  setCurrentFrame: function (threadNode, frameNode) {
+    if (threadNode !== this.getCurrentThread()) {
+      this._currentThread = threadNode;
+    }
     if (frameNode !== this.getCurrentFrame()) {
       this._currentFrame = frameNode;
     }
   },
 
   /**
    * Returns the current frame node for this view.
    *
    * @return {?FrameNode}
    */
-  getCurrentFrame: function (frameNode) {
+  getCurrentFrame: function () {
     return this._currentFrame;
   },
 
   /**
+   * Returns the current thread node for this view.
+   *
+   * @return {?ThreadNode}
+   */
+  getCurrentThread: function () {
+    return this._currentThread;
+  },
+
+  /**
    * Clears out data in the tree, sets to an empty state,
    * and removes current frame.
    */
   reset: function () {
-    this.setCurrentFrame(null);
+    this.setCurrentFrame(null, null);
     this.clear();
     this.el.classList.add("empty");
     this.emit(EVENTS.OPTIMIZATIONS_RESET);
     this.emit(EVENTS.OPTIMIZATIONS_RENDERED, this.getCurrentFrame());
   },
 
   /**
    * Clears out data in the tree.
@@ -118,20 +136,29 @@ let OptimizationsListView = {
 
     // An array of sorted OptimizationSites.
     let sites = frameNode.getOptimizations().optimizationSites;
 
     for (let site of sites) {
       this._renderSite(view, site, frameData);
     }
 
+    this._renderTierGraph();
+
     this.emit(EVENTS.OPTIMIZATIONS_RENDERED, this.getCurrentFrame());
   },
 
   /**
+   * Renders the optimization tier graph over time.
+   */
+  _renderTierGraph: function () {
+    this.graph.render(this.getCurrentThread(), this.getCurrentFrame());
+  },
+
+  /**
    * Creates an entry in the tree widget for an optimization site.
    */
   _renderSite: function (view, site, frameData) {
     let { id, samples, data } = site;
     let { types, attempts } = data;
     let siteNode = this._createSiteNode(frameData, site);
 
     // Cast `id` to a string so TreeWidget doesn't think it does not exist
@@ -170,17 +197,16 @@ let OptimizationsListView = {
         view.add([id, `${id}-types`, `${id}-types-${i}`, { node }]);
       }
     }
   },
 
   /**
    * Creates an element for insertion in the raw view for an OptimizationSite.
    */
-
   _createSiteNode: function (frameData, site) {
     let node = document.createElement("span");
     let desc = document.createElement("span");
     let line = document.createElement("span");
     let column = document.createElement("span");
     let urlNode = this._createDebuggerLinkNode(frameData.url, site.data.line);
 
     let attempts = site.getAttempts();
@@ -220,32 +246,30 @@ let OptimizationsListView = {
 
   /**
    * Creates an element for insertion in the raw view for an IonType.
    *
    * @see browser/devtools/performance/modules/logic/jit.js
    * @param {IonType} ionType
    * @return {Element}
    */
-
   _createIonNode: function (ionType) {
     let node = document.createElement("span");
     node.textContent = `${ionType.site} : ${ionType.mirType}`;
     node.className = "opt-ion-type";
     return node;
   },
 
   /**
    * Creates an element for insertion in the raw view for an ObservedType.
    *
    * @see browser/devtools/performance/modules/logic/jit.js
    * @param {ObservedType} type
    * @return {Element}
    */
-
   _createObservedTypeNode: function (type) {
     let node = document.createElement("span");
     let typeNode = document.createElement("span");
 
     typeNode.textContent = `${type.keyedBy}` + (type.name ? ` → ${type.name}` : "");
     typeNode.className = "opt-type";
     node.appendChild(typeNode);
 
@@ -275,17 +299,16 @@ let OptimizationsListView = {
 
   /**
    * Creates an element for insertion in the raw view for an OptimizationAttempt.
    *
    * @see browser/devtools/performance/modules/logic/jit.js
    * @param {OptimizationAttempt} attempt
    * @return {Element}
    */
-
   _createAttemptNode: function (attempt) {
     let node = document.createElement("span");
     let strategyNode = document.createElement("span");
     let outcomeNode = document.createElement("span");
 
     strategyNode.textContent = attempt.strategy;
     strategyNode.className = "opt-strategy";
     outcomeNode.textContent = attempt.outcome;
@@ -304,17 +327,16 @@ let OptimizationsListView = {
    * Can also optionally pass in an element to modify it rather than
    * creating a new one.
    *
    * @param {String} url
    * @param {Number} line
    * @param {?Element} el
    * @return {Element}
    */
-
   _createDebuggerLinkNode: function (url, line, el) {
     let node = el || document.createElement("span");
     node.className = "opt-url";
     let fileName;
 
     if (this._isLinkableURL(url)) {
       fileName = url.slice(url.lastIndexOf("/") + 1);
       node.classList.add("debugger-link");
@@ -324,17 +346,16 @@ let OptimizationsListView = {
     fileName = fileName || url || "";
     node.textContent = fileName ? `@${fileName}` : "";
     return node;
   },
 
   /**
    * Updates the headers with the current frame's data.
    */
-
   _setHeaders: function (frameData) {
     let isMeta = frameData.isMetaCategory;
     let name = isMeta ? frameData.categoryData.label : frameData.functionName;
     let url = isMeta ? "" : frameData.url;
     let line = isMeta ? "" : frameData.line;
 
     this.$headerName.textContent = name;
     this.$headerLine.textContent = line;
@@ -346,20 +367,27 @@ let OptimizationsListView = {
 
   /**
    * Takes a string and returns a boolean indicating whether or not
    * this is a valid url for linking to the debugger.
    *
    * @param {String} url
    * @return {Boolean}
    */
-
   _isLinkableURL: function (url) {
     return url && url.indexOf &&
        (url.indexOf("http") === 0 ||
         url.indexOf("resource://") === 0 ||
         url.indexOf("file://") === 0);
   },
 
+  /**
+   * Called when `devtools.theme` changes.
+   */
+  _onThemeChanged: function (_, theme) {
+    this.graph.setTheme(theme);
+    this.graph.refresh({ force: true });
+  },
+
   toString: () => "[object OptimizationsListView]"
 };
 
 EventEmitter.decorate(OptimizationsListView);
--- a/browser/devtools/shared/widgets/MountainGraphWidget.js
+++ b/browser/devtools/shared/widgets/MountainGraphWidget.js
@@ -7,17 +7,17 @@ const { AbstractCanvasGraph, CanvasGraph
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 // Bar graph constants.
 
 const GRAPH_DAMPEN_VALUES_FACTOR = 0.9;
 
 const GRAPH_BACKGROUND_COLOR = "#ddd";
-const GRAPH_STROKE_WIDTH = 2; // px
+const GRAPH_STROKE_WIDTH = 1; // px
 const GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
 const GRAPH_HELPER_LINES_DASH = [5]; // px
 const GRAPH_HELPER_LINES_WIDTH = 1; // px
 
 const GRAPH_CLIPHEAD_LINE_COLOR = "#fff";
 const GRAPH_SELECTION_LINE_COLOR = "#fff";
 const GRAPH_SELECTION_BACKGROUND_COLOR = "rgba(44,187,15,0.25)";
 const GRAPH_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
@@ -120,18 +120,18 @@ MountainGraphWidget.prototype = Heritage
       throw "The graph format traits are mandatory to style the data source.";
     }
     let { canvas, ctx } = this._getNamedCanvas("mountain-graph-data");
     let width = this._width;
     let height = this._height;
 
     let totalSections = this.format.length;
     let totalTicks = this._data.length;
-    let firstTick = this._data[0].delta;
-    let lastTick = this._data[totalTicks - 1].delta;
+    let firstTick = totalTicks ? this._data[0].delta : 0;
+    let lastTick = totalTicks ? this._data[totalTicks - 1].delta : 0;
 
     let duration = this.dataDuration || lastTick;
     let dataScaleX = this.dataScaleX = width / (duration - this.dataOffsetX);
     let dataScaleY = this.dataScaleY = height * this.dampenValuesFactor;
 
     // Draw the graph.
 
     let prevHeights = Array.from({ length: totalTicks }).fill(0);
--- a/browser/themes/shared/devtools/performance.css
+++ b/browser/themes/shared/devtools/performance.css
@@ -584,16 +584,24 @@ menuitem.marker-color-graphs-grey:before
 
 #jit-optimizations-view {
   width: 350px;
   overflow-x: hidden;
   overflow-y: auto;
   min-width: 200px;
 }
 
+#optimizations-graph {
+  height: 30px;
+}
+
+#jit-optimizations-view.empty #optimizations-graph {
+  display: none !important;
+}
+
 /* override default styles for tree widget */
 #jit-optimizations-view .tree-widget-empty-text {
   font-size: inherit;
   padding: 0px;
   margin: 8px;
 }
 
 #jit-optimizations-view:not(.empty) .tree-widget-empty-text {