merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 05 May 2015 11:58:18 +0200
changeset 273649 4c93d46ab92fe880f1be7afe6114d55b9972cdea
parent 273637 bf86d6afdff2273336dfc13fb307e75cd3bc320d (current diff)
parent 273648 16911df6b875f9bc9dc75d79424944e6fe705ea3 (diff)
child 273703 754579ec0e68068d32be534d553d5b191d918d84
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone40.0a1
first release with
nightly linux32
4c93d46ab92f / 40.0a1 / 20150505030206 / files
nightly linux64
4c93d46ab92f / 40.0a1 / 20150505030206 / files
nightly mac
4c93d46ab92f / 40.0a1 / 20150505030206 / files
nightly win32
4c93d46ab92f / 40.0a1 / 20150505030206 / files
nightly win64
4c93d46ab92f / 40.0a1 / 20150505030206 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -421,20 +421,18 @@ support-files =
   benignPage.html
 [browser_typeAheadFind.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 921935 - focusmanager issues with e10s (test calls waitForFocus)
 [browser_unknownContentType_title.js]
 [browser_unloaddialogs.js]
 skip-if = e10s # Bug 1100700 - test relies on unload event firing on closed tabs, which it doesn't
 [browser_urlHighlight.js]
 [browser_urlbarAutoFillTrimURLs.js]
-skip-if = e10s # Bug 1093941 - Waits indefinitely for onSearchComplete
 [browser_urlbarCopying.js]
 [browser_urlbarEnter.js]
-skip-if = e10s # Bug 1093941 - used to cause obscure non-windows child process crashes on try
 [browser_urlbarEnterAfterMouseOver.js]
 skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
 [browser_urlbarRevert.js]
 [browser_urlbarSearchSingleWordNotification.js]
 [browser_urlbarStop.js]
 [browser_urlbarTrimURLs.js]
 [browser_urlbar_search_healthreport.js]
 [browser_utilityOverlay.js]
--- a/browser/base/content/test/general/browser_urlbarEnter.js
+++ b/browser/base/content/test/general/browser_urlbarEnter.js
@@ -20,20 +20,25 @@ add_task(function* () {
 
   // Cleanup.
   gBrowser.removeCurrentTab();
 });
 
 add_task(function* () {
   info("Alt+Return keypress");
   let tab = gBrowser.selectedTab = gBrowser.addTab(START_VALUE);
+  // due to bug 691608, we must wait for the load event, else isTabEmpty() will
+  // return true on e10s for this tab, so it will be reused even with altKey.
+  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
 
   gURLBar.focus();
   EventUtils.synthesizeKey("VK_RETURN", {altKey: true});
-  yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+  // wait for the new tab to appear.
+  yield BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
 
   // Check url bar and selected tab.
   is(gURLBar.textValue, TEST_VALUE, "Urlbar should preserve the value on return keypress");
   isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
 
   // Cleanup.
   gBrowser.removeTab(tab);
   gBrowser.removeCurrentTab();
--- a/browser/devtools/animationinspector/animation-controller.js
+++ b/browser/devtools/animationinspector/animation-controller.js
@@ -107,16 +107,18 @@ let AnimationsController = {
     // Expose actor capabilities.
     this.hasToggleAll = yield target.actorHasMethod("animations", "toggleAll");
     this.hasSetCurrentTime = yield target.actorHasMethod("animationplayer",
                                                          "setCurrentTime");
     this.hasMutationEvents = yield target.actorHasMethod("animations",
                                                          "stopAnimationPlayerUpdates");
     this.hasSetPlaybackRate = yield target.actorHasMethod("animationplayer",
                                                           "setPlaybackRate");
+    this.hasTargetNode = yield target.actorHasMethod("domwalker",
+                                                     "getNodeFromActor");
 
     if (this.destroyed) {
       console.warn("Could not fully initialize the AnimationsController");
       return;
     }
 
     this.startListeners();
     yield this.onNewNodeFront();
--- a/browser/devtools/animationinspector/animation-panel.js
+++ b/browser/devtools/animationinspector/animation-panel.js
@@ -1,16 +1,23 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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";
 
+const {
+  PlayerMetaDataHeader,
+  PlaybackRateSelector,
+  AnimationTargetNode,
+  createNode
+} = require("devtools/animationinspector/components");
+
 /**
  * The main animations panel UI.
  */
 let AnimationsPanel = {
   UI_UPDATED_EVENT: "ui-updated",
   PANEL_INITIALIZED: "panel-initialized",
 
   initialize: Task.async(function*() {
@@ -194,16 +201,19 @@ function PlayerWidget(player, containerE
   this.onFastForwardBtnClick = this.onFastForwardBtnClick.bind(this);
   this.onCurrentTimeChanged = this.onCurrentTimeChanged.bind(this);
   this.onPlaybackRateChanged = this.onPlaybackRateChanged.bind(this);
 
   this.metaDataComponent = new PlayerMetaDataHeader();
   if (AnimationsController.hasSetPlaybackRate) {
     this.rateComponent = new PlaybackRateSelector();
   }
+  if (AnimationsController.hasTargetNode) {
+    this.targetNodeComponent = new AnimationTargetNode(gInspector);
+  }
 }
 
 PlayerWidget.prototype = {
   initialize: Task.async(function*() {
     if (this.initialized) {
       return;
     }
     this.initialized = true;
@@ -219,16 +229,19 @@ PlayerWidget.prototype = {
     this.destroyed = true;
 
     this.stopTimelineAnimation();
     this.stopListeners();
     this.metaDataComponent.destroy();
     if (this.rateComponent) {
       this.rateComponent.destroy();
     }
+    if (this.targetNodeComponent) {
+      this.targetNodeComponent.destroy();
+    }
 
     this.el.remove();
     this.playPauseBtnEl = this.rewindBtnEl = this.fastForwardBtnEl = null;
     this.currentTimeEl = this.timeDisplayEl = null;
     this.containerEl = this.el = this.player = null;
   }),
 
   startListeners: function() {
@@ -256,21 +269,27 @@ PlayerWidget.prototype = {
       this.rateComponent.off("rate-changed", this.onPlaybackRateChanged);
     }
   },
 
   createMarkup: function() {
     let state = this.player.state;
 
     this.el = createNode({
+      parent: this.containerEl,
       attributes: {
         "class": "player-widget " + state.playState
       }
     });
 
+    if (this.targetNodeComponent) {
+      this.targetNodeComponent.init(this.el);
+      this.targetNodeComponent.render(this.player);
+    }
+
     this.metaDataComponent.init(this.el);
     this.metaDataComponent.render(state);
 
     // Timeline widget.
     let timelineEl = createNode({
       parent: this.el,
       attributes: {
         "class": "timeline"
@@ -354,18 +373,16 @@ PlayerWidget.prototype = {
     // Time display
     this.timeDisplayEl = createNode({
       parent: timelineEl,
       attributes: {
         "class": "time-display"
       }
     });
 
-    this.containerEl.appendChild(this.el);
-
     // Show the initial time.
     this.displayTime(state.currentTime);
   },
 
   /**
    * Executed when the playPause button is clicked.
    * Note that tests may want to call this callback directly rather than
    * simulating a click on the button since it returns the promise returned by
@@ -566,266 +583,8 @@ PlayerWidget.prototype = {
    */
   stopTimelineAnimation: function() {
     if (this.rafID) {
       cancelAnimationFrame(this.rafID);
       this.rafID = null;
     }
   }
 };
-
-/**
- * UI component responsible for displaying and updating the player meta-data:
- * name, duration, iterations, delay.
- * The parent UI component for this should drive its updates by calling
- * render(state) whenever it wants the component to update.
- */
-function PlayerMetaDataHeader() {
-  // Store the various state pieces we need to only refresh the UI when things
-  // change.
-  this.state = {};
-}
-
-PlayerMetaDataHeader.prototype = {
-  init: function(containerEl) {
-    // The main title element.
-    this.el = createNode({
-      parent: containerEl,
-      attributes: {
-        "class": "animation-title"
-      }
-    });
-
-    // Animation name.
-    this.nameLabel = createNode({
-      parent: this.el,
-      nodeType: "span"
-    });
-
-    this.nameValue = createNode({
-      parent: this.el,
-      nodeType: "strong",
-      attributes: {
-        "style": "display:none;"
-      }
-    });
-
-    // Animation duration, delay and iteration container.
-    let metaData = createNode({
-      parent: this.el,
-      nodeType: "span",
-      attributes: {
-        "class": "meta-data"
-      }
-    });
-
-    // Animation duration.
-    this.durationLabel = createNode({
-      parent: metaData,
-      nodeType: "span"
-    });
-    this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel");
-
-    this.durationValue = createNode({
-      parent: metaData,
-      nodeType: "strong"
-    });
-
-    // Animation delay (hidden by default since there may not be a delay).
-    this.delayLabel = createNode({
-      parent: metaData,
-      nodeType: "span",
-      attributes: {
-        "style": "display:none;"
-      }
-    });
-    this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel");
-
-    this.delayValue = createNode({
-      parent: metaData,
-      nodeType: "strong"
-    });
-
-    // Animation iteration count (also hidden by default since we don't display
-    // single iterations).
-    this.iterationLabel = createNode({
-      parent: metaData,
-      nodeType: "span",
-      attributes: {
-        "style": "display:none;"
-      }
-    });
-    this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel");
-
-    this.iterationValue = createNode({
-      parent: metaData,
-      nodeType: "strong",
-      attributes: {
-        "style": "display:none;"
-      }
-    });
-  },
-
-  destroy: function() {
-    this.state = null;
-    this.el.remove();
-    this.el = null;
-    this.nameLabel = this.nameValue = null;
-    this.durationLabel = this.durationValue = null;
-    this.delayLabel = this.delayValue = null;
-    this.iterationLabel = this.iterationValue = null;
-  },
-
-  render: function(state) {
-    // Update the name if needed.
-    if (state.name !== this.state.name) {
-      if (state.name) {
-        // Animations (and transitions since bug 1122414) have names.
-        this.nameLabel.textContent = L10N.getStr("player.animationNameLabel");
-        this.nameValue.style.display = "inline";
-        this.nameValue.textContent = state.name;
-      } else {
-        // With older actors, Css transitions don't have names.
-        this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel");
-        this.nameValue.style.display = "none";
-      }
-    }
-
-    // update the duration value if needed.
-    if (state.duration !== this.state.duration) {
-      this.durationValue.textContent = L10N.getFormatStr("player.timeLabel",
-        L10N.numberWithDecimals(state.duration / 1000, 2));
-    }
-
-    // Update the delay if needed.
-    if (state.delay !== this.state.delay) {
-      if (state.delay) {
-        this.delayLabel.style.display = "inline";
-        this.delayValue.style.display = "inline";
-        this.delayValue.textContent = L10N.getFormatStr("player.timeLabel",
-          L10N.numberWithDecimals(state.delay / 1000, 2));
-      } else {
-        // Hide the delay elements if there is no delay defined.
-        this.delayLabel.style.display = "none";
-        this.delayValue.style.display = "none";
-      }
-    }
-
-    // Update the iterationCount if needed.
-    if (state.iterationCount !== this.state.iterationCount) {
-      if (state.iterationCount !== 1) {
-        this.iterationLabel.style.display = "inline";
-        this.iterationValue.style.display = "inline";
-        let count = state.iterationCount ||
-                    L10N.getStr("player.infiniteIterationCount");
-        this.iterationValue.innerHTML = count;
-      } else {
-        // Hide the iteration elements if iteration is 1.
-        this.iterationLabel.style.display = "none";
-        this.iterationValue.style.display = "none";
-      }
-    }
-
-    this.state = state;
-  }
-};
-
-/**
- * UI component responsible for displaying the playback rate drop-down in each
- * player widget, updating it when the state changes, and emitting events when
- * the user selects a new value.
- * The parent UI component for this should drive its updates by calling
- * render(state) whenever it wants the component to update.
- */
-function PlaybackRateSelector() {
-  this.currentRate = null;
-  this.onSelectionChanged = this.onSelectionChanged.bind(this);
-  EventEmitter.decorate(this);
-}
-
-PlaybackRateSelector.prototype = {
-  PRESETS: [.1, .5, 1, 2, 5, 10],
-
-  init: function(containerEl) {
-    // This component is simple enough that we can re-create the markup every
-    // time it's rendered. So here we only store the parentEl.
-    this.parentEl = containerEl;
-  },
-
-  destroy: function() {
-    this.removeSelect();
-    this.parentEl = this.el = null;
-  },
-
-  removeSelect: function() {
-    if (this.el) {
-      this.el.removeEventListener("change", this.onSelectionChanged);
-      this.el.remove();
-    }
-  },
-
-  /**
-   * Get the ordered list of presets, including the current playbackRate if
-   * different from the existing presets.
-   */
-  getCurrentPresets: function({playbackRate}) {
-    return [...new Set([...this.PRESETS, playbackRate])].sort((a,b) => a > b);
-  },
-
-  render: function(state) {
-    if (state.playbackRate === this.currentRate) {
-      return;
-    }
-
-    this.removeSelect();
-
-    this.el = createNode({
-      parent: this.parentEl,
-      nodeType: "select",
-      attributes: {
-        "class": "rate devtools-button"
-      }
-    });
-
-    for (let preset of this.getCurrentPresets(state)) {
-      let option = createNode({
-        parent: this.el,
-        nodeType: "option",
-        attributes: {
-          value: preset,
-        }
-      });
-      option.textContent = L10N.getFormatStr("player.playbackRateLabel", preset);
-      if (preset === state.playbackRate) {
-        option.setAttribute("selected", "");
-      }
-    }
-
-    this.el.addEventListener("change", this.onSelectionChanged);
-
-    this.currentRate = state.playbackRate;
-  },
-
-  onSelectionChanged: function(e) {
-    this.emit("rate-changed", parseFloat(this.el.value));
-  }
-};
-
-/**
- * DOM node creation helper function.
- * @param {Object} Options to customize the node to be created.
- * @return {DOMNode} The newly created node.
- */
-function createNode(options) {
-  let type = options.nodeType || "div";
-  let node = document.createElement(type);
-
-  for (let name in options.attributes || {}) {
-    let value = options.attributes[name];
-    node.setAttribute(name, value);
-  }
-
-  if (options.parent) {
-    options.parent.appendChild(node);
-  }
-
-  return node;
-}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/components.js
@@ -0,0 +1,502 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Set of reusable UI components for the animation-inspector UI.
+// All components in this module share a common API:
+// 1. construct the component:
+//    let c = new ComponentName();
+// 2. initialize the markup of the component in a given parent node:
+//    c.init(containerElement);
+// 3. render the component, passing in some sort of state:
+//    This may be called over and over again when the state changes, to update
+//    the component output.
+//    c.render(state);
+// 4. destroy the component:
+//    c.destroy();
+
+const {Cu} = require('chrome');
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+const STRINGS_URI = "chrome://browser/locale/devtools/animationinspector.properties";
+const L10N = new ViewHelpers.L10N(STRINGS_URI);
+
+/**
+ * UI component responsible for displaying and updating the player meta-data:
+ * name, duration, iterations, delay.
+ * The parent UI component for this should drive its updates by calling
+ * render(state) whenever it wants the component to update.
+ */
+function PlayerMetaDataHeader() {
+  // Store the various state pieces we need to only refresh the UI when things
+  // change.
+  this.state = {};
+}
+
+exports.PlayerMetaDataHeader = PlayerMetaDataHeader;
+
+PlayerMetaDataHeader.prototype = {
+  init: function(containerEl) {
+    // The main title element.
+    this.el = createNode({
+      parent: containerEl,
+      attributes: {
+        "class": "animation-title"
+      }
+    });
+
+    // Animation name.
+    this.nameLabel = createNode({
+      parent: this.el,
+      nodeType: "span"
+    });
+
+    this.nameValue = createNode({
+      parent: this.el,
+      nodeType: "strong",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+
+    // Animation duration, delay and iteration container.
+    let metaData = createNode({
+      parent: this.el,
+      nodeType: "span",
+      attributes: {
+        "class": "meta-data"
+      }
+    });
+
+    // Animation duration.
+    this.durationLabel = createNode({
+      parent: metaData,
+      nodeType: "span"
+    });
+    this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel");
+
+    this.durationValue = createNode({
+      parent: metaData,
+      nodeType: "strong"
+    });
+
+    // Animation delay (hidden by default since there may not be a delay).
+    this.delayLabel = createNode({
+      parent: metaData,
+      nodeType: "span",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+    this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel");
+
+    this.delayValue = createNode({
+      parent: metaData,
+      nodeType: "strong"
+    });
+
+    // Animation iteration count (also hidden by default since we don't display
+    // single iterations).
+    this.iterationLabel = createNode({
+      parent: metaData,
+      nodeType: "span",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+    this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel");
+
+    this.iterationValue = createNode({
+      parent: metaData,
+      nodeType: "strong",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+  },
+
+  destroy: function() {
+    this.state = null;
+    this.el.remove();
+    this.el = null;
+    this.nameLabel = this.nameValue = null;
+    this.durationLabel = this.durationValue = null;
+    this.delayLabel = this.delayValue = null;
+    this.iterationLabel = this.iterationValue = null;
+  },
+
+  render: function(state) {
+    // Update the name if needed.
+    if (state.name !== this.state.name) {
+      if (state.name) {
+        // Animations (and transitions since bug 1122414) have names.
+        this.nameLabel.textContent = L10N.getStr("player.animationNameLabel");
+        this.nameValue.style.display = "inline";
+        this.nameValue.textContent = state.name;
+      } else {
+        // With older actors, Css transitions don't have names.
+        this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel");
+        this.nameValue.style.display = "none";
+      }
+    }
+
+    // update the duration value if needed.
+    if (state.duration !== this.state.duration) {
+      this.durationValue.textContent = L10N.getFormatStr("player.timeLabel",
+        L10N.numberWithDecimals(state.duration / 1000, 2));
+    }
+
+    // Update the delay if needed.
+    if (state.delay !== this.state.delay) {
+      if (state.delay) {
+        this.delayLabel.style.display = "inline";
+        this.delayValue.style.display = "inline";
+        this.delayValue.textContent = L10N.getFormatStr("player.timeLabel",
+          L10N.numberWithDecimals(state.delay / 1000, 2));
+      } else {
+        // Hide the delay elements if there is no delay defined.
+        this.delayLabel.style.display = "none";
+        this.delayValue.style.display = "none";
+      }
+    }
+
+    // Update the iterationCount if needed.
+    if (state.iterationCount !== this.state.iterationCount) {
+      if (state.iterationCount !== 1) {
+        this.iterationLabel.style.display = "inline";
+        this.iterationValue.style.display = "inline";
+        let count = state.iterationCount ||
+                    L10N.getStr("player.infiniteIterationCount");
+        this.iterationValue.innerHTML = count;
+      } else {
+        // Hide the iteration elements if iteration is 1.
+        this.iterationLabel.style.display = "none";
+        this.iterationValue.style.display = "none";
+      }
+    }
+
+    this.state = state;
+  }
+};
+
+/**
+ * UI component responsible for displaying the playback rate drop-down in each
+ * player widget, updating it when the state changes, and emitting events when
+ * the user selects a new value.
+ * The parent UI component for this should drive its updates by calling
+ * render(state) whenever it wants the component to update.
+ */
+function PlaybackRateSelector() {
+  this.currentRate = null;
+  this.onSelectionChanged = this.onSelectionChanged.bind(this);
+  EventEmitter.decorate(this);
+}
+
+exports.PlaybackRateSelector = PlaybackRateSelector;
+
+PlaybackRateSelector.prototype = {
+  PRESETS: [.1, .5, 1, 2, 5, 10],
+
+  init: function(containerEl) {
+    // This component is simple enough that we can re-create the markup every
+    // time it's rendered. So here we only store the parentEl.
+    this.parentEl = containerEl;
+  },
+
+  destroy: function() {
+    this.removeSelect();
+    this.parentEl = this.el = null;
+  },
+
+  removeSelect: function() {
+    if (this.el) {
+      this.el.removeEventListener("change", this.onSelectionChanged);
+      this.el.remove();
+    }
+  },
+
+  /**
+   * Get the ordered list of presets, including the current playbackRate if
+   * different from the existing presets.
+   */
+  getCurrentPresets: function({playbackRate}) {
+    return [...new Set([...this.PRESETS, playbackRate])].sort((a,b) => a > b);
+  },
+
+  render: function(state) {
+    if (state.playbackRate === this.currentRate) {
+      return;
+    }
+
+    this.removeSelect();
+
+    this.el = createNode({
+      parent: this.parentEl,
+      nodeType: "select",
+      attributes: {
+        "class": "rate devtools-button"
+      }
+    });
+
+    for (let preset of this.getCurrentPresets(state)) {
+      let option = createNode({
+        parent: this.el,
+        nodeType: "option",
+        attributes: {
+          value: preset,
+        }
+      });
+      option.textContent = L10N.getFormatStr("player.playbackRateLabel", preset);
+      if (preset === state.playbackRate) {
+        option.setAttribute("selected", "");
+      }
+    }
+
+    this.el.addEventListener("change", this.onSelectionChanged);
+
+    this.currentRate = state.playbackRate;
+  },
+
+  onSelectionChanged: function(e) {
+    this.emit("rate-changed", parseFloat(this.el.value));
+  }
+};
+
+/**
+ * UI component responsible for displaying a preview of the target dom node of
+ * a given animation.
+ * @param {InspectorPanel} inspector Requires a reference to the inspector-panel
+ * to highlight and select the node, as well as refresh it when there are
+ * mutations.
+ */
+function AnimationTargetNode(inspector) {
+  this.inspector = inspector;
+
+  this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this);
+  this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this);
+  this.onSelectNodeClick = this.onSelectNodeClick.bind(this);
+  this.onMarkupMutations = this.onMarkupMutations.bind(this);
+
+  EventEmitter.decorate(this);
+}
+
+exports.AnimationTargetNode = AnimationTargetNode;
+
+AnimationTargetNode.prototype = {
+  init: function(containerEl) {
+    let document = containerEl.ownerDocument;
+
+    // Init the markup for displaying the target node.
+    this.el = createNode({
+      parent: containerEl,
+      attributes: {
+        "class": "animation-target"
+      }
+    });
+
+    // Icon to select the node in the inspector.
+    this.selectNodeEl = createNode({
+      parent: this.el,
+      nodeType: "span",
+      attributes: {
+        "class": "node-selector"
+      }
+    });
+
+    // Wrapper used for mouseover/out event handling.
+    this.previewEl = createNode({
+      parent: this.el,
+      nodeType: "span"
+    });
+
+    this.previewEl.appendChild(document.createTextNode("<"));
+
+    // Tag name.
+    this.tagNameEl = createNode({
+      parent: this.previewEl,
+      nodeType: "span",
+      attributes: {
+        "class": "tag-name theme-fg-color3"
+      }
+    });
+
+    // Id attribute container.
+    this.idEl = createNode({
+      parent: this.previewEl,
+      nodeType: "span"
+    });
+
+    createNode({
+      parent: this.idEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-name theme-fg-color2"
+      }
+    }).textContent = "id";
+
+    this.idEl.appendChild(document.createTextNode("=\""));
+
+    createNode({
+      parent: this.idEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-value theme-fg-color6"
+      }
+    });
+
+    this.idEl.appendChild(document.createTextNode("\""));
+
+    // Class attribute container.
+    this.classEl = createNode({
+      parent: this.previewEl,
+      nodeType: "span"
+    });
+
+    createNode({
+      parent: this.classEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-name theme-fg-color2"
+      }
+    }).textContent = "class";
+
+    this.classEl.appendChild(document.createTextNode("=\""));
+
+    createNode({
+      parent: this.classEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-value theme-fg-color6"
+      }
+    });
+
+    this.classEl.appendChild(document.createTextNode("\""));
+
+    this.previewEl.appendChild(document.createTextNode(">"));
+
+    // Init events for highlighting and selecting the node.
+    this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver);
+    this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut);
+    this.selectNodeEl.addEventListener("click", this.onSelectNodeClick);
+
+    // Start to listen for markupmutation events.
+    this.inspector.on("markupmutation", this.onMarkupMutations);
+  },
+
+  destroy: function() {
+    this.inspector.off("markupmutation", this.onMarkupMutations);
+    this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver);
+    this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut);
+    this.selectNodeEl.removeEventListener("click", this.onSelectNodeClick);
+    this.el.remove();
+    this.el = this.tagNameEl = this.idEl = this.classEl = null;
+    this.selectNodeEl = this.previewEl = null;
+    this.nodeFront = this.inspector = this.playerFront = null;
+  },
+
+  onPreviewMouseOver: function() {
+    if (!this.nodeFront) {
+      return;
+    }
+    this.inspector.toolbox.highlighterUtils.highlightNodeFront(this.nodeFront);
+  },
+
+  onPreviewMouseOut: function() {
+    this.inspector.toolbox.highlighterUtils.unhighlight();
+  },
+
+  onSelectNodeClick: function() {
+    if (!this.nodeFront) {
+      return;
+    }
+    this.inspector.selection.setNodeFront(this.nodeFront, "animationinspector");
+  },
+
+  onMarkupMutations: function(e, mutations) {
+    if (!this.nodeFront || !this.playerFront) {
+      return;
+    }
+
+    for (let {target} of mutations) {
+      if (target === this.nodeFront) {
+        // Re-render with the same nodeFront to update the output.
+        this.render(this.playerFront);
+        break;
+      }
+    }
+  },
+
+  render: function(playerFront) {
+    this.playerFront = playerFront;
+    this.inspector.walker.getNodeFromActor(playerFront.actorID, ["node"]).then(nodeFront => {
+      // We might have been destroyed in the meantime, or the node might not be found.
+      if (!this.el || !nodeFront) {
+        return;
+      }
+
+      this.nodeFront = nodeFront;
+      let {tagName, attributes} = nodeFront;
+
+      this.tagNameEl.textContent = tagName.toLowerCase();
+
+      let idIndex = attributes.findIndex(({name}) => name === "id");
+      if (idIndex > -1 && attributes[idIndex].value) {
+        this.idEl.querySelector(".attribute-value").textContent =
+          attributes[idIndex].value;
+        this.idEl.style.display = "inline";
+      } else {
+        this.idEl.style.display = "none";
+      }
+
+      let classIndex = attributes.findIndex(({name}) => name === "class");
+      if (classIndex > -1 && attributes[classIndex].value) {
+        this.classEl.querySelector(".attribute-value").textContent =
+          attributes[classIndex].value;
+        this.classEl.style.display = "inline";
+      } else {
+        this.classEl.style.display = "none";
+      }
+
+      this.emit("target-retrieved");
+    }, e => {
+      this.nodeFront = null;
+      if (!this.el) {
+        console.warn("Cound't retrieve the animation target node, widget destroyed");
+      } else {
+        console.error(e);
+      }
+    });
+  }
+};
+
+/**
+ * DOM node creation helper function.
+ * @param {Object} Options to customize the node to be created.
+ * - nodeType {String} Optional, defaults to "div",
+ * - attributes {Object} Optional attributes object like
+ *   {attrName1:value1, attrName2: value2, ...}
+ * - parent {DOMNode} Mandatory node to append the newly created node to.
+ * @return {DOMNode} The newly created node.
+ */
+function createNode(options) {
+  if (!options.parent) {
+    throw new Error("Missing parent DOMNode to create new node");
+  }
+
+  let type = options.nodeType || "div";
+  let node = options.parent.ownerDocument.createElement(type);
+
+  for (let name in options.attributes || {}) {
+    let value = options.attributes[name];
+    node.setAttribute(name, value);
+  }
+
+  options.parent.appendChild(node);
+  return node;
+}
+
+exports.createNode = createNode;
--- a/browser/devtools/animationinspector/moz.build
+++ b/browser/devtools/animationinspector/moz.build
@@ -1,7 +1,11 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # 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/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+EXTRA_JS_MODULES.devtools.animationinspector += [
+    'components.js',
+]
--- a/browser/devtools/animationinspector/test/browser.ini
+++ b/browser/devtools/animationinspector/test/browser.ini
@@ -15,24 +15,26 @@ support-files =
 [browser_animation_playerFronts_are_refreshed.js]
 [browser_animation_playerWidgets_appear_on_panel_init.js]
 [browser_animation_playerWidgets_destroy.js]
 [browser_animation_playerWidgets_disables_on_finished.js]
 [browser_animation_playerWidgets_dont_show_time_after_duration.js]
 [browser_animation_playerWidgets_have_control_buttons.js]
 [browser_animation_playerWidgets_meta_data.js]
 [browser_animation_playerWidgets_state_after_pause.js]
+[browser_animation_playerWidgets_target_nodes.js]
 [browser_animation_rate_select_shows_presets.js]
 [browser_animation_refresh_on_added_animation.js]
 [browser_animation_refresh_on_removed_animation.js]
 [browser_animation_refresh_when_active.js]
 [browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
 [browser_animation_setting_currentTime_works_and_pauses.js]
 [browser_animation_setting_playbackRate_works.js]
 [browser_animation_shows_player_on_valid_node.js]
+[browser_animation_target_highlight_select.js]
 [browser_animation_timeline_animates.js]
 [browser_animation_timeline_is_enabled.js]
 [browser_animation_timeline_waits_for_delay.js]
 [browser_animation_toggle_button_resets_on_navigate.js]
 [browser_animation_toggle_button_toggles_animations.js]
 [browser_animation_toggle_button_updates_playerWidgets.js]
 [browser_animation_toolbar_exists.js]
 [browser_animation_ui_updates_when_animation_changes.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_target_nodes.js
@@ -0,0 +1,31 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that player widgets display information about target nodes
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel} = yield openAnimationInspector();
+
+  info("Select the simple animated node");
+  yield selectNode(".animated", inspector);
+
+  let widget = panel.playerWidgets[0];
+
+  // Make sure to wait for the target-retrieved event if the nodeFront hasn't
+  // yet been retrieved by the TargetNodeComponent.
+  if (!widget.targetNodeComponent.nodeFront) {
+    yield widget.targetNodeComponent.once("target-retrieved");
+  }
+
+  let targetEl = widget.el.querySelector(".animation-target");
+  ok(targetEl, "The player widget has a target element");
+  is(targetEl.textContent, "<divid=\"\"class=\"ball animated\">",
+    "The target element's content is correct");
+
+  let selectorEl = targetEl.querySelector(".node-selector");
+  ok(selectorEl, "The icon to select the target element in the inspector exists");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_target_highlight_select.js
@@ -0,0 +1,62 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the DOM element targets displayed in animation player widgets can
+// be used to highlight elements in the DOM and select them in the inspector.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {toolbox, inspector, panel} = yield openAnimationInspector();
+
+  info("Select the simple animated node");
+  yield selectNode(".animated", inspector);
+
+  // Make sure to wait for the target-retrieved event if the nodeFront hasn't
+  // yet been retrieved by the TargetNodeComponent.
+  let targetNodeComponent = panel.playerWidgets[0].targetNodeComponent;
+  if (!targetNodeComponent.nodeFront) {
+    yield targetNodeComponent.once("target-retrieved");
+  }
+
+  info("Retrieve the part of the widget that highlights the node on hover");
+  let highlightingEl = targetNodeComponent.previewEl;
+
+  info("Listen to node-highlight event and mouse over the widget");
+  let onHighlight = toolbox.once("node-highlight");
+  EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseover"},
+                             highlightingEl.ownerDocument.defaultView);
+  let nodeFront = yield onHighlight;
+
+  ok(true, "The node-highlight event was fired");
+  is(targetNodeComponent.nodeFront, nodeFront,
+    "The highlighted node is the one stored on the animation widget");
+  is(nodeFront.tagName, "DIV", "The highlighted node has the correct tagName");
+  is(nodeFront.attributes[0].name, "class", "The highlighted node has the correct attributes");
+  is(nodeFront.attributes[0].value, "ball animated", "The highlighted node has the correct class");
+
+  info("Select the body node in order to have the list of all animations");
+  yield selectNode("body", inspector);
+
+  // Make sure to wait for the target-retrieved event if the nodeFront hasn't
+  // yet been retrieved by the TargetNodeComponent.
+  targetNodeComponent = panel.playerWidgets[0].targetNodeComponent;
+  if (!targetNodeComponent.nodeFront) {
+    yield targetNodeComponent.once("target-retrieved");
+  }
+
+  info("Click on the first animation widget's selector icon and wait for the selection to change");
+  let onSelection = inspector.selection.once("new-node-front");
+  let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
+  let selectIconEl = targetNodeComponent.selectNodeEl;
+  EventUtils.sendMouseEvent({type: "click"}, selectIconEl,
+                            selectIconEl.ownerDocument.defaultView);
+  yield onSelection;
+
+  is(inspector.selection.nodeFront, targetNodeComponent.nodeFront,
+    "The selected node is the one stored on the animation widget");
+
+  yield onPanelUpdated;
+});
--- a/browser/devtools/shared/moz.build
+++ b/browser/devtools/shared/moz.build
@@ -50,16 +50,17 @@ EXTRA_JS_MODULES.devtools.shared += [
     'devices.js',
     'doorhanger.js',
     'frame-script-utils.js',
     'getjson.js',
     'inplace-editor.js',
     'node-attribute-parser.js',
     'observable-object.js',
     'options-view.js',
+    'poller.js',
     'source-utils.js',
     'telemetry.js',
     'theme-switching.js',
     'theme.js',
     'undo.js',
 ]
 
 EXTRA_JS_MODULES.devtools.shared.widgets += [
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/poller.js
@@ -0,0 +1,115 @@
+/* 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";
+loader.lazyRequireGetter(this, "timers",
+  "resource://gre/modules/Timer.jsm");
+loader.lazyRequireGetter(this, "defer",
+  "sdk/core/promise", true);
+
+/**
+ * @constructor Poller
+ * Takes a function that is to be called on an interval,
+ * and can be turned on and off via methods to execute `fn` on the interval
+ * specified during `on`. If `fn` returns a promise, the polling waits for
+ * that promise to resolve before waiting the interval to call again.
+ *
+ * Specify the `wait` duration between polling here, and optionally
+ * an `immediate` boolean, indicating whether the function should be called
+ * immediately when toggling on.
+ *
+ * @param {function} fn
+ * @param {number} wait
+ * @param {boolean?} immediate
+ */
+function Poller (fn, wait, immediate) {
+  this._fn = fn;
+  this._wait = wait;
+  this._immediate = immediate;
+  this._poll = this._poll.bind(this);
+  this._preparePoll = this._preparePoll.bind(this);
+}
+exports.Poller = Poller;
+
+/**
+ * Returns a boolean indicating whether or not poller
+ * is polling.
+ *
+ * @return {boolean}
+ */
+Poller.prototype.isPolling = function pollerIsPolling () {
+  return !!this._timer;
+};
+
+/**
+ * Turns polling on.
+ *
+ * @return {Poller}
+ */
+Poller.prototype.on = function pollerOn () {
+  if (this._destroyed) {
+    throw Error("Poller cannot be turned on after destruction.");
+  }
+  if (this._timer) {
+    this.off();
+  }
+  this._immediate ? this._poll() : this._preparePoll();
+  return this;
+};
+
+/**
+ * Turns off polling. Returns a promise that resolves when
+ * the last outstanding `fn` call finishes if it's an async function.
+ *
+ * @return {Promise}
+ */
+Poller.prototype.off = function pollerOff () {
+  let { resolve, promise } = defer();
+  if (this._timer) {
+    timers.clearTimeout(this._timer);
+    this._timer = null;
+  }
+
+  // Settle an inflight poll call before resolving
+  // if using a promise-backed poll function
+  if (this._inflight) {
+    this._inflight.then(resolve);
+  } else {
+    resolve();
+  }
+  return promise;
+};
+
+/**
+ * Turns off polling and removes the reference to the poller function.
+ * Resolves when the last outstanding `fn` call finishes if it's an async function.
+ */
+Poller.prototype.destroy = function pollerDestroy () {
+  return this.off().then(() => {
+    this._destroyed = true;
+    this._fn = null
+  });
+};
+
+Poller.prototype._preparePoll = function pollerPrepare () {
+  this._timer = timers.setTimeout(this._poll, this._wait);
+};
+
+Poller.prototype._poll = function pollerPoll () {
+  let response = this._fn();
+  if (response && typeof response.then === "function") {
+    // Store the most recent in-flight polling
+    // call so we can clean it up when disabling
+    this._inflight = response;
+    response.then(() => {
+      // Only queue up the next call if poller was not turned off
+      // while this async poll call was in flight.
+      if (this._timer) {
+        this._preparePoll();
+      }
+    });
+  } else {
+    this._preparePoll();
+  }
+};
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -79,16 +79,17 @@ skip-if = e10s # Layouthelpers test shou
 skip-if = e10s # Layouthelpers test should not run in a content page.
 [browser_mdn-docs-01.js]
 [browser_mdn-docs-02.js]
 [browser_num-l10n.js]
 [browser_observableobject.js]
 [browser_options-view-01.js]
 [browser_outputparser.js]
 skip-if = e10s # Test intermittently fails with e10s. Bug 1124162.
+[browser_poller.js]
 [browser_prefs-01.js]
 [browser_prefs-02.js]
 [browser_require_basic.js]
 [browser_spectrum.js]
 [browser_theme.js]
 [browser_tableWidget_basic.js]
 [browser_tableWidget_keyboard_interaction.js]
 [browser_tableWidget_mouse_interaction.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_poller.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the Poller class.
+
+const { Poller } = devtools.require("devtools/shared/poller");
+
+add_task(function* () {
+  let count1 = 0, count2 = 0, count3 = 0;
+
+  let poller1 = new Poller(function () {
+    count1++;
+  }, 1000000000, true);
+  let poller2 = new Poller(function () {
+    count2++;
+  }, 10);
+  let poller3 = new Poller(function () {
+    count3++;
+  }, 1000000000);
+
+  poller2.on();
+
+  ok(!poller1.isPolling(), "isPolling() returns false for an off poller");
+  ok(poller2.isPolling(), "isPolling() returns true for an on poller");
+
+  yield waitUntil(() => count2 > 10);
+
+  ok(count2 > 10, "poller that was turned on polled several times");
+  ok(count1 === 0, "poller that was never turned on never polled");
+
+  yield poller2.off();
+  let currentCount2 = count2;
+
+  poller1.on(); // Really high poll time!
+  poller3.on(); // Really high poll time!
+
+  yield waitUntil(() => count1 === 1);
+  ok(true, "Poller calls fn immediately when `immediate` is true");
+  ok(count3 === 0, "Poller does not call fn immediately when `immediate` is not set");
+
+  ok(count2 === currentCount2, "a turned off poller does not continue to poll");
+  yield poller2.off();
+  yield poller2.off();
+  yield poller2.off();
+  ok(true, "Poller.prototype.off() is idempotent");
+
+  // This should still have not polled a second time
+  is(count1, 1, "wait time works");
+
+  ok(poller1.isPolling(), "isPolling() returns true for an on poller");
+  ok(!poller2.isPolling(), "isPolling() returns false for an off poller");
+});
+
+add_task(function *() {
+  let count = -1;
+  // Create a poller that returns a promise.
+  // The promise is resolved asynchronously after adding 9 to the count, ensuring
+  // that on every poll, we have a multiple of 10.
+  let asyncPoller = new Poller(function () {
+    count++;
+    ok(!(count%10), `Async poller called with a multiple of 10: ${count}`);
+    return new Promise(function (resolve, reject) {
+      let add9 = 9;
+      let interval = setInterval(() => {
+        if (add9--) {
+          count++;
+        } else {
+          clearInterval(interval);
+          resolve();
+        }
+      }, 10);
+    });
+  });
+
+  asyncPoller.on(1);
+  yield waitUntil(() => count > 50);
+  yield asyncPoller.off();
+});
+
+add_task(function *() {
+  // Create a poller that returns a promise. This poll call
+  // is called immediately, and then subsequently turned off.
+  // The call to `off` should not resolve until the inflight call
+  // finishes.
+  let inflightFinished = null;
+  let pollCalls = 0;
+  let asyncPoller = new Poller(function () {
+    pollCalls++;
+    return new Promise(function (resolve, reject) {
+      setTimeout(() => {
+        inflightFinished = true;
+        resolve();
+      }, 1000);
+    });
+  }, 1, true);
+  asyncPoller.on();
+
+  yield asyncPoller.off();
+  ok(inflightFinished, "off() method does not resolve until remaining inflight poll calls finish");
+  is(pollCalls, 1, "should only be one poll call to occur before turning off polling");
+});
+
+add_task(function *() {
+  // Create a poller that returns a promise. This poll call
+  // is called immediately, and then subsequently turned off.
+  // The call to `off` should not resolve until the inflight call
+  // finishes.
+  let inflightFinished = null;
+  let pollCalls = 0;
+  let asyncPoller = new Poller(function () {
+    pollCalls++;
+    return new Promise(function (resolve, reject) {
+      setTimeout(() => {
+        inflightFinished = true;
+        resolve();
+      }, 1000);
+    });
+  }, 1, true);
+  asyncPoller.on();
+
+  yield asyncPoller.destroy();
+  ok(inflightFinished, "destroy() method does not resolve until remaining inflight poll calls finish");
+  is(pollCalls, 1, "should only be one poll call to occur before destroying polling");
+  
+  try {
+    asyncPoller.on();
+    ok(false, "Calling on() after destruction should throw");
+  } catch (e) {
+    ok(true, "Calling on() after destruction should throw");
+  }
+});
--- a/browser/devtools/shared/test/head.js
+++ b/browser/devtools/shared/test/head.js
@@ -237,8 +237,29 @@ function* openAndCloseToolbox(nbOfTimes,
 
     // We use a timeout to check the toolbox's active time
     yield new Promise(resolve => setTimeout(resolve, usageTime));
 
     info("Closing toolbox " + (i + 1));
     yield gDevTools.closeToolbox(target);
   }
 }
+
+/**
+ * Waits until a predicate returns true.
+ *
+ * @param function predicate
+ *        Invoked once in a while until it returns true.
+ * @param number interval [optional]
+ *        How often the predicate is invoked, in milliseconds.
+ */
+function waitUntil(predicate, interval = 10) {
+  if (predicate()) {
+    return Promise.resolve(true);
+  }
+  return new Promise(resolve => {
+    setTimeout(function() {
+      waitUntil(predicate).then(() => resolve(true));
+    }, interval);
+  });
+}
+
+// EventUtils just doesn't work!
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -147,32 +147,34 @@ function CssHtmlTree(aStyleInspector, aP
   this.focusWindow = this.focusWindow.bind(this);
   this._onContextMenu = this._onContextMenu.bind(this);
   this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
   this._onSelectAll = this._onSelectAll.bind(this);
   this._onClick = this._onClick.bind(this);
   this._onCopy = this._onCopy.bind(this);
   this._onCopyColor = this._onCopyColor.bind(this);
   this._onFilterStyles = this._onFilterStyles.bind(this);
+  this._onFilterKeyPress = this._onFilterKeyPress.bind(this);
   this._onClearSearch = this._onClearSearch.bind(this);
   this._onIncludeBrowserStyles = this._onIncludeBrowserStyles.bind(this);
   this._onFilterTextboxContextMenu = this._onFilterTextboxContextMenu.bind(this);
 
   let doc = this.styleDocument;
   this.root = doc.getElementById("root");
   this.element = doc.getElementById("propertyContainer");
   this.searchField = doc.getElementById("computedview-searchbox");
   this.searchClearButton = doc.getElementById("computedview-searchinput-clear");
   this.includeBrowserStylesCheckbox = doc.getElementById("browser-style-checkbox");
 
   this.styleDocument.addEventListener("mousedown", this.focusWindow);
   this.element.addEventListener("click", this._onClick);
   this.element.addEventListener("copy", this._onCopy);
   this.element.addEventListener("contextmenu", this._onContextMenu);
   this.searchField.addEventListener("input", this._onFilterStyles);
+  this.searchField.addEventListener("keypress", this._onFilterKeyPress);
   this.searchField.addEventListener("contextmenu", this._onFilterTextboxContextMenu);
   this.searchClearButton.addEventListener("click", this._onClearSearch);
   this.includeBrowserStylesCheckbox.addEventListener("command",
     this._onIncludeBrowserStyles);
 
   this.searchClearButton.hidden = true;
 
   // No results text.
@@ -540,16 +542,56 @@ CssHtmlTree.prototype = {
       }
 
       this.refreshPanel();
       this._filterChangeTimeout = null;
     }, filterTimeout);
   },
 
   /**
+   * Handle the search box's keypress event. If the escape key is pressed,
+   * clear the search box field.
+   */
+  _onFilterKeyPress: function(aEvent) {
+    if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE &&
+        this._onClearSearch()) {
+      aEvent.preventDefault();
+      aEvent.stopPropagation();
+    }
+  },
+
+  /**
+   * Context menu handler for filter style search box.
+   */
+  _onFilterTextboxContextMenu: function(event) {
+    try {
+      this.styleDocument.defaultView.focus();
+      let contextmenu = this.inspector.toolbox.textboxContextMenuPopup;
+      contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
+    } catch(e) {
+      console.error(e);
+    }
+  },
+
+  /**
+   * Called when the user clicks on the clear button in the filter style search
+   * box. Returns true if the search box is cleared and false otherwise.
+   */
+  _onClearSearch: function() {
+    if (this.searchField.value) {
+      this.searchField.value = "";
+      this.searchField.focus();
+      this._onFilterStyles();
+      return true;
+    }
+
+    return false;
+  },
+
+  /**
    * The change event handler for the includeBrowserStyles checkbox.
    *
    * @param {Event} aEvent the DOM Event object.
    */
   _onIncludeBrowserStyles: function(aEvent)
   {
     this.refreshSourceFilter();
     this.refreshPanel();
@@ -826,39 +868,16 @@ CssHtmlTree.prototype = {
    *  Toggle the original sources pref.
    */
   _onToggleOrigSources: function()
   {
     let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
     Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
   },
 
-   /**
-   * Context menu handler for filter style search box.
-   */
-  _onFilterTextboxContextMenu: function(event) {
-    try {
-      this.styleDocument.defaultView.focus();
-      let contextmenu = this.inspector.toolbox.textboxContextMenuPopup;
-      contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
-    } catch(e) {
-      console.error(e);
-    }
-  },
-
-  /**
-   * Called when the user clicks on the clear button in the filter style search
-   * box.
-   */
-  _onClearSearch: function() {
-    this.searchField.value = "";
-    this.searchField.focus();
-    this._onFilterStyles();
-  },
-
   /**
    * Destructor for CssHtmlTree.
    */
   destroy: function CssHtmlTree_destroy()
   {
     this.viewedElement = null;
     this._outputParser = null;
 
@@ -901,16 +920,17 @@ CssHtmlTree.prototype = {
     this.highlighters.destroy();
 
     // Remove bound listeners
     this.styleDocument.removeEventListener("mousedown", this.focusWindow);
     this.element.removeEventListener("click", this._onClick);
     this.element.removeEventListener("copy", this._onCopy);
     this.element.removeEventListener("contextmenu", this._onContextMenu);
     this.searchField.removeEventListener("input", this._onFilterStyles);
+    this.searchField.removeEventListener("keypress", this._onFilterKeyPress);
     this.searchField.removeEventListener("contextmenu", this._onFilterTextboxContextMenu);
     this.searchClearButton.removeEventListener("click", this._onClearSearch);
     this.includeBrowserStylesCheckbox.removeEventListener("command",
       this.includeBrowserStylesChanged);
 
     // Nodes used in templating
     this.root = null;
     this.element = null;
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -1124,28 +1124,30 @@ function CssRuleView(aInspector, aDoc, a
   this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
   this._onAddRule = this._onAddRule.bind(this);
   this._onSelectAll = this._onSelectAll.bind(this);
   this._onCopy = this._onCopy.bind(this);
   this._onCopyColor = this._onCopyColor.bind(this);
   this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
   this._onShowMdnDocs = this._onShowMdnDocs.bind(this);
   this._onFilterStyles = this._onFilterStyles.bind(this);
+  this._onFilterKeyPress = this._onFilterKeyPress.bind(this);
   this._onClearSearch = this._onClearSearch.bind(this);
   this._onFilterTextboxContextMenu = this._onFilterTextboxContextMenu.bind(this);
 
   this.element = this.doc.getElementById("ruleview-container");
   this.searchField = this.doc.getElementById("ruleview-searchbox");
   this.searchClearButton = this.doc.getElementById("ruleview-searchinput-clear");
 
   this.searchClearButton.hidden = true;
 
   this.element.addEventListener("copy", this._onCopy);
   this.element.addEventListener("contextmenu", this._onContextMenu);
   this.searchField.addEventListener("input", this._onFilterStyles);
+  this.searchField.addEventListener("keypress", this._onFilterKeyPress);
   this.searchField.addEventListener("contextmenu", this._onFilterTextboxContextMenu);
   this.searchClearButton.addEventListener("click", this._onClearSearch);
 
   this._handlePrefChange = this._handlePrefChange.bind(this);
   this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this);
 
   this._prefObserver = new PrefObserver("devtools.");
   this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
@@ -1643,36 +1645,53 @@ CssRuleView.prototype = {
 
       this.inspector.emit("ruleview-filtered");
 
       this._filterChangeTimeout = null;
     }, filterTimeout);
   },
 
   /**
+   * Handle the search box's keypress event. If the escape key is pressed,
+   * clear the search box field.
+   */
+  _onFilterKeyPress: function(event) {
+    if (event.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE &&
+        this._onClearSearch()) {
+      event.preventDefault();
+      event.stopPropagation();
+    }
+  },
+
+  /**
    * Context menu handler for filter style search box.
    */
   _onFilterTextboxContextMenu: function(event) {
     try {
       this.doc.defaultView.focus();
       let contextmenu = this.inspector.toolbox.textboxContextMenuPopup;
       contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
     } catch(e) {
       console.error(e);
     }
   },
 
   /**
    * Called when the user clicks on the clear button in the filter style search
-   * box.
+   * box. Returns true if the search box is cleared and false otherwise.
    */
   _onClearSearch: function() {
-    this.searchField.value = "";
-    this.searchField.focus();
-    this._onFilterStyles();
+    if (this.searchField.value) {
+      this.searchField.value = "";
+      this.searchField.focus();
+      this._onFilterStyles();
+      return true;
+    }
+
+    return false;
   },
 
   destroy: function() {
     this.isDestroyed = true;
     this.clear();
 
     gDummyPromise = null;
 
@@ -1716,16 +1735,17 @@ CssRuleView.prototype = {
 
     this.tooltips.destroy();
     this.highlighters.destroy();
 
     // Remove bound listeners
     this.element.removeEventListener("copy", this._onCopy);
     this.element.removeEventListener("contextmenu", this._onContextMenu);
     this.searchField.removeEventListener("input", this._onFilterStyles);
+    this.searchField.removeEventListener("keypress", this._onFilterKeyPress);
     this.searchField.removeEventListener("contextmenu",
       this._onFilterTextboxContextMenu);
     this.searchClearButton.removeEventListener("click", this._onClearSearch);
     this.searchField = null;
     this.searchClearButton = null;
 
     if (this.element.parentNode) {
       this.element.parentNode.removeChild(this.element);
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -37,16 +37,17 @@ support-files =
 [browser_computedview_media-queries.js]
 [browser_computedview_no-results-placeholder.js]
 [browser_computedview_original-source-link.js]
 [browser_computedview_pseudo-element_01.js]
 [browser_computedview_refresh-on-style-change_01.js]
 [browser_computedview_search-filter.js]
 [browser_computedview_search-filter_clear.js]
 [browser_computedview_search-filter_context-menu.js]
+[browser_computedview_search-filter_escape-keypress.js]
 [browser_computedview_select-and-copy-styles.js]
 [browser_computedview_style-editor-link.js]
 [browser_ruleview_add-property-and-reselect.js]
 [browser_ruleview_add-property-cancel_01.js]
 [browser_ruleview_add-property-cancel_02.js]
 [browser_ruleview_add-property-cancel_03.js]
 [browser_ruleview_add-property_01.js]
 [browser_ruleview_add-property_02.js]
@@ -118,16 +119,17 @@ skip-if = e10s # Bug 1090340
 [browser_ruleview_search-filter_05.js]
 [browser_ruleview_search-filter_06.js]
 [browser_ruleview_search-filter_07.js]
 [browser_ruleview_search-filter_08.js]
 [browser_ruleview_search-filter_09.js]
 [browser_ruleview_search-filter_10.js]
 [browser_ruleview_search-filter_clear.js]
 [browser_ruleview_search-filter_context-menu.js]
+[browser_ruleview_search-filter_escape-keypress.js]
 [browser_ruleview_select-and-copy-styles.js]
 [browser_ruleview_selector-highlighter_01.js]
 [browser_ruleview_selector-highlighter_02.js]
 [browser_ruleview_selector-highlighter_03.js]
 [browser_ruleview_style-editor-link.js]
 skip-if = e10s # bug 1040670 Cannot open inline styles in viewSourceUtils
 [browser_ruleview_urls-clickable.js]
 [browser_ruleview_user-agent-styles.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_computedview_search-filter_escape-keypress.js
@@ -0,0 +1,71 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that search filter escape keypress will clear the search field.
+
+let TEST_URI = [
+  '<style type="text/css">',
+  '  .matches {',
+  '    color: #F00;',
+  '  }',
+  '</style>',
+  '<span id="matches" class="matches">Some styled text</span>'
+].join("\n");
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {toolbox, inspector, view} = yield openComputedView();
+  yield selectNode("#matches", inspector);
+  yield testAddTextInFilter(inspector, view);
+  yield testEscapeKeypress(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, computedView) {
+  info("Setting filter text to \"background-color\"");
+
+  let win = computedView.styleWindow;
+  let propertyViews = computedView.propertyViews;
+  let searchField = computedView.searchField;
+  let checkbox = computedView.includeBrowserStylesCheckbox;
+
+  info("Include browser styles");
+  checkbox.click();
+  yield inspector.once("computed-view-refreshed");
+
+  searchField.focus();
+  synthesizeKeys("background-color", win);
+  yield inspector.once("computed-view-refreshed");
+
+  info("Check that the correct properties are visible");
+
+  propertyViews.forEach((propView) => {
+    let name = propView.name;
+    is(propView.visible, name.indexOf("background-color") > -1,
+      "span " + name + " property visibility check");
+  });
+}
+
+function* testEscapeKeypress(inspector, computedView) {
+  info("Pressing the escape key on search filter");
+
+  let win = computedView.styleWindow;
+  let propertyViews = computedView.propertyViews;
+  let searchField = computedView.searchField;
+  let onRefreshed = inspector.once("computed-view-refreshed");
+
+  searchField.focus();
+  EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+  yield onRefreshed;
+
+  info("Check that the correct properties are visible");
+
+  ok(!searchField.value, "Search filter is cleared");
+  propertyViews.forEach((propView) => {
+    let name = propView.name;
+    is(propView.visible, true,
+      "span " + name + " property is visible");
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_search-filter_escape-keypress.js
@@ -0,0 +1,66 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view search filter escape keypress will clear the search
+// field.
+
+let TEST_URI = [
+  '<style type="text/css">',
+  '  #testid {',
+  '    background-color: #00F;',
+  '  }',
+  '  .testclass {',
+  '    width: 100%;',
+  '  }',
+  '</style>',
+  '<div id="testid" class="testclass">Styled Node</div>'
+].join("\n");
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {toolbox, inspector, view} = yield openRuleView();
+  yield selectNode("#testid", inspector);
+  yield testAddTextInFilter(inspector, view);
+  yield testEscapeKeypress(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, ruleView) {
+  info("Setting filter text to \"00F\"");
+
+  let win = ruleView.doc.defaultView;
+  let searchField = ruleView.searchField;
+  let onRuleViewFiltered = inspector.once("ruleview-filtered");
+
+  searchField.focus();
+  synthesizeKeys("00F", win);
+  yield onRuleViewFiltered;
+
+  info("Check that the correct rules are visible");
+  is(ruleView.element.children.length, 2, "Should have 2 rules.");
+  is(getRuleViewRuleEditor(ruleView, 0).rule.selectorText, "element", "First rule is inline element.");
+  is(getRuleViewRuleEditor(ruleView, 1).rule.selectorText, "#testid", "Second rule is #testid.");
+  ok(getRuleViewRuleEditor(ruleView, 1).rule.textProps[0].editor.element.classList.contains("ruleview-highlight"),
+    "background-color text property is correctly highlighted.");
+}
+
+function* testEscapeKeypress(inspector, ruleView) {
+  info("Pressing the escape key on search filter");
+
+  let doc = ruleView.doc;
+  let win = ruleView.doc.defaultView;
+  let searchField = ruleView.searchField;
+  let onRuleViewFiltered = inspector.once("ruleview-filtered");
+
+  searchField.focus();
+  EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+  yield onRuleViewFiltered;
+
+  info("Check the search filter is cleared and no rules are highlighted");
+  is(ruleView.element.children.length, 3, "Should have 3 rules.");
+  ok(!searchField.value, "Search filter is cleared");
+  ok(!doc.querySelectorAll(".ruleview-highlight").length &&
+     !ruleView._highlightedElements.length, "No rules are higlighted");
+}
--- a/browser/themes/shared/devtools/animationinspector.css
+++ b/browser/themes/shared/devtools/animationinspector.css
@@ -94,16 +94,46 @@ body {
     background-image: url("debugger-pause@2x.png");
   }
 
   #toggle-all.paused::before {
     background-image: url("debugger-play@2x.png");
   }
 }
 
+/* Animation target node gutter, contains a preview of the dom node */
+
+.animation-target {
+  background-color: var(--theme-toolbar-background);
+  padding: 1px 4px;
+  box-sizing: border-box;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.animation-target .attribute-name {
+  padding-left: 4px;
+}
+
+.animation-target .node-selector {
+  background: url("chrome://browser/skin/devtools/vview-open-inspector.png") no-repeat 0 0;
+  padding-left: 16px;
+  margin-right: 5px;
+  cursor: pointer;
+}
+
+.animation-target .node-selector:hover {
+  background-position: -32px 0;
+}
+
+.animation-target .node-selector:active {
+  background-position: -16px 0;
+}
+
 /* Animation title gutter, contains the name, duration, iteration */
 
 .animation-title {
   background-color: var(--theme-toolbar-background);
   border-bottom: 1px solid var(--theme-splitter-color);
   padding: 1px 4px;
   word-wrap: break-word;
   overflow: auto;
--- a/mobile/android/base/GuestSession.java
+++ b/mobile/android/base/GuestSession.java
@@ -36,17 +36,17 @@ public class GuestSession {
             return false;
         }
 
         return profile.locked();
     }
 
     private static PendingIntent getNotificationIntent(Context context) {
         Intent intent = new Intent(NOTIFICATION_INTENT);
-        intent.setClass(context, BrowserApp.class);
+        intent.setClassName(context, AppConstants.BROWSER_INTENT_CLASS_NAME);
         return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
     }
 
     public static void showNotification(Context context) {
         final NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
         final Resources res = context.getResources();
         builder.setContentTitle(res.getString(R.string.guest_browsing_notification_title))
                .setContentText(res.getString(R.string.guest_browsing_notification_text))
--- a/mobile/android/base/tabqueue/TabQueueDispatcher.java
+++ b/mobile/android/base/tabqueue/TabQueueDispatcher.java
@@ -1,17 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.tabqueue;
 
 import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.sync.setup.activities.WebURLFinder;
 
 import android.content.Intent;
@@ -64,17 +63,17 @@ public class TabQueueDispatcher extends 
         startService(intent);
         finish();
     }
 
     /**
      * Start fennec with the supplied intent.
      */
     private void loadNormally(Intent intent) {
-        intent.setClass(getApplicationContext(), BrowserApp.class);
+        intent.setClassName(getApplicationContext(), AppConstants.BROWSER_INTENT_CLASS_NAME);
         startActivity(intent);
         finish();
     }
 
     /**
      * Abort as we were started with no URL.
      * @param dataString
      */
--- a/mobile/android/base/tabqueue/TabQueueHelper.java
+++ b/mobile/android/base/tabqueue/TabQueueHelper.java
@@ -1,16 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.tabqueue;
 
-import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.util.ThreadUtils;
 
@@ -149,17 +149,18 @@ public class TabQueueHelper {
      * will be replaced.
      *
      * @param context
      * @param tabsQueued
      */
     public static void showNotification(final Context context, final int tabsQueued) {
         ThreadUtils.assertNotOnUiThread();
 
-        Intent resultIntent = new Intent(context, BrowserApp.class);
+        Intent resultIntent = new Intent();
+        resultIntent.setClassName(context, AppConstants.BROWSER_INTENT_CLASS_NAME);
         resultIntent.setAction(TabQueueHelper.LOAD_URLS_ACTION);
 
         PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, resultIntent, PendingIntent.FLAG_CANCEL_CURRENT);
 
         final String text;
         final Resources resources = context.getResources();
         if (tabsQueued == 1) {
             text = resources.getString(R.string.tab_queue_notification_text_singular);
--- a/mobile/android/base/tabqueue/TabQueueService.java
+++ b/mobile/android/base/tabqueue/TabQueueService.java
@@ -1,16 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.tabqueue;
 
-import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 
 import android.app.Service;
 import android.content.Context;
@@ -185,17 +185,17 @@ public class TabQueueService extends Ser
 
         tabQueueHandler.postDelayed(stopServiceRunnable, TOAST_TIMEOUT);
 
         return START_REDELIVER_INTENT;
     }
 
     private void openNow(Intent intent) {
         Intent forwardIntent = new Intent(intent);
-        forwardIntent.setClass(getApplicationContext(), BrowserApp.class);
+        forwardIntent.setClassName(getApplicationContext(), AppConstants.BROWSER_INTENT_CLASS_NAME);
         forwardIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         startActivity(forwardIntent);
 
         GeckoSharedPrefs.forApp(getApplicationContext()).edit().remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_SITE)
                                                                .remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_TIME)
                                                                .apply();
     }
 
--- a/toolkit/components/telemetry/TelemetryController.jsm
+++ b/toolkit/components/telemetry/TelemetryController.jsm
@@ -191,29 +191,16 @@ this.TelemetryController = Object.freeze
   /**
    * Sets a server to send pings to.
    */
   setServer: function(aServer) {
     return Impl.setServer(aServer);
   },
 
   /**
-   * Adds a ping to the pending ping list by moving it to the saved pings directory
-   * and adding it to the pending ping list.
-   *
-   * @param {String} aPingPath The path of the ping to add to the pending ping list.
-   * @param {Boolean} [aRemoveOriginal] If true, deletes the ping at aPingPath after adding
-   *                  it to the saved pings directory.
-   * @return {Promise} Resolved when the ping is correctly moved to the saved pings directory.
-   */
-  addPendingPingFromFile: function(aPingPath, aRemoveOriginal) {
-    return Impl.addPendingPingFromFile(aPingPath, aRemoveOriginal);
-  },
-
-  /**
    * Submit ping payloads to Telemetry. This will assemble a complete ping, adding
    * environment data, client id and some general info.
    * Depending on configuration, the ping will be sent to the server (immediately or later)
    * and archived locally.
    *
    * @param {String} aType The type of the ping.
    * @param {Object} aPayload The actual data payload for the ping.
    * @param {Object} [aOptions] Options object.
@@ -280,16 +267,26 @@ this.TelemetryController = Object.freeze
     options.addClientId = aOptions.addClientId || false;
     options.addEnvironment = aOptions.addEnvironment || false;
     options.overwrite = aOptions.overwrite || false;
 
     return Impl.addPendingPing(aType, aPayload, options);
   },
 
   /**
+   * Save an aborted-session ping to the pending pings and archive it.
+   *
+   * @param {String} aFilePath The path to the aborted-session checkpoint ping.
+   * @return {Promise} Promise that is resolved when the ping is saved.
+   */
+  addAbortedSessionPing: function addAbortedSessionPing(aFilePath) {
+    return Impl.addAbortedSessionPing(aFilePath);
+  },
+
+  /**
    * Write a ping to a specified location on the disk. Does not add the ping to the
    * pending pings.
    *
    * @param {String} aType The type of the ping.
    * @param {Object} aPayload The actual data payload for the ping.
    * @param {String} aFilePath The path to save the ping to.
    * @param {Object} [aOptions] Options object.
    * @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
@@ -496,33 +493,16 @@ let Impl = {
    * Track any pending ping send and save tasks through the promise passed here.
    * This is needed to block shutdown on any outstanding ping activity.
    */
   _trackPendingPingTask: function (aPromise) {
     this._connectionsBarrier.client.addBlocker("Waiting for ping task", aPromise);
   },
 
   /**
-   * Adds a ping to the pending ping list by moving it to the saved pings directory
-   * and adding it to the pending ping list.
-   *
-   * @param {String} aPingPath The path of the ping to add to the pending ping list.
-   * @param {Boolean} [aRemoveOriginal] If true, deletes the ping at aPingPath after adding
-   *                  it to the saved pings directory.
-   * @return {Promise} Resolved when the ping is correctly moved to the saved pings directory.
-   */
-  addPendingPingFromFile: function(aPingPath, aRemoveOriginal) {
-    return TelemetryStorage.addPendingPingFromFile(aPingPath).then(() => {
-        if (aRemoveOriginal) {
-          return OS.File.remove(aPingPath);
-        }
-      }, error => this._log.error("addPendingPingFromFile - Unable to add the pending ping", error));
-  },
-
-  /**
    * This helper calculates the next time that we can send pings at.
    * Currently this mostly redistributes ping sends around midnight to avoid submission
    * spikes around local midnight for daily pings.
    *
    * @param now Date The current time.
    * @return Number The next time (ms from UNIX epoch) when we can send pings.
    */
   _getNextPingSendTime: function(now) {
@@ -715,16 +695,36 @@ let Impl = {
   savePing: function savePing(aType, aPayload, aFilePath, aOptions) {
     this._log.trace("savePing - Type " + aType + ", Server " + this._server +
                     ", File Path " + aFilePath + ", aOptions " + JSON.stringify(aOptions));
     let pingData = this.assemblePing(aType, aPayload, aOptions);
     return TelemetryStorage.savePingToFile(pingData, aFilePath, aOptions.overwrite)
                         .then(() => pingData.id);
   },
 
+  /**
+   * Save an aborted-session ping to the pending pings and archive it.
+   *
+   * @param {String} aFilePath The path to the aborted-session checkpoint ping.
+   * @return {Promise} Promise that is resolved when the ping is saved.
+   */
+  addAbortedSessionPing: Task.async(function* addAbortedSessionPing(aFilePath) {
+    this._log.trace("addAbortedSessionPing");
+
+    let ping = yield TelemetryStorage.loadPingFile(aFilePath);
+    try {
+      yield TelemetryStorage.addPendingPing(ping);
+      yield TelemetryArchive.promiseArchivePing(ping);
+    } catch (e) {
+      this._log.error("addAbortedSessionPing - Unable to add the pending ping", e);
+    } finally {
+      yield OS.File.remove(aFilePath);
+    }
+  }),
+
   onPingRequestFinished: function(success, startTime, ping, isPersisted) {
     this._log.trace("onPingRequestFinished - success: " + success + ", persisted: " + isPersisted);
 
     let hping = Telemetry.getHistogramById("TELEMETRY_PING");
     let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
 
     hsuccess.add(success);
     hping.add(new Date() - startTime);
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -1527,22 +1527,21 @@ let Impl = {
     // Delay full telemetry initialization to give the browser time to
     // run various late initializers. Otherwise our gathered memory
     // footprint and other numbers would be too optimistic.
     this._delayedInitTaskDeferred = Promise.defer();
     this._delayedInitTask = new DeferredTask(function* () {
       try {
         this._initialized = true;
 
-        let hasLoaded = yield this._loadSessionData();
-        if (!hasLoaded) {
-          // We could not load a valid session data file. Create one.
-          yield this._saveSessionData(this._getSessionDataObject()).catch(() =>
-            this._log.error("setupChromeProcess - Could not write session data to disk."));
-        }
+        yield this._loadSessionData();
+        // Update the session data to keep track of new subsessions created before
+        // the initialization.
+        yield this._saveSessionData(this._getSessionDataObject());
+
         this.attachObservers();
         this.gatherMemory();
 
         Telemetry.asyncFetchTelemetryData(function () {});
 
         if (IS_UNIFIED_TELEMETRY) {
           // Check for a previously written aborted session ping.
           yield this._checkAbortedSessionPing();
@@ -1960,18 +1959,18 @@ let Impl = {
                                 SESSION_STATE_FILE_NAME);
 
     // Try to load the "profileSubsessionCounter" from the state file.
     try {
       let data = yield CommonUtils.readJSON(dataFile);
       if (data &&
           "profileSubsessionCounter" in data &&
           typeof(data.profileSubsessionCounter) == "number" &&
-          "previousSubsessionId" in data) {
-        this._previousSubsessionId = data.previousSubsessionId;
+          "subsessionId" in data) {
+        this._previousSubsessionId = data.subsessionId;
         // Add |_subsessionCounter| to the |_profileSubsessionCounter| to account for
         // new subsession while loading still takes place. This will always be exactly
         // 1 - the current subsessions.
         this._profileSubsessionCounter = data.profileSubsessionCounter +
                                          this._subsessionCounter;
         return true;
       }
     } catch (e) {
@@ -1980,17 +1979,17 @@ let Impl = {
     return false;
   }),
 
   /**
    * Get the session data object to serialise to disk.
    */
   _getSessionDataObject: function() {
     return {
-      previousSubsessionId: this._previousSubsessionId,
+      subsessionId: this._subsessionId,
       profileSubsessionCounter: this._profileSubsessionCounter,
     };
   },
 
   /**
    * Saves session data to disk.
    */
   _saveSessionData: Task.async(function* (sessionData) {
@@ -2004,18 +2003,17 @@ let Impl = {
       this._log.error("_saveSessionData - Failed to write session data to " + filePath, e);
     }
   }),
 
   _onEnvironmentChange: function(reason, oldEnvironment) {
     this._log.trace("_onEnvironmentChange", reason);
     let payload = this.getSessionPayload(REASON_ENVIRONMENT_CHANGE, true);
 
-    let clonedPayload = Cu.cloneInto(payload, myScope);
-    TelemetryScheduler.reschedulePings(REASON_ENVIRONMENT_CHANGE, clonedPayload);
+    TelemetryScheduler.reschedulePings(REASON_ENVIRONMENT_CHANGE, payload);
 
     let options = {
       retentionDays: RETENTION_DAYS,
       addClientId: true,
       addEnvironment: true,
       overrideEnvironment: oldEnvironment,
     };
     TelemetryController.submitExternalPing(getPingType(payload), payload, options);
@@ -2073,34 +2071,34 @@ let Impl = {
     yield OS.File.makeDir(ABORTED_SESSIONS_DIR, { ignoreExisting: true });
 
     const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
                                    ABORTED_SESSION_FILE_NAME);
     let abortedExists = yield OS.File.exists(FILE_PATH);
     if (abortedExists) {
       this._log.trace("_checkAbortedSessionPing - aborted session found: " + FILE_PATH);
       yield this._abortedSessionSerializer.enqueueTask(
-        () => TelemetryController.addPendingPingFromFile(FILE_PATH, true));
+        () => TelemetryController.addAbortedSessionPing(FILE_PATH));
     }
   }),
 
   /**
    * Saves the aborted session ping to disk.
    * @param {Object} [aProvidedPayload=null] A payload object to be used as an aborted
    *                 session ping. The reason of this payload is changed to aborted-session.
    *                 If not provided, a new payload is gathered.
    */
   _saveAbortedSessionPing: function(aProvidedPayload = null) {
     const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
                                    ABORTED_SESSION_FILE_NAME);
     this._log.trace("_saveAbortedSessionPing - ping path: " + FILE_PATH);
 
     let payload = null;
     if (aProvidedPayload) {
-      payload = aProvidedPayload;
+      payload = Cu.cloneInto(aProvidedPayload, myScope);
       // Overwrite the original reason.
       payload.info.reason = REASON_ABORTED_SESSION;
     } else {
       payload = this.getSessionPayload(REASON_ABORTED_SESSION, false);
     }
 
     let options = {
       retentionDays: RETENTION_DAYS,
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
@@ -0,0 +1,227 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/TelemetryArchive.jsm", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+const MS_IN_ONE_HOUR  = 60 * 60 * 1000;
+const MS_IN_ONE_DAY   = 24 * MS_IN_ONE_HOUR;
+
+const PREF_BRANCH = "toolkit.telemetry.";
+const PREF_ENABLED = PREF_BRANCH + "enabled";
+const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled";
+
+const REASON_ABORTED_SESSION = "aborted-session";
+const REASON_DAILY = "daily";
+const REASON_ENVIRONMENT_CHANGE = "environment-change";
+const REASON_SHUTDOWN = "shutdown";
+
+XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function() {
+  return OS.Path.join(OS.Constants.Path.profileDir, "datareporting");
+});
+
+let promiseValidateArchivedPings = Task.async(function*(aExpectedReasons) {
+  // The list of ping reasons which mark the session end (and must reset the subsession
+  // count).
+  const SESSION_END_PING_REASONS = new Set([ REASON_ABORTED_SESSION, REASON_SHUTDOWN ]);
+
+  let list = yield TelemetryArchive.promiseArchivedPingList();
+
+  // We're just interested in the "main" pings.
+  list = list.filter(p => p.type == "main");
+
+  Assert.equal(aExpectedReasons.length, list.length, "All the expected pings must be received.");
+
+  let previousPing = yield TelemetryArchive.promiseArchivedPingById(list[0].id);
+  Assert.equal(aExpectedReasons.shift(), previousPing.payload.info.reason,
+               "Telemetry should only get pings with expected reasons.");
+  Assert.equal(previousPing.payload.info.previousSubsessionId, null,
+               "The first subsession must report a null previous subsession id.");
+  Assert.equal(previousPing.payload.info.profileSubsessionCounter, 1,
+               "profileSubsessionCounter must be 1 the first time.");
+  Assert.equal(previousPing.payload.info.subsessionCounter, 1,
+               "subsessionCounter must be 1 the first time.");
+
+  let expectedSubsessionCounter = 1;
+
+  for (let i = 1; i < list.length; i++) {
+    let currentPing = yield TelemetryArchive.promiseArchivedPingById(list[i].id);
+    let currentInfo = currentPing.payload.info;
+    let previousInfo = previousPing.payload.info;
+    do_print("Archive entry " + i + " - id: " + currentPing.id + ", reason: " + currentInfo.reason);
+
+    Assert.equal(aExpectedReasons.shift(), currentInfo.reason,
+                 "Telemetry should only get pings with expected reasons.");
+    Assert.equal(currentInfo.previousSubsessionId, previousInfo.subsessionId,
+                 "Telemetry must correctly chain subsession identifiers.");
+    Assert.equal(currentInfo.profileSubsessionCounter, previousInfo.profileSubsessionCounter + 1,
+                 "Telemetry must correctly track the profile subsessions count.");
+    Assert.equal(currentInfo.subsessionCounter, expectedSubsessionCounter,
+                 "The subsession counter should be monotonically increasing.");
+
+    // Store the current ping as previous.
+    previousPing = currentPing;
+    // Reset the expected subsession counter, if required. Otherwise increment the expected
+    // subsession counter.
+    expectedSubsessionCounter =
+      SESSION_END_PING_REASONS.has(currentInfo.reason) ? 1 : (expectedSubsessionCounter + 1);
+  }
+});
+
+function run_test() {
+  do_test_pending();
+
+  // Addon manager needs a profile directory
+  do_get_profile();
+  loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+  Preferences.set(PREF_ENABLED, true);
+
+  run_next_test();
+}
+
+add_task(function* test_subsessionsChaining() {
+  if (gIsAndroid) {
+    // We don't support subsessions yet on Android, so skip the next checks.
+    return;
+  }
+
+  const PREF_TEST = PREF_BRANCH + "test.pref1";
+  const PREFS_TO_WATCH = new Map([
+    [PREF_TEST, TelemetryEnvironment.RECORD_PREF_VALUE],
+  ]);
+  Preferences.reset(PREF_TEST);
+
+  // Fake the clock data to manually trigger an aborted-session ping and a daily ping.
+  // This is also helpful to make sure we get the archived pings in an expected order.
+  let now = fakeNow(2009, 9, 18, 0, 0, 0);
+
+  let moveClockForward = (minutes) => {
+    now = futureDate(now, minutes * MILLISECONDS_PER_MINUTE);
+    fakeNow(now);
+  }
+
+  // Keep track of the ping reasons we're expecting in this test.
+  let expectedReasons = [];
+
+  // Start and shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 1,
+  // subsessionCounter: 1, subsessionId: A,  and previousSubsessionId: null to be archived.
+  yield TelemetrySession.reset();
+  yield TelemetrySession.shutdown();
+  expectedReasons.push(REASON_SHUTDOWN);
+
+  // Start Telemetry but don't wait for it to initialise before shutting down. We expect a
+  // shutdown ping with profileSubsessionCounter: 2, subsessionCounter: 1, subsessionId: B
+  // and previousSubsessionId: A to be archived.
+  moveClockForward(30);
+  TelemetrySession.reset();
+  yield TelemetrySession.shutdown();
+  expectedReasons.push(REASON_SHUTDOWN);
+
+  // Start Telemetry and simulate an aborted-session ping. We expect an aborted-session ping
+  // with profileSubsessionCounter: 3, subsessionCounter: 1, subsessionId: C and
+  // previousSubsessionId: B to be archived.
+  let schedulerTickCallback = null;
+  fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+  yield TelemetrySession.reset();
+  moveClockForward(6);
+  // Trigger the an aborted session ping save. When testing,we are not saving the aborted-session
+  // ping as soon as Telemetry starts, otherwise we would end up with unexpected pings being
+  // sent when calling |TelemetrySession.reset()|, thus breaking some tests.
+  Assert.ok(!!schedulerTickCallback);
+  yield schedulerTickCallback();
+  expectedReasons.push(REASON_ABORTED_SESSION);
+
+  // Start Telemetry and trigger an environment change through a pref modification. We expect
+  // an environment-change ping with profileSubsessionCounter: 4, subsessionCounter: 1,
+  // subsessionId: D and previousSubsessionId: C to be archived.
+  moveClockForward(30);
+  yield TelemetryController.reset();
+  yield TelemetrySession.reset();
+  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  moveClockForward(30);
+  Preferences.set(PREF_TEST, 1);
+  expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+  // Shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 5,
+  // subsessionCounter: 2, subsessionId: E and previousSubsessionId: D to be archived.
+  moveClockForward(30);
+  yield TelemetrySession.shutdown();
+  expectedReasons.push(REASON_SHUTDOWN);
+
+  // Start Telemetry and trigger a daily ping. We expect a daily ping with
+  // profileSubsessionCounter: 6, subsessionCounter: 1, subsessionId: F and
+  // previousSubsessionId: E to be archived.
+  moveClockForward(30);
+  yield TelemetrySession.reset();
+
+  // Delay the callback around midnight.
+  now = fakeNow(futureDate(now, MS_IN_ONE_DAY));
+  // Trigger the daily ping.
+  yield schedulerTickCallback();
+  expectedReasons.push(REASON_DAILY);
+
+  // Trigger an environment change ping. We expect an environment-changed ping with
+  // profileSubsessionCounter: 7, subsessionCounter: 2, subsessionId: G and
+  // previousSubsessionId: F to be archived.
+  moveClockForward(30);
+  Preferences.set(PREF_TEST, 0);
+  expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+  // Shut down Telemetry and trigger a shutdown ping.
+  moveClockForward(30);
+  yield TelemetrySession.shutdown();
+  expectedReasons.push(REASON_SHUTDOWN);
+
+  // Start Telemetry and trigger an environment change.
+  yield TelemetrySession.reset();
+  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  moveClockForward(30);
+  Preferences.set(PREF_TEST, 1);
+  expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+  // Don't shut down, instead trigger an aborted-session ping.
+  moveClockForward(6);
+  // Trigger the an aborted session ping save.
+  yield schedulerTickCallback();
+  expectedReasons.push(REASON_ABORTED_SESSION);
+
+  // Start Telemetry and trigger a daily ping.
+  moveClockForward(30);
+  yield TelemetryController.reset();
+  yield TelemetrySession.reset();
+  // Delay the callback around midnight.
+  now = futureDate(now, MS_IN_ONE_DAY);
+  fakeNow(now);
+  // Trigger the daily ping.
+  yield schedulerTickCallback();
+  expectedReasons.push(REASON_DAILY);
+
+  // Trigger an environment change.
+  moveClockForward(30);
+  Preferences.set(PREF_TEST, 0);
+  expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+  // And an aborted-session ping again.
+  moveClockForward(6);
+  // Trigger the an aborted session ping save.
+  yield schedulerTickCallback();
+  expectedReasons.push(REASON_ABORTED_SESSION);
+
+  // Make sure the aborted-session ping gets archived.
+  yield TelemetryController.reset();
+  yield TelemetrySession.reset();
+
+  yield promiseValidateArchivedPings(expectedReasons);
+});
+
+add_task(function* () {
+  do_test_finished();
+});
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -1162,17 +1162,17 @@ add_task(function* test_savedPingsOnShut
 add_task(function* test_savedSessionData() {
   // Create the directory which will contain the data file, if it doesn't already
   // exist.
   yield OS.File.makeDir(DATAREPORTING_PATH);
 
   // Write test data to the session data file.
   const dataFilePath = OS.Path.join(DATAREPORTING_PATH, "session-state.json");
   const sessionState = {
-    previousSubsessionId: null,
+    subsessionId: null,
     profileSubsessionCounter: 3785,
   };
   yield CommonUtils.writeJSON(sessionState, dataFilePath);
 
   const PREF_TEST = "toolkit.telemetry.test.pref1";
   Preferences.reset(PREF_TEST);
   const PREFS_TO_WATCH = new Map([
     [PREF_TEST, TelemetryEnvironment.RECORD_PREF_VALUE],
@@ -1204,17 +1204,50 @@ add_task(function* test_savedSessionData
 
   let payload = TelemetrySession.getPayload();
   Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
   yield TelemetrySession.shutdown();
 
   // Load back the serialised session data.
   let data = yield CommonUtils.readJSON(dataFilePath);
   Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
-  Assert.equal(data.previousSubsessionId, expectedUUID);
+  Assert.equal(data.subsessionId, expectedUUID);
+});
+
+add_task(function* test_sessionData_ShortSession() {
+  if (gIsAndroid) {
+    // We don't support subsessions yet on Android, so skip the next checks.
+    return;
+  }
+
+  const SESSION_STATE_PATH = OS.Path.join(DATAREPORTING_PATH, "session-state.json");
+
+  // Shut down Telemetry and remove the session state file.
+  yield TelemetrySession.shutdown();
+  yield OS.File.remove(SESSION_STATE_PATH, { ignoreAbsent: true });
+
+  const expectedUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+  fakeGenerateUUID(generateUUID, () => expectedUUID);
+
+  // We intentionally don't wait for the setup to complete and shut down to simulate
+  // short sessions. We expect the profile subsession counter to be 1.
+  TelemetrySession.reset();
+  yield TelemetrySession.shutdown();
+
+  // Restore the UUID generation functions.
+  fakeGenerateUUID(generateUUID, generateUUID);
+
+  // Start TelemetrySession so that it loads the session data file. We expect the profile
+  // subsession counter to be incremented by 1 again.
+  yield TelemetrySession.reset();
+
+  // We expect 2 profile subsession counter updates.
+  let payload = TelemetrySession.getPayload();
+  Assert.equal(payload.info.profileSubsessionCounter, 2);
+  Assert.equal(payload.info.previousSubsessionId, expectedUUID);
 });
 
 add_task(function* test_invalidSessionData() {
   // Create the directory which will contain the data file, if it doesn't already
   // exist.
   yield OS.File.makeDir(DATAREPORTING_PATH);
 
   // Write test data to the session data file.
@@ -1233,17 +1266,17 @@ add_task(function* test_invalidSessionDa
   yield TelemetrySession.reset();
   let payload = TelemetrySession.getPayload();
   Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
   yield TelemetrySession.shutdown();
 
   // Load back the serialised session data.
   let data = yield CommonUtils.readJSON(dataFilePath);
   Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
-  Assert.equal(data.previousSubsessionId, null);
+  Assert.equal(data.subsessionId, expectedUUID);
 });
 
 add_task(function* test_abortedSession() {
   if (gIsAndroid || gIsGonk) {
     // We don't have the aborted session ping here.
     return;
   }
 
--- a/toolkit/components/telemetry/tests/unit/xpcshell.ini
+++ b/toolkit/components/telemetry/tests/unit/xpcshell.ini
@@ -15,16 +15,17 @@ generated-files =
   dictionary.xpi
   experiment.xpi
   extension.xpi
   extension-2.xpi
   restartless.xpi
   theme.xpi
 
 [test_nsITelemetry.js]
+[test_SubsessionChaining.js]
 [test_TelemetryEnvironment.js]
 # Bug 1144395: crash on Android 4.3
 skip-if = android_version == "18"
 [test_PingAPI.js]
 [test_TelemetryFlagClear.js]
 [test_TelemetryLateWrites.js]
 [test_TelemetryLockCount.js]
 [test_TelemetryLog.js]
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -2880,37 +2880,85 @@ var WalkerActor = protocol.ActorClass({
       objectActorID: Arg(0, "string")
     },
     response: {
       nodeFront: RetVal("nullable:disconnectedNode")
     }
   }),
 
   /**
-   * Given an StyleSheetActor (identified by its ID), commonly used in the
+   * Given a StyleSheetActor (identified by its ID), commonly used in the
    * style-editor, get its ownerNode and return the corresponding walker's
-   * NodeActor
+   * NodeActor.
+   * Note that getNodeFromActor was added later and can now be used instead.
    */
   getStyleSheetOwnerNode: method(function(styleSheetActorID) {
-    let styleSheetActor = this.conn.getActor(styleSheetActorID);
-    let ownerNode = styleSheetActor.ownerNode;
-
-    if (!styleSheetActor || !ownerNode) {
-      return null;
-    }
-
-    return this.attachElement(ownerNode);
+    return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]);
   }, {
     request: {
       styleSheetActorID: Arg(0, "string")
     },
     response: {
       ownerNode: RetVal("nullable:disconnectedNode")
     }
   }),
+
+  /**
+   * This method can be used to retrieve NodeActor for DOM nodes from other
+   * actors in a way that they can later be highlighted in the page, or
+   * selected in the inspector.
+   * If an actor has a reference to a DOM node, and the UI needs to know about
+   * this DOM node (and possibly select it in the inspector), the UI should
+   * first retrieve a reference to the walkerFront:
+   *
+   * // Make sure the inspector/walker have been initialized first.
+   * toolbox.initInspector().then(() => {
+   *  // Retrieve the walker.
+   *  let walker = toolbox.walker;
+   * });
+   *
+   * And then call this method:
+   *
+   * // Get the nodeFront from my actor, passing the ID and properties path.
+   * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
+   *   // Use the nodeFront, e.g. select the node in the inspector.
+   *   toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
+   * });
+   *
+   * @param {String} actorID The ID for the actor that has a reference to the
+   * DOM node.
+   * @param {Array} path Where, on the actor, is the DOM node stored. If in the
+   * scope of the actor, the node is available as `this.data.node`, then this
+   * should be ["data", "node"].
+   * @return {NodeActor} The attached NodeActor, or null if it couldn't be found.
+   */
+  getNodeFromActor: method(function(actorID, path) {
+    let actor = this.conn.getActor(actorID);
+    if (!actor) {
+      return null;
+    }
+
+    let obj = actor;
+    for (let name of path) {
+      if (!(name in obj)) {
+        return null;
+      }
+      obj = obj[name];
+    }
+
+    return this.attachElement(obj);
+  }, {
+    request: {
+      actorID: Arg(0, "string"),
+      path: Arg(1, "array:string")
+    },
+    response: {
+      node: RetVal("nullable:disconnectedNode")
+    }
+  })
 });
 
 /**
  * Client side of the DOM walker.
  */
 var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
   // Set to true if cleanup should be requested after every mutation list.
   autoCleanup: true,
@@ -3058,16 +3106,24 @@ var WalkerFront = exports.WalkerFront = 
   getStyleSheetOwnerNode: protocol.custom(function(styleSheetActorID) {
     return this._getStyleSheetOwnerNode(styleSheetActorID).then(response => {
       return response ? response.node : null;
     });
   }, {
     impl: "_getStyleSheetOwnerNode"
   }),
 
+  getNodeFromActor: protocol.custom(function(actorID, path) {
+    return this._getNodeFromActor(actorID, path).then(response => {
+      return response ? response.node : null;
+    });
+  }, {
+    impl: "_getNodeFromActor"
+  }),
+
   _releaseFront: function(node, force) {
     if (node.retained && !force) {
       node.reparent(null);
       this._retainedOrphans.add(node);
       return;
     }
 
     if (node.retained) {
--- a/toolkit/devtools/server/actors/profiler.js
+++ b/toolkit/devtools/server/actors/profiler.js
@@ -54,16 +54,26 @@ ProfilerActor.prototype = {
    * Returns an array of feature strings, describing the profiler features
    * that are available on this platform. Can be called while the profiler
    * is stopped.
    */
   onGetFeatures: function() {
     return { features: nsIProfilerModule.GetFeatures([]) };
   },
 
+  onGetBufferInfo: function(request) {
+    let position = {}, totalSize = {}, generation = {};
+    nsIProfilerModule.GetBufferInfo(position, totalSize, generation);
+    return {
+      position: position.value,
+      totalSize: totalSize.value,
+      generation: generation.value
+    }
+  },
+
   /**
    * Returns the configuration used that was originally passed in to start up the
    * profiler. Used for tests, and does not account for others using nsIProfiler.
    */
   onGetStartOptions: function() {
     return this._profilerStartOptions || {};
   },
 
@@ -316,16 +326,17 @@ function checkProfilerConsumers() {
 
 /**
  * The request types this actor can handle.
  * At the moment there are two known users of the Profiler actor:
  * the devtools and the Gecko Profiler addon, which uses the debugger
  * protocol to get profiles from Fennec.
  */
 ProfilerActor.prototype.requestTypes = {
+  "getBufferInfo": ProfilerActor.prototype.onGetBufferInfo,
   "getFeatures": ProfilerActor.prototype.onGetFeatures,
   "startProfiler": ProfilerActor.prototype.onStartProfiler,
   "stopProfiler": ProfilerActor.prototype.onStopProfiler,
   "isActive": ProfilerActor.prototype.onIsActive,
   "getSharedLibraryInformation": ProfilerActor.prototype.onGetSharedLibraryInformation,
   "getProfile": ProfilerActor.prototype.onGetProfile,
   "registerEventNotifications": ProfilerActor.prototype.onRegisterEventNotifications,
   "unregisterEventNotifications": ProfilerActor.prototype.onUnregisterEventNotifications,
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -167,16 +167,19 @@ RootActor.prototype = {
     // no longer expose tab actors, but also that getProcess forbids
     // exposing actors for security reasons
     get allowChromeProcess() {
       return DebuggerServer.allowChromeProcess;
     },
     // Whether or not `getProfile()` supports specifying a `startTime`
     // and `endTime` to filter out samples. Fx40+
     profilerDataFilterable: true,
+    // Whether or not the profiler has a `getBufferInfo` method
+    // necessary as the profiler does not use the ActorFront class.
+    profilerBufferStatus: true,
   },
 
   /**
    * Return a 'hello' packet as specified by the Remote Debugging Protocol.
    */
   sayHello: function() {
     return {
       from: this.actorID,
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/server/tests/mochitest/chrome.ini
@@ -48,16 +48,17 @@ skip-if = buildapp == 'mulet'
 [test_getProcess.html]
 skip-if = buildapp == 'mulet'
 [test_inspector-anonymous.html]
 [test_inspector-changeattrs.html]
 [test_inspector-changevalue.html]
 [test_inspector-dead-nodes.html]
 [test_inspector_getImageData.html]
 skip-if = buildapp == 'mulet'
+[test_inspector_getNodeFromActor.html]
 [test_inspector-hide.html]
 [test_inspector-insert.html]
 [test_inspector-mutations-attr.html]
 [test_inspector-mutations-childlist.html]
 [test_inspector-mutations-frameload.html]
 [test_inspector-mutations-value.html]
 [test_inspector-pseudoclass-lock.html]
 [test_inspector-release.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector_getNodeFromActor.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1155653
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1155653</title>
+
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+Components.utils.import("resource://gre/modules/devtools/Loader.jsm");
+const inspector = devtools.require("devtools/server/actors/inspector");
+
+window.onload = function() {
+  SimpleTest.waitForExplicitFinish();
+  runNextTest();
+}
+
+var gWalker;
+
+addTest(function() {
+  let url = document.getElementById("inspectorContent").href;
+  attachURL(url, function(err, client, tab, doc) {
+    let {InspectorFront} = devtools.require("devtools/server/actors/inspector");
+    let inspector = InspectorFront(client, tab);
+
+    promiseDone(inspector.getWalker().then(walker => {
+      gWalker = walker;
+    }).then(runNextTest));
+  });
+});
+
+addTest(function() {
+  info("Try to get a NodeFront from an invalid actorID");
+  gWalker.getNodeFromActor("invalid", ["node"]).then(node => {
+    ok(!node, "The node returned is null");
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  info("Try to get a NodeFront from a valid actorID but invalid path");
+  gWalker.getNodeFromActor(gWalker.actorID, ["invalid", "path"]).then(node => {
+    ok(!node, "The node returned is null");
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  info("Try to get a NodeFront from a valid actorID and valid path");
+  gWalker.getNodeFromActor(gWalker.actorID, ["rootDoc"]).then(rootDocNode => {
+    ok(rootDocNode, "A node was returned");
+    is(rootDocNode, gWalker.rootNode, "The right node was returned");
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  info("Try to get a NodeFront from a valid actorID and valid complex path");
+  gWalker.getNodeFromActor(gWalker.actorID,
+    ["tabActor", "window", "document", "body"]).then(bodyNode => {
+    ok(bodyNode, "A node was returned");
+    gWalker.querySelector(gWalker.rootNode, "body").then(node => {
+      is(bodyNode, node, "The body node was returned");
+      runNextTest();
+    });
+  });
+});
+
+addTest(function() {
+  gWalker = null;
+  runNextTest();
+});
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1155653">Mozilla Bug 1155653</a>
+<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_profiler_getbufferinfo.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the profiler actor returns its buffer status via getBufferInfo.
+ */
+
+const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
+const INITIAL_WAIT_TIME = 100; // ms
+const MAX_WAIT_TIME = 20000; // ms
+const MAX_PROFILER_ENTRIES = 10000000;
+
+function run_test()
+{
+  get_chrome_actors((client, form) => {
+    let actor = form.profilerActor;
+    activate_profiler(client, actor, startTime => {
+      wait_for_samples(client, actor, () => {
+        check_buffer(client, actor, () => {
+          deactivate_profiler(client, actor, () => {
+            client.close(do_test_finished);
+          });
+        });
+      });
+    });
+  })
+
+  do_test_pending();
+}
+
+function check_buffer(client, actor, callback)
+{
+  client.request({ to: actor, type: "getBufferInfo" }, response => {
+    do_check_true(typeof response.position === "number");
+    do_check_true(typeof response.totalSize === "number");
+    do_check_true(typeof response.generation === "number");
+    do_check_true(response.position > 0 && response.position < response.totalSize);
+    do_check_true(response.totalSize === MAX_PROFILER_ENTRIES);
+    // There's no way we'll fill the buffer in this test.
+    do_check_true(response.generation === 0);
+
+    callback();
+  });
+}
+
+function activate_profiler(client, actor, callback)
+{
+  client.request({ to: actor, type: "startProfiler", entries: MAX_PROFILER_ENTRIES }, response => {
+    do_check_true(response.started);
+    client.request({ to: actor, type: "isActive" }, response => {
+      do_check_true(response.isActive);
+      callback(response.currentTime);
+    });
+  });
+}
+
+function deactivate_profiler(client, actor, callback)
+{
+  client.request({ to: actor, type: "stopProfiler" }, response => {
+    do_check_false(response.started);
+    client.request({ to: actor, type: "isActive" }, response => {
+      do_check_false(response.isActive);
+      callback();
+    });
+  });
+}
+
+function wait_for_samples(client, actor, callback)
+{
+  function attempt(delay)
+  {
+    // No idea why, but Components.stack.sourceLine returns null.
+    let funcLine = Components.stack.lineNumber - 3;
+
+    // Spin for the requested time, then take a sample.
+    let start = Date.now();
+    let stack;
+    do_print("Attempt: delay = " + delay);
+    while (Date.now() - start < delay) { stack = Components.stack; }
+    do_print("Attempt: finished waiting.");
+
+    client.request({ to: actor, type: "getProfile" }, response => {
+      // At this point, we may or may not have samples, depending on
+      // whether the spin loop above has given the profiler enough time
+      // to get started.
+      if (response.profile.threads[0].samples.length == 0) {
+        if (delay < MAX_WAIT_TIME) {
+          // Double the spin-wait time and try again.
+          do_print("Attempt: no samples, going around again.");
+          return attempt(delay * 2);
+        } else {
+          // We've waited long enough, so just fail.
+          do_print("Attempt: waited a long time, but no samples were collected.");
+          do_print("Giving up.");
+          do_check_true(false);
+          return;
+        }
+      }
+      callback();
+    });
+  }
+
+  // Start off with a 100 millisecond delay.
+  attempt(INITIAL_WAIT_TIME);
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -211,16 +211,17 @@ skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_breakpoint-actor-map.js]
 [test_profiler_activation-01.js]
 [test_profiler_activation-02.js]
 [test_profiler_close.js]
 [test_profiler_data.js]
 [test_profiler_events-01.js]
 [test_profiler_events-02.js]
+[test_profiler_getbufferinfo.js]
 [test_profiler_getfeatures.js]
 [test_profiler_getsharedlibraryinformation.js]
 [test_unsafeDereference.js]
 [test_add_actors.js]
 [test_trace_actor-01.js]
 [test_trace_actor-02.js]
 [test_trace_actor-03.js]
 [test_trace_actor-04.js]
--- a/toolkit/themes/shared/extensions/extensions.inc.css
+++ b/toolkit/themes/shared/extensions/extensions.inc.css
@@ -960,43 +960,39 @@ setting[type="radio"] > radiogroup {
   display: block !important;
 }
 
 button.button-link {
   -moz-appearance: none;
   background: transparent;
   border: none;
   box-shadow: none;
-  text-decoration: underline;
   color: #0095dd;
   cursor: pointer;
   min-width: 0;
   height: 20px;
   margin: 0 6px;
 }
 
 button.button-link:not(:-moz-focusring) > .button-box {
   border-width: 0;
   margin: 1px;
 }
 
-.text-link {
-  color: #0095dd;
-  font-size: inherit;
-}
-
-button.button-link:hover,
-.text-link:hover {
-  color: #4cb1ff;
+button.button-link:hover {
   background-color: transparent;
+  color: #178ce5;
+  text-decoration: underline;
 }
 
 /* Needed to override normal button style from inContent.css */
 button.button-link:not([disabled="true"]):active:hover {
   background-color: transparent;
+  color: #ff9500;
+  text-decoration: none;
 }
 
 
 /*** telemetry experiments ***/
 
 #detail-experiment-container {
   font-size: 80%;
   margin-bottom: 1em;