Bug 1069421 - Add a memory graph to the timeline, r=pbrosset,paul
authorVictor Porof <vporof@mozilla.com>
Thu, 13 Nov 2014 09:18:19 -0500
changeset 215610 5e58c0e599649cb6134e59219533d5ada3464af4
parent 215609 fd4bd4b3fbf83159aae9c368826a0598df820750
child 215611 5b4fe2faa70d366de44f94cafc6c418cc01a3987
push id51796
push userryanvm@gmail.com
push dateThu, 13 Nov 2014 20:47:14 +0000
treeherdermozilla-inbound@a05b5362429f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbrosset, paul
bugs1069421
milestone36.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 1069421 - Add a memory graph to the timeline, r=pbrosset,paul
browser/devtools/shared/test/browser.ini
browser/devtools/shared/test/browser_graphs-09.js
browser/devtools/shared/test/browser_graphs-09a.js
browser/devtools/shared/test/browser_graphs-09b.js
browser/devtools/shared/test/browser_graphs-09c.js
browser/devtools/shared/widgets/Graphs.jsm
browser/devtools/timeline/moz.build
browser/devtools/timeline/test/browser.ini
browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js
browser/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js
browser/devtools/timeline/test/browser_timeline_overview-update.js
browser/devtools/timeline/test/browser_timeline_recording-without-memory.js
browser/devtools/timeline/test/browser_timeline_recording.js
browser/devtools/timeline/test/browser_timeline_waterfall-background.js
browser/devtools/timeline/test/browser_timeline_waterfall-generic.js
browser/devtools/timeline/test/browser_timeline_waterfall-styles.js
browser/devtools/timeline/timeline.js
browser/devtools/timeline/timeline.xul
browser/devtools/timeline/widgets/markers-overview.js
browser/devtools/timeline/widgets/memory-overview.js
browser/devtools/timeline/widgets/overview.js
browser/devtools/timeline/widgets/waterfall.js
browser/locales/en-US/chrome/browser/devtools/timeline.dtd
browser/locales/en-US/chrome/browser/devtools/timeline.properties
browser/themes/shared/devtools/timeline.inc.css
browser/themes/shared/devtools/widgets.inc.css
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -19,17 +19,19 @@ support-files =
 [browser_graphs-02.js]
 [browser_graphs-03.js]
 [browser_graphs-04.js]
 [browser_graphs-05.js]
 [browser_graphs-06.js]
 [browser_graphs-07a.js]
 [browser_graphs-07b.js]
 [browser_graphs-08.js]
-[browser_graphs-09.js]
+[browser_graphs-09a.js]
+[browser_graphs-09b.js]
+[browser_graphs-09c.js]
 [browser_graphs-10a.js]
 [browser_graphs-10b.js]
 [browser_graphs-11a.js]
 [browser_graphs-11b.js]
 [browser_graphs-12.js]
 [browser_graphs-13.js]
 [browser_graphs-14.js]
 [browser_inplace-editor.js]
rename from browser/devtools/shared/test/browser_graphs-09.js
rename to browser/devtools/shared/test/browser_graphs-09a.js
--- a/browser/devtools/shared/test/browser_graphs-09.js
+++ b/browser/devtools/shared/test/browser_graphs-09a.js
@@ -22,31 +22,47 @@ function* performTest() {
 
   yield testGraph(graph);
 
   graph.destroy();
   host.destroy();
 }
 
 function* testGraph(graph) {
-  info("Should be able to set the grpah data before waiting for the ready event.");
+  info("Should be able to set the graph data before waiting for the ready event.");
 
   yield graph.setDataWhenReady(TEST_DATA);
   ok(graph.hasData(), "Data was set successfully.");
 
+  is(graph._gutter.hidden, false,
+    "The gutter should not be hidden because the tooltips have arrows.");
+  is(graph._maxTooltip.hidden, false,
+    "The max tooltip should not be hidden.");
+  is(graph._avgTooltip.hidden, false,
+    "The avg tooltip should not be hidden.");
+  is(graph._minTooltip.hidden, false,
+    "The min tooltip should not be hidden.");
+
+  is(graph._maxTooltip.getAttribute("with-arrows"), "true",
+    "The maximum tooltip has the correct 'with-arrows' attribute.");
+  is(graph._avgTooltip.getAttribute("with-arrows"), "true",
+    "The average tooltip has the correct 'with-arrows' attribute.");
+  is(graph._minTooltip.getAttribute("with-arrows"), "true",
+    "The minimum tooltip has the correct 'with-arrows' attribute.");
+
   is(graph._maxTooltip.querySelector("[text=info]").textContent, "max",
     "The maximum tooltip displays the correct info.");
   is(graph._avgTooltip.querySelector("[text=info]").textContent, "avg",
     "The average tooltip displays the correct info.");
   is(graph._minTooltip.querySelector("[text=info]").textContent, "min",
     "The minimum tooltip displays the correct info.");
 
   is(graph._maxTooltip.querySelector("[text=value]").textContent, "60",
     "The maximum tooltip displays the correct value.");
-  is(graph._avgTooltip.querySelector("[text=value]").textContent, "41",
+  is(graph._avgTooltip.querySelector("[text=value]").textContent, "41.71",
     "The average tooltip displays the correct value.");
   is(graph._minTooltip.querySelector("[text=value]").textContent, "10",
     "The minimum tooltip displays the correct value.");
 
   is(graph._maxTooltip.querySelector("[text=metric]").textContent, "fps",
     "The maximum tooltip displays the correct metric.");
   is(graph._avgTooltip.querySelector("[text=metric]").textContent, "fps",
     "The average tooltip displays the correct metric.");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_graphs-09b.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that line graphs properly use the tooltips configuration properties.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
+
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* performTest() {
+  let [host, win, doc] = yield createHost();
+  let graph = new LineGraphWidget(doc.body, "fps");
+  graph.withTooltipArrows = false;
+  graph.withFixedTooltipPositions = true;
+
+  yield testGraph(graph);
+
+  graph.destroy();
+  host.destroy();
+}
+
+function* testGraph(graph) {
+  yield graph.setDataWhenReady(TEST_DATA);
+
+  is(graph._gutter.hidden, true,
+    "The gutter should be hidden because the tooltips don't have arrows.");
+  is(graph._maxTooltip.hidden, false,
+    "The max tooltip should not be hidden.");
+  is(graph._avgTooltip.hidden, false,
+    "The avg tooltip should not be hidden.");
+  is(graph._minTooltip.hidden, false,
+    "The min tooltip should not be hidden.");
+
+  is(graph._maxTooltip.getAttribute("with-arrows"), "false",
+    "The maximum tooltip has the correct 'with-arrows' attribute.");
+  is(graph._avgTooltip.getAttribute("with-arrows"), "false",
+    "The average tooltip has the correct 'with-arrows' attribute.");
+  is(graph._minTooltip.getAttribute("with-arrows"), "false",
+    "The minimum tooltip has the correct 'with-arrows' attribute.");
+
+  is(parseInt(graph._maxTooltip.style.top), 8,
+    "The maximum tooltip is positioned correctly.");
+  is(parseInt(graph._avgTooltip.style.top), 8,
+    "The average tooltip is positioned correctly.");
+  is(parseInt(graph._minTooltip.style.top), 142,
+    "The minimum tooltip is positioned correctly.");
+
+  is(parseInt(graph._maxGutterLine.style.top), 22,
+    "The maximum gutter line is positioned correctly.");
+  is(parseInt(graph._avgGutterLine.style.top), 61,
+    "The average gutter line is positioned correctly.");
+  is(parseInt(graph._minGutterLine.style.top), 128,
+    "The minimum gutter line is positioned correctly.");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_graphs-09c.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that line graphs hide the tooltips when there's no data available.
+
+const TEST_DATA = [];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
+
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* performTest() {
+  let [host, win, doc] = yield createHost();
+  let graph = new LineGraphWidget(doc.body, "fps");
+
+  yield testGraph(graph);
+
+  graph.destroy();
+  host.destroy();
+}
+
+function* testGraph(graph) {
+  yield graph.setDataWhenReady(TEST_DATA);
+
+  is(graph._gutter.hidden, false,
+    "The gutter should not be hidden.");
+  is(graph._maxTooltip.hidden, true,
+    "The max tooltip should be hidden.");
+  is(graph._avgTooltip.hidden, true,
+    "The avg tooltip should be hidden.");
+  is(graph._minTooltip.hidden, true,
+    "The min tooltip should be hidden.");
+}
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -14,16 +14,17 @@ this.EXPORTED_SYMBOLS = [
   "AbstractCanvasGraph",
   "LineGraphWidget",
   "BarGraphWidget",
   "CanvasGraphUtils"
 ];
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
+const L10N = new ViewHelpers.L10N();
 
 // Generic constants.
 
 const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
 const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00075;
 const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.1;
 const GRAPH_WHEEL_MIN_SELECTION_WIDTH = 10; // px
 
@@ -39,18 +40,19 @@ const GRAPH_STRIPE_PATTERN_WIDTH = 16; /
 const GRAPH_STRIPE_PATTERN_HEIGHT = 16; // px
 const GRAPH_STRIPE_PATTERN_LINE_WIDTH = 2; // px
 const GRAPH_STRIPE_PATTERN_LINE_SPACING = 4; // px
 
 // Line graph constants.
 
 const LINE_GRAPH_DAMPEN_VALUES = 0.85;
 const LINE_GRAPH_MIN_SQUARED_DISTANCE_BETWEEN_POINTS = 400; // 20 px
-const LINE_GRAPH_TOOLTIP_SAFE_BOUNDS = 10; // px
+const LINE_GRAPH_TOOLTIP_SAFE_BOUNDS = 8; // px
 
+const LINE_GRAPH_BACKGROUND_COLOR = "#0088cc";
 const LINE_GRAPH_STROKE_WIDTH = 1; // px
 const LINE_GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
 const LINE_GRAPH_HELPER_LINES_DASH = [5]; // px
 const LINE_GRAPH_HELPER_LINES_WIDTH = 1; // px
 const LINE_GRAPH_MAXIMUM_LINE_COLOR = "rgba(255,255,255,0.4)";
 const LINE_GRAPH_AVERAGE_LINE_COLOR = "rgba(255,255,255,0.7)";
 const LINE_GRAPH_MINIMUM_LINE_COLOR = "rgba(255,255,255,0.9)";
 const LINE_GRAPH_BACKGROUND_GRADIENT_START = "rgba(255,255,255,0.25)";
@@ -482,26 +484,28 @@ AbstractCanvasGraph.prototype = {
     this.emit("deselecting");
   },
 
   /**
    * Gets whether or not this graph has a selection.
    * @return boolean
    */
   hasSelection: function() {
-    return this._selection.start != null && this._selection.end != null;
+    return this._selection &&
+      this._selection.start != null && this._selection.end != null;
   },
 
   /**
    * Gets whether or not a selection is currently being made, for example
    * via a click+drag operation.
    * @return boolean
    */
   hasSelectionInProgress: function() {
-    return this._selection.start != null && this._selection.end == null;
+    return this._selection &&
+      this._selection.start != null && this._selection.end == null;
   },
 
   /**
    * Specifies whether or not mouse selection is allowed.
    * @type boolean
    */
   selectionEnabled: true,
 
@@ -547,17 +551,17 @@ AbstractCanvasGraph.prototype = {
     this._shouldRedraw = true;
   },
 
   /**
    * Gets whether or not this graph has a visible cursor.
    * @return boolean
    */
   hasCursor: function() {
-    return this._cursor.x != null;
+    return this._cursor && this._cursor.x != null;
   },
 
   /**
    * Specifies if this graph's selection is different from another one.
    *
    * @param object other
    *        The other graph's selection, as { start, end } values.
    */
@@ -1171,76 +1175,104 @@ this.LineGraphWidget = function(parent, 
     this._minGutterLine = this._createGutterLine("minimum");
     this._maxTooltip = this._createTooltip("maximum", "start", "max", metric);
     this._avgTooltip = this._createTooltip("average", "end", "avg", metric);
     this._minTooltip = this._createTooltip("minimum", "start", "min", metric);
   });
 }
 
 LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+  backgroundColor: LINE_GRAPH_BACKGROUND_COLOR,
+  backgroundGradientStart: LINE_GRAPH_BACKGROUND_GRADIENT_START,
+  backgroundGradientEnd: LINE_GRAPH_BACKGROUND_GRADIENT_END,
+  strokeColor: LINE_GRAPH_STROKE_COLOR,
+  strokeWidth: LINE_GRAPH_STROKE_WIDTH,
+  maximumLineColor: LINE_GRAPH_MAXIMUM_LINE_COLOR,
+  averageLineColor: LINE_GRAPH_AVERAGE_LINE_COLOR,
+  minimumLineColor: LINE_GRAPH_MINIMUM_LINE_COLOR,
   clipheadLineColor: LINE_GRAPH_CLIPHEAD_LINE_COLOR,
   selectionLineColor: LINE_GRAPH_SELECTION_LINE_COLOR,
   selectionBackgroundColor: LINE_GRAPH_SELECTION_BACKGROUND_COLOR,
   selectionStripesColor: LINE_GRAPH_SELECTION_STRIPES_COLOR,
   regionBackgroundColor: LINE_GRAPH_REGION_BACKGROUND_COLOR,
   regionStripesColor: LINE_GRAPH_REGION_STRIPES_COLOR,
 
   /**
    * Optionally offsets the `delta` in the data source by this scalar.
    */
   dataOffsetX: 0,
 
   /**
+   * The scalar used to multiply the graph values to leave some headroom
+   * on the top.
+   */
+  dampenValuesFactor: LINE_GRAPH_DAMPEN_VALUES,
+
+  /**
    * Points that are too close too each other in the graph will not be rendered.
    * This scalar specifies the required minimum squared distance between points.
    */
   minDistanceBetweenPoints: LINE_GRAPH_MIN_SQUARED_DISTANCE_BETWEEN_POINTS,
 
   /**
+   * Specifies if min/max/avg tooltips have arrow handlers on their sides.
+   */
+  withTooltipArrows: true,
+
+  /**
+   * Specifies if min/max/avg tooltips are positioned based on the actual
+   * values, or just placed next to the graph corners.
+   */
+  withFixedTooltipPositions: false,
+
+  /**
    * Renders the graph's data source.
    * @see AbstractCanvasGraph.prototype.buildGraphImage
    */
   buildGraphImage: function() {
     let { canvas, ctx } = this._getNamedCanvas("line-graph-data");
     let width = this._width;
     let height = this._height;
 
     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 maxValue = Number.MIN_SAFE_INTEGER;
     let minValue = Number.MAX_SAFE_INTEGER;
     let sumValues = 0;
 
     for (let { delta, value } of this._data) {
       maxValue = Math.max(value, maxValue);
       minValue = Math.min(value, minValue);
       sumValues += value;
     }
 
     let dataScaleX = this.dataScaleX = width / (lastTick - this.dataOffsetX);
-    let dataScaleY = this.dataScaleY = height / maxValue * LINE_GRAPH_DAMPEN_VALUES;
+    let dataScaleY = this.dataScaleY = height / maxValue * this.dampenValuesFactor;
 
     /**
      * Calculates the squared distance between two 2D points.
      */
     function distSquared(x0, y0, x1, y1) {
       let xs = x1 - x0;
       let ys = y1 - y0;
       return xs * xs + ys * ys;
     }
 
     // Draw the graph.
 
+    ctx.fillStyle = this.backgroundColor;
+    ctx.fillRect(0, 0, width, height);
+
     let gradient = ctx.createLinearGradient(0, height / 2, 0, height);
-    gradient.addColorStop(0, LINE_GRAPH_BACKGROUND_GRADIENT_START);
-    gradient.addColorStop(1, LINE_GRAPH_BACKGROUND_GRADIENT_END);
+    gradient.addColorStop(0, this.backgroundGradientStart);
+    gradient.addColorStop(1, this.backgroundGradientEnd);
     ctx.fillStyle = gradient;
-    ctx.strokeStyle = LINE_GRAPH_STROKE_COLOR;
-    ctx.lineWidth = LINE_GRAPH_STROKE_WIDTH * this._pixelRatio;
+    ctx.strokeStyle = this.strokeColor;
+    ctx.lineWidth = this.strokeWidth * this._pixelRatio;
     ctx.beginPath();
 
     let prevX = 0;
     let prevY = 0;
 
     for (let { delta, value } of this._data) {
       let currX = (delta - this.dataOffsetX) * dataScaleX;
       let currY = height - value * dataScaleY;
@@ -1263,77 +1295,93 @@ LineGraphWidget.prototype = Heritage.ext
       }
     }
 
     ctx.fill();
     ctx.stroke();
 
     // Draw the maximum value horizontal line.
 
-    ctx.strokeStyle = LINE_GRAPH_MAXIMUM_LINE_COLOR;
+    ctx.strokeStyle = this.maximumLineColor;
     ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
     ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
     ctx.beginPath();
-    let maximumY = height - maxValue * dataScaleY - ctx.lineWidth;
+    let maximumY = height - maxValue * dataScaleY;
     ctx.moveTo(0, maximumY);
     ctx.lineTo(width, maximumY);
     ctx.stroke();
 
     // Draw the average value horizontal line.
 
-    ctx.strokeStyle = LINE_GRAPH_AVERAGE_LINE_COLOR;
+    ctx.strokeStyle = this.averageLineColor;
     ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
     ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
     ctx.beginPath();
-    let avgValue = sumValues / totalTicks;
-    let averageY = height - avgValue * dataScaleY - ctx.lineWidth;
+    let avgValue = totalTicks ? sumValues / totalTicks : 0;
+    let averageY = height - avgValue * dataScaleY;
     ctx.moveTo(0, averageY);
     ctx.lineTo(width, averageY);
     ctx.stroke();
 
     // Draw the minimum value horizontal line.
 
-    ctx.strokeStyle = LINE_GRAPH_MINIMUM_LINE_COLOR;
+    ctx.strokeStyle = this.minimumLineColor;
     ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
     ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
     ctx.beginPath();
-    let minimumY = height - minValue * dataScaleY - ctx.lineWidth;
+    let minimumY = height - minValue * dataScaleY;
     ctx.moveTo(0, minimumY);
     ctx.lineTo(width, minimumY);
     ctx.stroke();
 
     // Update the tooltips text and gutter lines.
 
-    this._maxTooltip.querySelector("[text=value]").textContent = maxValue|0;
-    this._avgTooltip.querySelector("[text=value]").textContent = avgValue|0;
-    this._minTooltip.querySelector("[text=value]").textContent = minValue|0;
+    this._maxTooltip.querySelector("[text=value]").textContent =
+      L10N.numberWithDecimals(maxValue, 2);
+    this._avgTooltip.querySelector("[text=value]").textContent =
+      L10N.numberWithDecimals(avgValue, 2);
+    this._minTooltip.querySelector("[text=value]").textContent =
+      L10N.numberWithDecimals(minValue, 2);
 
     /**
      * Constrains a value to a range.
      */
     function clamp(value, min, max) {
       if (value < min) return min;
       if (value > max) return max;
       return value;
     }
 
     let bottom = height / this._pixelRatio;
-    let maxPosY = map(maxValue * LINE_GRAPH_DAMPEN_VALUES, 0, maxValue, bottom, 0);
-    let avgPosY = map(avgValue * LINE_GRAPH_DAMPEN_VALUES, 0, maxValue, bottom, 0);
-    let minPosY = map(minValue * LINE_GRAPH_DAMPEN_VALUES, 0, maxValue, bottom, 0);
+    let maxPosY = map(maxValue * this.dampenValuesFactor, 0, maxValue, bottom, 0);
+    let avgPosY = map(avgValue * this.dampenValuesFactor, 0, maxValue, bottom, 0);
+    let minPosY = map(minValue * this.dampenValuesFactor, 0, maxValue, bottom, 0);
 
     let safeTop = LINE_GRAPH_TOOLTIP_SAFE_BOUNDS;
     let safeBottom = bottom - LINE_GRAPH_TOOLTIP_SAFE_BOUNDS;
 
-    this._maxTooltip.style.top = clamp(maxPosY, safeTop, safeBottom) + "px";
-    this._avgTooltip.style.top = clamp(avgPosY, safeTop, safeBottom) + "px";
-    this._minTooltip.style.top = clamp(minPosY, safeTop, safeBottom) + "px";
-    this._maxGutterLine.style.top = clamp(maxPosY, safeTop, safeBottom) + "px";
-    this._avgGutterLine.style.top = clamp(avgPosY, safeTop, safeBottom) + "px";
-    this._minGutterLine.style.top = clamp(minPosY, safeTop, safeBottom) + "px";
+    this._maxTooltip.style.top = (this.withFixedTooltipPositions
+      ? safeTop : clamp(maxPosY, safeTop, safeBottom)) + "px";
+    this._avgTooltip.style.top = (this.withFixedTooltipPositions
+      ? safeTop : clamp(avgPosY, safeTop, safeBottom)) + "px";
+    this._minTooltip.style.top = (this.withFixedTooltipPositions
+      ? safeBottom : clamp(minPosY, safeTop, safeBottom)) + "px";
+
+    this._maxGutterLine.style.top = maxPosY + "px";
+    this._avgGutterLine.style.top = avgPosY + "px";
+    this._minGutterLine.style.top = minPosY + "px";
+
+    this._maxTooltip.setAttribute("with-arrows", this.withTooltipArrows);
+    this._avgTooltip.setAttribute("with-arrows", this.withTooltipArrows);
+    this._minTooltip.setAttribute("with-arrows", this.withTooltipArrows);
+
+    this._gutter.hidden = !this.withTooltipArrows;
+    this._maxTooltip.hidden = !totalTicks;
+    this._avgTooltip.hidden = !totalTicks;
+    this._minTooltip.hidden = !totalTicks;
 
     return canvas;
   },
 
   /**
    * Creates the gutter node when constructing this graph.
    * @return nsIDOMNode
    */
@@ -1462,16 +1510,22 @@ BarGraphWidget.prototype = Heritage.exte
   format: null,
 
   /**
    * Optionally offsets the `delta` in the data source by this scalar.
    */
   dataOffsetX: 0,
 
   /**
+   * The scalar used to multiply the graph values to leave some headroom
+   * on the top.
+   */
+  dampenValuesFactor: BAR_GRAPH_DAMPEN_VALUES,
+
+  /**
    * Bars that are too close too each other in the graph will be combined.
    * This scalar specifies the required minimum width of each bar.
    */
   minBarsWidth: BAR_GRAPH_MIN_BARS_WIDTH,
 
   /**
    * Blocks in a bar that are too thin inside the bar will not be rendered.
    * This scalar specifies the required minimum height of each block.
@@ -1515,17 +1569,17 @@ BarGraphWidget.prototype = Heritage.exte
     let minBarsWidth = this.minBarsWidth * this._pixelRatio;
     let minBlocksHeight = this.minBlocksHeight * this._pixelRatio;
 
     let dataScaleX = this.dataScaleX = width / (lastTick - this.dataOffsetX);
     let dataScaleY = this.dataScaleY = height / this._calcMaxHeight({
       data: this._data,
       dataScaleX: dataScaleX,
       minBarsWidth: minBarsWidth
-    }) * BAR_GRAPH_DAMPEN_VALUES;
+    }) * this.dampenValuesFactor;
 
     // Draw the graph.
 
     // Iterate over the blocks, then the bars, to draw all rectangles of
     // the same color in a single pass. See the @constructor for more
     // information about the data source, and how a "bar" contains "blocks".
 
     this._blocksBoundingRects = [];
@@ -1918,16 +1972,23 @@ this.CanvasGraphUtils = {
 
   /**
    * Makes sure selections in one graph are reflected in another.
    */
   linkSelection: function(graph1, graph2) {
     if (!graph1 || !graph2) {
       return;
     }
+
+    if (graph1.hasSelection()) {
+      graph2.setSelection(graph1.getSelection());
+    } else {
+      graph2.dropSelection();
+    }
+
     graph1.on("selecting", () => {
       graph2.setSelection(graph1.getSelection());
     });
     graph2.on("selecting", () => {
       graph1.setSelection(graph2.getSelection());
     });
     graph1.on("deselecting", () => {
       graph2.dropSelection();
--- a/browser/devtools/timeline/moz.build
+++ b/browser/devtools/timeline/moz.build
@@ -1,13 +1,14 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 EXTRA_JS_MODULES.devtools.timeline += [
     'panel.js',
     'widgets/global.js',
-    'widgets/overview.js',
+    'widgets/markers-overview.js',
+    'widgets/memory-overview.js',
     'widgets/waterfall.js'
 ]
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/browser/devtools/timeline/test/browser.ini
+++ b/browser/devtools/timeline/test/browser.ini
@@ -5,12 +5,13 @@ support-files =
   head.js
 
 [browser_timeline_aaa_run_first_leaktest.js]
 [browser_timeline_blueprint.js]
 [browser_timeline_overview-initial-selection-01.js]
 [browser_timeline_overview-initial-selection-02.js]
 [browser_timeline_overview-update.js]
 [browser_timeline_panels.js]
+[browser_timeline_recording-without-memory.js]
 [browser_timeline_recording.js]
 [browser_timeline_waterfall-background.js]
 [browser_timeline_waterfall-generic.js]
 [browser_timeline_waterfall-styles.js]
--- a/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js
+++ b/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js
@@ -3,39 +3,45 @@
 
 /**
  * Tests if the overview has an initial selection when recording has finished
  * and there is data available.
  */
 
 let test = Task.async(function*() {
   let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
+  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
   let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin;
 
+  $("#memory-checkbox").checked = true;
+  yield TimelineController.updateMemoryRecording();
+
   yield TimelineController.toggleRecording();
   ok(true, "Recording has started.");
 
   let updated = 0;
   panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
 
   ok((yield waitUntil(() => updated > 10)),
     "The overview graph was updated a bunch of times.");
   ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
     "There are some markers available.");
+  ok((yield waitUntil(() => TimelineController.getMemory().length > 0)),
+    "There are some memory measurements available now.");
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has ended.");
 
+  let interval = TimelineController.getInterval();
   let markers = TimelineController.getMarkers();
-  let selection = TimelineView.overview.getSelection();
+  let selection = TimelineView.markersOverview.getSelection();
 
   is((selection.start) | 0,
-     ((markers[0].start - markers.startTime) * TimelineView.overview.dataScaleX) | 0,
+     ((markers[0].start - interval.startTime) * TimelineView.markersOverview.dataScaleX) | 0,
     "The initial selection start is correct.");
 
   is((selection.end - selection.start) | 0,
-     (selectionRatio * TimelineView.overview.width) | 0,
+     (selectionRatio * TimelineView.markersOverview.width) | 0,
     "The initial selection end is correct.");
 
   yield teardown(panel);
   finish();
 });
--- a/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js
+++ b/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js
@@ -3,30 +3,36 @@
 
 /**
  * Tests if the overview has no initial selection when recording has finished
  * and there is no data available.
  */
 
 let test = Task.async(function*() {
   let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
+  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
   let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin;
 
+  $("#memory-checkbox").checked = true;
+  yield TimelineController.updateMemoryRecording();
+
   yield TimelineController.toggleRecording();
   ok(true, "Recording has started.");
 
   yield TimelineController._stopRecordingAndDiscardData();
   ok(true, "Recording has ended.");
 
   let markers = TimelineController.getMarkers();
-  let selection = TimelineView.overview.getSelection();
+  let memory = TimelineController.getMemory();
+  let selection = TimelineView.markersOverview.getSelection();
 
   is(markers.length, 0,
     "There are no markers available.");
+  is(memory.length, 0,
+    "There are no memory measurements available.");
   is(selection.start, null,
     "The initial selection start is correct.");
   is(selection.end, null,
     "The initial selection end is correct.");
 
   yield teardown(panel);
   finish();
 });
--- a/browser/devtools/timeline/test/browser_timeline_overview-update.js
+++ b/browser/devtools/timeline/test/browser_timeline_overview-update.js
@@ -1,48 +1,74 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Tests if the overview graph is continuously updated.
+ * Tests if the markers and memory overviews are continuously updated.
  */
 
 let test = Task.async(function*() {
   let { target, panel } = yield initTimelinePanel("about:blank");
-  let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
+  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
 
-  yield DevToolsUtils.waitForTime(1000);
+  $("#memory-checkbox").checked = true;
+  yield TimelineController.updateMemoryRecording();
+
   yield TimelineController.toggleRecording();
   ok(true, "Recording has started.");
 
-  ok("selectionEnabled" in TimelineView.overview,
-    "The selection should not be enabled for the overview graph (1).");
-  is(TimelineView.overview.selectionEnabled, false,
-    "The selection should not be enabled for the overview graph (2).");
-  is(TimelineView.overview.hasSelection(), false,
-    "The overview graph shouldn't have a selection before recording.");
+  ok("selectionEnabled" in TimelineView.markersOverview,
+    "The selection should not be enabled for the markers overview (1).");
+  is(TimelineView.markersOverview.selectionEnabled, false,
+    "The selection should not be enabled for the markers overview (2).");
+  is(TimelineView.markersOverview.hasSelection(), false,
+    "The markers overview shouldn't have a selection before recording.");
+
+  ok("selectionEnabled" in TimelineView.memoryOverview,
+    "The selection should not be enabled for the memory overview (1).");
+  is(TimelineView.memoryOverview.selectionEnabled, false,
+    "The selection should not be enabled for the memory overview (2).");
+  is(TimelineView.memoryOverview.hasSelection(), false,
+    "The memory overview shouldn't have a selection before recording.");
 
   let updated = 0;
   panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
 
   ok((yield waitUntil(() => updated > 10)),
-    "The overview graph was updated a bunch of times.");
+    "The overviews were updated a bunch of times.");
+  ok((yield waitUntil(() => TimelineController.getMemory().length > 10)),
+    "There are some memory measurements available now.");
 
-  ok("selectionEnabled" in TimelineView.overview,
-    "The selection should still not be enabled for the overview graph (3).");
-  is(TimelineView.overview.selectionEnabled, false,
-    "The selection should still not be enabled for the overview graph (4).");
-  is(TimelineView.overview.hasSelection(), false,
-    "The overview graph should not have a selection while recording.");
+  ok("selectionEnabled" in TimelineView.markersOverview,
+    "The selection should still not be enabled for the markers overview (3).");
+  is(TimelineView.markersOverview.selectionEnabled, false,
+    "The selection should still not be enabled for the markers overview (4).");
+  is(TimelineView.markersOverview.hasSelection(), false,
+    "The markers overview should not have a selection while recording.");
+
+  ok("selectionEnabled" in TimelineView.memoryOverview,
+    "The selection should still not be enabled for the memory overview (3).");
+  is(TimelineView.memoryOverview.selectionEnabled, false,
+    "The selection should still not be enabled for the memory overview (4).");
+  is(TimelineView.memoryOverview.hasSelection(), false,
+    "The memory overview should not have a selection while recording.");
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has ended.");
 
   is(TimelineController.getMarkers().length, 0,
     "There are no markers available.");
-  is(TimelineView.overview.selectionEnabled, true,
-    "The selection should now be enabled for the overview graph.");
-  is(TimelineView.overview.hasSelection(), false,
-    "The overview graph should not have a selection after recording.");
+  isnot(TimelineController.getMemory().length, 0,
+    "There are some memory measurements available.");
+
+  is(TimelineView.markersOverview.selectionEnabled, true,
+    "The selection should now be enabled for the markers overview.");
+  is(TimelineView.markersOverview.hasSelection(), false,
+    "The markers overview should not have a selection after recording.");
+
+  is(TimelineView.memoryOverview.selectionEnabled, true,
+    "The selection should now be enabled for the memory overview.");
+  is(TimelineView.memoryOverview.hasSelection(), false,
+    "The memory overview should not have a selection after recording.");
 
   yield teardown(panel);
   finish();
 });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_recording-without-memory.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the timeline actor isn't unnecessarily asked to record memory.
+ */
+
+let test = Task.async(function*() {
+  let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
+  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has started.");
+
+  let updated = 0;
+  panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+
+  ok((yield waitUntil(() => updated > 10)),
+    "The overview graph was updated a bunch of times.");
+  ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+    "There are some markers available.");
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has ended.");
+
+  let markers = TimelineController.getMarkers();
+  let memory = TimelineController.getMemory();
+
+  isnot(markers.length, 0,
+    "There are some markers available.");
+  is(memory.length, 0,
+    "There are no memory measurements available.");
+
+  yield teardown(panel);
+  finish();
+});
--- a/browser/devtools/timeline/test/browser_timeline_recording.js
+++ b/browser/devtools/timeline/test/browser_timeline_recording.js
@@ -2,33 +2,39 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the timeline can properly start and stop a recording.
  */
 
 let test = Task.async(function*() {
   let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { gFront, TimelineController } = panel.panelWin;
+  let { $, gFront, TimelineController } = panel.panelWin;
+
+  $("#memory-checkbox").checked = true;
+  yield TimelineController.updateMemoryRecording();
 
   is((yield gFront.isRecording()), false,
     "The timeline actor should not be recording when the tool starts.");
   is(TimelineController.getMarkers().length, 0,
     "There should be no markers available when the tool starts.");
 
   yield TimelineController.toggleRecording();
 
   is((yield gFront.isRecording()), true,
     "The timeline actor should be recording now.");
   ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
     "There are some markers available now.");
+  ok((yield waitUntil(() => TimelineController.getMemory().length > 0)),
+    "There are some memory measurements available now.");
 
-  ok("startTime" in TimelineController.getMarkers(),
-    "A `startTime` field was set on the markers array.");
-  ok("endTime" in TimelineController.getMarkers(),
-    "An `endTime` field was set on the markers array.");
-  ok(TimelineController.getMarkers().endTime >
-     TimelineController.getMarkers().startTime,
+  ok("startTime" in TimelineController.getInterval(),
+    "A `startTime` field was set on the recording data.");
+  ok("endTime" in TimelineController.getInterval(),
+    "An `endTime` field was set on the recording data.");
+
+  ok(TimelineController.getInterval().endTime >
+     TimelineController.getInterval().startTime,
     "Some time has passed since the recording started.");
 
   yield teardown(panel);
   finish();
 });
--- a/browser/devtools/timeline/test/browser_timeline_waterfall-background.js
+++ b/browser/devtools/timeline/test/browser_timeline_waterfall-background.js
@@ -12,17 +12,17 @@ let test = Task.async(function*() {
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has started.");
 
   let updated = 0;
   panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
 
   ok((yield waitUntil(() => updated > 0)),
-    "The overview graph was updated a bunch of times.");
+    "The overview graphs were updated a bunch of times.");
   ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
     "There are some markers available.");
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has ended.");
 
   // Test the waterfall background.
 
--- a/browser/devtools/timeline/test/browser_timeline_waterfall-generic.js
+++ b/browser/devtools/timeline/test/browser_timeline_waterfall-generic.js
@@ -11,58 +11,58 @@ let test = Task.async(function*() {
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has started.");
 
   let updated = 0;
   panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
 
   ok((yield waitUntil(() => updated > 0)),
-    "The overview graph was updated a bunch of times.");
+    "The overview graphs were updated a bunch of times.");
   ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
     "There are some markers available.");
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has ended.");
 
   // Test the header container.
 
-  ok($(".timeline-header-container"),
+  ok($(".waterfall-header-container"),
     "A header container should have been created.");
 
   // Test the header sidebar (left).
 
-  ok($(".timeline-header-sidebar"),
+  ok($(".waterfall-header-container > .waterfall-sidebar"),
     "A header sidebar node should have been created.");
-  ok($(".timeline-header-sidebar > .timeline-header-name"),
+  ok($(".waterfall-header-container > .waterfall-sidebar > .waterfall-header-name"),
     "A header name label should have been created inside the sidebar.");
 
   // Test the header ticks (right).
 
-  ok($(".timeline-header-ticks"),
+  ok($(".waterfall-header-ticks"),
     "A header ticks node should have been created.");
-  ok($$(".timeline-header-ticks > .timeline-header-tick").length > 0,
+  ok($$(".waterfall-header-ticks > .waterfall-header-tick").length > 0,
     "Some header tick labels should have been created inside the tick node.");
 
   // Test the markers container.
 
-  ok($(".timeline-marker-container"),
+  ok($(".waterfall-marker-container"),
     "A marker container should have been created.");
 
   // Test the markers sidebar (left).
 
-  ok($$(".timeline-marker-sidebar").length,
+  ok($$(".waterfall-marker-container > .waterfall-sidebar").length,
     "Some marker sidebar nodes should have been created.");
-  ok($$(".timeline-marker-sidebar:not(spacer) > .timeline-marker-bullet").length,
+  ok($$(".waterfall-marker-container > .waterfall-sidebar:not(spacer) > .waterfall-marker-bullet").length,
     "Some marker color bullets should have been created inside the sidebar.");
-  ok($$(".timeline-marker-sidebar:not(spacer) > .timeline-marker-name").length,
+  ok($$(".waterfall-marker-container > .waterfall-sidebar:not(spacer) > .waterfall-marker-name").length,
     "Some marker name labels should have been created inside the sidebar.");
 
   // Test the markers waterfall (right).
 
-  ok($$(".timeline-marker-waterfall").length,
+  ok($$(".waterfall-marker-item").length,
     "Some marker waterfall nodes should have been created.");
-  ok($$(".timeline-marker-waterfall:not(spacer) > .timeline-marker-bar").length,
+  ok($$(".waterfall-marker-item:not(spacer) > .waterfall-marker-bar").length,
     "Some marker color bars should have been created inside the waterfall.");
 
   yield teardown(panel);
   finish();
 });
--- a/browser/devtools/timeline/test/browser_timeline_waterfall-styles.js
+++ b/browser/devtools/timeline/test/browser_timeline_waterfall-styles.js
@@ -22,17 +22,17 @@ let test = Task.async(function*() {
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has started.");
 
   let updated = 0;
   panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
 
   ok((yield waitUntil(() => updated > 0)),
-    "The overview graph was updated a bunch of times.");
+    "The overview graphs were updated a bunch of times.");
   ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
     "There are some markers available.");
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has ended.");
 
   // Test the table sidebars.
 
--- a/browser/devtools/timeline/timeline.js
+++ b/browser/devtools/timeline/timeline.js
@@ -7,34 +7,39 @@ const { classes: Cc, interfaces: Ci, uti
 
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 
 devtools.lazyRequireGetter(this, "promise");
 devtools.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 
-devtools.lazyRequireGetter(this, "Overview",
-  "devtools/timeline/overview", true);
+devtools.lazyRequireGetter(this, "MarkersOverview",
+  "devtools/timeline/markers-overview", true);
+devtools.lazyRequireGetter(this, "MemoryOverview",
+  "devtools/timeline/memory-overview", true);
 devtools.lazyRequireGetter(this, "Waterfall",
   "devtools/timeline/waterfall", true);
 
+devtools.lazyImporter(this, "CanvasGraphUtils",
+  "resource:///modules/devtools/Graphs.jsm");
+
 devtools.lazyImporter(this, "PluralForm",
   "resource://gre/modules/PluralForm.jsm");
 
 const OVERVIEW_UPDATE_INTERVAL = 200;
 const OVERVIEW_INITIAL_SELECTION_RATIO = 0.15;
 
 // The panel's window global is an EventEmitter firing the following events:
 const EVENTS = {
   // When a recording is started or stopped, via the `stopwatch` button.
   RECORDING_STARTED: "Timeline:RecordingStarted",
   RECORDING_ENDED: "Timeline:RecordingEnded",
 
-  // When the overview graph is populated with new markers.
+  // When the overview graphs are populated with new markers.
   OVERVIEW_UPDATED: "Timeline:OverviewUpdated",
 
   // When the waterfall view is populated with new markers.
   WATERFALL_UPDATED: "Timeline:WaterfallUpdated"
 };
 
 /**
  * The current target and the timeline front, set by this tool's host.
@@ -58,225 +63,330 @@ let shutdownTimeline = Task.async(functi
   yield gFront.stop();
 });
 
 /**
  * Functions handling the timeline frontend controller.
  */
 let TimelineController = {
   /**
-   * Permanent storage for the markers streamed by the backend.
+   * Permanent storage for the markers and the memory measurements streamed by
+   * the backend, along with the start and end timestamps.
    */
+  _starTime: 0,
+  _endTime: 0,
   _markers: [],
+  _memory: [],
 
   /**
    * Initialization function, called when the tool is started.
    */
   initialize: function() {
     this._onRecordingTick = this._onRecordingTick.bind(this);
     this._onMarkers = this._onMarkers.bind(this);
+    this._onMemory = this._onMemory.bind(this);
     gFront.on("markers", this._onMarkers);
+    gFront.on("memory", this._onMemory);
   },
 
   /**
    * Destruction function, called when the tool is closed.
    */
   destroy: function() {
     gFront.off("markers", this._onMarkers);
+    gFront.off("memory", this._onMemory);
+  },
+
+  /**
+   * Gets the { stat, end } time interval for this recording.
+   * @return object
+   */
+  getInterval: function() {
+    return { startTime: this._startTime, endTime: this._endTime };
   },
 
   /**
    * Gets the accumulated markers in this recording.
-   * @return array.
+   * @return array
    */
   getMarkers: function() {
     return this._markers;
   },
 
   /**
+   * Gets the accumulated memory measurements in this recording.
+   * @return array
+   */
+  getMemory: function() {
+    return this._memory;
+  },
+
+  /**
+   * Updates the views to show or hide the memory recording data.
+   */
+  updateMemoryRecording: Task.async(function*() {
+    if ($("#memory-checkbox").checked) {
+      yield TimelineView.showMemoryOverview();
+    } else {
+      yield TimelineView.hideMemoryOverview();
+    }
+  }),
+
+  /**
    * Starts/stops the timeline recording and streaming.
    */
   toggleRecording: Task.async(function*() {
     let isRecording = yield gFront.isRecording();
     if (isRecording == false) {
       yield this._startRecording();
     } else {
       yield this._stopRecording();
     }
   }),
 
   /**
    * Starts the recording, updating the UI as needed.
    */
   _startRecording: function*() {
     TimelineView.handleRecordingStarted();
-    let startTime = yield gFront.start();
+
+    let withMemory = $("#memory-checkbox").checked;
+    let startTime = yield gFront.start({ withMemory });
+
     // Times must come from the actor in order to be self-consistent.
     // However, we also want to update the view with the elapsed time
     // even when the actor is not generating data.  To do this we get
     // the local time and use it to compute a reasonable elapsed time.
     // See _onRecordingTick.
     this._localStartTime = performance.now();
-
+    this._startTime = startTime;
+    this._endTime = startTime;
     this._markers = [];
-    this._markers.startTime = startTime;
-    this._markers.endTime = startTime;
+    this._memory = [];
     this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
   },
 
   /**
    * Stops the recording, updating the UI as needed.
    */
   _stopRecording: function*() {
     clearInterval(this._updateId);
 
     // Sorting markers is only important when displayed in the waterfall.
     this._markers = this._markers.sort((a,b) => (a.start > b.start));
 
-    TimelineView.handleMarkersUpdate(this._markers);
+    TimelineView.handleRecordingUpdate();
     TimelineView.handleRecordingEnded();
     yield gFront.stop();
   },
 
   /**
    * Used in tests. Stops the recording, discarding the accumulated markers and
    * updating the UI as needed.
    */
   _stopRecordingAndDiscardData: function*() {
     yield this._stopRecording();
     this._markers.length = 0;
+    this._memory.length = 0;
   },
 
   /**
    * Callback handling the "markers" event on the timeline front.
    *
    * @param array markers
    *        A list of new markers collected since the last time this
    *        function was invoked.
    * @param number endTime
    *        A time after the last marker in markers was collected.
    */
   _onMarkers: function(markers, endTime) {
     Array.prototype.push.apply(this._markers, markers);
-    this._markers.endTime = endTime;
+    this._endTime = endTime;
+  },
+
+  /**
+   * Callback handling the "memory" event on the timeline front.
+   *
+   * @param number delta
+   *        The number of milliseconds elapsed since epoch.
+   * @param object measurement
+   *        A detailed breakdown of the current memory usage.
+   */
+  _onMemory: function(delta, measurement) {
+    this._memory.push({ delta, value: measurement.total / 1024 / 1024 });
   },
 
   /**
    * Callback invoked at a fixed interval while recording.
-   * Updates the markers store with the current time and the timeline overview.
+   * Updates the current time and the timeline overview.
    */
   _onRecordingTick: function() {
     // Compute an approximate ending time for the view.  This is
     // needed to ensure that the view updates even when new data is
     // not being generated.
-    let fakeTime = this._markers.startTime + (performance.now() - this._localStartTime);
-    if (fakeTime > this._markers.endTime) {
-      this._markers.endTime = fakeTime;
+    let fakeTime = this._startTime + (performance.now() - this._localStartTime);
+    if (fakeTime > this._endTime) {
+      this._endTime = fakeTime;
     }
-    TimelineView.handleMarkersUpdate(this._markers);
+    TimelineView.handleRecordingUpdate();
   }
 };
 
 /**
  * Functions handling the timeline frontend view.
  */
 let TimelineView = {
   /**
    * Initialization function, called when the tool is started.
    */
   initialize: Task.async(function*() {
-    this.overview = new Overview($("#timeline-overview"));
+    this.markersOverview = new MarkersOverview($("#markers-overview"));
     this.waterfall = new Waterfall($("#timeline-waterfall"));
 
     this._onSelecting = this._onSelecting.bind(this);
     this._onRefresh = this._onRefresh.bind(this);
-    this.overview.on("selecting", this._onSelecting);
-    this.overview.on("refresh", this._onRefresh);
+    this.markersOverview.on("selecting", this._onSelecting);
+    this.markersOverview.on("refresh", this._onRefresh);
 
-    yield this.overview.ready();
+    yield this.markersOverview.ready();
     yield this.waterfall.recalculateBounds();
   }),
 
   /**
    * Destruction function, called when the tool is closed.
    */
   destroy: function() {
-    this.overview.off("selecting", this._onSelecting);
-    this.overview.off("refresh", this._onRefresh);
-    this.overview.destroy();
+    this.markersOverview.off("selecting", this._onSelecting);
+    this.markersOverview.off("refresh", this._onRefresh);
+    this.markersOverview.destroy();
+
+    // The memory overview graph is not always available.
+    if (this.memoryOverview) {
+      this.memoryOverview.destroy();
+    }
+  },
+
+  /**
+   * Shows the memory overview graph.
+   */
+  showMemoryOverview: Task.async(function*() {
+    this.memoryOverview = new MemoryOverview($("#memory-overview"));
+    yield this.memoryOverview.ready();
+
+    let interval = TimelineController.getInterval();
+    let memory = TimelineController.getMemory();
+    this.memoryOverview.setData({ interval, memory });
+
+    CanvasGraphUtils.linkAnimation(this.markersOverview, this.memoryOverview);
+    CanvasGraphUtils.linkSelection(this.markersOverview, this.memoryOverview);
+  }),
+
+  /**
+   * Hides the memory overview graph.
+   */
+  hideMemoryOverview: function() {
+    if (!this.memoryOverview) {
+      return;
+    }
+    this.memoryOverview.destroy();
+    this.memoryOverview = null;
   },
 
   /**
    * Signals that a recording session has started and triggers the appropriate
    * changes in the UI.
    */
   handleRecordingStarted: function() {
     $("#record-button").setAttribute("checked", "true");
+    $("#memory-checkbox").setAttribute("disabled", "true");
     $("#timeline-pane").selectedPanel = $("#recording-notice");
 
-    this.overview.selectionEnabled = false;
-    this.overview.dropSelection();
-    this.overview.setData([]);
+    this.markersOverview.clearView();
+
+    // The memory overview graph is not always available.
+    if (this.memoryOverview) {
+      this.memoryOverview.clearView();
+    }
+
     this.waterfall.clearView();
 
     window.emit(EVENTS.RECORDING_STARTED);
   },
 
   /**
    * Signals that a recording session has ended and triggers the appropriate
    * changes in the UI.
    */
   handleRecordingEnded: function() {
     $("#record-button").removeAttribute("checked");
+    $("#memory-checkbox").removeAttribute("disabled");
     $("#timeline-pane").selectedPanel = $("#timeline-waterfall");
 
-    this.overview.selectionEnabled = true;
+    this.markersOverview.selectionEnabled = true;
 
+    // The memory overview graph is not always available.
+    if (this.memoryOverview) {
+      this.memoryOverview.selectionEnabled = true;
+    }
+
+    let interval = TimelineController.getInterval();
     let markers = TimelineController.getMarkers();
+    let memory = TimelineController.getMemory();
+
     if (markers.length) {
-      let start = (markers[0].start - markers.startTime) * this.overview.dataScaleX;
-      let end = start + this.overview.width * OVERVIEW_INITIAL_SELECTION_RATIO;
-      this.overview.setSelection({ start, end });
+      let start = (markers[0].start - interval.startTime) * this.markersOverview.dataScaleX;
+      let end = start + this.markersOverview.width * OVERVIEW_INITIAL_SELECTION_RATIO;
+      this.markersOverview.setSelection({ start, end });
     } else {
-      let duration = markers.endTime - markers.startTime;
-      this.waterfall.setData(markers, markers.startTime, markers.endTime);
+      let timeStart = interval.startTime;
+      let timeEnd = interval.endTime;
+      this.waterfall.setData(markers, timeStart, timeStart, timeEnd);
     }
 
     window.emit(EVENTS.RECORDING_ENDED);
   },
 
   /**
    * Signals that a new set of markers was made available by the controller,
    * or that the overview graph needs to be updated.
-   *
-   * @param array markers
-   *        A list of new markers collected since the recording has started.
    */
-  handleMarkersUpdate: function(markers) {
-    this.overview.setData(markers);
+  handleRecordingUpdate: function() {
+    let interval = TimelineController.getInterval();
+    let markers = TimelineController.getMarkers();
+    let memory = TimelineController.getMemory();
+
+    this.markersOverview.setData({ interval, markers });
+
+    // The memory overview graph is not always available.
+    if (this.memoryOverview) {
+      this.memoryOverview.setData({ interval, memory });
+    }
+
     window.emit(EVENTS.OVERVIEW_UPDATED);
   },
 
   /**
    * Callback handling the "selecting" event on the timeline overview.
    */
   _onSelecting: function() {
-    if (!this.overview.hasSelection() &&
-        !this.overview.hasSelectionInProgress()) {
+    if (!this.markersOverview.hasSelection() &&
+        !this.markersOverview.hasSelectionInProgress()) {
       this.waterfall.clearView();
       return;
     }
-    let selection = this.overview.getSelection();
-    let start = selection.start / this.overview.dataScaleX;
-    let end = selection.end / this.overview.dataScaleX;
+    let selection = this.markersOverview.getSelection();
+    let start = selection.start / this.markersOverview.dataScaleX;
+    let end = selection.end / this.markersOverview.dataScaleX;
 
     let markers = TimelineController.getMarkers();
-    let timeStart = markers.startTime + Math.min(start, end);
-    let timeEnd = markers.startTime + Math.max(start, end);
-    this.waterfall.setData(markers, timeStart, timeEnd);
+    let interval = TimelineController.getInterval();
+
+    let timeStart = interval.startTime + Math.min(start, end);
+    let timeEnd = interval.startTime + Math.max(start, end);
+    this.waterfall.setData(markers, interval.startTime, timeStart, timeEnd);
   },
 
   /**
    * Callback handling the "refresh" event on the timeline overview.
    */
   _onRefresh: function() {
     this.waterfall.recalculateBounds();
     this._onSelecting();
--- a/browser/devtools/timeline/timeline.xul
+++ b/browser/devtools/timeline/timeline.xul
@@ -20,23 +20,27 @@
              class="devtools-toolbar">
       <hbox id="recordings-controls"
             class="devtools-toolbarbutton-group"
             align="center">
         <toolbarbutton id="record-button"
                        class="devtools-toolbarbutton"
                        oncommand="TimelineController.toggleRecording()"
                        tooltiptext="&timelineUI.recordButton.tooltip;"/>
-        <spacer flex="1"/>
+        <checkbox id="memory-checkbox"
+                  label="&timelineUI.memoryCheckbox.label;"
+                  oncommand="TimelineController.updateMemoryRecording()"
+                  tooltiptext="&timelineUI.memoryCheckbox.tooltip;"/>
         <label id="record-label"
                value="&timelineUI.recordLabel;"/>
       </hbox>
     </toolbar>
 
-    <vbox id="timeline-overview"/>
+    <vbox id="markers-overview"/>
+    <vbox id="memory-overview"/>
 
     <deck id="timeline-pane"
           flex="1">
       <hbox id="empty-notice"
             class="notice-container"
             align="center"
             pack="center"
             flex="1">
rename from browser/devtools/timeline/widgets/overview.js
rename to browser/devtools/timeline/widgets/markers-overview.js
--- a/browser/devtools/timeline/widgets/overview.js
+++ b/browser/devtools/timeline/widgets/markers-overview.js
@@ -1,73 +1,73 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 /**
- * This file contains the "overview" graph, which is a minimap of all the
- * timeline data. Regions inside it may be selected, determining which markers
- * are visible in the "waterfall".
+ * This file contains the "markers overview" graph, which is a minimap of all
+ * the timeline data. Regions inside it may be selected, determining which
+ * markers are visible in the "waterfall".
  */
 
 const {Cc, Ci, Cu, Cr} = require("chrome");
 
 Cu.import("resource:///modules/devtools/Graphs.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 loader.lazyRequireGetter(this, "L10N",
   "devtools/timeline/global", true);
 loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
   "devtools/timeline/global", true);
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
-const OVERVIEW_HEADER_HEIGHT = 20; // px
-const OVERVIEW_BODY_HEIGHT = 50; // px
+const OVERVIEW_HEADER_HEIGHT = 14; // px
+const OVERVIEW_BODY_HEIGHT = 55; // 11px * 5 groups
 
 const OVERVIEW_BACKGROUND_COLOR = "#fff";
 const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
 const OVERVIEW_SELECTION_LINE_COLOR = "#555";
 const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)";
 const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
 
 const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms
 const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px
 const OVERVIEW_HEADER_SAFE_BOUNDS = 50; // px
-const OVERVIEW_HEADER_BACKGROUND = "#ebeced";
+const OVERVIEW_HEADER_BACKGROUND = "#fff";
 const OVERVIEW_HEADER_TEXT_COLOR = "#18191a";
 const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
 const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
-const OVERVIEW_HEADER_TEXT_PADDING = 6; // px
-const OVERVIEW_TIMELINE_STROKES = "#aaa";
+const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6; // px
+const OVERVIEW_HEADER_TEXT_PADDING_TOP = 1; // px
+const OVERVIEW_TIMELINE_STROKES = "#ccc";
 const OVERVIEW_MARKERS_COLOR_STOPS = [0, 0.1, 0.75, 1];
 const OVERVIEW_MARKER_DURATION_MIN = 4; // ms
-const OVERVIEW_GROUP_VERTICAL_PADDING = 6; // px
+const OVERVIEW_GROUP_VERTICAL_PADDING = 5; // px
 const OVERVIEW_GROUP_ALTERNATING_BACKGROUND = "rgba(0,0,0,0.05)";
 
 /**
- * An overview for the timeline data.
+ * An overview for the markers data.
  *
  * @param nsIDOMNode parent
  *        The parent node holding the overview.
  */
-function Overview(parent, ...args) {
-  AbstractCanvasGraph.apply(this, [parent, "timeline-overview", ...args]);
+function MarkersOverview(parent, ...args) {
+  AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
   this.once("ready", () => {
+    // Set the list of names, properties and colors used to paint this overview.
     this.setBlueprint(TIMELINE_BLUEPRINT);
 
-    var preview = [];
-    preview.startTime = 0;
-    preview.endTime = 1000;
-    this.setData(preview);
+    // Populate this overview with some dummy initial data.
+    this.setData({ interval: { startTime: 0, endTime: 1000 }, markers: [] });
   });
 }
 
-Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
   fixedHeight: OVERVIEW_HEADER_HEIGHT + OVERVIEW_BODY_HEIGHT,
   clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
   selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
   selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR,
   selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR,
 
   /**
    * List of names and colors used to paint this overview.
@@ -79,83 +79,97 @@ Overview.prototype = Heritage.extend(Abs
 
     for (let type in blueprint) {
       this._paintBatches.set(type, { style: blueprint[type], batch: [] });
       this._lastGroup = Math.max(this._lastGroup, blueprint[type].group);
     }
   },
 
   /**
+   * Disables selection and empties this graph.
+   */
+  clearView: function() {
+    this.selectionEnabled = false;
+    this.dropSelection();
+    this.setData({ interval: { startTime: 0, endTime: 0 }, markers: [] });
+  },
+
+  /**
    * Renders the graph's data source.
    * @see AbstractCanvasGraph.prototype.buildGraphImage
    */
   buildGraphImage: function() {
+    let { interval, markers } = this._data;
+    let { startTime, endTime } = interval;
+
     let { canvas, ctx } = this._getNamedCanvas("overview-data");
     let canvasWidth = this._width;
     let canvasHeight = this._height;
     let safeBounds = OVERVIEW_HEADER_SAFE_BOUNDS * this._pixelRatio;
     let availableWidth = canvasWidth - safeBounds;
 
     // Group markers into separate paint batches. This is necessary to
     // draw all markers sharing the same style at once.
 
-    for (let marker of this._data) {
+    for (let marker of markers) {
       this._paintBatches.get(marker.name).batch.push(marker);
     }
 
     // Calculate each group's height, and the time-based scaling.
 
     let totalGroups = this._lastGroup + 1;
     let headerHeight = OVERVIEW_HEADER_HEIGHT * this._pixelRatio;
     let groupHeight = OVERVIEW_BODY_HEIGHT * this._pixelRatio / totalGroups;
     let groupPadding = OVERVIEW_GROUP_VERTICAL_PADDING * this._pixelRatio;
 
-    let totalTime = (this._data.endTime - this._data.startTime) || 0;
+    let totalTime = (endTime - startTime) || 0;
     let dataScale = this.dataScaleX = availableWidth / totalTime;
 
     // Draw the header and overview background.
 
     ctx.fillStyle = OVERVIEW_HEADER_BACKGROUND;
     ctx.fillRect(0, 0, canvasWidth, headerHeight);
 
     ctx.fillStyle = OVERVIEW_BACKGROUND_COLOR;
     ctx.fillRect(0, headerHeight, canvasWidth, canvasHeight);
 
     // Draw the alternating odd/even group backgrounds.
 
     ctx.fillStyle = OVERVIEW_GROUP_ALTERNATING_BACKGROUND;
     ctx.beginPath();
 
-    for (let i = 1; i < totalGroups; i += 2) {
+    for (let i = 0; i < totalGroups; i += 2) {
       let top = headerHeight + i * groupHeight;
       ctx.rect(0, top, canvasWidth, groupHeight);
     }
 
     ctx.fill();
 
     // Draw the timeline header ticks.
 
-    ctx.textBaseline = "middle";
     let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
     let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
+    let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
+    let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
+    let tickInterval = this._findOptimalTickInterval(dataScale);
+
+    ctx.textBaseline = "middle";
     ctx.font = fontSize + "px " + fontFamily;
     ctx.fillStyle = OVERVIEW_HEADER_TEXT_COLOR;
     ctx.strokeStyle = OVERVIEW_TIMELINE_STROKES;
     ctx.beginPath();
 
-    let tickInterval = this._findOptimalTickInterval(dataScale);
-    let headerTextPadding = OVERVIEW_HEADER_TEXT_PADDING * this._pixelRatio;
-
     for (let x = 0; x < availableWidth; x += tickInterval) {
-      let left = x + headerTextPadding;
+      let lineLeft = x;
+      let textLeft = lineLeft + textPaddingLeft;
       let time = Math.round(x / dataScale);
       let label = L10N.getFormatStr("timeline.tick", time);
-      ctx.fillText(label, left, headerHeight / 2 + 1);
-      ctx.moveTo(x, 0);
-      ctx.lineTo(x, canvasHeight);
+      ctx.fillText(label, textLeft, headerHeight / 2 + textPaddingTop);
+      ctx.moveTo(lineLeft, 0);
+      ctx.lineTo(lineLeft, canvasHeight);
     }
 
     ctx.stroke();
 
     // Draw the timeline markers.
 
     for (let [, { style, batch }] of this._paintBatches) {
       let top = headerHeight + style.group * groupHeight + groupPadding / 2;
@@ -165,18 +179,18 @@ Overview.prototype = Heritage.extend(Abs
       gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[0], style.stroke);
       gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[1], style.fill);
       gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[2], style.fill);
       gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[3], style.stroke);
       ctx.fillStyle = gradient;
       ctx.beginPath();
 
       for (let { start, end } of batch) {
-        start -= this._data.startTime;
-        end -= this._data.startTime;
+        start -= interval.startTime;
+        end -= interval.startTime;
 
         let left = start * dataScale;
         let duration = Math.max(end - start, OVERVIEW_MARKER_DURATION_MIN);
         let width = Math.max(duration * dataScale, this._pixelRatio);
         ctx.rect(left, top, width, height);
       }
 
       ctx.fill();
@@ -203,9 +217,9 @@ Overview.prototype = Heritage.extend(Abs
         timingStep <<= 1;
         continue;
       }
       return scaledStep;
     }
   }
 });
 
-exports.Overview = Overview;
+exports.MarkersOverview = MarkersOverview;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/widgets/memory-overview.js
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * This file contains the "memory overview" graph, a simple representation of
+ * of all the memory measurements taken while streaming the timeline data.
+ */
+
+const {Cc, Ci, Cu, Cr} = require("chrome");
+
+Cu.import("resource:///modules/devtools/Graphs.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+loader.lazyRequireGetter(this, "L10N",
+  "devtools/timeline/global", true);
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const OVERVIEW_HEIGHT = 30; // px
+
+const OVERVIEW_BACKGROUND_COLOR = "#fff";
+const OVERVIEW_BACKGROUND_GRADIENT_START = "rgba(0,136,204,0.1)";
+const OVERVIEW_BACKGROUND_GRADIENT_END = "rgba(0,136,204,0.0)";
+const OVERVIEW_STROKE_WIDTH = 1; // px
+const OVERVIEW_STROKE_COLOR = "rgba(0,136,204,1)";
+const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
+const OVERVIEW_SELECTION_LINE_COLOR = "#555";
+const OVERVIEW_MAXIMUM_LINE_COLOR = "rgba(0,136,204,0.4)";
+const OVERVIEW_AVERAGE_LINE_COLOR = "rgba(0,136,204,0.7)";
+const OVERVIEW_MINIMUM_LINE_COLOR = "rgba(0,136,204,0.9)";
+
+const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)";
+const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
+
+/**
+ * An overview for the memory data.
+ *
+ * @param nsIDOMNode parent
+ *        The parent node holding the overview.
+ */
+function MemoryOverview(parent) {
+  LineGraphWidget.call(this, parent, L10N.getStr("graphs.memory"));
+
+  this.once("ready", () => {
+    // Populate this overview with some dummy initial data.
+    this.setData({ interval: { startTime: 0, endTime: 1000 }, memory: [] });
+  });
+}
+
+MemoryOverview.prototype = Heritage.extend(LineGraphWidget.prototype, {
+  dampenValuesFactor: 0.95,
+  fixedHeight: OVERVIEW_HEIGHT,
+  backgroundColor: OVERVIEW_BACKGROUND_COLOR,
+  backgroundGradientStart: OVERVIEW_BACKGROUND_GRADIENT_START,
+  backgroundGradientEnd: OVERVIEW_BACKGROUND_GRADIENT_END,
+  strokeColor: OVERVIEW_STROKE_COLOR,
+  strokeWidth: OVERVIEW_STROKE_WIDTH,
+  maximumLineColor: OVERVIEW_MAXIMUM_LINE_COLOR,
+  averageLineColor: OVERVIEW_AVERAGE_LINE_COLOR,
+  minimumLineColor: OVERVIEW_MINIMUM_LINE_COLOR,
+  clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
+  selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
+  selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR,
+  selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR,
+  withTooltipArrows: false,
+  withFixedTooltipPositions: true,
+
+  /**
+   * Disables selection and empties this graph.
+   */
+  clearView: function() {
+    this.selectionEnabled = false;
+    this.dropSelection();
+    this.setData({ interval: { startTime: 0, endTime: 0 }, memory: [] });
+  },
+
+  /**
+   * Sets the data source for this graph.
+   */
+  setData: function({ interval, memory }) {
+    this.dataOffsetX = interval.startTime;
+    LineGraphWidget.prototype.setData.call(this, memory);
+  }
+});
+
+exports.MemoryOverview = MemoryOverview;
--- a/browser/devtools/timeline/widgets/waterfall.js
+++ b/browser/devtools/timeline/widgets/waterfall.js
@@ -17,51 +17,51 @@ loader.lazyRequireGetter(this, "TIMELINE
 
 loader.lazyImporter(this, "setNamedTimeout",
   "resource:///modules/devtools/ViewHelpers.jsm");
 loader.lazyImporter(this, "clearNamedTimeout",
   "resource:///modules/devtools/ViewHelpers.jsm");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
-const TIMELINE_IMMEDIATE_DRAW_MARKERS_COUNT = 30;
-const TIMELINE_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms
+const WATERFALL_SIDEBAR_WIDTH = 150; // px
 
-const TIMELINE_HEADER_TICKS_MULTIPLE = 5; // ms
-const TIMELINE_HEADER_TICKS_SPACING_MIN = 50; // px
-const TIMELINE_HEADER_TEXT_PADDING = 3; // px
+const WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT = 30;
+const WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms
 
-const TIMELINE_MARKER_SIDEBAR_WIDTH = 150; // px
-const TIMELINE_MARKER_BAR_WIDTH_MIN = 5; // px
+const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
+const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px
+const WATERFALL_HEADER_TEXT_PADDING = 3; // px
 
 const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
 const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
 const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
 const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
 const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
 const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
+const WATERFALL_MARKER_BAR_WIDTH_MIN = 5; // px
 
 /**
  * A detailed waterfall view for the timeline data.
  *
  * @param nsIDOMNode parent
  *        The parent node holding the waterfall.
  */
 function Waterfall(parent) {
   this._parent = parent;
   this._document = parent.ownerDocument;
   this._fragment = this._document.createDocumentFragment();
   this._outstandingMarkers = [];
 
   this._headerContents = this._document.createElement("hbox");
-  this._headerContents.className = "timeline-header-contents";
+  this._headerContents.className = "waterfall-header-contents";
   this._parent.appendChild(this._headerContents);
 
   this._listContents = this._document.createElement("vbox");
-  this._listContents.className = "timeline-list-contents";
+  this._listContents.className = "waterfall-list-contents";
   this._listContents.setAttribute("flex", "1");
   this._parent.appendChild(this._listContents);
 
   this._isRTL = this._getRTL();
 
   // Lazy require is a bit slow, and these are hot objects.
   this._l10n = L10N;
   this._blueprint = TIMELINE_BLUEPRINT;
@@ -70,28 +70,31 @@ function Waterfall(parent) {
 }
 
 Waterfall.prototype = {
   /**
    * Populates this view with the provided data source.
    *
    * @param array markers
    *        A list of markers received from the controller.
+   * @param number timeEpoch
+   *        The absolute time (in milliseconds) when the recording started.
    * @param number timeStart
    *        The time (in milliseconds) to start drawing from.
    * @param number timeEnd
    *        The time (in milliseconds) to end drawing at.
    */
-  setData: function(markers, timeStart, timeEnd) {
+  setData: function(markers, timeEpoch, timeStart, timeEnd) {
     this.clearView();
 
     let dataScale = this._waterfallWidth / (timeEnd - timeStart);
     this._drawWaterfallBackground(dataScale);
+
     // Label the header as if the first possible marker was at T=0.
-    this._buildHeader(this._headerContents, timeStart - markers.startTime, dataScale);
+    this._buildHeader(this._headerContents, timeStart - timeEpoch, dataScale);
     this._buildMarkers(this._listContents, markers, timeStart, timeEnd, dataScale);
   },
 
   /**
    * Depopulates this view.
    */
   clearView: function() {
     while (this._headerContents.hasChildNodes()) {
@@ -106,66 +109,66 @@ Waterfall.prototype = {
   },
 
   /**
    * Calculates and stores the available width for the waterfall.
    * This should be invoked every time the container window is resized.
    */
   recalculateBounds: function() {
     let bounds = this._parent.getBoundingClientRect();
-    this._waterfallWidth = bounds.width - TIMELINE_MARKER_SIDEBAR_WIDTH;
+    this._waterfallWidth = bounds.width - WATERFALL_SIDEBAR_WIDTH;
   },
 
   /**
    * Creates the header part of this view.
    *
    * @param nsIDOMNode parent
    *        The parent node holding the header.
    * @param number timeStart
    *        @see Waterfall.prototype.setData
    * @param number dataScale
    *        The time scale of the data source.
    */
   _buildHeader: function(parent, timeStart, dataScale) {
     let container = this._document.createElement("hbox");
-    container.className = "timeline-header-container";
+    container.className = "waterfall-header-container";
     container.setAttribute("flex", "1");
 
     let sidebar = this._document.createElement("hbox");
-    sidebar.className = "timeline-header-sidebar theme-sidebar";
-    sidebar.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
+    sidebar.className = "waterfall-sidebar theme-sidebar";
+    sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
     sidebar.setAttribute("align", "center");
     container.appendChild(sidebar);
 
     let name = this._document.createElement("label");
-    name.className = "plain timeline-header-name";
+    name.className = "plain waterfall-header-name";
     name.setAttribute("value", this._l10n.getStr("timeline.records"));
     sidebar.appendChild(name);
 
     let ticks = this._document.createElement("hbox");
-    ticks.className = "timeline-header-ticks";
+    ticks.className = "waterfall-header-ticks waterfall-background-ticks";
     ticks.setAttribute("align", "center");
     ticks.setAttribute("flex", "1");
     container.appendChild(ticks);
 
     let offset = this._isRTL ? this._waterfallWidth : 0;
     let direction = this._isRTL ? -1 : 1;
     let tickInterval = this._findOptimalTickInterval({
-      ticksMultiple: TIMELINE_HEADER_TICKS_MULTIPLE,
-      ticksSpacingMin: TIMELINE_HEADER_TICKS_SPACING_MIN,
+      ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
+      ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
       dataScale: dataScale
     });
 
     for (let x = 0; x < this._waterfallWidth; x += tickInterval) {
-      let start = x + direction * TIMELINE_HEADER_TEXT_PADDING;
+      let start = x + direction * WATERFALL_HEADER_TEXT_PADDING;
       let time = Math.round(timeStart + x / dataScale);
       let label = this._l10n.getFormatStr("timeline.tick", time);
 
       let node = this._document.createElement("label");
-      node.className = "plain timeline-header-tick";
+      node.className = "plain waterfall-header-tick";
       node.style.transform = "translateX(" + (start - offset) + "px)";
       node.setAttribute("value", label);
       ticks.appendChild(node);
     }
 
     parent.appendChild(container);
   },
 
@@ -185,32 +188,32 @@ Waterfall.prototype = {
     for (let marker of markers) {
       if (!isMarkerInRange(marker, timeStart, timeEnd)) {
         continue;
       }
       // Only build and display a finite number of markers initially, to
       // preserve a snappy UI. After a certain delay, continue building the
       // outstanding markers while there's (hopefully) no user interaction.
       let arguments_ = [this._fragment, marker, timeStart, dataScale];
-      if (processed++ < TIMELINE_IMMEDIATE_DRAW_MARKERS_COUNT) {
+      if (processed++ < WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT) {
         this._buildMarker.apply(this, arguments_);
       } else {
         this._outstandingMarkers.push(arguments_);
       }
     }
 
     // If there are no outstanding markers, add a dummy "spacer" at the end
     // to fill up any remaining available space in the UI.
     if (!this._outstandingMarkers.length) {
       this._buildMarker(this._fragment, null);
     }
     // Otherwise prepare flushing the outstanding markers after a small delay.
     else {
       this._setNamedTimeout("flush-outstanding-markers",
-        TIMELINE_FLUSH_OUTSTANDING_MARKERS_DELAY,
+        WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY,
         () => this._buildOutstandingMarkers(parent));
     }
 
     parent.appendChild(this._fragment);
   },
 
   /**
    * Finishes building the outstanding markers in this view.
@@ -236,17 +239,17 @@ Waterfall.prototype = {
    *        The { name, start, end } marker in the data source.
    * @param timeStart
    *        @see Waterfall.prototype.setData
    * @param number dataScale
    *        @see Waterfall.prototype._buildMarkers
    */
   _buildMarker: function(parent, marker, timeStart, dataScale) {
     let container = this._document.createElement("hbox");
-    container.className = "timeline-marker-container";
+    container.className = "waterfall-marker-container";
 
     if (marker) {
       this._buildMarkerSidebar(container, marker);
       this._buildMarkerWaterfall(container, marker, timeStart, dataScale);
     } else {
       this._buildMarkerSpacer(container);
       container.setAttribute("flex", "1");
       container.setAttribute("is-spacer", "");
@@ -262,31 +265,31 @@ Waterfall.prototype = {
    *        The container node representing the marker in this view.
    * @param object marker
    *        @see Waterfall.prototype._buildMarker
    */
   _buildMarkerSidebar: function(container, marker) {
     let blueprint = this._blueprint[marker.name];
 
     let sidebar = this._document.createElement("hbox");
-    sidebar.className = "timeline-marker-sidebar theme-sidebar";
-    sidebar.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
+    sidebar.className = "waterfall-sidebar theme-sidebar";
+    sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
     sidebar.setAttribute("align", "center");
 
     let bullet = this._document.createElement("hbox");
-    bullet.className = "timeline-marker-bullet";
+    bullet.className = "waterfall-marker-bullet";
     bullet.style.backgroundColor = blueprint.fill;
     bullet.style.borderColor = blueprint.stroke;
     bullet.setAttribute("type", marker.name);
     sidebar.appendChild(bullet);
 
     let name = this._document.createElement("label");
     name.setAttribute("crop", "end");
     name.setAttribute("flex", "1");
-    name.className = "plain timeline-marker-name";
+    name.className = "plain waterfall-marker-name";
 
     let label;
     if (marker.detail && marker.detail.causeName) {
       label = this._l10n.getFormatStr("timeline.markerDetailFormat",
                                       blueprint.label,
                                       marker.detail.causeName);
     } else {
       label = blueprint.label;
@@ -309,48 +312,49 @@ Waterfall.prototype = {
    *        @see Waterfall.prototype.setData
    * @param number dataScale
    *        @see Waterfall.prototype._buildMarkers
    */
   _buildMarkerWaterfall: function(container, marker, timeStart, dataScale) {
     let blueprint = this._blueprint[marker.name];
 
     let waterfall = this._document.createElement("hbox");
-    waterfall.className = "timeline-marker-waterfall";
+    waterfall.className = "waterfall-marker-item waterfall-background-ticks";
+    waterfall.setAttribute("align", "center");
     waterfall.setAttribute("flex", "1");
 
     let start = (marker.start - timeStart) * dataScale;
     let width = (marker.end - marker.start) * dataScale;
     let offset = this._isRTL ? this._waterfallWidth : 0;
 
     let bar = this._document.createElement("hbox");
-    bar.className = "timeline-marker-bar";
+    bar.className = "waterfall-marker-bar";
     bar.style.backgroundColor = blueprint.fill;
     bar.style.borderColor = blueprint.stroke;
     bar.style.transform = "translateX(" + (start - offset) + "px)";
     bar.setAttribute("type", marker.name);
-    bar.setAttribute("width", Math.max(width, TIMELINE_MARKER_BAR_WIDTH_MIN));
+    bar.setAttribute("width", Math.max(width, WATERFALL_MARKER_BAR_WIDTH_MIN));
     waterfall.appendChild(bar);
 
     container.appendChild(waterfall);
   },
 
   /**
    * Creates a dummy spacer as an empty marker.
    *
    * @param nsIDOMNode container
    *        The container node representing the marker.
    */
   _buildMarkerSpacer: function(container) {
     let sidebarSpacer = this._document.createElement("spacer");
-    sidebarSpacer.className = "timeline-marker-sidebar theme-sidebar";
-    sidebarSpacer.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
+    sidebarSpacer.className = "waterfall-sidebar theme-sidebar";
+    sidebarSpacer.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
 
     let waterfallSpacer = this._document.createElement("spacer");
-    waterfallSpacer.className = "timeline-marker-waterfall";
+    waterfallSpacer.className = "waterfall-marker-item waterfall-background-ticks";
     waterfallSpacer.setAttribute("flex", "1");
 
     container.appendChild(sidebarSpacer);
     container.appendChild(waterfallSpacer);
   },
 
   /**
    * Creates the background displayed on the marker's waterfall.
--- a/browser/locales/en-US/chrome/browser/devtools/timeline.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/timeline.dtd
@@ -10,20 +10,29 @@
   - You want to make that choice consistent across the developer tools.
   - A good criteria is the language in which you'd find the best
   - documentation on web development on the web. -->
 
 <!-- LOCALIZATION NOTE (timelineUI.recordButton): This string is displayed
   -  on a button that starts a new recording. -->
 <!ENTITY timelineUI.recordButton.tooltip "Record timeline operations">
 
-<!-- LOCALIZATION NOTE (timelineUI.recordButton): This string is displayed
+<!-- LOCALIZATION NOTE (timelineUI.recordLabel): This string is displayed
   -  as a label to signal that a recording is in progress. -->
 <!ENTITY timelineUI.recordLabel "Recording…">
 
+<!-- LOCALIZATION NOTE (timelineUI.timelineUI.memoryCheckbox.label): This string
+  -  is displayed next to a checkbox determining whether or not memory
+  -  measurements are enabled. -->
+<!ENTITY timelineUI.memoryCheckbox.label "Memory">
+
+<!-- LOCALIZATION NOTE (timelineUI.timelineUI.memoryCheckbox.tooltip): This string
+  -  is displayed next to the memory checkbox -->
+<!ENTITY timelineUI.memoryCheckbox.tooltip "Enable memory measurements">
+
 <!-- LOCALIZATION NOTE (timelineUI.emptyNotice1/2): This is the label shown
   -  in the timeline view when empty. -->
 <!ENTITY timelineUI.emptyNotice1    "Click on the">
 <!ENTITY timelineUI.emptyNotice2    "button to start recording timeline events.">
 
 <!-- LOCALIZATION NOTE (timelineUI.stopNotice1/2): This is the label shown
   -  in the timeline view while recording. -->
 <!ENTITY timelineUI.stopNotice1    "Click on the">
--- a/browser/locales/en-US/chrome/browser/devtools/timeline.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/timeline.properties
@@ -36,13 +36,19 @@ timeline.records=RECORDS
 # LOCALIZATION NOTE (timeline.label.*):
 # These strings are displayed in the timeline waterfall, identifying markers.
 timeline.label.styles=Styles
 timeline.label.reflow=Reflow
 timeline.label.paint=Paint
 timeline.label.domevent=DOM Event
 timeline.label.consoleTime=Console
 
+# LOCALIZATION NOTE (graphs.memory):
+# This string is displayed in the memory graph of the Performance tool,
+# as the unit used to memory consumption. This label should be kept
+# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
+graphs.memory=MB
+
 # LOCALIZATION NOTE (timeline.markerDetailFormat):
 # Some timeline markers come with details, like a size, a name, a js function.
 # %1$S is replaced with one of the above label (timeline.label.*) and %2$S
 # with the details. For examples: Paint (200x100), or console.time (FOO)
 timeline.markerDetailFormat=%1$S (%2$S)
--- a/browser/themes/shared/devtools/timeline.inc.css
+++ b/browser/themes/shared/devtools/timeline.inc.css
@@ -43,117 +43,113 @@
   list-style-image: url(profiler-stopwatch-checked.svg);
 }
 
 #empty-notice button .button-text,
 #recording-notice button .button-text {
   display: none;
 }
 
-.theme-dark #timeline-overview {
-  border-bottom: 1px solid #000;
+.theme-dark #timeline-pane {
+  border-top: 1px solid #000;
 }
 
-.theme-light #timeline-overview {
-  border-bottom: 1px solid #aaa;
+.theme-light #timeline-pane {
+  border-top: 1px solid #aaa;
 }
 
-.timeline-list-contents {
+.waterfall-list-contents {
   /* Hack: force hardware acceleration */
   transform: translateZ(1px);
   overflow-x: hidden;
   overflow-y: auto;
 }
 
-.timeline-header-ticks,
-.timeline-marker-waterfall {
+.waterfall-background-ticks {
   /* Background created on a <canvas> in js. */
   /* @see browser/devtools/timeline/widgets/waterfall.js */
   background-image: -moz-element(#waterfall-background);
   background-repeat: repeat-y;
   background-position: -1px center;
 }
 
-.timeline-marker-waterfall {
-  overflow: hidden;
-}
-
-.timeline-marker-container[is-spacer] {
+.waterfall-marker-container[is-spacer] {
   pointer-events: none;
 }
 
-.theme-dark .timeline-marker-container:not([is-spacer]):nth-child(2n) {
+.theme-dark .waterfall-marker-container:not([is-spacer]):nth-child(2n) {
   background-color: rgba(255,255,255,0.03);
 }
 
-.theme-light .timeline-marker-container:not([is-spacer]):nth-child(2n) {
+.theme-light .waterfall-marker-container:not([is-spacer]):nth-child(2n) {
   background-color: rgba(128,128,128,0.03);
 }
 
-.theme-dark .timeline-marker-container:hover {
+.theme-dark .waterfall-marker-container:hover {
   background-color: rgba(255,255,255,0.1) !important;
 }
 
-.theme-light .timeline-marker-container:hover {
+.theme-light .waterfall-marker-container:hover {
   background-color: rgba(128,128,128,0.1) !important;
 }
 
-.timeline-header-sidebar,
-.timeline-marker-sidebar {
+.waterfall-marker-item {
+  overflow: hidden;
+}
+
+.waterfall-sidebar {
   -moz-border-end: 1px solid;
 }
 
-.theme-dark .timeline-header-sidebar,
-.theme-dark .timeline-marker-sidebar {
+.theme-dark .waterfall-sidebar {
   -moz-border-end-color: #000;
 }
 
-.theme-light .timeline-header-sidebar,
-.theme-light .timeline-marker-sidebar {
+.theme-light .waterfall-sidebar {
   -moz-border-end-color: #aaa;
 }
 
-.timeline-header-sidebar {
-  padding: 5px;
-}
-
-.timeline-marker-sidebar {
-  padding: 2px;
-}
-
-.timeline-marker-container:hover > .timeline-marker-sidebar {
+.waterfall-marker-container:hover > .waterfall-sidebar {
   background-color: transparent;
 }
 
-.timeline-header-tick {
+.waterfall-header-name {
+  padding: 4px;
+}
+
+.waterfall-header-tick {
   width: 100px;
   font-size: 9px;
   transform-origin: left center;
 }
 
-.theme-dark .timeline-header-tick {
+.theme-dark .waterfall-header-tick {
   color: #a9bacb;
 }
 
-.theme-light .timeline-header-tick {
+.theme-light .waterfall-header-tick {
   color: #292e33;
 }
 
-.timeline-header-tick:not(:first-child) {
+.waterfall-header-tick:not(:first-child) {
   -moz-margin-start: -100px !important; /* Don't affect layout. */
 }
 
-.timeline-marker-bullet {
+.waterfall-marker-bullet {
   width: 8px;
   height: 8px;
   -moz-margin-start: 8px;
   -moz-margin-end: 6px;
   border: 1px solid;
   border-radius: 1px;
 }
 
-.timeline-marker-bar {
-  margin-top: 4px;
-  margin-bottom: 4px;
+.waterfall-marker-name {
+  font-size: 95%;
+  padding-bottom: 1px !important;
+}
+
+.waterfall-marker-bar {
+  height: 9px;
   border: 1px solid;
   border-radius: 1px;
   transform-origin: left center;
 }
--- a/browser/themes/shared/devtools/widgets.inc.css
+++ b/browser/themes/shared/devtools/widgets.inc.css
@@ -933,36 +933,31 @@
 }
 
 .graph-widget-canvas[input=dragging-selection-contents] {
   cursor: grabbing;
 }
 
 /* Line graph widget */
 
-.line-graph-widget-canvas {
-  background: #0088cc;
-}
-
 .line-graph-widget-gutter {
   position: absolute;
   background: rgba(255,255,255,0.75);
   width: 10px;
   height: 100%;
   top: 0;
   left: 0;
   border-right: 1px solid rgba(255,255,255,0.25);
   pointer-events: none;
 }
 
 .line-graph-widget-gutter-line {
   position: absolute;
   width: 100%;
   border-top: 1px solid;
-  transform: translateY(-1px);
 }
 
 .line-graph-widget-gutter-line[type=maximum] {
   border-color: #2cbb0f;
 }
 
 .line-graph-widget-gutter-line[type=minimum] {
   border-color: #ed2655;
@@ -970,55 +965,66 @@
 
 .line-graph-widget-gutter-line[type=average] {
   border-color: #d97e00;
 }
 
 .line-graph-widget-tooltip {
   position: absolute;
   background: rgba(255,255,255,0.75);
-  box-shadow: 0 2px 1px rgba(0,0,0,0.1);
   border-radius: 2px;
   line-height: 15px;
   -moz-padding-start: 6px;
   -moz-padding-end: 6px;
   transform: translateY(-50%);
   font-size: 80%;
   z-index: 1;
   pointer-events: none;
 }
 
-.line-graph-widget-tooltip::before {
+.line-graph-widget-tooltip[with-arrows=true]::before {
   content: "";
   position: absolute;
   border-top: 3px solid transparent;
   border-bottom: 3px solid transparent;
   top: calc(50% - 3px);
 }
 
-.line-graph-widget-tooltip[arrow=start]::before {
+.line-graph-widget-tooltip[arrow=start][with-arrows=true]::before {
   -moz-border-end: 3px solid rgba(255,255,255,0.75);
   left: -3px;
 }
 
-.line-graph-widget-tooltip[arrow=end]::before {
+.line-graph-widget-tooltip[arrow=end][with-arrows=true]::before {
   -moz-border-start: 3px solid rgba(255,255,255,0.75);
   right: -3px;
 }
 
 .line-graph-widget-tooltip[type=maximum] {
-  left: calc(10px + 6px);
+  left: -1px;
 }
 
 .line-graph-widget-tooltip[type=minimum] {
-  left: calc(10px + 6px);
+  left: -1px;
 }
 
 .line-graph-widget-tooltip[type=average] {
-  right: 6px;
+  right: -1px;
+}
+
+.line-graph-widget-tooltip[type=maximum][with-arrows=true] {
+  left: 14px;
+}
+
+.line-graph-widget-tooltip[type=minimum][with-arrows=true] {
+  left: 14px;
+}
+
+.line-graph-widget-tooltip[type=average][with-arrows=true] {
+  right: 4px;
 }
 
 .line-graph-widget-tooltip > [text=info] {
   color: #18191a;
 }
 
 .line-graph-widget-tooltip > [text=value] {
   -moz-margin-start: 3px;