Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Mon, 12 Jan 2015 15:14:56 -0500
changeset 249122 182904efdc21a48b08d55f78154ae7afe2033a22
parent 249077 cac64af410a11b92d6d091c38e4ffe0b750b0273 (current diff)
parent 249121 7a776d0c87e6be3863a5ad389d13afff948a5a5a (diff)
child 249139 34c1ede59bebf0bf01cb614718939c135ddf0bbf
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c. a=merge
addon-sdk/source/README
addon-sdk/source/lib/sdk/panel/window.js
addon-sdk/source/lib/sdk/util/registry.js
addon-sdk/source/lib/sdk/windows/loader.js
addon-sdk/source/test/addons/layout-change/main.js
addon-sdk/source/test/addons/places/favicon-helpers.js
addon-sdk/source/test/addons/places/main.js
addon-sdk/source/test/addons/places/places-helper.js
addon-sdk/source/test/addons/places/tests/test-places-bookmarks.js
addon-sdk/source/test/addons/places/tests/test-places-events.js
addon-sdk/source/test/addons/places/tests/test-places-favicon.js
addon-sdk/source/test/addons/places/tests/test-places-utils.js
addon-sdk/source/test/fixtures/test-page-worker.html
addon-sdk/source/test/fixtures/test-page-worker.js
addon-sdk/source/test/test-registry.js
addon-sdk/source/test/test-window-loader.js
browser/components/downloads/DownloadsUI.js
mobile/android/base/TabsAccessor.java
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3794,18 +3794,21 @@ function addToUrlbarHistory(aUrlToAdd) {
 
 function toJavaScriptConsole()
 {
   toOpenWindowByType("global:console", "chrome://global/content/console.xul");
 }
 
 function BrowserDownloadsUI()
 {
-  Cc["@mozilla.org/download-manager-ui;1"].
-  getService(Ci.nsIDownloadManagerUI).show(window);
+  if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+    openUILinkIn("about:downloads", "tab");
+  } else {
+    PlacesCommandHook.showPlacesOrganizer("Downloads");
+  }
 }
 
 function toOpenWindowByType(inType, uri, features)
 {
   var topWindow = Services.wm.getMostRecentWindow(inType);
 
   if (topWindow)
     topWindow.focus();
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -983,23 +983,28 @@
       <xul:tree anonid="tree" flex="1"
                 class="autocomplete-tree plain search-panel-tree"
                 hidecolumnpicker="true" seltype="single">
         <xul:treecols anonid="treecols">
           <xul:treecol id="treecolAutoCompleteValue" class="autocomplete-treecol" flex="1" overflow="true"/>
         </xul:treecols>
         <xul:treechildren class="autocomplete-treebody"/>
       </xul:tree>
-      <xul:hbox anonid="search-panel-one-offs-header"
+      <xul:deck anonid="search-panel-one-offs-header"
+                selectedIndex="0"
                 class="search-panel-header search-panel-current-input"
                 xbl:inherits="hidden=showonlysettings">
-        <xul:label anonid="searchbar-oneoffheader-before" value="&searchFor.label;"/>
-        <xul:label anonid="searchbar-oneoffheader-searchtext" flex="1" crop="end" class="search-panel-input-value"/>
-        <xul:label anonid="searchbar-oneoffheader-after" flex="10000" value="&searchWith.label;"/>
-      </xul:hbox>
+        <xul:label anonid="searchbar-oneoffheader-search" value="&searchWithHeader.label;"/>
+        <xul:hbox anonid="search-panel-searchforwith"
+                  class="search-panel-current-input">
+          <xul:label anonid="searchbar-oneoffheader-before" value="&searchFor.label;"/>
+          <xul:label anonid="searchbar-oneoffheader-searchtext" flex="1" crop="end" class="search-panel-input-value"/>
+          <xul:label anonid="searchbar-oneoffheader-after" flex="10000" value="&searchWith.label;"/>
+        </xul:hbox>
+      </xul:deck>
       <xul:description anonid="search-panel-one-offs"
                        class="search-panel-one-offs"
                        xbl:inherits="hidden=showonlysettings"/>
       <xul:vbox anonid="add-engines"/>
       <xul:button anonid="search-settings"
                   xbl:inherits="showonlysettings"
                   oncommand="BrowserUITelemetry.countSearchSettingsEvent('searchbar');openPreferences('paneSearch')"
                   class="search-setting-button search-panel-header"
@@ -1054,22 +1059,30 @@
 
         // Show the current default engine in the top header of the panel.
         this.updateHeader();
 
         // Update the 'Search for <keywords> with:" header.
         let headerSearchText =
           document.getAnonymousElementByAttribute(this, "anonid",
                                                   "searchbar-oneoffheader-searchtext");
+        let headerPanel =
+          document.getAnonymousElementByAttribute(this, "anonid",
+                                                  "search-panel-one-offs-header");
         let textbox = searchbar.textbox;
         let self = this;
         let inputHandler = function() {
           headerSearchText.setAttribute("value", textbox.value);
-          if (textbox.value)
+          if (textbox.value) {
             self.removeAttribute("showonlysettings");
+            headerPanel.selectedIndex = 1;
+          }
+          else {
+            headerPanel.selectedIndex = 0;
+          }
         };
         textbox.addEventListener("input", inputHandler);
         this.addEventListener("popuphiding", function hiding() {
           textbox.removeEventListener("input", inputHandler);
           this.removeEventListener("popuphiding", hiding);
         });
         inputHandler();
 
--- a/browser/components/downloads/BrowserDownloads.manifest
+++ b/browser/components/downloads/BrowserDownloads.manifest
@@ -1,4 +1,3 @@
 component {49507fe5-2cee-4824-b6a3-e999150ce9b8} DownloadsStartup.js
 contract @mozilla.org/browser/downloadsstartup;1 {49507fe5-2cee-4824-b6a3-e999150ce9b8}
 category profile-after-change DownloadsStartup @mozilla.org/browser/downloadsstartup;1
-component {4d99321e-d156-455b-81f7-e7aa2308134f} DownloadsUI.js
--- a/browser/components/downloads/DownloadsStartup.js
+++ b/browser/components/downloads/DownloadsStartup.js
@@ -18,22 +18,16 @@
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 /**
- * CID and Contract ID of our implementation of nsIDownloadManagerUI.
- */
-const kDownloadsUICid = Components.ID("{4d99321e-d156-455b-81f7-e7aa2308134f}");
-const kDownloadsUIContractId = "@mozilla.org/download-manager-ui;1";
-
-/**
  * CID and Contract ID of the JavaScript implementation of nsITransfer.
  */
 const kTransferCid = Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}");
 const kTransferContractId = "@mozilla.org/transfer;1";
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadsStartup
 
@@ -49,28 +43,16 @@ DownloadsStartup.prototype = {
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsIObserver
 
   observe: function DS_observe(aSubject, aTopic, aData)
   {
-    if (aTopic != "profile-after-change") {
-      Cu.reportError("Unexpected observer notification.");
-      return;
-    }
-
-    // Override Toolkit's nsIDownloadManagerUI implementation with our own.
-    // This must be done at application startup and not in the manifest to
-    // ensure that our implementation overrides the original one.
-    Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
-                      .registerFactory(kDownloadsUICid, "",
-                                       kDownloadsUIContractId, null);
-
     // Override Toolkit's nsITransfer implementation with the one from the
     // JavaScript API for downloads.
     Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
                       .registerFactory(kTransferCid, "",
                                        kTransferContractId, null);
   },
 };
 
deleted file mode 100644
--- a/browser/components/downloads/DownloadsUI.js
+++ /dev/null
@@ -1,128 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
-/* 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/. */
-
-/**
- * This component implements the nsIDownloadManagerUI interface and opens the
- * Downloads view for the most recent browser window when requested.
- */
-
-"use strict";
-
-////////////////////////////////////////////////////////////////////////////////
-//// Globals
-
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
-const Cr = Components.results;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
-                                  "resource:///modules/DownloadsCommon.jsm");
-XPCOMUtils.defineLazyServiceGetter(this, "gBrowserGlue",
-                                   "@mozilla.org/browser/browserglue;1",
-                                   "nsIBrowserGlue");
-XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
-                                  "resource:///modules/RecentWindow.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
-                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
-
-////////////////////////////////////////////////////////////////////////////////
-//// DownloadsUI
-
-function DownloadsUI()
-{
-}
-
-DownloadsUI.prototype = {
-  classID: Components.ID("{4d99321e-d156-455b-81f7-e7aa2308134f}"),
-
-  _xpcom_factory: XPCOMUtils.generateSingletonFactory(DownloadsUI),
-
-  //////////////////////////////////////////////////////////////////////////////
-  //// nsISupports
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]),
-
-  //////////////////////////////////////////////////////////////////////////////
-  //// nsIDownloadManagerUI
-
-  show: function DUI_show(aWindowContext, aDownload, aReason, aUsePrivateUI)
-  {
-    if (!aReason) {
-      aReason = Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED;
-    }
-
-    if (aReason == Ci.nsIDownloadManagerUI.REASON_NEW_DOWNLOAD) {
-      const kMinimized = Ci.nsIDOMChromeWindow.STATE_MINIMIZED;
-      let browserWin = gBrowserGlue.getMostRecentBrowserWindow();
-
-      if (!browserWin || browserWin.windowState == kMinimized) {
-        this._showDownloadManagerUI(aWindowContext, aUsePrivateUI);
-      }
-      else {
-        // If the indicator is visible, then new download notifications are
-        // already handled by the panel service.
-        browserWin.DownloadsButton.checkIsVisible(function(isVisible) {
-          if (!isVisible) {
-            this._showDownloadManagerUI(aWindowContext, aUsePrivateUI);
-          }
-        }.bind(this));
-      }
-    } else {
-      this._showDownloadManagerUI(aWindowContext, aUsePrivateUI);
-    }
-  },
-
-  get visible() true,
-
-  getAttention: function () {},
-
-  //////////////////////////////////////////////////////////////////////////////
-  //// Private
-
-  /**
-   * Helper function that opens the download manager UI.
-   */
-  _showDownloadManagerUI: function (aWindowContext, aUsePrivateUI)
-  {
-    // If we weren't given a window context, try to find a browser window
-    // to use as our parent - and if that doesn't work, error out and give up.
-    let parentWindow = aWindowContext;
-    if (!parentWindow) {
-      parentWindow = RecentWindow.getMostRecentBrowserWindow({ private: !!aUsePrivateUI });
-      if (!parentWindow) {
-        Components.utils.reportError(
-          "Couldn't find a browser window to open the Places Downloads View " +
-          "from.");
-        return;
-      }
-    }
-
-    // If window is private then show it in a tab.
-    if (PrivateBrowsingUtils.isWindowPrivate(parentWindow)) {
-      parentWindow.openUILinkIn("about:downloads", "tab");
-      return;
-    } else {
-      let organizer = Services.wm.getMostRecentWindow("Places:Organizer");
-      if (!organizer) {
-        parentWindow.openDialog("chrome://browser/content/places/places.xul",
-                                "", "chrome,toolbar=yes,dialog=no,resizable",
-                                "Downloads");
-      } else {
-        organizer.PlacesOrganizer.selectLeftPaneQuery("Downloads");
-        organizer.focus();
-      }
-    }
-  }
-};
-
-////////////////////////////////////////////////////////////////////////////////
-//// Module
-
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DownloadsUI]);
--- a/browser/components/downloads/moz.build
+++ b/browser/components/downloads/moz.build
@@ -7,16 +7,15 @@
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
 EXTRA_COMPONENTS += [
     'BrowserDownloads.manifest',
     'DownloadsStartup.js',
-    'DownloadsUI.js',
 ]
 
 EXTRA_JS_MODULES += [
     'DownloadsCommon.jsm',
     'DownloadsLogger.jsm',
     'DownloadsTaskbar.jsm',
 ]
--- a/browser/components/preferences/cookies.js
+++ b/browser/components/preferences/cookies.js
@@ -521,42 +521,39 @@ var gCookiesWindow = {
       for (i = 0; i < ids.length; ++i)
         document.getElementById(ids[i]).disabled = true;
     }
     for (var property in properties)
       document.getElementById(property).value = properties[property];
   },
 
   onCookieSelected: function () {
-    var properties, item;
+    var item;
     var seln = this._tree.view.selection;
     if (!this._view._filtered)
       item = this._view._getItemAtIndex(seln.currentIndex);
     else
       item = this._view._filterSet[seln.currentIndex];
 
     this._updateCookieData(item);
 
     var rangeCount = seln.getRangeCount();
     var selectedCookieCount = 0;
     for (var i = 0; i < rangeCount; ++i) {
       var min = {}; var max = {};
       seln.getRangeAt(i, min, max);
       for (var j = min.value; j <= max.value; ++j) {
         item = this._view._getItemAtIndex(j);
         if (!item) continue;
-        if (item.container && !item.open)
+        if (item.container)
           selectedCookieCount += item.cookies.length;
         else if (!item.container)
           ++selectedCookieCount;
       }
     }
-    var item = this._view._getItemAtIndex(seln.currentIndex);
-    if (item && seln.count == 1 && item.container && item.open)
-      selectedCookieCount += 2;
 
     let buttonLabel = this._bundle.getString("removeSelectedCookies");
     let removeSelectedCookies = document.getElementById("removeSelectedCookies");
     removeSelectedCookies.label = PluralForm.get(selectedCookieCount, buttonLabel)
                                             .replace("#1", selectedCookieCount);
 
     removeSelectedCookies.disabled = !(seln.count > 0);
   },
--- a/browser/components/preferences/search.js
+++ b/browser/components/preferences/search.js
@@ -175,16 +175,21 @@ var gSearchPane = {
       hiddenList.join(",");
   },
 
   setDefaultEngine: function () {
     if (document.documentElement.instantApply) {
       Services.search.currentEngine =
         document.getElementById("defaultEngine").selectedItem.engine;
     }
+  },
+
+  loadAddEngines: function () {
+    window.opener.BrowserSearch.loadAddEngines();
+    window.document.documentElement.acceptDialog();
   }
 };
 
 function onDragEngineStart(event) {
   var selectedIndex = gEngineView.selectedIndex;
   if (selectedIndex >= 0) {
     event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString());
     event.dataTransfer.effectAllowed = "move";
--- a/browser/components/preferences/search.xul
+++ b/browser/components/preferences/search.xul
@@ -77,15 +77,15 @@
                 disabled="true"
                 oncommand="gSearchPane.remove();"/>
       </hbox>
 
       <separator class="thin"/>
 
       <hbox pack="start">
         <label id="addEngines" class="text-link" value="&addMoreSearchEngines.label;"
-               onclick="if (event.button == 0) { Services.wm.getMostRecentWindow('navigator:browser').BrowserSearch.loadAddEngines(); }"/>
+               onclick="if (event.button == 0) { gSearchPane.loadAddEngines(); }"/>
       </hbox>
     </groupbox>
 
   </prefpane>
 
 </overlay>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/animation-controller.js
@@ -0,0 +1,223 @@
+/* -*- 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+Cu.import("resource://gre/modules/devtools/Console.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+devtools.lazyRequireGetter(this, "promise");
+devtools.lazyRequireGetter(this, "EventEmitter",
+                                 "devtools/toolkit/event-emitter");
+devtools.lazyRequireGetter(this, "AnimationsFront",
+                                 "devtools/server/actors/animation", true);
+
+const require = devtools.require;
+
+const STRINGS_URI = "chrome://browser/locale/devtools/animationinspector.properties";
+const L10N = new ViewHelpers.L10N(STRINGS_URI);
+
+// Global toolbox/inspector, set when startup is called.
+let gToolbox, gInspector;
+
+/**
+ * Startup the animationinspector controller and view, called by the sidebar
+ * widget when loading/unloading the iframe into the tab.
+ */
+let startup = Task.async(function*(inspector) {
+  gInspector = inspector;
+  gToolbox = inspector.toolbox;
+
+  // Don't assume that AnimationsPanel is defined here, it's in another file.
+  if (!typeof AnimationsPanel === "undefined") {
+    throw new Error("AnimationsPanel was not loaded in the animationinspector window");
+  }
+
+  yield promise.all([
+    AnimationsController.initialize(),
+    AnimationsPanel.initialize()
+  ]).then(null, Cu.reportError);
+});
+
+/**
+ * Shutdown the animationinspector controller and view, called by the sidebar
+ * widget when loading/unloading the iframe into the tab.
+ */
+let shutdown = Task.async(function*() {
+  yield promise.all([
+    AnimationsController.destroy(),
+    // Don't assume that AnimationsPanel is defined here, it's in another file.
+    typeof AnimationsPanel !== "undefined"
+      ? AnimationsPanel.destroy()
+      : promise.resolve()
+  ]).then(() => {
+    gToolbox = gInspector = null;
+  }, Cu.reportError);
+});
+
+// This is what makes the sidebar widget able to load/unload the panel.
+function setPanel(panel) {
+  return startup(panel);
+}
+function destroy() {
+  return shutdown();
+}
+
+/**
+ * The animationinspector controller's job is to retrieve AnimationPlayerFronts
+ * from the server. It is also responsible for keeping the list of players up to
+ * date when the node selection changes in the inspector, as well as making sure
+ * no updates are done when the animationinspector sidebar panel is not visible.
+ *
+ * AnimationPlayerFronts are available in AnimationsController.animationPlayers.
+ *
+ * Note also that all AnimationPlayerFronts handled by the controller are set to
+ * auto-refresh (except when the sidebar panel is not visible).
+ *
+ * Usage example:
+ *
+ * AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT, onPlayers);
+ * function onPlayers() {
+ *   for (let player of AnimationsController.animationPlayers) {
+ *     // do something with player
+ *   }
+ * }
+ */
+let AnimationsController = {
+  PLAYERS_UPDATED_EVENT: "players-updated",
+
+  initialize: Task.async(function*() {
+    if (this.initialized) {
+      return this.initialized.promise;
+    }
+    this.initialized = promise.defer();
+
+    let target = gToolbox.target;
+    this.animationsFront = new AnimationsFront(target.client, target.form);
+
+    this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
+    this.onNewNodeFront = this.onNewNodeFront.bind(this);
+
+    this.startListeners();
+
+    yield this.onNewNodeFront();
+
+    this.initialized.resolve();
+  }),
+
+  destroy: Task.async(function*() {
+    if (!this.initialized) {
+      return;
+    }
+
+    if (this.destroyed) {
+      return this.destroyed.promise;
+    }
+    this.destroyed = promise.defer();
+
+    this.stopListeners();
+    yield this.destroyAnimationPlayers();
+    this.nodeFront = null;
+
+    if (this.animationsFront) {
+      this.animationsFront.destroy();
+      this.animationsFront = null;
+    }
+
+    this.destroyed.resolve();
+  }),
+
+  startListeners: function() {
+    // Re-create the list of players when a new node is selected, except if the
+    // sidebar isn't visible. And set the players to auto-refresh when needed.
+    gInspector.selection.on("new-node-front", this.onNewNodeFront);
+    gInspector.sidebar.on("select", this.onPanelVisibilityChange);
+    gToolbox.on("select", this.onPanelVisibilityChange);
+  },
+
+  stopListeners: function() {
+    gInspector.selection.off("new-node-front", this.onNewNodeFront);
+    gInspector.sidebar.off("select", this.onPanelVisibilityChange);
+    gToolbox.off("select", this.onPanelVisibilityChange);
+  },
+
+  isPanelVisible: function() {
+    return gToolbox.currentToolId === "inspector" &&
+           gInspector.sidebar &&
+           gInspector.sidebar.getCurrentTabID() == "animationinspector";
+  },
+
+  onPanelVisibilityChange: Task.async(function*(e, id) {
+    if (this.isPanelVisible()) {
+      this.onNewNodeFront();
+      this.startAllAutoRefresh();
+    } else {
+      this.stopAllAutoRefresh();
+    }
+  }),
+
+  onNewNodeFront: Task.async(function*() {
+    // Ignore if the panel isn't visible or the node selection hasn't changed.
+    if (!this.isPanelVisible() || this.nodeFront === gInspector.selection.nodeFront) {
+      return;
+    }
+
+    let done = gInspector.updating("animationscontroller");
+
+    if(!gInspector.selection.isConnected() ||
+       !gInspector.selection.isElementNode()) {
+      yield this.destroyAnimationPlayers();
+      this.emit(this.PLAYERS_UPDATED_EVENT);
+      done();
+      return;
+    }
+
+    this.nodeFront = gInspector.selection.nodeFront;
+    yield this.refreshAnimationPlayers(this.nodeFront);
+    this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
+
+    done();
+  }),
+
+  // AnimationPlayerFront objects are managed by this controller. They are
+  // retrieved when refreshAnimationPlayers is called, stored in the
+  // animationPlayers array, and destroyed when refreshAnimationPlayers is
+  // called again.
+  animationPlayers: [],
+
+  refreshAnimationPlayers: Task.async(function*(nodeFront) {
+    yield this.destroyAnimationPlayers();
+
+    this.animationPlayers = yield this.animationsFront.getAnimationPlayersForNode(nodeFront);
+    this.startAllAutoRefresh();
+  }),
+
+  startAllAutoRefresh: function() {
+    for (let front of this.animationPlayers) {
+      front.startAutoRefresh();
+    }
+  },
+
+  stopAllAutoRefresh: function() {
+    for (let front of this.animationPlayers) {
+      front.stopAutoRefresh();
+    }
+  },
+
+  destroyAnimationPlayers: Task.async(function*() {
+    this.stopAllAutoRefresh();
+    for (let front of this.animationPlayers) {
+      yield front.release();
+    }
+    this.animationPlayers = [];
+  })
+};
+
+EventEmitter.decorate(AnimationsController);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/animation-inspector.xhtml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<!DOCTYPE html [
+<!ENTITY % animationinspectorDTD SYSTEM "chrome://browser/locale/devtools/animationinspector.dtd" >
+ %animationinspectorDTD;
+]>
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <title>&title;</title>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+    <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
+    <link rel="stylesheet" href="chrome://browser/skin/devtools/animationinspector.css" type="text/css"/>
+    <script type="application/javascript;version=1.8" src="chrome://browser/content/devtools/theme-switching.js"/>
+  </head>
+  <body class="theme-sidebar devtools-monospace" role="application">
+    <div id="players" class="theme-toolbar"></div>
+    <div id="error-message">
+      <p>&invalidElement;</p>
+      <p>&selectElement;</p>
+      <button id="element-picker" standalone="true" class="devtools-button"></button>
+    </div>
+    <script type="application/javascript;version=1.8" src="animation-controller.js"></script>
+    <script type="application/javascript;version=1.8" src="animation-panel.js"></script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/animation-panel.js
@@ -0,0 +1,432 @@
+/* -*- 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";
+
+/**
+ * The main animations panel UI.
+ */
+let AnimationsPanel = {
+  UI_UPDATED_EVENT: "ui-updated",
+
+  initialize: Task.async(function*() {
+    if (this.initialized) {
+      return this.initialized.promise;
+    }
+    this.initialized = promise.defer();
+
+    this.playersEl = document.querySelector("#players");
+    this.errorMessageEl = document.querySelector("#error-message");
+    this.pickerButtonEl = document.querySelector("#element-picker");
+
+    let hUtils = gToolbox.highlighterUtils;
+    this.togglePicker = hUtils.togglePicker.bind(hUtils);
+    this.onPickerStarted = this.onPickerStarted.bind(this);
+    this.onPickerStopped = this.onPickerStopped.bind(this);
+    this.createPlayerWidgets = this.createPlayerWidgets.bind(this);
+
+    this.startListeners();
+
+    this.initialized.resolve();
+  }),
+
+  destroy: Task.async(function*() {
+    if (!this.initialized) {
+      return;
+    }
+
+    if (this.destroyed) {
+      return this.destroyed.promise;
+    }
+    this.destroyed = promise.defer();
+
+    this.stopListeners();
+    yield this.destroyPlayerWidgets();
+
+    this.playersEl = this.errorMessageEl = null;
+
+    this.destroyed.resolve();
+  }),
+
+  startListeners: function() {
+    AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
+      this.createPlayerWidgets);
+    this.pickerButtonEl.addEventListener("click", this.togglePicker, false);
+    gToolbox.on("picker-started", this.onPickerStarted);
+    gToolbox.on("picker-stopped", this.onPickerStopped);
+  },
+
+  stopListeners: function() {
+    AnimationsController.off(AnimationsController.PLAYERS_UPDATED_EVENT,
+      this.createPlayerWidgets);
+    this.pickerButtonEl.removeEventListener("click", this.togglePicker, false);
+    gToolbox.off("picker-started", this.onPickerStarted);
+    gToolbox.off("picker-stopped", this.onPickerStopped);
+  },
+
+  displayErrorMessage: function() {
+    this.errorMessageEl.style.display = "block";
+  },
+
+  hideErrorMessage: function() {
+    this.errorMessageEl.style.display = "none";
+  },
+
+  onPickerStarted: function() {
+    this.pickerButtonEl.setAttribute("checked", "true");
+  },
+
+  onPickerStopped: function() {
+    this.pickerButtonEl.removeAttribute("checked");
+  },
+
+  createPlayerWidgets: Task.async(function*() {
+    let done = gInspector.updating("animationspanel");
+
+    // Empty the whole panel first.
+    this.hideErrorMessage();
+    yield this.destroyPlayerWidgets();
+
+    // If there are no players to show, show the error message instead and return.
+    if (!AnimationsController.animationPlayers.length) {
+      this.displayErrorMessage();
+      this.emit(this.UI_UPDATED_EVENT);
+      done();
+      return;
+    }
+
+    // Otherwise, create player widgets.
+    this.playerWidgets = [];
+    let initPromises = [];
+
+    for (let player of AnimationsController.animationPlayers) {
+      let widget = new PlayerWidget(player, this.playersEl);
+      initPromises.push(widget.initialize());
+      this.playerWidgets.push(widget);
+    }
+
+    yield initPromises;
+    this.emit(this.UI_UPDATED_EVENT);
+    done();
+  }),
+
+  destroyPlayerWidgets: Task.async(function*() {
+    if (!this.playerWidgets) {
+      return;
+    }
+
+    let destroyers = this.playerWidgets.map(widget => widget.destroy());
+    yield promise.all(destroyers);
+    this.playerWidgets = null;
+    this.playersEl.innerHTML = "";
+  })
+};
+
+EventEmitter.decorate(AnimationsPanel);
+
+/**
+ * An AnimationPlayer UI widget
+ */
+function PlayerWidget(player, containerEl) {
+  EventEmitter.decorate(this);
+
+  this.player = player;
+  this.containerEl = containerEl;
+
+  this.onStateChanged = this.onStateChanged.bind(this);
+  this.onPlayPauseBtnClick = this.onPlayPauseBtnClick.bind(this);
+}
+
+PlayerWidget.prototype = {
+  initialize: Task.async(function*() {
+    if (this.initialized) {
+      return;
+    }
+    this.initialized = true;
+
+    this.createMarkup();
+    this.startListeners();
+  }),
+
+  destroy: Task.async(function*() {
+    if (this.destroyed) {
+      return;
+    }
+    this.destroyed = true;
+
+    this.stopTimelineAnimation();
+    this.stopListeners();
+
+    this.el.remove();
+    this.playPauseBtnEl = this.currentTimeEl = this.timeDisplayEl = null;
+    this.containerEl = this.el = this.player = null;
+  }),
+
+  startListeners: function() {
+    this.player.on(this.player.AUTO_REFRESH_EVENT, this.onStateChanged);
+    this.playPauseBtnEl.addEventListener("click", this.onPlayPauseBtnClick);
+  },
+
+  stopListeners: function() {
+    this.player.off(this.player.AUTO_REFRESH_EVENT, this.onStateChanged);
+    this.playPauseBtnEl.removeEventListener("click", this.onPlayPauseBtnClick);
+  },
+
+  createMarkup: function() {
+    let state = this.player.state;
+
+    this.el = createNode({
+      attributes: {
+        "class": "player-widget " + state.playState
+      }
+    });
+
+    // Animation header
+    let titleEl = createNode({
+      parent: this.el,
+      attributes: {
+        "class": "animation-title"
+      }
+    });
+    let titleHTML = "";
+
+    // Name
+    if (state.name) {
+      // Css animations have names
+      titleHTML += L10N.getStr("player.animationNameLabel");
+      titleHTML += "<strong>" + state.name + "</strong>";
+    } else {
+      // Css transitions don't
+      titleHTML += L10N.getStr("player.transitionNameLabel");
+    }
+
+    // Duration and iteration count
+    titleHTML += "<span class='meta-data'>";
+    titleHTML += L10N.getStr("player.animationDurationLabel");
+    titleHTML += "<strong>" + L10N.getFormatStr("player.timeLabel",
+      this.getFormattedTime(state.duration)) + "</strong>";
+    titleHTML += L10N.getStr("player.animationIterationCountLabel");
+    let count = state.iterationCount || L10N.getStr("player.infiniteIterationCount");
+    titleHTML += "<strong>" + count + "</strong>";
+    titleHTML += "</span>"
+
+    titleEl.innerHTML = titleHTML;
+
+    // Timeline widget
+    let timelineEl = createNode({
+      parent: this.el,
+      attributes: {
+        "class": "timeline"
+      }
+    });
+
+    // Playback control buttons container
+    let playbackControlsEl = createNode({
+      parent: timelineEl,
+      attributes: {
+        "class": "playback-controls"
+      }
+    });
+
+    // Control buttons (when currentTime becomes settable, rewind and
+    // fast-forward can be added here).
+    this.playPauseBtnEl = createNode({
+      parent: playbackControlsEl,
+      nodeType: "button",
+      attributes: {
+        "class": "toggle devtools-button"
+      }
+    });
+
+    // Sliders container
+    let slidersContainerEl = createNode({
+      parent: timelineEl,
+      attributes: {
+        "class": "sliders-container",
+      }
+    });
+
+    let max = state.duration; // Infinite iterations
+    if (state.iterationCount) {
+      // Finite iterations
+      max = state.iterationCount * state.duration;
+    }
+
+    // For now, keyframes aren't exposed by the actor. So the only range <input>
+    // displayed in the container is the currentTime. When keyframes are
+    // available, one input per keyframe can be added here.
+    this.currentTimeEl = createNode({
+      nodeType: "input",
+      parent: slidersContainerEl,
+      attributes: {
+        "type": "range",
+        "class": "current-time",
+        "min": "0",
+        "max": max,
+        "step": "10",
+        // The currentTime isn't settable yet, so disable the timeline slider
+        "disabled": "true"
+      }
+    });
+
+    // Time display
+    this.timeDisplayEl = createNode({
+      parent: timelineEl,
+      attributes: {
+        "class": "time-display"
+      }
+    });
+    this.timeDisplayEl.textContent = L10N.getFormatStr("player.timeLabel",
+      this.getFormattedTime());
+
+    this.containerEl.appendChild(this.el);
+  },
+
+  /**
+   * Format time as a string.
+   * @param {Number} time Defaults to the player's currentTime.
+   * @return {String} The formatted time, e.g. "10.55"
+   */
+  getFormattedTime: function(time=this.player.state.currentTime) {
+    let str = time/1000 + "";
+    str = str.split(".");
+    if (str.length === 1) {
+      return str[0] + ".00";
+    } else {
+      return str[0] + "." + str[1].substring(0, 2);
+    }
+  },
+
+  /**
+   * 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
+   * play and paused.
+   * @return {Promise}
+   */
+  onPlayPauseBtnClick: function() {
+    if (this.player.state.playState === "running") {
+      return this.pause();
+    } else {
+      return this.play();
+    }
+  },
+
+  /**
+   * Whenever a player state update is received.
+   */
+  onStateChanged: function() {
+    let state = this.player.state;
+    this.updatePlayPauseButton(state.playState);
+
+    switch (state.playState) {
+      case "finished":
+        this.destroy();
+        break;
+      case "running":
+        this.startTimelineAnimation();
+        break;
+      case "paused":
+        this.stopTimelineAnimation();
+        this.displayTime(this.player.state.currentTime);
+        break;
+    }
+  },
+
+  /**
+   * Pause the animation player via this widget.
+   * @return {Promise} Resolves when the player is paused, the button is
+   * switched to the right state, and the timeline animation is stopped.
+   */
+  pause: function() {
+    // Switch to the right className on the element right away to avoid waiting
+    // for the next state update to change the playPause icon.
+    this.updatePlayPauseButton("paused");
+    return this.player.pause().then(() => {
+      this.stopTimelineAnimation();
+    });
+  },
+
+  /**
+   * Play the animation player via this widget.
+   * @return {Promise} Resolves when the player is playing, the button is
+   * switched to the right state, and the timeline animation is started.
+   */
+  play: function() {
+    // Switch to the right className on the element right away to avoid waiting
+    // for the next state update to change the playPause icon.
+    this.updatePlayPauseButton("running");
+    this.startTimelineAnimation();
+    return this.player.play();
+  },
+
+  updatePlayPauseButton: function(playState) {
+    this.el.className = "player-widget " + playState;
+  },
+
+  /**
+   * Make the timeline progress smoothly, even though the currentTime is only
+   * updated at some intervals. This uses a local animation loop.
+   */
+  startTimelineAnimation: function() {
+    this.stopTimelineAnimation();
+
+    let start = performance.now();
+    let loop = () => {
+      this.rafID = requestAnimationFrame(loop);
+      let now = this.player.state.currentTime + performance.now() - start;
+      this.displayTime(now);
+    };
+
+    loop();
+  },
+
+  /**
+   * Display the time in the timeDisplayEl and in the currentTimeEl slider.
+   */
+  displayTime: function(time) {
+    let state = this.player.state;
+
+    this.timeDisplayEl.textContent = L10N.getFormatStr("player.timeLabel",
+      this.getFormattedTime(time));
+    if (!state.iterationCount && time !== state.duration) {
+      this.currentTimeEl.value = time % state.duration;
+    } else {
+      this.currentTimeEl.value = time;
+    }
+  },
+
+  /**
+   * Stop the animation loop that makes the timeline progress.
+   */
+  stopTimelineAnimation: function() {
+    if (this.rafID) {
+      cancelAnimationFrame(this.rafID);
+      this.rafID = null;
+    }
+  }
+};
+
+/**
+ * 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/moz.build
@@ -0,0 +1,7 @@
+# -*- 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']
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser.ini
@@ -0,0 +1,18 @@
+[DEFAULT]
+subsuite = devtools
+support-files =
+  doc_frame_script.js
+  doc_simple_animation.html
+  head.js
+
+[browser_animation_empty_on_invalid_nodes.js]
+[browser_animation_panel_exists.js]
+[browser_animation_participate_in_inspector_update.js]
+[browser_animation_play_pause_button.js]
+[browser_animation_playerFronts_are_refreshed.js]
+[browser_animation_playerWidgets_destroy.js]
+[browser_animation_refresh_when_active.js]
+[browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
+[browser_animation_shows_player_on_valid_node.js]
+[browser_animation_timeline_animates.js]
+[browser_animation_ui_updates_when_animation_changes.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_empty_on_invalid_nodes.js
@@ -0,0 +1,24 @@
+/* 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 panel shows no animation data for invalid or not animated nodes
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel} = yield openAnimationInspector();
+
+  info("Select node .still and check that the panel is empty");
+  let stillNode = yield getNodeFront(".still", inspector);
+  yield selectNode(stillNode, inspector);
+  ok(!panel.playerWidgets || !panel.playerWidgets.length,
+    "No player widgets displayed for a still node");
+
+  info("Select the comment text node and check that the panel is empty");
+  let commentNode = yield inspector.walker.previousSibling(stillNode);
+  yield selectNode(commentNode, inspector);
+  ok(!panel.playerWidgets || !panel.playerWidgets.length,
+    "No player widgets displayed for a text node");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_panel_exists.js
@@ -0,0 +1,18 @@
+/* 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 animation panel sidebar exists
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8,welcome to the animation panel");
+  let {panel, controller} = yield openAnimationInspector();
+
+  ok(controller, "The animation controller exists");
+  ok(controller.animationsFront, "The animation controller has been initialized");
+
+  ok(panel, "The animation panel exists");
+  ok(panel.playersEl, "The animation panel has been initialized");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_participate_in_inspector_update.js
@@ -0,0 +1,39 @@
+/* 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 update of the animation panel participate in the
+// inspector-updated event. This means that the test verifies that the
+// inspector-updated event is emitted *after* the animation panel is ready.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel, controller} = yield openAnimationInspector();
+
+  info("Listen for the players-updated, ui-updated and inspector-updated events");
+  let receivedEvents = [];
+  controller.once(controller.PLAYERS_UPDATED_EVENT, () => {
+    receivedEvents.push(controller.PLAYERS_UPDATED_EVENT);
+  });
+  panel.once(panel.UI_UPDATED_EVENT, () => {
+    receivedEvents.push(panel.UI_UPDATED_EVENT);
+  })
+  inspector.once("inspector-updated", () => {
+    receivedEvents.push("inspector-updated");
+  });
+
+  info("Selecting an animated node");
+  let node = yield getNodeFront(".animated", inspector);
+  yield selectNode(node, inspector);
+
+  info("Check that all events were received, and in the right order");
+  is(receivedEvents.length, 3, "3 events were received");
+  is(receivedEvents[0], controller.PLAYERS_UPDATED_EVENT,
+    "The first event received was the players-updated event");
+  is(receivedEvents[1], panel.UI_UPDATED_EVENT,
+    "The second event received was the ui-updated event");
+  is(receivedEvents[2], "inspector-updated",
+    "The third event received was the inspector-updated event");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_play_pause_button.js
@@ -0,0 +1,32 @@
+/* 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";
+
+// Check that the play/pause button actually plays and pauses the player.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel, controller} = yield openAnimationInspector();
+
+  info("Selecting an animated node");
+  yield selectNode(".animated", inspector);
+
+  let player = controller.animationPlayers[0];
+  let widget = panel.playerWidgets[0];
+
+  info("Click the pause button");
+  yield togglePlayPauseButton(widget);
+
+  is(player.state.playState, "paused", "The AnimationPlayerFront is paused");
+  ok(widget.el.classList.contains("paused"), "The button's state has changed");
+  ok(!widget.rafID, "The smooth timeline animation has been stopped");
+
+  info("Click on the play button");
+  yield togglePlayPauseButton(widget);
+
+  is(player.state.playState, "running", "The AnimationPlayerFront is running");
+  ok(widget.el.classList.contains("running"), "The button's state has changed");
+  ok(widget.rafID, "The smooth timeline animation has been started");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_playerFronts_are_refreshed.js
@@ -0,0 +1,58 @@
+/* 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";
+
+// Check that the AnimationPlayerFront objects lifecycle is managed by the
+// AnimationController.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {controller, inspector} = yield openAnimationInspector();
+
+  is(controller.animationPlayers.length, 0,
+    "There are no AnimationPlayerFront objects at first");
+
+  info("Selecting an animated node");
+  // selectNode waits for the inspector-updated event before resolving, which
+  // means the controller.PLAYERS_UPDATED_EVENT event has been emitted before
+  // and players are ready.
+  yield selectNode(".animated", inspector);
+
+  is(controller.animationPlayers.length, 1,
+    "One AnimationPlayerFront has been created");
+  ok(controller.animationPlayers[0].autoRefreshTimer,
+    "The AnimationPlayerFront has been set to auto-refresh");
+
+  info("Selecting a node with mutliple animations");
+  yield selectNode(".multi", inspector);
+
+  is(controller.animationPlayers.length, 2,
+    "2 AnimationPlayerFronts have been created");
+  ok(controller.animationPlayers[0].autoRefreshTimer &&
+     controller.animationPlayers[1].autoRefreshTimer,
+    "The AnimationPlayerFronts have been set to auto-refresh");
+
+  // Hold on to one of the AnimationPlayerFront objects and mock its release
+  // method to test that it is released correctly and that its auto-refresh is
+  // stopped.
+  let retainedFront = controller.animationPlayers[0];
+  let oldRelease = retainedFront.release;
+  let releaseCalled = false;
+  retainedFront.release = () => {
+    releaseCalled = true;
+  };
+
+  info("Selecting a node with no animations");
+  yield selectNode(".still", inspector);
+
+  is(controller.animationPlayers.length, 0,
+    "There are no more AnimationPlayerFront objects");
+
+  info("Checking the destroyed AnimationPlayerFront object");
+  ok(releaseCalled, "The AnimationPlayerFront has been released");
+  ok(!retainedFront.autoRefreshTimer,
+    "The released AnimationPlayerFront's auto-refresh mode has been turned off");
+  yield oldRelease.call(retainedFront);
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_destroy.js
@@ -0,0 +1,23 @@
+/* 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 are destroyed correctly when needed.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel} = yield openAnimationInspector();
+
+  info("Select an animated node");
+  yield selectNode(".multi", inspector);
+
+  info("Hold on to one of the player widget instances to test it after destroy");
+  let widget = panel.playerWidgets[0];
+
+  info("Select another node to get the previous widgets destroyed");
+  yield selectNode(".animated", inspector);
+
+  ok(widget.destroyed, "The widget's destroyed flag is true");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_refresh_when_active.js
@@ -0,0 +1,47 @@
+/* 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 panel only refreshes when it is visible in the sidebar.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {toolbox, inspector, panel} = yield openAnimationInspector();
+
+  info("Select a non animated node");
+  yield selectNode(".still", inspector);
+
+  info("Switch to the rule-view panel");
+  inspector.sidebar.select("ruleview");
+
+  info("Select the animated node now");
+  yield selectNode(".animated", inspector);
+
+  ok(!panel.playerWidgets || !panel.playerWidgets.length,
+    "The panel doesn't show the animation data while inactive");
+
+  info("Switch to the animation panel");
+  inspector.sidebar.select("animationinspector");
+  yield panel.once(panel.UI_UPDATED_EVENT);
+
+  is(panel.playerWidgets.length, 1,
+    "The panel shows the animation data after selecting it");
+
+  info("Switch again to the rule-view");
+  inspector.sidebar.select("ruleview");
+
+  info("Select the non animated node again");
+  yield selectNode(".still", inspector);
+
+  is(panel.playerWidgets.length, 1,
+    "The panel still shows the previous animation data since it is inactive");
+
+  info("Switch to the animation panel again");
+  inspector.sidebar.select("animationinspector");
+  yield panel.once(panel.UI_UPDATED_EVENT);
+
+  ok(!panel.playerWidgets || !panel.playerWidgets.length,
+    "The panel is now empty after refreshing");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_same_nb_of_playerWidgets_and_playerFronts.js
@@ -0,0 +1,25 @@
+/* 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";
+
+// Check that when playerFronts are updated, the same number of playerWidgets
+// are created in the panel.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel, controller} = yield openAnimationInspector();
+
+  info("Selecting the test animated node");
+  yield selectNode(".multi", inspector);
+
+  is(controller.animationPlayers.length, panel.playerWidgets.length,
+    "As many playerWidgets were created as there are playerFronts");
+
+  for (let widget of panel.playerWidgets) {
+    ok(widget.initialized, "The player widget is initialized");
+    is(widget.el.parentNode, panel.playersEl,
+      "The player widget has been appended to the panel");
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_shows_player_on_valid_node.js
@@ -0,0 +1,20 @@
+/* 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 panel shows an animation player when an animated node is
+// selected.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel} = yield openAnimationInspector();
+
+  info("Select node .animated and check that the panel is not empty");
+  let node = yield getNodeFront(".animated", inspector);
+  yield selectNode(node, inspector);
+
+  is(panel.playerWidgets.length, 1,
+    "Exactly 1 player widget is shown for animated node");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_animates.js
@@ -0,0 +1,28 @@
+/* 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 currentTime timeline widget actually progresses with the
+// animation itself.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel} = yield openAnimationInspector();
+
+  info("Select the animated node");
+  yield selectNode(".animated", inspector);
+
+  info("Get the player widget's timeline element and its current position");
+  let widget = panel.playerWidgets[0];
+  let timeline = widget.currentTimeEl;
+
+  yield widget.player.once(widget.player.AUTO_REFRESH_EVENT);
+  ok(widget.rafID, "The widget is updating the timeline with a rAF loop");
+
+  info("Pause the animation");
+  yield togglePlayPauseButton(widget);
+
+  ok(!widget.rafID, "The rAF loop has been stopped after the animation was paused");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_ui_updates_when_animation_changes.js
@@ -0,0 +1,49 @@
+/* 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";
+
+// Verify that if the animation object changes in content, then the widget
+// reflects that change.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {panel, inspector} = yield openAnimationInspector();
+
+  info("Select the test node");
+  yield selectNode(".animated", inspector);
+
+  info("Get the player widget");
+  let widget = panel.playerWidgets[0];
+
+  info("Pause the animation via the content DOM");
+  yield executeInContent("Test:ToggleAnimationPlayer", {
+    animationIndex: 0,
+    pause: true
+  }, {
+    node: getNode(".animated")
+  });
+
+  info("Wait for the next state update");
+  yield widget.player.once(widget.player.AUTO_REFRESH_EVENT);
+
+  is(widget.player.state.playState, "paused", "The AnimationPlayerFront is paused");
+  ok(widget.el.classList.contains("paused"), "The button's state has changed");
+  ok(!widget.rafID, "The smooth timeline animation has been stopped");
+
+  info("Play the animation via the content DOM");
+  yield executeInContent("Test:ToggleAnimationPlayer", {
+    animationIndex: 0,
+    pause: false
+  }, {
+    node: getNode(".animated")
+  });
+
+  info("Wait for the next state update");
+  yield widget.player.once(widget.player.AUTO_REFRESH_EVENT);
+
+  is(widget.player.state.playState, "running", "The AnimationPlayerFront is running");
+  ok(widget.el.classList.contains("running"), "The button's state has changed");
+  ok(widget.rafID, "The smooth timeline animation has been started");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/doc_frame_script.js
@@ -0,0 +1,28 @@
+/* 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";
+
+// A helper frame-script for brower/devtools/animationinspector tests.
+
+/**
+ * Toggle (play or pause) one of the animation players of a given node.
+ * @param {Object} data
+ * - {Number} animationIndex The index of the node's animationPlayers to play or pause
+ * @param {Object} objects
+ * - {DOMNode} node The node to use
+ */
+addMessageListener("Test:ToggleAnimationPlayer", function(msg) {
+  let {animationIndex, pause} = msg.data;
+  let {node} = msg.objects;
+
+  let player = node.getAnimationPlayers()[animationIndex];
+  if (pause) {
+    player.pause();
+  } else {
+    player.play();
+  }
+
+  sendAsyncMessage("Test:ToggleAnimationPlayer");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/doc_simple_animation.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <style>
+    .ball {
+      width: 100px;
+      height: 100px;
+      border-radius: 50%;
+      background: #f06;
+
+      position: absolute;
+    }
+
+    .still {
+      top: 50px;
+      left: 50px;
+    }
+
+    .animated {
+      top: 200px;
+      left: 200px;
+
+      animation: simple-animation 2s infinite alternate;
+    }
+
+    .multi {
+      top: 100px;
+      left: 400px;
+
+      animation: simple-animation 2s infinite alternate,
+                 other-animation 5s infinite alternate;
+    }
+
+    @keyframes simple-animation {
+      100% {
+        transform: translateX(300px);
+      }
+    }
+
+    @keyframes other-animation {
+      100% {
+        background: blue;
+      }
+    }
+  </style>
+</head>
+<body>
+  <!-- Comment node -->
+  <div class="ball still"></div>
+  <div class="ball animated"></div>
+  <div class="ball multi"></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/head.js
@@ -0,0 +1,274 @@
+/* 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";
+
+const Cu = Components.utils;
+let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+let TargetFactory = devtools.TargetFactory;
+let {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {});
+
+// All tests are asynchronous
+waitForExplicitFinish();
+
+const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/animationinspector/test/";
+const ROOT_TEST_DIR = getRootDirectory(gTestPath);
+const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
+
+// Auto clean-up when a test ends
+registerCleanupFunction(function*() {
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  yield gDevTools.closeToolbox(target);
+
+  while (gBrowser.tabs.length > 1) {
+    gBrowser.removeCurrentTab();
+  }
+});
+
+// Uncomment this pref to dump all devtools emitted events to the console.
+// Services.prefs.setBoolPref("devtools.dump.emit", true);
+
+// Uncomment this pref to dump all devtools protocol traffic
+// Services.prefs.setBoolPref("devtools.debugger.log", true);
+
+// Set the testing flag on gDevTools and reset it when the test ends
+gDevTools.testing = true;
+registerCleanupFunction(() => gDevTools.testing = false);
+
+// Clean-up all prefs that might have been changed during a test run
+// (safer here because if the test fails, then the pref is never reverted)
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("devtools.dump.emit");
+  Services.prefs.clearUserPref("devtools.debugger.log");
+});
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url The url to be loaded in the new tab
+ * @return a promise that resolves to the tab object when the url is loaded
+ */
+function addTab(url) {
+  info("Adding a new tab with URL: '" + url + "'");
+  let def = promise.defer();
+
+  window.focus();
+
+  let tab = window.gBrowser.selectedTab = window.gBrowser.addTab(url);
+  let browser = tab.linkedBrowser;
+
+  info("Loading the helper frame script " + FRAME_SCRIPT_URL);
+  browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
+
+  browser.addEventListener("load", function onload() {
+    browser.removeEventListener("load", onload, true);
+    info("URL '" + url + "' loading complete");
+
+    def.resolve(tab);
+  }, true);
+
+  return def.promise;
+}
+
+/**
+ * Simple DOM node accesor function that takes either a node or a string css
+ * selector as argument and returns the corresponding node
+ * @param {String|DOMNode} nodeOrSelector
+ * @return {DOMNode|CPOW} Note that in e10s mode a CPOW object is returned which
+ * doesn't implement *all* of the DOMNode's properties
+ */
+function getNode(nodeOrSelector) {
+  info("Getting the node for '" + nodeOrSelector + "'");
+  return typeof nodeOrSelector === "string" ?
+    content.document.querySelector(nodeOrSelector) :
+    nodeOrSelector;
+}
+
+/**
+ * Get the NodeFront for a given css selector, via the protocol
+ * @param {String} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {Promise} Resolves to the NodeFront instance
+ */
+function getNodeFront(selector, {walker}) {
+  return walker.querySelector(walker.rootNode, selector);
+}
+
+/*
+ * Set the inspector's current selection to a node or to the first match of the
+ * given css selector.
+ * @param {String|NodeFront}
+ *        data The node to select
+ * @param {InspectorPanel} inspector
+ *        The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @param {String} reason
+ *        Defaults to "test" which instructs the inspector not
+ *        to highlight the node upon selection
+ * @return {Promise} Resolves when the inspector is updated with the new node
+ */
+let selectNode = Task.async(function*(data, inspector, reason="test") {
+  info("Selecting the node for '" + data + "'");
+  let nodeFront = data;
+  if (!data._form) {
+    nodeFront = yield getNodeFront(data, inspector);
+  }
+  let updated = inspector.once("inspector-updated");
+  inspector.selection.setNodeFront(nodeFront, reason);
+  yield updated;
+});
+
+/**
+ * Open the toolbox, with the inspector tool visible and the animationinspector
+ * sidebar selected.
+ * @return a promise that resolves when the inspector is ready
+ */
+let openAnimationInspector = Task.async(function*() {
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+  info("Opening the toolbox with the inspector selected");
+  let toolbox = yield gDevTools.showToolbox(target, "inspector");
+  yield waitForToolboxFrameFocus(toolbox);
+
+  info("Switching to the animationinspector");
+  let inspector = toolbox.getPanel("inspector");
+  let initPromises = [
+    inspector.once("inspector-updated"),
+    inspector.sidebar.once("animationinspector-ready")
+  ];
+  inspector.sidebar.select("animationinspector");
+
+  info("Waiting for the inspector and sidebar to be ready");
+  yield promise.all(initPromises);
+
+  let win = inspector.sidebar.getWindowForTab("animationinspector");
+  let {AnimationsController, AnimationsPanel} = win;
+
+  yield promise.all([
+    AnimationsController.initialized,
+    AnimationsPanel.initialized
+  ]);
+
+  return {
+    toolbox: toolbox,
+    inspector: inspector,
+    controller: AnimationsController,
+    panel: AnimationsPanel,
+    window: win
+  };
+});
+
+/**
+ * Wait for the toolbox frame to receive focus after it loads
+ * @param {Toolbox} toolbox
+ * @return a promise that resolves when focus has been received
+ */
+function waitForToolboxFrameFocus(toolbox) {
+  info("Making sure that the toolbox's frame is focused");
+  let def = promise.defer();
+  let win = toolbox.frame.contentWindow;
+  waitForFocus(def.resolve, win);
+  return def.promise;
+}
+
+/**
+ * Checks whether the inspector's sidebar corresponding to the given id already
+ * exists
+ * @param {InspectorPanel}
+ * @param {String}
+ * @return {Boolean}
+ */
+function hasSideBarTab(inspector, id) {
+  return !!inspector.sidebar.getWindowForTab(id);
+}
+
+/**
+ * Wait for eventName on target.
+ * @param {Object} target An observable object that either supports on/off or
+ * addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function once(target, eventName, useCapture=false) {
+  info("Waiting for event: '" + eventName + "' on " + target + ".");
+
+  let deferred = promise.defer();
+
+  for (let [add, remove] of [
+    ["addEventListener", "removeEventListener"],
+    ["addListener", "removeListener"],
+    ["on", "off"]
+  ]) {
+    if ((add in target) && (remove in target)) {
+      target[add](eventName, function onEvent(...aArgs) {
+        target[remove](eventName, onEvent, useCapture);
+        deferred.resolve.apply(deferred, aArgs);
+      }, useCapture);
+      break;
+    }
+  }
+
+  return deferred.promise;
+}
+
+/**
+ * Wait for a content -> chrome message on the message manager (the window
+ * messagemanager is used).
+ * @param {String} name The message name
+ * @return {Promise} A promise that resolves to the response data when the
+ * message has been received
+ */
+function waitForContentMessage(name) {
+  info("Expecting message " + name + " from content");
+
+  let mm = gBrowser.selectedBrowser.messageManager;
+
+  let def = promise.defer();
+  mm.addMessageListener(name, function onMessage(msg) {
+    mm.removeMessageListener(name, onMessage);
+    def.resolve(msg.data);
+  });
+  return def.promise;
+}
+
+/**
+ * Send an async message to the frame script (chrome -> content) and wait for a
+ * response message with the same name (content -> chrome).
+ * @param {String} name The message name. Should be one of the messages defined
+ * in doc_frame_script.js
+ * @param {Object} data Optional data to send along
+ * @param {Object} objects Optional CPOW objects to send along
+ * @param {Boolean} expectResponse If set to false, don't wait for a response
+ * with the same name from the content script. Defaults to true.
+ * @return {Promise} Resolves to the response data if a response is expected,
+ * immediately resolves otherwise
+ */
+function executeInContent(name, data={}, objects={}, expectResponse=true) {
+  info("Sending message " + name + " to content");
+  let mm = gBrowser.selectedBrowser.messageManager;
+
+  mm.sendAsyncMessage(name, data, objects);
+  if (expectResponse) {
+    return waitForContentMessage(name);
+  } else {
+    return promise.resolve();
+  }
+}
+
+/**
+ * Simulate a click on the playPause button of a playerWidget.
+ */
+let togglePlayPauseButton = Task.async(function*(widget) {
+  // Note that instead of simulating a real event here, the callback is just
+  // called. This is better because the callback returns a promise, so we know
+  // when the player is paused, and we don't really care to test that simulating
+  // a DOM event actually works.
+  yield widget.onPlayPauseBtnClick();
+
+  // Wait for the next sate change event to make sure the state is updated
+  yield widget.player.once(widget.player.AUTO_REFRESH_EVENT);
+});
--- a/browser/devtools/devtools-clhandler.js
+++ b/browser/devtools/devtools-clhandler.js
@@ -18,16 +18,28 @@ devtoolsCommandlineHandler.prototype = {
     let consoleFlag = cmdLine.handleFlag("jsconsole", false);
     let debuggerFlag = cmdLine.handleFlag("jsdebugger", false);
     if (consoleFlag) {
       this.handleConsoleFlag(cmdLine);
     }
     if (debuggerFlag) {
       this.handleDebuggerFlag(cmdLine);
     }
+    let debuggerServerFlag;
+    try {
+      debuggerServerFlag =
+        cmdLine.handleFlagWithParam("start-debugger-server", false);
+    } catch(e) {
+      // We get an error if the option is given but not followed by a value.
+      // By catching and trying again, the value is effectively optional.
+      debuggerServerFlag = cmdLine.handleFlag("start-debugger-server", false);
+    }
+    if (debuggerServerFlag) {
+      this.handleDebuggerServerFlag(cmdLine, debuggerServerFlag);
+    }
   },
 
   handleConsoleFlag: function(cmdLine) {
     let window = Services.wm.getMostRecentWindow("devtools:webconsole");
     if (!window) {
       let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
       // Load the browser devtools main module as the loader's main module.
       Cu.import("resource:///modules/devtools/gDevTools.jsm");
@@ -38,40 +50,88 @@ devtoolsCommandlineHandler.prototype = {
       window.focus(); // the Browser Console was already open
     }
 
     if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
       cmdLine.preventDefault = true;
     }
   },
 
-  handleDebuggerFlag: function(cmdLine) {
+  _isRemoteDebuggingEnabled() {
     let remoteDebuggingEnabled = false;
     try {
       remoteDebuggingEnabled = kDebuggerPrefs.every((pref) => Services.prefs.getBoolPref(pref));
     } catch (ex) {
       Cu.reportError(ex);
-      return;
+      return false;
     }
-    if (remoteDebuggingEnabled) {
-      Cu.import("resource:///modules/devtools/ToolboxProcess.jsm");
-      BrowserToolboxProcess.init();
-    } else {
+    if (!remoteDebuggingEnabled) {
       let errorMsg = "Could not run chrome debugger! You need the following prefs " +
                      "to be set to true: " + kDebuggerPrefs.join(", ");
       Cu.reportError(errorMsg);
-      // Dump as well, as we're doing this from a commandline, make sure people don't miss it:
+      // Dump as well, as we're doing this from a commandline, make sure people
+      // don't miss it:
       dump(errorMsg + "\n");
     }
+    return remoteDebuggingEnabled;
+  },
+
+  handleDebuggerFlag: function(cmdLine) {
+    if (!this._isRemoteDebuggingEnabled()) {
+      return;
+    }
+    Cu.import("resource:///modules/devtools/ToolboxProcess.jsm");
+    BrowserToolboxProcess.init();
+
+    if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
+      cmdLine.preventDefault = true;
+    }
+  },
+
+  handleDebuggerServerFlag: function(cmdLine, portOrPath) {
+    if (!this._isRemoteDebuggingEnabled()) {
+      return;
+    }
+    if (portOrPath === true) {
+      // Default to TCP port 6000 if no value given
+      portOrPath = 6000;
+    }
+    let { DevToolsLoader } =
+      Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+
+    try {
+      // Create a separate loader instance, so that we can be sure to receive
+      // a separate instance of the DebuggingServer from the rest of the
+      // devtools.  This allows us to safely use the tools against even the
+      // actors and DebuggingServer itself, especially since we can mark
+      // serverLoader as invisible to the debugger (unlike the usual loader
+      // settings).
+      let serverLoader = new DevToolsLoader();
+      serverLoader.invisibleToDebugger = true;
+      serverLoader.main("devtools/server/main");
+      let debuggerServer = serverLoader.DebuggerServer;
+      debuggerServer.init();
+      debuggerServer.addBrowserActors();
+
+      let listener = debuggerServer.createListener();
+      listener.portOrPath = portOrPath;
+      listener.open();
+      dump("Started debugger server on " + portOrPath + "\n");
+    } catch(e) {
+      dump("Unable to start debugger server on " + portOrPath + ": " + e);
+    }
 
     if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
       cmdLine.preventDefault = true;
     }
   },
 
   helpInfo : "  --jsconsole        Open the Browser Console.\n" +
-             "  --jsdebugger       Open the Browser Toolbox.\n",
+             "  --jsdebugger       Open the Browser Toolbox.\n" +
+             "  --start-debugger-server [port|path] " +
+             "Start the debugger server on a TCP port or " +
+             "Unix domain socket path.  Defaults to TCP port 6000.\n",
 
   classID: Components.ID("{9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]),
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([devtoolsCommandlineHandler]);
--- a/browser/devtools/framework/sidebar.js
+++ b/browser/devtools/framework/sidebar.js
@@ -252,26 +252,29 @@ ToolSidebar.prototype = {
       return promise.resolve(null);
     }
     this._destroyed = true;
 
     Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
 
     this._tabbox.tabpanels.removeEventListener("select", this, true);
 
-    while (this._tabbox.tabpanels.hasChildNodes()) {
+    // Note that we check for the existence of this._tabbox.tabpanels at each
+    // step as the container window may have been closed by the time one of the
+    // panel's destroy promise resolves.
+    while (this._tabbox.tabpanels && this._tabbox.tabpanels.hasChildNodes()) {
       let panel = this._tabbox.tabpanels.firstChild;
       let win = panel.firstChild.contentWindow;
       if ("destroy" in win) {
         yield win.destroy();
       }
       panel.remove();
     }
 
-    while (this._tabbox.tabs.hasChildNodes()) {
+    while (this._tabbox.tabs && this._tabbox.tabs.hasChildNodes()) {
       this._tabbox.tabs.removeChild(this._tabbox.tabs.firstChild);
     }
 
     if (this._currentTool) {
       this._telemetry.toolClosed(this._currentTool);
     }
 
     this._toolPanel.emit("sidebar-destroyed", this);
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -336,16 +336,22 @@ InspectorPanel.prototype = {
                           "chrome://browser/content/devtools/fontinspector/font-inspector.xhtml",
                           "fontinspector" == defaultTab);
     }
 
     this.sidebar.addTab("layoutview",
                         "chrome://browser/content/devtools/layoutview/view.xhtml",
                         "layoutview" == defaultTab);
 
+    if (this.target.form.animationsActor) {
+      this.sidebar.addTab("animationinspector",
+                          "chrome://browser/content/devtools/animationinspector/animation-inspector.xhtml",
+                          "animationinspector" == defaultTab);
+    }
+
     let ruleViewTab = this.sidebar.getTab("ruleview");
 
     this.sidebar.show();
   },
 
   /**
    * Reset the inspector on new root mutation.
    */
--- a/browser/devtools/inspector/selector-search.js
+++ b/browser/devtools/inspector/selector-search.js
@@ -1,14 +1,16 @@
 /* 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 { Cu } = require("chrome");
+
 const promise = require("resource://gre/modules/Promise.jsm").Promise;
 loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter"));
 loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
 
 // Maximum number of selector suggestions shown in the panel.
 const MAX_SUGGESTIONS = 15;
 
 /**
@@ -260,17 +262,17 @@ SelectorSearch.prototype = {
         this._lastValidSearch = query + "*";
       }
       else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
         let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0];
         this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
       }
       this.searchBox.classList.add("devtools-no-search-result");
       return this.showSuggestions();
-    }).then(() => this.emit("processing-done"));
+    }).then(() => this.emit("processing-done"), Cu.reportError);
   },
 
   /**
    * Handles keypresses inside the input box.
    */
   _onSearchKeypress: function(aEvent) {
     let query = this.searchBox.value;
     switch(aEvent.keyCode) {
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -31,16 +31,19 @@ browser.jar:
     content/browser/devtools/cssruleview.xhtml                         (styleinspector/cssruleview.xhtml)
     content/browser/devtools/ruleview.css                              (styleinspector/ruleview.css)
     content/browser/devtools/layoutview/view.js                        (layoutview/view.js)
     content/browser/devtools/layoutview/view.xhtml                     (layoutview/view.xhtml)
     content/browser/devtools/layoutview/view.css                       (layoutview/view.css)
     content/browser/devtools/fontinspector/font-inspector.js           (fontinspector/font-inspector.js)
     content/browser/devtools/fontinspector/font-inspector.xhtml        (fontinspector/font-inspector.xhtml)
     content/browser/devtools/fontinspector/font-inspector.css          (fontinspector/font-inspector.css)
+    content/browser/devtools/animationinspector/animation-controller.js (animationinspector/animation-controller.js)
+    content/browser/devtools/animationinspector/animation-panel.js     (animationinspector/animation-panel.js)
+    content/browser/devtools/animationinspector/animation-inspector.xhtml (animationinspector/animation-inspector.xhtml)
     content/browser/devtools/codemirror/codemirror.js                  (sourceeditor/codemirror/codemirror.js)
     content/browser/devtools/codemirror/codemirror.css                 (sourceeditor/codemirror/codemirror.css)
     content/browser/devtools/codemirror/javascript.js                  (sourceeditor/codemirror/mode/javascript.js)
     content/browser/devtools/codemirror/xml.js                         (sourceeditor/codemirror/mode/xml.js)
     content/browser/devtools/codemirror/css.js                         (sourceeditor/codemirror/mode/css.js)
     content/browser/devtools/codemirror/htmlmixed.js                   (sourceeditor/codemirror/mode/htmlmixed.js)
     content/browser/devtools/codemirror/clike.js                       (sourceeditor/codemirror/mode/clike.js)
     content/browser/devtools/codemirror/activeline.js                  (sourceeditor/codemirror/selection/active-line.js)
@@ -89,16 +92,17 @@ browser.jar:
 #ifdef MOZ_DEVTOOLS_PERFTOOLS
     content/browser/devtools/performance.xul                           (performance/performance.xul)
     content/browser/devtools/performance/performance-controller.js     (performance/performance-controller.js)
     content/browser/devtools/performance/performance-view.js           (performance/performance-view.js)
     content/browser/devtools/performance/views/overview.js             (performance/views/overview.js)
     content/browser/devtools/performance/views/details.js              (performance/views/details.js)
     content/browser/devtools/performance/views/details-call-tree.js    (performance/views/details-call-tree.js)
     content/browser/devtools/performance/views/details-waterfall.js    (performance/views/details-waterfall.js)
+    content/browser/devtools/performance/views/details-flamegraph.js   (performance/views/details-flamegraph.js)
 #endif
     content/browser/devtools/responsivedesign/resize-commands.js       (responsivedesign/resize-commands.js)
     content/browser/devtools/commandline.css                           (commandline/commandline.css)
     content/browser/devtools/commandlineoutput.xhtml                   (commandline/commandlineoutput.xhtml)
     content/browser/devtools/commandlinetooltip.xhtml                  (commandline/commandlinetooltip.xhtml)
     content/browser/devtools/commandline/commands-index.js             (commandline/commands-index.js)
     content/browser/devtools/framework/toolbox-window.xul              (framework/toolbox-window.xul)
     content/browser/devtools/framework/toolbox-options.xul             (framework/toolbox-options.xul)
--- a/browser/devtools/moz.build
+++ b/browser/devtools/moz.build
@@ -1,15 +1,16 @@
 # -*- 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/.
 
 DIRS += [
+    'animationinspector',
     'app-manager',
     'canvasdebugger',
     'commandline',
     'debugger',
     'eyedropper',
     'fontinspector',
     'framework',
     'inspector',
--- a/browser/devtools/netmonitor/netmonitor-controller.js
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -3,16 +3,17 @@
 /* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 const NET_STRINGS_URI = "chrome://browser/locale/devtools/netmonitor.properties";
+const PKI_STRINGS_URI = "chrome://pippki/locale/pippki.properties";
 const LISTENERS = [ "NetworkActivity" ];
 const NET_PREFS = { "NetworkMonitor.saveRequestAndResponseBodies": true };
 
 // The panel's window global is an EventEmitter firing the following events:
 const EVENTS = {
   // When the monitored target begins and finishes navigating.
   TARGET_WILL_NAVIGATE: "NetMonitor:TargetWillNavigate",
   TARGET_DID_NAVIGATE: "NetMonitor:TargetNavigate",
@@ -29,16 +30,20 @@ const EVENTS = {
   // When request cookies begin and finish receiving.
   UPDATING_REQUEST_COOKIES: "NetMonitor:NetworkEventUpdating:RequestCookies",
   RECEIVED_REQUEST_COOKIES: "NetMonitor:NetworkEventUpdated:RequestCookies",
 
   // When request post data begins and finishes receiving.
   UPDATING_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdating:RequestPostData",
   RECEIVED_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdated:RequestPostData",
 
+  // When security information begins and finishes receiving.
+  UPDATING_SECURITY_INFO: "NetMonitor::NetworkEventUpdating:SecurityInfo",
+  RECEIVED_SECURITY_INFO: "NetMonitor::NetworkEventUpdated:SecurityInfo",
+
   // When response headers begin and finish receiving.
   UPDATING_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdating:ResponseHeaders",
   RECEIVED_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdated:ResponseHeaders",
 
   // When response cookies begin and finish receiving.
   UPDATING_RESPONSE_COOKIES: "NetMonitor:NetworkEventUpdating:ResponseCookies",
   RECEIVED_RESPONSE_COOKIES: "NetMonitor:NetworkEventUpdated:ResponseCookies",
 
@@ -131,16 +136,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/PluralForm.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils",
   "resource://gre/modules/devtools/DevToolsUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
   "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
 
+XPCOMUtils.defineLazyServiceGetter(this, "DOMParser",
+  "@mozilla.org/xmlextras/domparser;1", "nsIDOMParser");
+
 Object.defineProperty(this, "NetworkHelper", {
   get: function() {
     return require("devtools/toolkit/webconsole/network-helper");
   },
   configurable: true,
   enumerable: true
 });
 
@@ -565,16 +573,23 @@ NetworkEventsHandler.prototype = {
       case "requestCookies":
         this.webConsoleClient.getRequestCookies(actor, this._onRequestCookies);
         window.emit(EVENTS.UPDATING_REQUEST_COOKIES, actor);
         break;
       case "requestPostData":
         this.webConsoleClient.getRequestPostData(actor, this._onRequestPostData);
         window.emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
         break;
+      case "securityInfo":
+        NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
+          securityState: aPacket.state,
+        });
+        this.webConsoleClient.getSecurityInfo(actor, this._onSecurityInfo);
+        window.emit(EVENTS.UPDATING_SECURITY_INFO, actor);
+        break;
       case "responseHeaders":
         this.webConsoleClient.getResponseHeaders(actor, this._onResponseHeaders);
         window.emit(EVENTS.UPDATING_RESPONSE_HEADERS, actor);
         break;
       case "responseCookies":
         this.webConsoleClient.getResponseCookies(actor, this._onResponseCookies);
         window.emit(EVENTS.UPDATING_RESPONSE_COOKIES, actor);
         break;
@@ -640,16 +655,30 @@ NetworkEventsHandler.prototype = {
   _onRequestPostData: function(aResponse) {
     NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
       requestPostData: aResponse
     });
     window.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, aResponse.from);
   },
 
   /**
+   * Handles additional information received for a "securityInfo" packet.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   */
+   _onSecurityInfo: function(aResponse) {
+     NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+       securityInfo: aResponse.securityInfo
+     });
+
+     window.emit(EVENTS.RECEIVED_SECURITY_INFO, aResponse.from);
+   },
+
+  /**
    * Handles additional information received for a "responseHeaders" packet.
    *
    * @param object aResponse
    *        The message received from the server.
    */
   _onResponseHeaders: function(aResponse) {
     NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
       responseHeaders: aResponse
@@ -733,16 +762,17 @@ NetworkEventsHandler.prototype = {
     return deferred.promise;
   }
 };
 
 /**
  * Localization convenience methods.
  */
 let L10N = new ViewHelpers.L10N(NET_STRINGS_URI);
+let PKI_L10N = new ViewHelpers.L10N(PKI_STRINGS_URI);
 
 /**
  * Shortcuts for accessing various network monitor preferences.
  */
 let Prefs = new ViewHelpers.Prefs("devtools.netmonitor", {
   networkDetailsWidth: ["Int", "panes-network-details-width"],
   networkDetailsHeight: ["Int", "panes-network-details-height"],
   statistics: ["Bool", "statistics"],
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -331,16 +331,17 @@ function RequestsMenuView() {
   this._flushRequests = this._flushRequests.bind(this);
   this._onHover = this._onHover.bind(this);
   this._onSelect = this._onSelect.bind(this);
   this._onSwap = this._onSwap.bind(this);
   this._onResize = this._onResize.bind(this);
   this._byFile = this._byFile.bind(this);
   this._byDomain = this._byDomain.bind(this);
   this._byType = this._byType.bind(this);
+  this._onSecurityIconClick = this._onSecurityIconClick.bind(this);
 }
 
 RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
   /**
    * Initialization function, called when the network monitor is started.
    */
   initialize: function() {
     dumpn("Initializing the RequestsMenuView");
@@ -1051,16 +1052,27 @@ RequestsMenuView.prototype = Heritage.ex
   refreshTooltip: function(aItem) {
     let tooltip = aItem.attachment.tooltip;
     tooltip.hide();
     tooltip.startTogglingOnHover(aItem.target, this._onHover);
     tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION;
   },
 
   /**
+   * Attaches security icon click listener for the given request menu item.
+   *
+   * @param object item
+   *        The network request item to attach the listener to.
+   */
+  attachSecurityIconClickListener: function ({ target }) {
+    let icon = $(".requests-security-state-icon", target);
+    icon.addEventListener("click", this._onSecurityIconClick);
+  },
+
+  /**
    * Schedules adding additional information to a network request.
    *
    * @param string aId
    *        An identifier coming from the network monitor controller.
    * @param object aData
    *        An object containing several { key: value } tuples of network info.
    *        Supported keys are "httpVersion", "status", "statusText" etc.
    */
@@ -1128,16 +1140,23 @@ RequestsMenuView.prototype = Heritage.ex
               // The `getString` promise is async, so we need to refresh the
               // information displayed in the network details pane again here.
               refreshNetworkDetailsPaneIfNecessary(currentItem);
             });
 
             requestItem.attachment.requestPostData = value;
             requestItem.attachment.requestHeadersFromUploadStream = currentStore;
             break;
+          case "securityState":
+            requestItem.attachment.securityState = value;
+            this.updateMenuView(requestItem, key, value);
+            break;
+          case "securityInfo":
+            requestItem.attachment.securityInfo = value;
+            break;
           case "responseHeaders":
             requestItem.attachment.responseHeaders = value;
             break;
           case "responseCookies":
             requestItem.attachment.responseCookies = value;
             break;
           case "httpVersion":
             requestItem.attachment.httpVersion = value;
@@ -1281,16 +1300,25 @@ RequestsMenuView.prototype = Heritage.ex
         file.setAttribute("value", nameWithQuery);
         file.setAttribute("tooltiptext", nameWithQuery);
 
         let domain = $(".requests-menu-domain", target);
         domain.setAttribute("value", hostPort);
         domain.setAttribute("tooltiptext", hostPort);
         break;
       }
+      case "securityState": {
+        let tooltip = L10N.getStr("netmonitor.security.state." + aValue);
+        let icon = $(".requests-security-state-icon", target);
+        icon.classList.add("security-state-" + aValue);
+        icon.setAttribute("tooltiptext", tooltip);
+
+        this.attachSecurityIconClickListener(aItem);
+        break;
+      }
       case "status": {
         let node = $(".requests-menu-status", target);
         let codeNode = $(".requests-menu-status-code", target);
         codeNode.setAttribute("value", aValue);
         node.setAttribute("code", aValue);
         break;
       }
       case "statusText": {
@@ -1570,16 +1598,21 @@ RequestsMenuView.prototype = Heritage.ex
    * The swap listener for this container.
    * Called when two items switch places, when the contents are sorted.
    */
   _onSwap: function({ detail: [firstItem, secondItem] }) {
     // Sorting will create new anchor nodes for all the swapped request items
     // in this container, so it's necessary to refresh the Tooltip instances.
     this.refreshTooltip(firstItem);
     this.refreshTooltip(secondItem);
+
+    // Reattach click listener to the security icons
+    this.attachSecurityIconClickListener(firstItem);
+    this.attachSecurityIconClickListener(secondItem);
+
   },
 
   /**
    * The predicate used when deciding whether a popup should be shown
    * over a request item or not.
    *
    * @param nsIDOMNode aTarget
    *        The element node currently being hovered.
@@ -1605,16 +1638,28 @@ RequestsMenuView.prototype = Heritage.ex
         let src = "data:" + mimeType + ";" + encoding + "," + aString;
         aTooltip.setImageContent(src, { maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM });
         return anchor;
       });
     }
   },
 
   /**
+   * A handler that opens the security tab in the details view if secure or
+   * broken security indicator is clicked.
+   */
+  _onSecurityIconClick: function(e) {
+    let state = this.selectedItem.attachment.securityState;
+    if (state === "broken" || state === "secure") {
+      // Choose the security tab.
+      NetMonitorView.NetworkDetails.widget.selectedIndex = 5;
+    }
+  },
+
+  /**
    * The resize listener for this container's window.
    */
   _onResize: function(e) {
     // Allow requests to settle down first.
     setNamedTimeout(
       "resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
   },
 
@@ -2043,19 +2088,30 @@ NetworkDetailsView.prototype = {
     $("#response-content-image-box").hidden = true;
 
     let isHtml = RequestsMenuView.prototype.isHtml({ attachment: aData });
 
     // Show the "Preview" tabpanel only for plain HTML responses.
     $("#preview-tab").hidden = !isHtml;
     $("#preview-tabpanel").hidden = !isHtml;
 
+    // Show the "Security" tab only for requests that
+    //   1) are https (state != insecure)
+    //   2) come from a target that provides security information.
+    let hasSecurityInfo = aData.securityState &&
+                          aData.securityState !== "insecure";
+
+    $("#security-tab").hidden = !hasSecurityInfo;
+
     // Switch to the "Headers" tabpanel if the "Preview" previously selected
-    // and this is not an HTML response.
-    if (!isHtml && this.widget.selectedIndex == 5) {
+    // and this is not an HTML response or "Security" was selected but this
+    // request has no security information.
+
+    if (!isHtml && this.widget.selectedPanel === $("#preview-tabpanel") ||
+        !hasSecurityInfo && this.widget.selectedPanel === $("#security-tabpanel")) {
       this.widget.selectedIndex = 0;
     }
 
     this._headers.empty();
     this._cookies.empty();
     this._params.empty();
     this._json.empty();
 
@@ -2112,17 +2168,20 @@ NetworkDetailsView.prototype = {
             src.requestPostData);
           break;
         case 3: // "Response"
           yield view._setResponseBody(src.url, src.responseContent);
           break;
         case 4: // "Timings"
           yield view._setTimingsInformation(src.eventTimings);
           break;
-        case 5: // "Preview"
+        case 5: // "Security"
+          yield view._setSecurityInfo(src.securityInfo, src.url);
+          break;
+        case 6: // "Preview"
           yield view._setHtmlPreview(src.responseContent);
           break;
       }
       viewState.updating[tab] = false;
     }).then(() => {
       if (tab == this.widget.selectedIndex) {
         if (viewState.dirty[tab]) {
           // The request information was updated while the task was running.
@@ -2617,16 +2676,108 @@ NetworkDetailsView.prototype = {
     // Always disable JS when previewing HTML responses.
     let iframe = $("#response-preview");
     iframe.contentDocument.docShell.allowJavascript = false;
     iframe.contentDocument.documentElement.innerHTML = responseBody;
 
     window.emit(EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED);
   }),
 
+  /**
+   * Sets the security information shown in this view.
+   *
+   * @param object securityInfo
+   *        The data received from server
+   * @param string url
+   *        The URL of this request
+   * @return object
+   *        A promise that is resolved when the security info is rendered.
+   */
+  _setSecurityInfo: Task.async(function* (securityInfo, url) {
+    if (!securityInfo) {
+      // We don't have security info. This could mean one of two things:
+      // 1) This connection is not secure and this tab is not visible and thus
+      //    we shouldn't be here.
+      // 2) We have already received securityState and the tab is visible BUT
+      //    the rest of the information is still on its way. Once it arrives
+      //    this method is called again.
+      return;
+    }
+
+    /**
+     * A helper that sets label text to specified value.
+     *
+     * @param string selector
+     *        A selector for the label.
+     * @param string value
+     *        The value label should have. If this evaluates to false a
+     *        placeholder string <Not Available> is used instead.
+     */
+    function setLabel(selector, value) {
+      let label = $(selector);
+      if (!value) {
+        label.value = L10N.getStr("netmonitor.security.notAvailable");
+        label.setAttribute("tooltiptext", label.value);
+      } else {
+        label.value = value;
+        label.setAttribute("tooltiptext", value);
+      }
+    }
+
+    let errorbox = $("#security-error");
+    let infobox = $("#security-information");
+
+    if (securityInfo.state === "secure") {
+      infobox.hidden = false;
+      errorbox.hidden = true;
+
+      let enabledLabel = L10N.getStr("netmonitor.security.enabled");
+      let disabledLabel = L10N.getStr("netmonitor.security.disabled");
+
+      // Connection parameters
+      setLabel("#security-protocol-version-value", securityInfo.protocolVersion);
+      setLabel("#security-ciphersuite-value", securityInfo.cipherSuite);
+
+      // Host header
+      let domain = NetMonitorView.RequestsMenu._getUriHostPort(url);
+      let hostHeader = L10N.getFormatStr("netmonitor.security.hostHeader", domain);
+      setLabel("#security-info-host-header", hostHeader);
+
+      // Parameters related to the domain
+      setLabel("#security-http-strict-transport-security-value",
+                securityInfo.hsts ? enabledLabel : disabledLabel);
+
+      setLabel("#security-public-key-pinning-value",
+                securityInfo.hpkp ? enabledLabel : disabledLabel);
+
+      // Certificate parameters
+      let cert = securityInfo.cert;
+      setLabel("#security-cert-subject-cn", cert.subject.commonName);
+      setLabel("#security-cert-subject-o", cert.subject.organization);
+      setLabel("#security-cert-subject-ou", cert.subject.organizationalUnit);
+
+      setLabel("#security-cert-issuer-cn", cert.issuer.commonName);
+      setLabel("#security-cert-issuer-o", cert.issuer.organization);
+      setLabel("#security-cert-issuer-ou", cert.issuer.organizationalUnit);
+
+      setLabel("#security-cert-validity-begins", cert.validity.start);
+      setLabel("#security-cert-validity-expires", cert.validity.end);
+
+      setLabel("#security-cert-sha1-fingerprint", cert.fingerprint.sha1);
+      setLabel("#security-cert-sha256-fingerprint", cert.fingerprint.sha256);
+    } else {
+      infobox.hidden = true;
+      errorbox.hidden = false;
+
+      // Strip any HTML from the message.
+      let plain = DOMParser.parseFromString(securityInfo.errorMessage, "text/html");
+      $("#security-error-message").textContent = plain.body.textContent;
+    }
+  }),
+
   _dataSrc: null,
   _headers: null,
   _cookies: null,
   _params: null,
   _json: null,
   _paramsQueryString: "",
   _paramsFormData: "",
   _paramsPostPayload: "",
--- a/browser/devtools/netmonitor/netmonitor.xul
+++ b/browser/devtools/netmonitor/netmonitor.xul
@@ -6,16 +6,18 @@
 <?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/content/devtools/netmonitor.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/devtools/netmonitor.css" type="text/css"?>
 <!DOCTYPE window [
   <!ENTITY % netmonitorDTD SYSTEM "chrome://browser/locale/devtools/netmonitor.dtd">
   %netmonitorDTD;
+  <!ENTITY % certManagerDTD SYSTEM "chrome://pippki/locale/certManager.dtd">
+  %certManagerDTD;
 ]>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml">
 
   <script type="application/javascript;version=1.8"
           src="chrome://browser/content/devtools/theme-switching.js"/>
   <script type="text/javascript" src="netmonitor-controller.js"/>
@@ -77,20 +79,20 @@
                 <button id="requests-menu-file-button"
                         class="requests-menu-header-button requests-menu-file"
                         data-key="file"
                         label="&netmonitorUI.toolbar.file;"
                         flex="1">
                 </button>
               </hbox>
               <hbox id="requests-menu-domain-header-box"
-                    class="requests-menu-header requests-menu-domain"
+                    class="requests-menu-header requests-menu-security-and-domain"
                     align="center">
                 <button id="requests-menu-domain-button"
-                        class="requests-menu-header-button requests-menu-domain"
+                        class="requests-menu-header-button requests-menu-security-and-domain"
                         data-key="domain"
                         label="&netmonitorUI.toolbar.domain;"
                         flex="1">
                 </button>
               </hbox>
               <hbox id="requests-menu-type-header-box"
                     class="requests-menu-header requests-menu-type"
                     align="center">
@@ -165,18 +167,23 @@
               </hbox>
               <hbox class="requests-menu-subitem requests-menu-icon-and-file"
                     align="center">
                 <image class="requests-menu-icon" hidden="true"/>
                 <label class="plain requests-menu-file"
                        crop="end"
                        flex="1"/>
               </hbox>
-              <label class="plain requests-menu-subitem requests-menu-domain"
-                     crop="end"/>
+              <hbox class="requests-menu-subitem requests-menu-security-and-domain"
+                    align="center">
+                <image class="requests-security-state-icon" />
+                <label class="plain requests-menu-domain"
+                       crop="end"
+                       flex="1"/>
+              </hbox>
               <label class="plain requests-menu-subitem requests-menu-type"
                      crop="end"/>
               <label class="plain requests-menu-subitem requests-menu-size"
                      crop="end"/>
               <hbox class="requests-menu-subitem requests-menu-waterfall"
                     align="center"
                     flex="1">
                 <hbox class="requests-menu-timings"
@@ -261,16 +268,18 @@
               <tab id="cookies-tab"
                    label="&netmonitorUI.tab.cookies;"/>
               <tab id="params-tab"
                    label="&netmonitorUI.tab.params;"/>
               <tab id="response-tab"
                    label="&netmonitorUI.tab.response;"/>
               <tab id="timings-tab"
                    label="&netmonitorUI.tab.timings;"/>
+              <tab id="security-tab"
+                   label="&netmonitorUI.tab.security;"/>
               <tab id="preview-tab"
                    label="&netmonitorUI.tab.preview;"/>
             </tabs>
             <tabpanels flex="1">
               <tabpanel id="headers-tabpanel"
                         class="tabpanel-content">
                 <vbox flex="1">
                   <hbox id="headers-summary-url"
@@ -455,16 +464,204 @@
                         align="center">
                     <label class="plain tabpanel-summary-label"
                            value="&netmonitorUI.timings.receive;"/>
                     <hbox class="requests-menu-timings-box receive"/>
                     <label class="plain requests-menu-timings-total"/>
                   </hbox>
                 </vbox>
               </tabpanel>
+              <tabpanel id="security-tabpanel"
+                        class="tabpanel-content">
+                  <vbox id="security-error"
+                        class="tabpanel-summary-container"
+                        flex="1">
+                    <label class="plain tabpanel-summary-label"
+                           value="&netmonitorUI.security.error;"/>
+                    <description id="security-error-message" flex="1"/>
+                  </vbox>
+                  <vbox id="security-information"
+                        flex="1">
+                    <vbox id="security-info-connection"
+                          class="tabpanel-summary-container">
+                      <label class="plain tabpanel-summary-label"
+                             value="&netmonitorUI.security.connection;"/>
+                      <vbox class="security-info-section">
+                        <hbox id="security-protocol-version"
+                              class="tabpanel-summary-container"
+                              align="center">
+                          <label class="plain tabpanel-summary-label"
+                                 value="&netmonitorUI.security.protocolVersion;"/>
+                          <label id="security-protocol-version-value"
+                                 class="plain tabpanel-summary-value devtools-monospace"
+                                 crop="end"
+                                 flex="1"/>
+                        </hbox>
+                        <hbox id="security-ciphersuite"
+                              class="tabpanel-summary-container"
+                              align="center">
+                          <label class="plain tabpanel-summary-label"
+                                 value="&netmonitorUI.security.cipherSuite;"/>
+                          <label id="security-ciphersuite-value"
+                                 class="plain tabpanel-summary-value devtools-monospace"
+                                 crop="end"
+                                 flex="1"/>
+                        </hbox>
+                      </vbox>
+                    </vbox>
+                    <vbox id="security-info-domain"
+                          class="tabpanel-summary-container">
+                      <label class="plain tabpanel-summary-label"
+                             id="security-info-host-header"/>
+                      <vbox class="security-info-section">
+                        <hbox id="security-http-strict-transport-security"
+                              class="tabpanel-summary-container"
+                              align="center">
+                          <label class="plain tabpanel-summary-label"
+                                 value="&netmonitorUI.security.hsts;"/>
+                          <label id="security-http-strict-transport-security-value"
+                                 class="plain tabpanel-summary-value devtools-monospace"
+                                 crop="end"
+                                 flex="1"/>
+                        </hbox>
+                        <hbox id="security-public-key-pinning"
+                              class="tabpanel-summary-container"
+                              align="center">
+                          <label class="plain tabpanel-summary-label"
+                                 value="&netmonitorUI.security.hpkp;"/>
+                          <label id="security-public-key-pinning-value"
+                                 class="plain tabpanel-summary-value devtools-monospace"
+                                 crop="end"
+                                 flex="1"/>
+                        </hbox>
+                      </vbox>
+                    </vbox>
+                    <vbox id="security-info-certificate"
+                          class="tabpanel-summary-container">
+                        <label class="plain tabpanel-summary-label"
+                               value="&netmonitorUI.security.certificate;"/>
+                      <vbox class="security-info-section">
+                        <vbox class="tabpanel-summary-container">
+                          <label class="plain tabpanel-summary-label"
+                                 value="&certmgr.subjectinfo.label;" flex="1"/>
+                        </vbox>
+                        <vbox class="security-info-section">
+                          <hbox class="tabpanel-summary-container"
+                                align="center">
+                            <label class="plain tabpanel-summary-label"
+                                   value="&certmgr.certdetail.cn;:"/>
+                            <label id="security-cert-subject-cn"
+                                   class="plain tabpanel-summary-value devtools-monospace"
+                                   crop="end"
+                                   flex="1"/>
+                          </hbox>
+                          <hbox class="tabpanel-summary-container"
+                                align="center">
+                            <label class="plain tabpanel-summary-label"
+                                   value="&certmgr.certdetail.o;:"/>
+                            <label id="security-cert-subject-o"
+                                   class="plain tabpanel-summary-value devtools-monospace"
+                                   crop="end"
+                                   flex="1"/>
+                          </hbox>
+                          <hbox class="tabpanel-summary-container"
+                                align="center">
+                            <label class="plain tabpanel-summary-label"
+                                   value="&certmgr.certdetail.ou;:"/>
+                            <label id="security-cert-subject-ou"
+                                   class="plain tabpanel-summary-value devtools-monospace"
+                                   crop="end"
+                                   flex="1"/>
+                          </hbox>
+                        </vbox>
+                        <vbox class="tabpanel-summary-container">
+                          <label class="plain tabpanel-summary-label"
+                                 value="&certmgr.issuerinfo.label;" flex="1"/>
+                        </vbox>
+                        <vbox class="security-info-section">
+                          <hbox class="tabpanel-summary-container"
+                                align="center">
+                            <label class="plain tabpanel-summary-label"
+                                   value="&certmgr.certdetail.cn;:"/>
+                            <label id="security-cert-issuer-cn"
+                                   class="plain tabpanel-summary-value devtools-monospace"
+                                   crop="end"
+                                   flex="1"/>
+                          </hbox>
+                          <hbox class="tabpanel-summary-container"
+                                align="center">
+                            <label class="plain tabpanel-summary-label"
+                                   value="&certmgr.certdetail.o;:"/>
+                            <label id="security-cert-issuer-o"
+                                   class="plain tabpanel-summary-value devtools-monospace"
+                                   crop="end"
+                                   flex="1"/>
+                          </hbox>
+                          <hbox class="tabpanel-summary-container"
+                                align="center">
+                            <label class="plain tabpanel-summary-label"
+                                   value="&certmgr.certdetail.ou;:"/>
+                            <label id="security-cert-issuer-ou"
+                                   class="plain tabpanel-summary-value devtools-monospace"
+                                   crop="end"
+                                   flex="1"/>
+                          </hbox>
+                        </vbox>
+                        <vbox class="tabpanel-summary-container">
+                          <label class="plain tabpanel-summary-label"
+                                 value="&certmgr.periodofvalidity.label;" flex="1"/>
+                        </vbox>
+                        <vbox class="security-info-section">
+                          <hbox class="tabpanel-summary-container"
+                                align="center">
+                            <label class="plain tabpanel-summary-label"
+                                   value="&certmgr.begins;:"/>
+                            <label id="security-cert-validity-begins"
+                                   class="plain tabpanel-summary-value devtools-monospace"
+                                   crop="end"
+                                   flex="1"/>
+                          </hbox>
+                          <hbox class="tabpanel-summary-container"
+                                align="center">
+                            <label class="plain tabpanel-summary-label"
+                                   value="&certmgr.expires;:"/>
+                            <label id="security-cert-validity-expires"
+                                   class="plain tabpanel-summary-value devtools-monospace"
+                                   crop="end"
+                                   flex="1"/>
+                          </hbox>
+                        </vbox>
+                        <vbox class="tabpanel-summary-container">
+                          <label class="plain tabpanel-summary-label"
+                                 value="&certmgr.fingerprints.label;" flex="1"/>
+                        </vbox>
+                        <vbox class="security-info-section">
+                          <hbox class="tabpanel-summary-container"
+                                align="center">
+                            <label class="plain tabpanel-summary-label"
+                                   value="&certmgr.certdetail.sha256fingerprint;:"/>
+                            <label id="security-cert-sha256-fingerprint"
+                                   class="plain tabpanel-summary-value devtools-monospace"
+                                   crop="end"
+                                   flex="1"/>
+                          </hbox>
+                          <hbox class="tabpanel-summary-container"
+                                align="center">
+                            <label class="plain tabpanel-summary-label"
+                                   value="&certmgr.certdetail.sha1fingerprint;:"/>
+                            <label id="security-cert-sha1-fingerprint"
+                                   class="plain tabpanel-summary-value devtools-monospace"
+                                   crop="end"
+                                   flex="1"/>
+                          </hbox>
+                        </vbox>
+                      </vbox>
+                    </vbox>
+                  </vbox>
+              </tabpanel>
               <tabpanel id="preview-tabpanel"
                         class="tabpanel-content">
                 <html:iframe id="response-preview"
                              frameborder="0"
                              sandbox=""/>
               </tabpanel>
             </tabpanels>
           </tabbox>
--- a/browser/devtools/netmonitor/test/browser.ini
+++ b/browser/devtools/netmonitor/test/browser.ini
@@ -21,16 +21,18 @@ support-files =
   html_post-raw-with-headers-test-page.html
   html_simple-test-page.html
   html_sorting-test-page.html
   html_statistics-test-page.html
   html_status-codes-test-page.html
   html_copy-as-curl.html
   html_curl-utils.html
   sjs_content-type-test-server.sjs
+  sjs_cors-test-server.sjs
+  sjs_https-redirect-test-server.sjs
   sjs_simple-test-server.sjs
   sjs_sorting-test-server.sjs
   sjs_status-codes-test-server.sjs
   test-image.png
 
 [browser_net_aaa_leaktest.js]
 [browser_net_accessibility-01.js]
 [browser_net_accessibility-02.js]
@@ -78,16 +80,23 @@ skip-if = e10s # Bug 1091603
 [browser_net_post-data-03.js]
 [browser_net_prefs-and-l10n.js]
 [browser_net_prefs-reload.js]
 [browser_net_raw_headers.js]
 [browser_net_reload-button.js]
 [browser_net_req-resp-bodies.js]
 [browser_net_resend.js]
 skip-if = e10s # Bug 1091612
+[browser_net_security-details.js]
+[browser_net_security-error.js]
+[browser_net_security-icon-click.js]
+[browser_net_security-redirect.js]
+[browser_net_security-state.js]
+[browser_net_security-tab-deselect.js]
+[browser_net_security-tab-visibility.js]
 [browser_net_simple-init.js]
 [browser_net_simple-request-data.js]
 [browser_net_simple-request-details.js]
 [browser_net_simple-request.js]
 [browser_net_sort-01.js]
 [browser_net_sort-02.js]
 [browser_net_sort-03.js]
 [browser_net_statistics-01.js]
--- a/browser/devtools/netmonitor/test/browser_net_html-preview.js
+++ b/browser/devtools/netmonitor/test/browser_net_html-preview.js
@@ -21,20 +21,20 @@ function test() {
       is($("#event-details-pane").selectedIndex, 0,
         "The first tab in the details pane should be selected.");
       is($("#preview-tab").hidden, true,
         "The preview tab should be hidden for non html responses.");
       is($("#preview-tabpanel").hidden, true,
         "The preview tabpanel should be hidden for non html responses.");
 
       RequestsMenu.selectedIndex = 4;
-      NetMonitorView.toggleDetailsPane({ visible: true, animated: false }, 5);
+      NetMonitorView.toggleDetailsPane({ visible: true, animated: false }, 6);
 
-      is($("#event-details-pane").selectedIndex, 5,
-        "The fifth tab in the details pane should be selected.");
+      is($("#event-details-pane").selectedIndex, 6,
+        "The sixth tab in the details pane should be selected.");
       is($("#preview-tab").hidden, false,
         "The preview tab should be visible now.");
       is($("#preview-tabpanel").hidden, false,
         "The preview tabpanel should be visible now.");
 
       waitFor(aMonitor.panelWin, EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED).then(() => {
         let iframe = $("#response-preview");
         ok(iframe,
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_security-details.js
@@ -0,0 +1,90 @@
+/* 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";
+
+/**
+ * Test that Security details tab contains the expected data.
+ */
+
+add_task(function* () {
+  let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
+  let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu, NetworkDetails } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
+
+  info("Performing a secure request.");
+  debuggee.performRequests(1, "https://example.com" + CORS_SJS_PATH);
+
+  yield waitForNetworkEvents(monitor, 1);
+
+  info("Selecting the request.");
+  RequestsMenu.selectedIndex = 0;
+
+  info("Waiting for details pane to be updated.");
+  yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+  info("Selecting security tab.");
+  NetworkDetails.widget.selectedIndex = 5;
+
+  info("Waiting for security tab to be updated.");
+  yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+  let errorbox = $("#security-error");
+  let infobox = $("#security-information");
+
+  is(errorbox.hidden, true, "Error box is hidden.");
+  is(infobox.hidden, false, "Information box visible.");
+
+  // Connection
+  checkLabel("#security-protocol-version-value", "TLSv1");
+  checkLabel("#security-ciphersuite-value", "TLS_RSA_WITH_AES_128_CBC_SHA");
+
+  // Host
+  checkLabel("#security-info-host-header", "Host example.com:");
+  checkLabel("#security-http-strict-transport-security-value", "Disabled");
+  checkLabel("#security-public-key-pinning-value", "Disabled");
+
+  // Cert
+  checkLabel("#security-cert-subject-cn", "example.com");
+  checkLabel("#security-cert-subject-o", "<Not Available>");
+  checkLabel("#security-cert-subject-ou", "<Not Available>");
+
+  checkLabel("#security-cert-issuer-cn", "Temporary Certificate Authority");
+  checkLabel("#security-cert-issuer-o", "Mozilla Testing");
+  checkLabel("#security-cert-issuer-ou", "<Not Available>");
+
+  // Locale sensitive and varies between timezones. Cant't compare equality or
+  // the test fails depending on which part of the world the test is executed.
+  checkLabelNotEmpty("#security-cert-validity-begins");
+  checkLabelNotEmpty("#security-cert-validity-expires");
+
+  checkLabelNotEmpty("#security-cert-sha1-fingerprint");
+  checkLabelNotEmpty("#security-cert-sha256-fingerprint");
+  yield teardown(monitor);
+
+  /**
+   * A helper that compares value attribute of a label with given selector to the
+   * expected value.
+   */
+  function checkLabel(selector, expected) {
+    info("Checking label " + selector);
+
+    let element = $(selector);
+
+    ok(element, "Selector matched an element.");
+    is(element.value, expected, "Label has the expected value.");
+  }
+
+  /**
+   * A helper that checks the label with given selector is not an empty string.
+   */
+  function checkLabelNotEmpty(selector) {
+    info("Checking that label " + selector + " is non-empty.");
+
+    let element = $(selector);
+
+    ok(element, "Selector matched an element.");
+    isnot(element.value, "", "Label was not empty.");
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_security-error.js
@@ -0,0 +1,67 @@
+/* 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";
+
+/**
+ * Test that Security details tab shows an error message with broken connections.
+ */
+
+add_task(function* () {
+  let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
+  let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu, NetworkDetails } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
+
+  info("Requesting a resource that has a certificate problem.");
+  debuggee.performRequests(1, "https://nocert.example.com");
+
+  yield waitForSecurityBrokenNetworkEvent();
+
+  info("Selecting the request.");
+  RequestsMenu.selectedIndex = 0;
+
+  info("Waiting for details pane to be updated.");
+  yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+  info("Selecting security tab.");
+  NetworkDetails.widget.selectedIndex = 5;
+
+  info("Waiting for security tab to be updated.");
+  yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+  let errorbox = $("#security-error");
+  let errormsg = $("#security-error-message");
+  let infobox = $("#security-information");
+
+  is(errorbox.hidden, false, "Error box is visble.");
+  is(infobox.hidden, true, "Information box is hidden.");
+
+  isnot(errormsg.textContent, "", "Error message is not empty.");
+
+  yield teardown(monitor);
+
+  /**
+   * Returns a promise that's resolved once a request with security issues is
+   * completed.
+   */
+  function waitForSecurityBrokenNetworkEvent() {
+    let awaitedEvents = [
+      "UPDATING_REQUEST_HEADERS",
+      "RECEIVED_REQUEST_HEADERS",
+      "UPDATING_REQUEST_COOKIES",
+      "RECEIVED_REQUEST_COOKIES",
+      "STARTED_RECEIVING_RESPONSE",
+      "UPDATING_RESPONSE_CONTENT",
+      "RECEIVED_RESPONSE_CONTENT",
+      "UPDATING_EVENT_TIMINGS",
+      "RECEIVED_EVENT_TIMINGS",
+    ];
+
+    let promises = awaitedEvents.map((event) => {
+      return monitor.panelWin.once(EVENTS[event]);
+    });
+
+    return Promise.all(promises);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_security-icon-click.js
@@ -0,0 +1,53 @@
+/* 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";
+
+/**
+ * Test that clicking on the security indicator opens the security details tab.
+ */
+
+add_task(function* () {
+  let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
+  let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu, NetworkDetails } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
+
+  info("Requesting a resource over HTTPS.");
+  debuggee.performRequests(1, "https://example.com" + CORS_SJS_PATH + "?request_2");
+  yield waitForNetworkEvents(monitor, 1);
+
+  debuggee.performRequests(1, "https://example.com" + CORS_SJS_PATH + "?request_1");
+  yield waitForNetworkEvents(monitor, 1);
+
+  is(RequestsMenu.itemCount, 2, "Two events event logged.");
+
+  yield clickAndTestSecurityIcon();
+
+  info("Selecting headers panel again.");
+  NetworkDetails.widget.selectedIndex = 0;
+  yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+  info("Sorting the items by filename.");
+  EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+
+  info("Testing that security icon can be clicked after the items were sorted.");
+  yield clickAndTestSecurityIcon();
+
+  yield teardown(monitor);
+
+  function* clickAndTestSecurityIcon() {
+    let item = RequestsMenu.items[0];
+    let icon = $(".requests-security-state-icon", item.target);
+
+    info("Clicking security icon of the first request and waiting for the " +
+         "panel to update.");
+
+    icon.click();
+    yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+    is(NetworkDetails.widget.selectedPanel, $("#security-tabpanel"),
+      "Security tab is selected.");
+  }
+
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_security-redirect.js
@@ -0,0 +1,35 @@
+/* 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";
+
+/**
+ * Test a http -> https redirect shows secure icon only for redirected https
+ * request.
+ */
+
+add_task(function* () {
+  let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
+  let { $, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
+
+  debuggee.performRequests(1, HTTPS_REDIRECT_SJS);
+  yield waitForNetworkEvents(monitor, 2);
+
+  is(RequestsMenu.itemCount, 2, "There were two requests due to redirect.");
+
+  let initial = RequestsMenu.items[0];
+  let redirect = RequestsMenu.items[1];
+
+  let initialSecurityIcon = $(".requests-security-state-icon", initial.target);
+  let redirectSecurityIcon = $(".requests-security-state-icon", redirect.target);
+
+  ok(initialSecurityIcon.classList.contains("security-state-insecure"),
+     "Initial request was marked insecure.");
+
+  ok(redirectSecurityIcon.classList.contains("security-state-secure"),
+     "Redirected request was marked secure.");
+
+  yield teardown(monitor);
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_security-state.js
@@ -0,0 +1,99 @@
+/* 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";
+
+/**
+ * Test that correct security state indicator appears depending on the security
+ * state.
+ */
+
+add_task(function* () {
+  const EXPECTED_SECURITY_STATES = {
+    "test1.example.com": "security-state-insecure",
+    "example.com": "security-state-secure",
+    "nocert.example.com": "security-state-broken",
+  };
+
+  let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
+  let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
+
+  yield performRequests();
+
+  for (let item of RequestsMenu.items) {
+    let domain = $(".requests-menu-domain", item.target).value;
+
+    info("Found a request to " + domain);
+    ok(domain in EXPECTED_SECURITY_STATES, "Domain " + domain + " was expected.");
+
+    let classes = $(".requests-security-state-icon", item.target).classList;
+    let expectedClass = EXPECTED_SECURITY_STATES[domain];
+
+    info("Classes of security state icon are: " + classes);
+    info("Security state icon is expected to contain class: " + expectedClass);
+    ok(classes.contains(expectedClass), "Icon contained the correct class name.");
+  }
+
+  yield teardown(monitor);
+
+  /**
+   * A helper that performs requests to
+   *  - https://nocert.example.com (broken)
+   *  - https://example.com (secure)
+   *  - http://test1.example.com (insecure)
+   * and waits until NetworkMonitor has handled all packets sent by the server.
+   */
+  function* performRequests() {
+    // waitForNetworkEvents does not work for requests with security errors as
+    // those only emit 9/13 events of a successful request.
+    let done = waitForSecurityBrokenNetworkEvent();
+
+    info("Requesting a resource that has a certificate problem.");
+    debuggee.performRequests(1, "https://nocert.example.com");
+
+    // Wait for the request to complete before firing another request. Otherwise
+    // the request with security issues interfere with waitForNetworkEvents.
+    info("Waiting for request to complete.");
+    yield done;
+
+    // Next perform a request over HTTP. If done the other way around the latter
+    // occasionally hangs waiting for event timings that don't seem to appear...
+    done = waitForNetworkEvents(monitor, 1);
+    info("Requesting a resource over HTTP.");
+    debuggee.performRequests(1, "http://test1.example.com" + CORS_SJS_PATH);
+    yield done;
+
+    done = waitForNetworkEvents(monitor, 1);
+    info("Requesting a resource over HTTPS.");
+    debuggee.performRequests(1, "https://example.com" + CORS_SJS_PATH);
+    yield done;
+
+    is(RequestsMenu.itemCount, 3, "Three events logged.");
+  }
+
+  /**
+   * Returns a promise that's resolved once a request with security issues is
+   * completed.
+   */
+  function waitForSecurityBrokenNetworkEvent() {
+    let awaitedEvents = [
+      "UPDATING_REQUEST_HEADERS",
+      "RECEIVED_REQUEST_HEADERS",
+      "UPDATING_REQUEST_COOKIES",
+      "RECEIVED_REQUEST_COOKIES",
+      "STARTED_RECEIVING_RESPONSE",
+      "UPDATING_RESPONSE_CONTENT",
+      "RECEIVED_RESPONSE_CONTENT",
+      "UPDATING_EVENT_TIMINGS",
+      "RECEIVED_EVENT_TIMINGS",
+    ];
+
+    let promises = awaitedEvents.map((event) => {
+      return monitor.panelWin.once(EVENTS[event]);
+    });
+
+    return Promise.all(promises);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_security-tab-deselect.js
@@ -0,0 +1,38 @@
+/* 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";
+
+/**
+ * Test that security details tab is no longer selected if an insecure request
+ * is selected.
+ */
+
+add_task(function* () {
+  let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
+  let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu, NetworkDetails } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
+
+  info("Performing requests.");
+  debuggee.performRequests(1, "https://example.com" + CORS_SJS_PATH);
+  debuggee.performRequests(1, "http://example.com" + CORS_SJS_PATH);
+  yield waitForNetworkEvents(monitor, 2);
+
+  info("Selecting secure request.");
+  RequestsMenu.selectedIndex = 0;
+
+  info("Selecting security tab.");
+  NetworkDetails.widget.selectedIndex = 5;
+
+  info("Selecting insecure request.");
+  RequestsMenu.selectedIndex = 1;
+
+  info("Waiting for security tab to be updated.");
+  yield monitor.panelWin.once(EVENTS.NETWORKDETAILSVIEW_POPULATED);
+
+  is(NetworkDetails.widget.selectedIndex, 0,
+    "Selected tab was reset when selected security tab was hidden.");
+
+  yield teardown(monitor);
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_security-tab-visibility.js
@@ -0,0 +1,111 @@
+/* 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";
+
+/**
+ * Test that security details tab is visible only when it should.
+ */
+
+add_task(function* () {
+  const TEST_DATA = [
+    {
+      desc: "http request",
+      uri: "http://example.com" + CORS_SJS_PATH,
+      visibleOnNewEvent: false,
+      visibleOnSecurityInfo: false,
+      visibleOnceComplete: false,
+    }, {
+      desc: "working https request",
+      uri: "https://example.com" + CORS_SJS_PATH,
+      visibleOnNewEvent: false,
+      visibleOnSecurityInfo: true,
+      visibleOnceComplete: true,
+    }, {
+      desc: "broken https request",
+      uri: "https://nocert.example.com",
+      isBroken: true,
+      visibleOnNewEvent: false,
+      visibleOnSecurityInfo: true,
+      visibleOnceComplete: true,
+    }
+  ];
+
+  let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
+  let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
+
+  for (let testcase of TEST_DATA) {
+    info("Testing Security tab visibility for " + testcase.desc);
+    let onNewItem = monitor.panelWin.once(EVENTS.NETWORK_EVENT);
+    let onSecurityInfo = monitor.panelWin.once(EVENTS.RECEIVED_SECURITY_INFO);
+    let onComplete = testcase.isBroken ?
+                       waitForSecurityBrokenNetworkEvent() :
+                       waitForNetworkEvents(monitor, 1);
+
+    let tab = $("#security-tab");
+
+    info("Performing a request to " + testcase.uri);
+    debuggee.performRequests(1, testcase.uri);
+
+    info("Waiting for new network event.");
+    yield onNewItem;
+
+    info("Selecting the request.");
+    RequestsMenu.selectedIndex = 0;
+
+    is(RequestsMenu.selectedItem.attachment.securityState, undefined,
+       "Security state has not yet arrived.");
+    is(tab.hidden, !testcase.visibleOnNewEvent,
+       "Security tab is " +
+        (testcase.visibleOnNewEvent ? "visible" : "hidden") +
+       " after new request was added to the menu.");
+
+    info("Waiting for security information to arrive.");
+    yield onSecurityInfo;
+
+    ok(RequestsMenu.selectedItem.attachment.securityState,
+       "Security state arrived.");
+    is(tab.hidden, !testcase.visibleOnSecurityInfo,
+       "Security tab is " +
+        (testcase.visibleOnSecurityInfo ? "visible" : "hidden") +
+       " after security information arrived.");
+
+    info("Waiting for request to complete.");
+    yield onComplete;
+    is(tab.hidden, !testcase.visibleOnceComplete,
+       "Security tab is " +
+        (testcase.visibleOnceComplete ? "visible" : "hidden") +
+       " after request has been completed.");
+
+    info("Clearing requests.");
+    RequestsMenu.clear();
+  }
+
+  yield teardown(monitor);
+
+  /**
+   * Returns a promise that's resolved once a request with security issues is
+   * completed.
+   */
+  function waitForSecurityBrokenNetworkEvent() {
+    let awaitedEvents = [
+      "UPDATING_REQUEST_HEADERS",
+      "RECEIVED_REQUEST_HEADERS",
+      "UPDATING_REQUEST_COOKIES",
+      "RECEIVED_REQUEST_COOKIES",
+      "STARTED_RECEIVING_RESPONSE",
+      "UPDATING_RESPONSE_CONTENT",
+      "RECEIVED_RESPONSE_CONTENT",
+      "UPDATING_EVENT_TIMINGS",
+      "RECEIVED_EVENT_TIMINGS",
+    ];
+
+    let promises = awaitedEvents.map((event) => {
+      return monitor.panelWin.once(EVENTS[event]);
+    });
+
+    return Promise.all(promises);
+  }
+});
--- a/browser/devtools/netmonitor/test/head.js
+++ b/browser/devtools/netmonitor/test/head.js
@@ -38,16 +38,18 @@ const SINGLE_GET_URL = EXAMPLE_URL + "ht
 const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html";
 const CURL_URL = EXAMPLE_URL + "html_copy-as-curl.html";
 const CURL_UTILS_URL = EXAMPLE_URL + "html_curl-utils.html";
 
 const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
 const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
 const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
 const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
+const HTTPS_REDIRECT_SJS = EXAMPLE_URL + "sjs_https-redirect-test-server.sjs";
+const CORS_SJS_PATH = "/browser/browser/devtools/netmonitor/test/sjs_cors-test-server.sjs";
 
 const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
 const TEST_IMAGE_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
 
 gDevTools.testing = true;
 SimpleTest.registerCleanupFunction(() => {
   gDevTools.testing = false;
 });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/sjs_cors-test-server.sjs
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "Och Aye");
+
+  response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+  response.setHeader("Pragma", "no-cache");
+  response.setHeader("Expires", "0");
+
+  response.setHeader("Access-Control-Allow-Origin", "*", false);
+
+  response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+
+  response.write("Access-Control-Allow-Origin: *");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/sjs_https-redirect-test-server.sjs
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+  response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+  response.setHeader("Pragma", "no-cache");
+  response.setHeader("Expires", "0");
+
+  response.setHeader("Access-Control-Allow-Origin", "*", false);
+
+  if (request.scheme === "http") {
+    response.setStatusLine(request.httpVersion, 302, "Found");
+    response.setHeader("Location", "https://" + request.host + request.path);
+  } else {
+    response.setStatusLine(request.httpVersion, 200, "OK");
+    response.write("Page was accessed over HTTPS!");
+  }
+
+}
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -12,40 +12,46 @@ Cu.import("resource:///modules/devtools/
 
 devtools.lazyRequireGetter(this, "Services");
 devtools.lazyRequireGetter(this, "promise");
 devtools.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 devtools.lazyRequireGetter(this, "DevToolsUtils",
   "devtools/toolkit/DevToolsUtils");
 
+devtools.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
+  "devtools/timeline/global", true);
 devtools.lazyRequireGetter(this, "L10N",
   "devtools/profiler/global", true);
 devtools.lazyRequireGetter(this, "PerformanceIO",
   "devtools/performance/io", true);
 devtools.lazyRequireGetter(this, "MarkersOverview",
   "devtools/timeline/markers-overview", true);
 devtools.lazyRequireGetter(this, "MemoryOverview",
   "devtools/timeline/memory-overview", true);
 devtools.lazyRequireGetter(this, "Waterfall",
   "devtools/timeline/waterfall", true);
 devtools.lazyRequireGetter(this, "MarkerDetails",
   "devtools/timeline/marker-details", true);
 devtools.lazyRequireGetter(this, "CallView",
   "devtools/profiler/tree-view", true);
 devtools.lazyRequireGetter(this, "ThreadNode",
   "devtools/profiler/tree-model", true);
-devtools.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
-  "devtools/timeline/global", true);
+
 
 devtools.lazyImporter(this, "CanvasGraphUtils",
   "resource:///modules/devtools/Graphs.jsm");
 devtools.lazyImporter(this, "LineGraphWidget",
   "resource:///modules/devtools/Graphs.jsm");
 
+devtools.lazyImporter(this, "FlameGraphUtils",
+  "resource:///modules/devtools/FlameGraph.jsm");
+devtools.lazyImporter(this, "FlameGraph",
+  "resource:///modules/devtools/FlameGraph.jsm");
+
 // Events emitted by various objects in the panel.
 const EVENTS = {
   // Emitted by the PerformanceView on record button click
   UI_START_RECORDING: "Performance:UI:StartRecording",
   UI_STOP_RECORDING: "Performance:UI:StopRecording",
 
   // Emitted by the PerformanceView on import or export button click
   UI_IMPORT_RECORDING: "Performance:UI:ImportRecording",
@@ -79,17 +85,20 @@ const EVENTS = {
   // Emitted by the CallTreeView when a call tree has been rendered
   CALL_TREE_RENDERED: "Performance:UI:CallTreeRendered",
 
   // When a source is shown in the JavaScript Debugger at a specific location.
   SOURCE_SHOWN_IN_JS_DEBUGGER: "Performance:UI:SourceShownInJsDebugger",
   SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger",
 
   // Emitted by the WaterfallView when it has been rendered
-  WATERFALL_RENDERED: "Performance:UI:WaterfallRendered"
+  WATERFALL_RENDERED: "Performance:UI:WaterfallRendered",
+
+  // Emitted by the FlameGraphView when it has been rendered
+  FLAMEGRAPH_RENDERED: "Performance:UI:FlameGraphRendered"
 };
 
 // Constant defining the end time for a recording that hasn't finished
 // or is not yet available.
 const RECORDING_IN_PROGRESS = -1;
 const RECORDING_UNAVAILABLE = null;
 
 /**
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -15,16 +15,17 @@
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script src="chrome://browser/content/devtools/theme-switching.js"/>
   <script type="application/javascript" src="performance/performance-controller.js"/>
   <script type="application/javascript" src="performance/performance-view.js"/>
   <script type="application/javascript" src="performance/views/overview.js"/>
   <script type="application/javascript" src="performance/views/details.js"/>
   <script type="application/javascript" src="performance/views/details-call-tree.js"/>
   <script type="application/javascript" src="performance/views/details-waterfall.js"/>
+  <script type="application/javascript" src="performance/views/details-flamegraph.js"/>
 
   <vbox class="theme-body" flex="1">
     <toolbar id="performance-toolbar" class="devtools-toolbar">
       <hbox id="performance-toolbar-controls-recordings" class="devtools-toolbarbutton-group">
         <toolbarbutton id="record-button"
                        class="devtools-toolbarbutton"
                        tooltiptext="&profilerUI.recordButton.tooltip;"/>
         <toolbarbutton id="clear-button"
@@ -38,45 +39,46 @@
                        label="&profilerUI.importButton;"/>
         <toolbarbutton id="export-button"
                        class="devtools-toolbarbutton"
                        label="&profilerUI.exportButton;"/>
       </hbox>
     </toolbar>
 
     <vbox id="overview-pane">
-      <hbox id="time-framerate"/>
       <hbox id="markers-overview"/>
       <hbox id="memory-overview"/>
+      <hbox id="time-framerate"/>
     </vbox>
 
     <toolbar id="details-toolbar" class="devtools-toolbar">
       <hbox class="devtools-toolbarbutton-group">
         <toolbarbutton id="select-waterfall-view"
                        class="devtools-toolbarbutton"
-                       tooltiptext="waterfall"
                        data-view="waterfall" />
         <toolbarbutton id="select-calltree-view"
                        class="devtools-toolbarbutton"
-                       tooltiptext="calltree"
                        data-view="calltree" />
+        <toolbarbutton id="select-flamegraph-view"
+                       class="devtools-toolbarbutton"
+                       data-view="flamegraph" />
       </hbox>
     </toolbar>
 
     <deck id="details-pane" flex="1">
-      <hbox id="waterfall-view">
+      <hbox id="waterfall-view" flex="1">
         <vbox id="waterfall-graph" flex="1" />
         <splitter class="devtools-side-splitter"/>
         <vbox id="waterfall-details"
               class="theme-sidebar"
               width="150"
               height="150"/>
       </hbox>
 
-      <vbox id="calltree-view" class="call-tree" flex="1">
+      <vbox id="calltree-view" flex="1">
         <hbox class="call-tree-headers-container">
           <label class="plain call-tree-header"
                  type="duration"
                  crop="end"
                  value="&profilerUI.table.totalDuration;"/>
           <label class="plain call-tree-header"
                  type="percentage"
                  crop="end"
@@ -95,11 +97,14 @@
                  value="&profilerUI.table.samples;"/>
           <label class="plain call-tree-header"
                  type="function"
                  crop="end"
                  value="&profilerUI.table.function;"/>
         </hbox>
         <vbox class="call-tree-cells-container" flex="1"/>
       </vbox>
+
+      <hbox id="flamegraph-view" flex="1">
+      </hbox>
     </deck>
   </vbox>
 </window>
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -8,16 +8,17 @@ support-files =
 # Commented out tests are profiler tests
 # that need to be moved over to performance tool
 
 [browser_perf-aaa-run-first-leaktest.js]
 [browser_perf-data-massaging-01.js]
 [browser_perf-data-samples.js]
 [browser_perf-details-calltree-render-01.js]
 [browser_perf-details-calltree-render-02.js]
+[browser_perf-details-flamegraph-render-01.js]
 [browser_perf-details-waterfall-render-01.js]
 [browser_perf-details.js]
 [browser_perf-front-basic-profiler-01.js]
 [browser_perf-front-basic-timeline-01.js]
 #[browser_perf-front-profiler-01.js] bug 1077464
 [browser_perf-front-profiler-02.js]
 [browser_perf-front-profiler-03.js]
 [browser_perf-front-profiler-04.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-details-flamegraph-render-01.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the flamegraph view renders content after recording.
+ */
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, PerformanceController, FlameGraphView } = panel.panelWin;
+
+  yield startRecording(panel);
+  yield waitUntil(() => PerformanceController.getMarkers().length);
+
+  let rendered = once(FlameGraphView, EVENTS.FLAMEGRAPH_RENDERED);
+  yield stopRecording(panel);
+  yield rendered;
+
+  ok(true, "FlameGraphView rendered after recording is stopped.");
+
+  yield teardown(panel);
+  finish();
+}
--- a/browser/devtools/performance/test/browser_perf-details.js
+++ b/browser/devtools/performance/test/browser_perf-details.js
@@ -20,16 +20,23 @@ function spawnTest () {
 
   // Select waterfall view
   viewChanged = onceSpread(DetailsView, EVENTS.DETAILS_VIEW_SELECTED);
   command($("toolbarbutton[data-view='waterfall']"));
   [_, viewName] = yield viewChanged;
   is(viewName, "waterfall", "DETAILS_VIEW_SELECTED fired with view name");
   checkViews(DetailsView, doc, "waterfall");
 
+  // Select flamegraph view
+  viewChanged = onceSpread(DetailsView, EVENTS.DETAILS_VIEW_SELECTED);
+  command($("toolbarbutton[data-view='flamegraph']"));
+  [_, viewName] = yield viewChanged;
+  is(viewName, "flamegraph", "DETAILS_VIEW_SELECTED fired with view name");
+  checkViews(DetailsView, doc, "flamegraph");
+
   yield teardown(panel);
   finish();
 }
 
 function checkViews (DetailsView, doc, currentView) {
   for (let viewName in DetailsView.viewIndexes) {
     let button = doc.querySelector(`toolbarbutton[data-view="${viewName}"]`);
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/views/details-flamegraph.js
@@ -0,0 +1,69 @@
+/* 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";
+
+/**
+ * FlameGraph view containing a pyramid-like visualization of a profile,
+ * controlled by DetailsView.
+ */
+let FlameGraphView = {
+  /**
+   * Sets up the view with event binding.
+   */
+  initialize: Task.async(function* () {
+    this._onRecordingStopped = this._onRecordingStopped.bind(this);
+    this._onRangeChange = this._onRangeChange.bind(this);
+
+    this.graph = new FlameGraph($("#flamegraph-view"));
+    this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
+    yield this.graph.ready();
+
+    PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
+    OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
+  }),
+
+  /**
+   * Unbinds events.
+   */
+  destroy: function () {
+    PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
+    OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
+  },
+
+  /**
+   * Method for handling all the set up for rendering a new flamegraph.
+   */
+  render: function (profilerData) {
+    // Empty recordings might yield no profiler data.
+    if (profilerData.profile == null) {
+      return;
+    }
+    let samples = profilerData.profile.threads[0].samples;
+    let dataSrc = FlameGraphUtils.createFlameGraphDataFromSamples(samples);
+    this.graph.setData(dataSrc);
+    this.emit(EVENTS.FLAMEGRAPH_RENDERED);
+  },
+
+  /**
+   * Called when recording is stopped.
+   */
+  _onRecordingStopped: function () {
+    let profilerData = PerformanceController.getProfilerData();
+    this.render(profilerData);
+  },
+
+  /**
+   * Fired when a range is selected or cleared in the OverviewView.
+   */
+  _onRangeChange: function (_, params) {
+    // TODO bug 1105014
+  }
+};
+
+/**
+ * Convenient way of emitting events from the view.
+ */
+EventEmitter.decorate(FlameGraphView);
--- a/browser/devtools/performance/views/details.js
+++ b/browser/devtools/performance/views/details.js
@@ -10,64 +10,68 @@ const DEFAULT_DETAILS_SUBVIEW = "waterfa
  * subviews and toggles visibility between them.
  */
 let DetailsView = {
   /**
    * Name to index mapping of subviews, used by selecting view.
    */
   viewIndexes: {
     waterfall: 0,
-    calltree: 1
+    calltree: 1,
+    flamegraph: 2
   },
 
   /**
    * Sets up the view with event binding, initializes subviews.
    */
   initialize: Task.async(function *() {
     this.el = $("#details-pane");
 
     this._onViewToggle = this._onViewToggle.bind(this);
 
     for (let button of $$("toolbarbutton[data-view]", $("#details-toolbar"))) {
       button.addEventListener("command", this._onViewToggle);
     }
 
     yield CallTreeView.initialize();
     yield WaterfallView.initialize();
+    yield FlameGraphView.initialize();
 
     this.selectView(DEFAULT_DETAILS_SUBVIEW);
   }),
 
   /**
    * Unbinds events, destroys subviews.
    */
   destroy: Task.async(function *() {
     for (let button of $$("toolbarbutton[data-view]", $("#details-toolbar"))) {
       button.removeEventListener("command", this._onViewToggle);
     }
 
     yield CallTreeView.destroy();
     yield WaterfallView.destroy();
+    yield FlameGraphView.destroy();
   }),
 
   /**
    * Select one of the DetailView's subviews to be rendered,
    * hiding the others.
    *
    * @params {String} selectedView
    *         Name of the view to be shown.
    */
   selectView: function (selectedView) {
     this.el.selectedIndex = this.viewIndexes[selectedView];
 
     for (let button of $$("toolbarbutton[data-view]", $("#details-toolbar"))) {
-      if (button.getAttribute("data-view") === selectedView)
+      if (button.getAttribute("data-view") === selectedView) {
         button.setAttribute("checked", true);
-      else
+      } else {
         button.removeAttribute("checked");
+      }
     }
 
     this.emit(EVENTS.DETAILS_VIEW_SELECTED, selectedView);
   },
 
   /**
    * Called when a view button is clicked.
    */
--- a/browser/devtools/performance/views/overview.js
+++ b/browser/devtools/performance/views/overview.js
@@ -7,19 +7,19 @@
 // backend. Make sure this isn't lower than DEFAULT_TIMELINE_DATA_PULL_TIMEOUT
 // in toolkit/devtools/server/actors/timeline.js
 const OVERVIEW_UPDATE_INTERVAL = 200; // ms
 
 const FRAMERATE_GRAPH_LOW_RES_INTERVAL = 100; // ms
 const FRAMERATE_GRAPH_HIGH_RES_INTERVAL = 16; // ms
 
 const FRAMERATE_GRAPH_HEIGHT = 45; // px
-const MARKERS_GRAPH_HEADER_HEIGHT = 12; // px
-const MARKERS_GRAPH_BODY_HEIGHT = 45; // 9px * 5 groups
-const MARKERS_GROUP_VERTICAL_PADDING = 3.5; // px
+const MARKERS_GRAPH_HEADER_HEIGHT = 14; // px
+const MARKERS_GRAPH_ROW_HEIGHT = 11; // px
+const MARKERS_GROUP_VERTICAL_PADDING = 5; // px
 const MEMORY_GRAPH_HEIGHT = 30; // px
 
 const GRAPH_SCROLL_EVENTS_DRAIN = 50; // ms
 
 /**
  * View handler for the overview panel's time view, displaying
  * framerate, markers and memory over time.
  */
@@ -77,17 +77,17 @@ let OverviewView = {
   }),
 
   /**
    * Sets up the markers overivew graph.
    */
   _showMarkersGraph: Task.async(function *() {
     this.markersOverview = new MarkersOverview($("#markers-overview"), TIMELINE_BLUEPRINT);
     this.markersOverview.headerHeight = MARKERS_GRAPH_HEADER_HEIGHT;
-    this.markersOverview.bodyHeight = MARKERS_GRAPH_BODY_HEIGHT;
+    this.markersOverview.rowHeight = MARKERS_GRAPH_ROW_HEIGHT;
     this.markersOverview.groupPadding = MARKERS_GROUP_VERTICAL_PADDING;
     yield this.markersOverview.ready();
 
     CanvasGraphUtils.linkAnimation(this.framerateGraph, this.markersOverview);
     CanvasGraphUtils.linkSelection(this.framerateGraph, this.markersOverview);
   }),
 
   /**
--- a/browser/devtools/shared/moz.build
+++ b/browser/devtools/shared/moz.build
@@ -17,16 +17,17 @@ EXTRA_JS_MODULES.devtools += [
     'Parser.jsm',
     'SplitView.jsm',
 ]
 
 EXTRA_JS_MODULES.devtools += [
     'widgets/AbstractTreeItem.jsm',
     'widgets/BreadcrumbsWidget.jsm',
     'widgets/Chart.jsm',
+    'widgets/FlameGraph.jsm',
     'widgets/Graphs.jsm',
     'widgets/GraphsWorker.js',
     'widgets/SideMenuWidget.jsm',
     'widgets/SimpleListWidget.jsm',
     'widgets/VariablesView.jsm',
     'widgets/VariablesViewController.jsm',
     'widgets/ViewHelpers.jsm',
 ]
--- a/browser/devtools/shared/telemetry.js
+++ b/browser/devtools/shared/telemetry.js
@@ -100,16 +100,21 @@ Telemetry.prototype = {
       userHistogram: "DEVTOOLS_LAYOUTVIEW_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_LAYOUTVIEW_TIME_ACTIVE_SECONDS"
     },
     fontinspector: {
       histogram: "DEVTOOLS_FONTINSPECTOR_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_FONTINSPECTOR_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS"
     },
+    animationinspector: {
+      histogram: "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_BOOLEAN",
+      userHistogram: "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_PER_USER_FLAG",
+      timerHistogram: "DEVTOOLS_ANIMATIONINSPECTOR_TIME_ACTIVE_SECONDS"
+    },
     jsdebugger: {
       histogram: "DEVTOOLS_JSDEBUGGER_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_JSDEBUGGER_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_JSDEBUGGER_TIME_ACTIVE_SECONDS"
     },
     jsbrowserdebugger: {
       histogram: "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_PER_USER_FLAG",
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -10,16 +10,22 @@ support-files =
   browser_toolbar_webconsole_errors_count.html
   head.js
   leakhunt.js
 
 [browser_css_color.js]
 [browser_cubic-bezier-01.js]
 [browser_cubic-bezier-02.js]
 [browser_cubic-bezier-03.js]
+[browser_flame-graph-01.js]
+[browser_flame-graph-02.js]
+[browser_flame-graph-03a.js]
+[browser_flame-graph-03b.js]
+[browser_flame-graph-04.js]
+[browser_flame-graph-utils.js]
 [browser_graphs-01.js]
 [browser_graphs-02.js]
 [browser_graphs-03.js]
 [browser_graphs-04.js]
 [browser_graphs-05.js]
 [browser_graphs-06.js]
 [browser_graphs-07a.js]
 [browser_graphs-07b.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_flame-graph-01.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that flame graph widget works properly.
+
+let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
+
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* performTest() {
+  let [host, win, doc] = yield createHost();
+  doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+  let graph = new FlameGraph(doc.body);
+
+  let readyEventEmitted;
+  graph.once("ready", () => readyEventEmitted = true);
+
+  yield graph.ready();
+  ok(readyEventEmitted, "The 'ready' event should have been emitted");
+
+  testGraph(host, graph);
+
+  graph.destroy();
+  host.destroy();
+}
+
+function testGraph(host, graph) {
+  ok(graph._container.classList.contains("flame-graph-widget-container"),
+    "The correct graph container was created.");
+  ok(graph._canvas.classList.contains("flame-graph-widget-canvas"),
+    "The correct graph container was created.");
+
+  let bounds = host.frame.getBoundingClientRect();
+
+  is(graph.width, bounds.width * window.devicePixelRatio,
+    "The graph has the correct width.");
+  is(graph.height, bounds.height * window.devicePixelRatio,
+    "The graph has the correct height.");
+
+  ok(graph._selection.start === null,
+    "The graph's selection start value is initially null.");
+  ok(graph._selection.end === null,
+    "The graph's selection end value is initially null.");
+
+  ok(graph._selectionDragger.origin === null,
+    "The graph's dragger origin value is initially null.");
+  ok(graph._selectionDragger.anchor.start === null,
+    "The graph's dragger anchor start value is initially null.");
+  ok(graph._selectionDragger.anchor.end === null,
+    "The graph's dragger anchor end value is initially null.");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_flame-graph-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that flame graph widgets may have a fixed width or height.
+
+let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
+
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* performTest() {
+  let [host, win, doc] = yield createHost();
+  doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+  let graph = new FlameGraph(doc.body);
+  graph.fixedWidth = 200;
+  graph.fixedHeight = 100;
+
+  yield graph.ready();
+  testGraph(host, graph);
+
+  graph.destroy();
+  host.destroy();
+}
+
+function testGraph(host, graph) {
+  let bounds = host.frame.getBoundingClientRect();
+
+  isnot(graph.width, bounds.width * window.devicePixelRatio,
+    "The graph should not span all the parent node's width.");
+  isnot(graph.height, bounds.height * window.devicePixelRatio,
+    "The graph should not span all the parent node's height.");
+
+  is(graph.width, graph.fixedWidth * window.devicePixelRatio,
+    "The graph has the correct width.");
+  is(graph.height, graph.fixedHeight * window.devicePixelRatio,
+    "The graph has the correct height.");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_flame-graph-03a.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that selections in the flame graph widget work properly.
+
+let TEST_DATA = [{ color: "#f00", blocks: [{ x: 0, y: 0, width: 50, height: 20, text: "FOO" }, { x: 50, y: 0, width: 100, height: 20, text: "BAR" }] }, { color: "#00f", blocks: [{ x: 0, y: 30, width: 30, height: 20, text: "BAZ" }] }];
+let TEST_WIDTH = 200;
+let TEST_HEIGHT = 100;
+
+let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
+
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* performTest() {
+  let [host, win, doc] = yield createHost();
+  doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+  let graph = new FlameGraph(doc.body, 1);
+  graph.fixedWidth = TEST_WIDTH;
+  graph.fixedHeight = TEST_HEIGHT;
+
+  yield graph.ready();
+
+  testGraph(graph);
+
+  graph.destroy();
+  host.destroy();
+}
+
+function testGraph(graph) {
+  graph.setData(TEST_DATA);
+
+  is(graph.getDataWindowStart(), 0,
+    "The selection start boundary is correct (1).");
+  is(graph.getDataWindowEnd(), TEST_WIDTH,
+    "The selection end boundary is correct (1).");
+
+  scroll(graph, 200, HORIZONTAL_AXIS, 10);
+  is(graph.getDataWindowStart() | 0, 100,
+    "The selection start boundary is correct (2).");
+  is(graph.getDataWindowEnd() | 0, 200,
+    "The selection end boundary is correct (2).");
+
+  scroll(graph, -200, HORIZONTAL_AXIS, 10);
+  is(graph.getDataWindowStart() | 0, 50,
+    "The selection start boundary is correct (3).");
+  is(graph.getDataWindowEnd() | 0, 150,
+    "The selection end boundary is correct (3).");
+
+  scroll(graph, 200, VERTICAL_AXIS, TEST_WIDTH / 2);
+  is(graph.getDataWindowStart() | 0, 46,
+    "The selection start boundary is correct (4).");
+  is(graph.getDataWindowEnd() | 0, 153,
+    "The selection end boundary is correct (4).");
+
+  scroll(graph, -200, VERTICAL_AXIS, TEST_WIDTH / 2);
+  is(graph.getDataWindowStart() | 0, 50,
+    "The selection start boundary is correct (5).");
+  is(graph.getDataWindowEnd() | 0, 149,
+    "The selection end boundary is correct (5).");
+
+  dragStart(graph, TEST_WIDTH / 2);
+  is(graph.getDataWindowStart() | 0, 50,
+    "The selection start boundary is correct (6).");
+  is(graph.getDataWindowEnd() | 0, 149,
+    "The selection end boundary is correct (6).");
+
+  hover(graph, TEST_WIDTH / 2 - 10);
+  is(graph.getDataWindowStart() | 0, 55,
+    "The selection start boundary is correct (7).");
+  is(graph.getDataWindowEnd() | 0, 154,
+    "The selection end boundary is correct (7).");
+
+  dragStop(graph, 10);
+  is(graph.getDataWindowStart() | 0, 95,
+    "The selection start boundary is correct (8).");
+  is(graph.getDataWindowEnd() | 0, 194,
+    "The selection end boundary is correct (8).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+  x /= window.devicePixelRatio;
+  y /= window.devicePixelRatio;
+  graph._onMouseMove({ clientX: x, clientY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+  x /= window.devicePixelRatio;
+  y /= window.devicePixelRatio;
+  graph._onMouseMove({ clientX: x, clientY: y });
+  graph._onMouseDown({ clientX: x, clientY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+  x /= window.devicePixelRatio;
+  y /= window.devicePixelRatio;
+  graph._onMouseMove({ clientX: x, clientY: y });
+  graph._onMouseUp({ clientX: x, clientY: y });
+}
+
+let HORIZONTAL_AXIS = 1;
+let VERTICAL_AXIS = 2;
+
+function scroll(graph, wheel, axis, x, y = 1) {
+  x /= window.devicePixelRatio;
+  y /= window.devicePixelRatio;
+  graph._onMouseMove({ clientX: x, clientY: y });
+  graph._onMouseWheel({ clientX: x, clientY: y, axis, detail: wheel, axis,
+    HORIZONTAL_AXIS,
+    VERTICAL_AXIS
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_flame-graph-03b.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that selections in the flame graph widget work properly on HiDPI.
+
+let TEST_DATA = [{ color: "#f00", blocks: [{ x: 0, y: 0, width: 50, height: 20, text: "FOO" }, { x: 50, y: 0, width: 100, height: 20, text: "BAR" }] }, { color: "#00f", blocks: [{ x: 0, y: 30, width: 30, height: 20, text: "BAZ" }] }];
+let TEST_WIDTH = 200;
+let TEST_HEIGHT = 100;
+let TEST_DPI_DENSITIY = 2;
+
+let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
+
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* performTest() {
+  let [host, win, doc] = yield createHost();
+  doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+  let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
+  graph.fixedWidth = TEST_WIDTH;
+  graph.fixedHeight = TEST_HEIGHT;
+
+  yield graph.ready();
+
+  testGraph(graph);
+
+  graph.destroy();
+  host.destroy();
+}
+
+function testGraph(graph) {
+  graph.setData(TEST_DATA);
+
+  is(graph.getDataWindowStart(), 0,
+    "The selection start boundary is correct on HiDPI (1).");
+  is(graph.getDataWindowEnd(), TEST_WIDTH * TEST_DPI_DENSITIY,
+    "The selection end boundary is correct on HiDPI (1).");
+
+  scroll(graph, 10000, HORIZONTAL_AXIS, 1);
+
+  is(graph.getDataWindowStart(), 380,
+    "The selection start boundary is correct on HiDPI (2).");
+  is(graph.getDataWindowEnd(), TEST_WIDTH * TEST_DPI_DENSITIY,
+    "The selection end boundary is correct on HiDPI (2).");
+}
+
+// EventUtils just doesn't work!
+
+let HORIZONTAL_AXIS = 1;
+let VERTICAL_AXIS = 2;
+
+function scroll(graph, wheel, axis, x, y = 1) {
+  x /= window.devicePixelRatio;
+  y /= window.devicePixelRatio;
+  graph._onMouseMove({ clientX: x, clientY: y });
+  graph._onMouseWheel({ clientX: x, clientY: y, axis, detail: wheel, axis,
+    HORIZONTAL_AXIS,
+    VERTICAL_AXIS
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_flame-graph-04.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that text metrics in the flame graph widget work properly.
+
+let HTML_NS = "http://www.w3.org/1999/xhtml";
+let FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 9; // px
+let FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = "sans-serif";
+let {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
+let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
+
+let L10N = new ViewHelpers.L10N();
+
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* performTest() {
+  let [host, win, doc] = yield createHost();
+  let graph = new FlameGraph(doc.body, 1);
+  yield graph.ready();
+
+  testGraph(graph);
+
+  graph.destroy();
+  host.destroy();
+}
+
+function testGraph(graph) {
+  is(graph._averageCharWidth, getAverageCharWidth(),
+    "The average char width was calculated correctly.");
+  is(graph._overflowCharWidth, getCharWidth(L10N.ellipsis),
+    "The ellipsis char width was calculated correctly.");
+
+  is(graph._getTextWidthApprox("This text is maybe overflowing"),
+    getAverageCharWidth() * 30,
+    "The approximate width was calculated correctly.");
+
+  is(graph._getFittedText("This text is maybe overflowing", 1000),
+    "This text is maybe overflowing",
+    "The fitted text for 1000px width is correct.");
+
+  isnot(graph._getFittedText("This text is maybe overflowing", 100),
+    "This text is maybe overflowing",
+    "The fitted text for 100px width is correct (1).");
+
+  ok(graph._getFittedText("This text is maybe overflowing", 100)
+    .contains(L10N.ellipsis),
+    "The fitted text for 100px width is correct (2).");
+
+  is(graph._getFittedText("This text is maybe overflowing", 10),
+    L10N.ellipsis,
+    "The fitted text for 10px width is correct.");
+
+  is(graph._getFittedText("This text is maybe overflowing", 1),
+    "",
+    "The fitted text for 1px width is correct.");
+}
+
+function getAverageCharWidth() {
+  let letterWidthsSum = 0;
+  let start = 32; // space
+  let end = 123; // "z"
+
+  for (let i = start; i < end; i++) {
+    let char = String.fromCharCode(i);
+    letterWidthsSum += getCharWidth(char);
+  }
+
+  return letterWidthsSum / (end - start);
+}
+
+function getCharWidth(char) {
+  let canvas = document.createElementNS(HTML_NS, "canvas");
+  let ctx = canvas.getContext("2d");
+
+  let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE;
+  let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
+  ctx.font = fontSize + "px " + fontFamily;
+
+  return ctx.measureText(char).width;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_flame-graph-utils.js
@@ -0,0 +1,261 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that text metrics in the flame graph widget work properly.
+
+let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* performTest() {
+  let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA);
+
+  ok(out, "Some data was outputted properly");
+  is(out.length, 10, "The outputted length is correct.");
+
+  info("Got flame graph data:\n" + out.toSource() + "\n");
+
+  for (let i = 0; i < out.length; i++) {
+    let found = out[i];
+    let expected = EXPECTED_OUTPUT[i];
+
+    is(found.blocks.length, expected.blocks.length,
+      "The correct number of blocks were found in this bucket.");
+
+    for (let j = 0; j < found.blocks.length; j++) {
+      is(found.blocks[j].x, expected.blocks[j].x,
+        "The expected block X position is correct for this frame.");
+      is(found.blocks[j].y, expected.blocks[j].y,
+        "The expected block Y position is correct for this frame.");
+      is(found.blocks[j].width, expected.blocks[j].width,
+        "The expected block width is correct for this frame.");
+      is(found.blocks[j].height, expected.blocks[j].height,
+        "The expected block height is correct for this frame.");
+      is(found.blocks[j].text, expected.blocks[j].text,
+        "The expected block text is correct for this frame.");
+    }
+  }
+}
+
+let TEST_DATA = [{
+  frames: [{
+    location: "M"
+  }, {
+    location: "N",
+  }, {
+    location: "P"
+  }],
+  time: 50,
+}, {
+  frames: [{
+    location: "A"
+  }, {
+    location: "B",
+  }, {
+    location: "C"
+  }],
+  time: 100,
+}, {
+  frames: [{
+    location: "A"
+  }, {
+    location: "B",
+  }, {
+    location: "D"
+  }],
+  time: 210,
+}, {
+  frames: [{
+    location: "A"
+  }, {
+    location: "E",
+  }, {
+    location: "F"
+  }],
+  time: 330,
+}, {
+  frames: [{
+    location: "A"
+  }, {
+    location: "B",
+  }, {
+    location: "C"
+  }],
+  time: 460,
+}, {
+  frames: [{
+    location: "X"
+  }, {
+    location: "Y",
+  }, {
+    location: "Z"
+  }],
+  time: 500
+}];
+
+let EXPECTED_OUTPUT = [{
+  blocks: []
+}, {
+  blocks: []
+}, {
+  blocks: [{
+    srcData: {
+      startTime: 50,
+      rawLocation: "A"
+    },
+    x: 50,
+    y: 0,
+    width: 410,
+    height: 12,
+    text: "A"
+  }]
+}, {
+  blocks: [{
+    srcData: {
+      startTime: 50,
+      rawLocation: "B"
+    },
+    x: 50,
+    y: 12,
+    width: 160,
+    height: 12,
+    text: "B"
+  }, {
+    srcData: {
+      startTime: 330,
+      rawLocation: "B"
+    },
+    x: 330,
+    y: 12,
+    width: 130,
+    height: 12,
+    text: "B"
+  }]
+}, {
+  blocks: [{
+    srcData: {
+      startTime: 0,
+      rawLocation: "M"
+    },
+    x: 0,
+    y: 0,
+    width: 50,
+    height: 12,
+    text: "M"
+  }, {
+    srcData: {
+      startTime: 50,
+      rawLocation: "C"
+    },
+    x: 50,
+    y: 24,
+    width: 50,
+    height: 12,
+    text: "C"
+  }, {
+    srcData: {
+      startTime: 330,
+      rawLocation: "C"
+    },
+    x: 330,
+    y: 24,
+    width: 130,
+    height: 12,
+    text: "C"
+  }]
+}, {
+  blocks: [{
+    srcData: {
+      startTime: 0,
+      rawLocation: "N"
+    },
+    x: 0,
+    y: 12,
+    width: 50,
+    height: 12,
+    text: "N"
+  }, {
+    srcData: {
+      startTime: 100,
+      rawLocation: "D"
+    },
+    x: 100,
+    y: 24,
+    width: 110,
+    height: 12,
+    text: "D"
+  }, {
+    srcData: {
+      startTime: 460,
+      rawLocation: "X"
+    },
+    x: 460,
+    y: 0,
+    width: 40,
+    height: 12,
+    text: "X"
+  }]
+}, {
+  blocks: [{
+    srcData: {
+      startTime: 210,
+      rawLocation: "E"
+    },
+    x: 210,
+    y: 12,
+    width: 120,
+    height: 12,
+    text: "E"
+  }, {
+    srcData: {
+      startTime: 460,
+      rawLocation: "Y"
+    },
+    x: 460,
+    y: 12,
+    width: 40,
+    height: 12,
+    text: "Y"
+  }]
+}, {
+  blocks: [{
+    srcData: {
+      startTime: 0,
+      rawLocation: "P"
+    },
+    x: 0,
+    y: 24,
+    width: 50,
+    height: 12,
+    text: "P"
+  }, {
+    srcData: {
+      startTime: 210,
+      rawLocation: "F"
+    },
+    x: 210,
+    y: 24,
+    width: 120,
+    height: 12,
+    text: "F"
+  }, {
+    srcData: {
+      startTime: 460,
+      rawLocation: "Z"
+    },
+    x: 460,
+    y: 24,
+    width: 40,
+    height: 12,
+    text: "Z"
+  }]
+}, {
+  blocks: []
+}, {
+  blocks: []
+}];
--- a/browser/devtools/shared/test/browser_graphs-09b.js
+++ b/browser/devtools/shared/test/browser_graphs-09b.js
@@ -26,18 +26,18 @@ function* performTest() {
 
   graph.destroy();
   host.destroy();
 }
 
 function* testGraph(graph) {
   yield graph.setDataWhenReady(TEST_DATA);
 
-  is(graph._gutter.hidden, true,
-    "The gutter should be hidden because the tooltips don't have arrows.");
+  is(graph._gutter.hidden, false,
+    "The gutter should be visible even if the tooltips don't have arrows.");
   is(graph._maxTooltip.hidden, false,
     "The max tooltip should not be hidden.");
   is(graph._avgTooltip.hidden, false,
     "The avg tooltip should not be hidden.");
   is(graph._minTooltip.hidden, false,
     "The min tooltip should not be hidden.");
 
   is(graph._maxTooltip.getAttribute("with-arrows"), "false",
--- a/browser/devtools/shared/test/browser_telemetry_sidebar.js
+++ b/browser/devtools/shared/test/browser_telemetry_sidebar.js
@@ -35,17 +35,18 @@ function init() {
 
 function testSidebar() {
   info("Testing sidebar");
 
   let target = TargetFactory.forTab(gBrowser.selectedTab);
 
   gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
     let inspector = toolbox.getCurrentPanel();
-    let sidebarTools = ["ruleview", "computedview", "fontinspector", "layoutview"];
+    let sidebarTools = ["ruleview", "computedview", "fontinspector",
+                        "layoutview", "animationinspector"];
 
     // Concatenate the array with itself so that we can open each tool twice.
     sidebarTools.push.apply(sidebarTools, sidebarTools);
 
     // See TOOL_DELAY for why we need setTimeout here
     setTimeout(function selectSidebarTab() {
       let tool = sidebarTools.pop();
       if (tool) {
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/FlameGraph.jsm
@@ -0,0 +1,916 @@
+/* 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 Cu = Components.utils;
+
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/Graphs.jsm");
+const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
+const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
+
+this.EXPORTED_SYMBOLS = [
+  "FlameGraph",
+  "FlameGraphUtils"
+];
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
+const L10N = new ViewHelpers.L10N();
+
+const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
+
+const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00035;
+const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.5;
+const GRAPH_MIN_SELECTION_WIDTH = 10; // ms
+
+const TIMELINE_TICKS_MULTIPLE = 5; // ms
+const TIMELINE_TICKS_SPACING_MIN = 75; // px
+
+const OVERVIEW_HEADER_HEIGHT = 18; // px
+const OVERVIEW_HEADER_SAFE_BOUNDS = 50; // px
+const OVERVIEW_HEADER_TEXT_COLOR = "#18191a";
+const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
+const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
+const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6; // px
+const OVERVIEW_HEADER_TEXT_PADDING_TOP = 5; // px
+const OVERVIEW_TIMELINE_STROKES = "#ddd";
+
+const FLAME_GRAPH_BLOCK_BORDER = 1; // px
+const FLAME_GRAPH_BLOCK_TEXT_COLOR = "#000";
+const FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 9; // px
+const FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = "sans-serif";
+const FLAME_GRAPH_BLOCK_TEXT_PADDING_TOP = 1; // px
+const FLAME_GRAPH_BLOCK_TEXT_PADDING_LEFT = 3; // px
+const FLAME_GRAPH_BLOCK_TEXT_PADDING_RIGHT = 3; // px
+
+/**
+ * A flamegraph visualization. This implementation is responsable only with
+ * drawing the graph, using a data source consisting of rectangles and
+ * their corresponding widths.
+ *
+ * Example usage:
+ *   let graph = new FlameGraph(node);
+ *   let src = FlameGraphUtils.createFlameGraphDataFromSamples(samples);
+ *   graph.once("ready", () => {
+ *     graph.setData(src);
+ *   });
+ *
+ * Data source format:
+ *   [
+ *     {
+ *       color: "string",
+ *       blocks: [
+ *         {
+ *           x: number,
+ *           y: number,
+ *           width: number,
+ *           height: number,
+ *           text: "string"
+ *         },
+ *         ...
+ *       ]
+ *     },
+ *     {
+ *       color: "string",
+ *       blocks: [...]
+ *     },
+ *     ...
+ *     {
+ *       color: "string",
+ *       blocks: [...]
+ *     }
+ *   ]
+ *
+ * Use `FlameGraphUtils` to convert profiler data (or any other data source)
+ * into a drawable format.
+ *
+ * @param nsIDOMNode parent
+ *        The parent node holding the graph.
+ * @param number sharpness [optional]
+ *        Defaults to the current device pixel ratio.
+ */
+function FlameGraph(parent, sharpness) {
+  EventEmitter.decorate(this);
+
+  this._parent = parent;
+  this._ready = promise.defer();
+
+  AbstractCanvasGraph.createIframe(GRAPH_SRC, parent, iframe => {
+    this._iframe = iframe;
+    this._window = iframe.contentWindow;
+    this._document = iframe.contentDocument;
+    this._pixelRatio = sharpness || this._window.devicePixelRatio;
+
+    let container = this._container = this._document.getElementById("graph-container");
+    container.className = "flame-graph-widget-container graph-widget-container";
+
+    let canvas = this._canvas = this._document.getElementById("graph-canvas");
+    canvas.className = "flame-graph-widget-canvas graph-widget-canvas";
+
+    let bounds = parent.getBoundingClientRect();
+    bounds.width = this.fixedWidth || bounds.width;
+    bounds.height = this.fixedHeight || bounds.height;
+    iframe.setAttribute("width", bounds.width);
+    iframe.setAttribute("height", bounds.height);
+
+    this._width = canvas.width = bounds.width * this._pixelRatio;
+    this._height = canvas.height = bounds.height * this._pixelRatio;
+    this._ctx = canvas.getContext("2d");
+
+    this._selection = new GraphSelection();
+    this._selectionDragger = new GraphSelectionDragger();
+
+    // Calculating text widths is necessary to trim the text inside the blocks
+    // while the scaling changes (e.g. via scrolling). This is very expensive,
+    // so maintain a cache of string contents to text widths.
+    this._textWidthsCache = {};
+
+    let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE * this._pixelRatio;
+    let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
+    this._ctx.font = fontSize + "px " + fontFamily;
+    this._averageCharWidth = this._calcAverageCharWidth();
+    this._overflowCharWidth = this._getTextWidth(this.overflowChar);
+
+    this._onAnimationFrame = this._onAnimationFrame.bind(this);
+    this._onMouseMove = this._onMouseMove.bind(this);
+    this._onMouseDown = this._onMouseDown.bind(this);
+    this._onMouseUp = this._onMouseUp.bind(this);
+    this._onMouseWheel = this._onMouseWheel.bind(this);
+    this._onResize = this._onResize.bind(this);
+    this.refresh = this.refresh.bind(this);
+
+    container.addEventListener("mousemove", this._onMouseMove);
+    container.addEventListener("mousedown", this._onMouseDown);
+    container.addEventListener("mouseup", this._onMouseUp);
+    container.addEventListener("MozMousePixelScroll", this._onMouseWheel);
+
+    let ownerWindow = this._parent.ownerDocument.defaultView;
+    ownerWindow.addEventListener("resize", this._onResize);
+
+    this._animationId = this._window.requestAnimationFrame(this._onAnimationFrame);
+
+    this._ready.resolve(this);
+    this.emit("ready", this);
+  });
+}
+
+FlameGraph.prototype = {
+  /**
+   * Read-only width and height of the canvas.
+   * @return number
+   */
+  get width() {
+    return this._width;
+  },
+  get height() {
+    return this._height;
+  },
+
+  /**
+   * Returns a promise resolved once this graph is ready to receive data.
+   */
+  ready: function() {
+    return this._ready.promise;
+  },
+
+  /**
+   * Destroys this graph.
+   */
+  destroy: function() {
+    let container = this._container;
+    container.removeEventListener("mousemove", this._onMouseMove);
+    container.removeEventListener("mousedown", this._onMouseDown);
+    container.removeEventListener("mouseup", this._onMouseUp);
+    container.removeEventListener("MozMousePixelScroll", this._onMouseWheel);
+
+    let ownerWindow = this._parent.ownerDocument.defaultView;
+    ownerWindow.removeEventListener("resize", this._onResize);
+
+    this._window.cancelAnimationFrame(this._animationId);
+    this._iframe.remove();
+
+    this._selection = null;
+    this._selectionDragger = null;
+
+    this._data = null;
+
+    this.emit("destroyed");
+  },
+
+  /**
+   * Rendering options. Subclasses should override these.
+   */
+  overviewHeaderTextColor: OVERVIEW_HEADER_TEXT_COLOR,
+  overviewTimelineStrokes: OVERVIEW_TIMELINE_STROKES,
+  blockTextColor: FLAME_GRAPH_BLOCK_TEXT_COLOR,
+
+  /**
+   * Makes sure the canvas graph is of the specified width or height, and
+   * doesn't flex to fit all the available space.
+   */
+  fixedWidth: null,
+  fixedHeight: null,
+
+  /**
+   * The units used in the overhead ticks. Could be "ms", for example.
+   * Overwrite this with your own localized format.
+   */
+  timelineTickUnits: "",
+
+  /**
+   * Character used when a block's text is overflowing.
+   * Defaults to an ellipsis.
+   */
+  overflowChar: L10N.ellipsis,
+
+  /**
+   * Sets the data source for this graph.
+   *
+   * @param object data
+   *        The data source. See the constructor for more information.
+   */
+  setData: function(data) {
+    this._data = data;
+    this._selection = { start: 0, end: this._width };
+    this._shouldRedraw = true;
+  },
+
+  /**
+   * Same as `setData`, but waits for this graph to finish initializing first.
+   *
+   * @param object data
+   *        The data source. See the constructor for more information.
+   * @return promise
+   *         A promise resolved once the data is set.
+   */
+  setDataWhenReady: Task.async(function*(data) {
+    yield this.ready();
+    this.setData(data);
+  }),
+
+  /**
+   * Gets whether or not this graph has a data source.
+   * @return boolean
+   */
+  hasData: function() {
+    return !!this._data;
+  },
+
+  /**
+   * Gets the start or end of this graph's selection, i.e. the 'data window'.
+   * @return number
+   */
+  getDataWindowStart: function() {
+    return this._selection.start;
+  },
+  getDataWindowEnd: function() {
+    return this._selection.end;
+  },
+
+  /**
+   * Updates this graph to reflect the new dimensions of the parent node.
+   */
+  refresh: function() {
+    let bounds = this._parent.getBoundingClientRect();
+    let newWidth = this.fixedWidth || bounds.width;
+    let newHeight = this.fixedHeight || bounds.height;
+
+    // Prevent redrawing everything if the graph's width & height won't change.
+    if (this._width == newWidth * this._pixelRatio &&
+        this._height == newHeight * this._pixelRatio) {
+      this.emit("refresh-cancelled");
+      return;
+    }
+
+    bounds.width = newWidth;
+    bounds.height = newHeight;
+    this._iframe.setAttribute("width", bounds.width);
+    this._iframe.setAttribute("height", bounds.height);
+    this._width = this._canvas.width = bounds.width * this._pixelRatio;
+    this._height = this._canvas.height = bounds.height * this._pixelRatio;
+
+    this._shouldRedraw = true;
+    this.emit("refresh");
+  },
+
+  /**
+   * The contents of this graph are redrawn only when something changed,
+   * like the data source, or the selection bounds etc. This flag tracks
+   * if the rendering is "dirty" and needs to be refreshed.
+   */
+  _shouldRedraw: false,
+
+  /**
+   * Animation frame callback, invoked on each tick of the refresh driver.
+   */
+  _onAnimationFrame: function() {
+    this._animationId = this._window.requestAnimationFrame(this._onAnimationFrame);
+    this._drawWidget();
+  },
+
+  /**
+   * Redraws the widget when necessary. The actual graph is not refreshed
+   * every time this function is called, only the cliphead, selection etc.
+   */
+  _drawWidget: function() {
+    if (!this._shouldRedraw) {
+      return;
+    }
+    let ctx = this._ctx;
+    let canvasWidth = this._width;
+    let canvasHeight = this._height;
+    ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+
+    let selection = this._selection;
+    let selectionWidth = selection.end - selection.start;
+    let selectionScale = canvasWidth / selectionWidth;
+    this._drawTicks(selection.start, selectionScale);
+    this._drawPyramid(this._data, selection.start, selectionScale);
+
+    this._shouldRedraw = false;
+  },
+
+  /**
+   * Draws the overhead ticks in this graph.
+   *
+   * @param number dataOffset, dataScale
+   *        Offsets and scales the data source by the specified amount.
+   *        This is used for scrolling the visualization.
+   */
+  _drawTicks: function(dataOffset, dataScale) {
+    let ctx = this._ctx;
+    let canvasWidth = this._width;
+    let canvasHeight = this._height;
+    let scaledOffset = dataOffset * dataScale;
+
+    let safeBounds = OVERVIEW_HEADER_SAFE_BOUNDS * this._pixelRatio;
+    let availableWidth = canvasWidth - safeBounds;
+
+    let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
+    let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
+    let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
+    let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
+    let tickInterval = this._findOptimalTickInterval(dataScale);
+
+    ctx.textBaseline = "top";
+    ctx.font = fontSize + "px " + fontFamily;
+    ctx.fillStyle = this.overviewHeaderTextColor;
+    ctx.strokeStyle = this.overviewTimelineStrokes;
+    ctx.beginPath();
+
+    for (let x = 0; x < availableWidth + scaledOffset; x += tickInterval) {
+      let lineLeft = x - scaledOffset;
+      let textLeft = lineLeft + textPaddingLeft;
+      let time = Math.round(x / dataScale / this._pixelRatio);
+      let label = time + " " + this.timelineTickUnits;
+      ctx.fillText(label, textLeft, textPaddingTop);
+      ctx.moveTo(lineLeft, 0);
+      ctx.lineTo(lineLeft, canvasHeight);
+    }
+
+    ctx.stroke();
+  },
+
+  /**
+   * Draws the blocks and text in this graph.
+   *
+   * @param object dataSource
+   *        The data source. See the constructor for more information.
+   * @param number dataOffset, dataScale
+   *        Offsets and scales the data source by the specified amount.
+   *        This is used for scrolling the visualization.
+   */
+  _drawPyramid: function(dataSource, dataOffset, dataScale) {
+    let ctx = this._ctx;
+
+    let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE * this._pixelRatio;
+    let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
+    let visibleBlocks = this._drawPyramidFill(dataSource, dataOffset, dataScale);
+
+    ctx.textBaseline = "middle";
+    ctx.font = fontSize + "px " + fontFamily;
+    ctx.fillStyle = this.blockTextColor;
+
+    this._drawPyramidText(visibleBlocks, dataOffset, dataScale);
+  },
+
+  /**
+   * Fills all block inside this graph's pyramid.
+   * @see FlameGraph.prototype._drawPyramid
+   */
+  _drawPyramidFill: function(dataSource, dataOffset, dataScale) {
+    let visibleBlocksStore = [];
+    let minVisibleBlockWidth = this._overflowCharWidth;
+
+    for (let { color, blocks } of dataSource) {
+      this._drawBlocksFill(
+        color, blocks, dataOffset, dataScale,
+        visibleBlocksStore, minVisibleBlockWidth);
+    }
+
+    return visibleBlocksStore;
+  },
+
+  /**
+   * Adds the text for all block inside this graph's pyramid.
+   * @see FlameGraph.prototype._drawPyramid
+   */
+  _drawPyramidText: function(blocks, dataOffset, dataScale) {
+    for (let block of blocks) {
+      this._drawBlockText(block, dataOffset, dataScale);
+    }
+  },
+
+  /**
+   * Fills a group of blocks sharing the same style.
+   *
+   * @param string color
+   *        The color used as the block's background.
+   * @param array blocks
+   *        A list of { x, y, width, height } objects visually representing
+   *        all the blocks sharing this particular style.
+   * @param number dataOffset, dataScale
+   *        Offsets and scales the data source by the specified amount.
+   *        This is used for scrolling the visualization.
+   * @param array visibleBlocksStore
+   *        An array to store all the visible blocks into, after drawing them.
+   *        The provided array will be populated.
+   * @param number minVisibleBlockWidth
+   *        The minimum width of the blocks that will be added into
+   *        the `visibleBlocksStore`.
+   */
+  _drawBlocksFill: function(
+    color, blocks, dataOffset, dataScale,
+    visibleBlocksStore, minVisibleBlockWidth)
+  {
+    let ctx = this._ctx;
+    let canvasWidth = this._width;
+    let canvasHeight = this._height;
+    let scaledOffset = dataOffset * dataScale;
+
+    ctx.fillStyle = color;
+    ctx.beginPath();
+
+    for (let block of blocks) {
+      let { x, y, width, height } = block;
+      let rectLeft = x * this._pixelRatio * dataScale - scaledOffset;
+      let rectTop = (y + OVERVIEW_HEADER_HEIGHT) * this._pixelRatio;
+      let rectWidth = width * this._pixelRatio * dataScale;
+      let rectHeight = height * this._pixelRatio;
+
+      if (rectLeft > canvasWidth || // Too far right.
+          rectLeft < -rectWidth ||  // Too far left.
+          rectTop > canvasHeight) { // Too far bottom.
+        continue;
+      }
+
+      // Clamp the blocks position to start at 0. Avoid negative X coords,
+      // to properly place the text inside the blocks.
+      if (rectLeft < 0) {
+        rectWidth += rectLeft;
+        rectLeft = 0;
+      }
+
+      // Avoid drawing blocks that are too narrow.
+      if (rectWidth <= FLAME_GRAPH_BLOCK_BORDER ||
+          rectHeight <= FLAME_GRAPH_BLOCK_BORDER) {
+        continue;
+      }
+
+      ctx.rect(
+        rectLeft, rectTop,
+        rectWidth - FLAME_GRAPH_BLOCK_BORDER,
+        rectHeight - FLAME_GRAPH_BLOCK_BORDER);
+
+      // Populate the visible blocks store with this block if the width
+      // is longer than a given threshold.
+      if (rectWidth > minVisibleBlockWidth) {
+        visibleBlocksStore.push(block);
+      }
+    }
+
+    ctx.fill();
+  },
+
+  /**
+   * Adds text for a single block.
+   *
+   * @param object block
+   *        A single { x, y, width, height, text } object visually representing
+   *        the block containing the text.
+   * @param number dataOffset, dataScale
+   *        Offsets and scales the data source by the specified amount.
+   *        This is used for scrolling the visualization.
+   */
+  _drawBlockText: function(block, dataOffset, dataScale) {
+    let ctx = this._ctx;
+    let scaledOffset = dataOffset * dataScale;
+
+    let { x, y, width, height, text } = block;
+
+    let paddingTop = FLAME_GRAPH_BLOCK_TEXT_PADDING_TOP * this._pixelRatio;
+    let paddingLeft = FLAME_GRAPH_BLOCK_TEXT_PADDING_LEFT * this._pixelRatio;
+    let paddingRight = FLAME_GRAPH_BLOCK_TEXT_PADDING_RIGHT * this._pixelRatio;
+    let totalHorizontalPadding = paddingLeft + paddingRight;
+
+    let rectLeft = x * this._pixelRatio * dataScale - scaledOffset;
+    let rectWidth = width * this._pixelRatio * dataScale;
+
+    // Clamp the blocks position to start at 0. Avoid negative X coords,
+    // to properly place the text inside the blocks.
+    if (rectLeft < 0) {
+      rectWidth += rectLeft;
+      rectLeft = 0;
+    }
+
+    let textLeft = rectLeft + paddingLeft;
+    let textTop = (y + height / 2 + OVERVIEW_HEADER_HEIGHT) * this._pixelRatio + paddingTop;
+    let textAvailableWidth = rectWidth - totalHorizontalPadding;
+
+    // Massage the text to fit inside a given width. This clamps the string
+    // at the end to avoid overflowing.
+    let fittedText = this._getFittedText(text, textAvailableWidth);
+    if (fittedText.length < 1) {
+      return;
+    }
+
+    ctx.fillText(fittedText, textLeft, textTop);
+  },
+
+  /**
+   * Calculating text widths is necessary to trim the text inside the blocks
+   * while the scaling changes (e.g. via scrolling). This is very expensive,
+   * so maintain a cache of string contents to text widths.
+   */
+  _textWidthsCache: null,
+  _overflowCharWidth: null,
+  _averageCharWidth: null,
+
+  /**
+   * Gets the width of the specified text, for the current context state
+   * (font size, family etc.).
+   *
+   * @param string text
+   *        The text to analyze.
+   * @return number
+   *         The text width.
+   */
+  _getTextWidth: function(text) {
+    let cachedWidth = this._textWidthsCache[text];
+    if (cachedWidth) {
+      return cachedWidth;
+    }
+    let metrics = this._ctx.measureText(text);
+    return (this._textWidthsCache[text] = metrics.width);
+  },
+
+  /**
+   * Gets an approximate width of the specified text. This is much faster
+   * than `_getTextWidth`, but inexact.
+   *
+   * @param string text
+   *        The text to analyze.
+   * @return number
+   *         The approximate text width.
+   */
+  _getTextWidthApprox: function(text) {
+    return text.length * this._averageCharWidth;
+  },
+
+  /**
+   * Gets the average letter width in the English alphabet, for the current
+   * context state (font size, family etc.). This provides a close enough
+   * value to use in `_getTextWidthApprox`.
+   *
+   * @return number
+   *         The average letter width.
+   */
+  _calcAverageCharWidth: function() {
+    let letterWidthsSum = 0;
+    let start = 32; // space
+    let end = 123; // "z"
+
+    for (let i = start; i < end; i++) {
+      let char = String.fromCharCode(i);
+      letterWidthsSum += this._getTextWidth(char);
+    }
+
+    return letterWidthsSum / (end - start);
+  },
+
+  /**
+   * Massage a text to fit inside a given width. This clamps the string
+   * at the end to avoid overflowing.
+   *
+   * @param string text
+   *        The text to fit inside the given width.
+   * @param number maxWidth
+   *        The available width for the given text.
+   * @return string
+   *         The fitted text.
+   */
+  _getFittedText: function(text, maxWidth) {
+    let textWidth = this._getTextWidth(text);
+    if (textWidth < maxWidth) {
+      return text;
+    }
+    if (this._overflowCharWidth > maxWidth) {
+      return "";
+    }
+    for (let i = 1, len = text.length; i <= len; i++) {
+      let trimmedText = text.substring(0, len - i);
+      let trimmedWidth = this._getTextWidthApprox(trimmedText) + this._overflowCharWidth;
+      if (trimmedWidth < maxWidth) {
+        return trimmedText + this.overflowChar;
+      }
+    }
+    return "";
+  },
+
+  /**
+   * Listener for the "mousemove" event on the graph's container.
+   */
+  _onMouseMove: function(e) {
+    let offset = this._getContainerOffset();
+    let mouseX = (e.clientX - offset.left) * this._pixelRatio;
+
+    let canvasWidth = this._width;
+    let canvasHeight = this._height;
+
+    let selection = this._selection;
+    let selectionWidth = selection.end - selection.start;
+    let selectionScale = canvasWidth / selectionWidth;
+
+    let dragger = this._selectionDragger;
+    if (dragger.origin != null) {
+      selection.start = dragger.anchor.start + (dragger.origin - mouseX) / selectionScale;
+      selection.end = dragger.anchor.end + (dragger.origin - mouseX) / selectionScale;
+      this._normalizeSelectionBounds();
+      this._shouldRedraw = true;
+    }
+  },
+
+  /**
+   * Listener for the "mousedown" event on the graph's container.
+   */
+  _onMouseDown: function(e) {
+    let offset = this._getContainerOffset();
+    let mouseX = (e.clientX - offset.left) * this._pixelRatio;
+
+    this._selectionDragger.origin = mouseX;
+    this._selectionDragger.anchor.start = this._selection.start;
+    this._selectionDragger.anchor.end = this._selection.end;
+    this._canvas.setAttribute("input", "adjusting-selection-boundary");
+  },
+
+  /**
+   * Listener for the "mouseup" event on the graph's container.
+   */
+  _onMouseUp: function() {
+    this._selectionDragger.origin = null;
+    this._canvas.removeAttribute("input");
+  },
+
+  /**
+   * Listener for the "wheel" event on the graph's container.
+   */
+  _onMouseWheel: function(e) {
+    let offset = this._getContainerOffset();
+    let mouseX = (e.clientX - offset.left) * this._pixelRatio;
+
+    let canvasWidth = this._width;
+    let canvasHeight = this._height;
+
+    let selection = this._selection;
+    let selectionWidth = selection.end - selection.start;
+    let selectionScale = canvasWidth / selectionWidth;
+
+    switch (e.axis) {
+      case e.VERTICAL_AXIS: {
+        let distFromStart = mouseX;
+        let distFromEnd = canvasWidth - mouseX;
+        let vector = e.detail * GRAPH_WHEEL_ZOOM_SENSITIVITY / selectionScale;
+        selection.start -= distFromStart * vector;
+        selection.end += distFromEnd * vector;
+        break;
+      }
+      case e.HORIZONTAL_AXIS: {
+        let vector = e.detail * GRAPH_WHEEL_SCROLL_SENSITIVITY / selectionScale;
+        selection.start += vector;
+        selection.end += vector;
+        break;
+      }
+    }
+
+    this._normalizeSelectionBounds();
+    this._shouldRedraw = true;
+  },
+
+  /**
+   * Makes sure the start and end points of the current selection
+   * are withing the graph's visible bounds, and that they form a selection
+   * wider than the allowed minimum width.
+   */
+  _normalizeSelectionBounds: function() {
+    let canvasWidth = this._width;
+    let canvasHeight = this._height;
+
+    let { start, end } = this._selection;
+    let minSelectionWidth = GRAPH_MIN_SELECTION_WIDTH * this._pixelRatio;
+
+    if (start < 0) {
+      start = 0;
+    }
+    if (end < 0) {
+      start = 0;
+      end = minSelectionWidth;
+    }
+    if (end > canvasWidth) {
+      end = canvasWidth;
+    }
+    if (start > canvasWidth) {
+      end = canvasWidth;
+      start = canvasWidth - minSelectionWidth;
+    }
+    if (end - start < minSelectionWidth) {
+      let midPoint = (start + end) / 2;
+      start = midPoint - minSelectionWidth / 2;
+      end = midPoint + minSelectionWidth / 2;
+    }
+
+    this._selection.start = start;
+    this._selection.end = end;
+  },
+
+  /**
+   *
+   * Finds the optimal tick interval between time markers in this graph.
+   *
+   * @param number dataScale
+   * @return number
+   */
+  _findOptimalTickInterval: function(dataScale) {
+    let timingStep = TIMELINE_TICKS_MULTIPLE;
+    let spacingMin = TIMELINE_TICKS_SPACING_MIN * this._pixelRatio;
+
+    if (dataScale > spacingMin) {
+      return dataScale;
+    }
+
+    while (true) {
+      let scaledStep = dataScale * timingStep;
+      if (scaledStep < spacingMin) {
+        timingStep <<= 1;
+        continue;
+      }
+      return scaledStep;
+    }
+  },
+
+  /**
+   * Gets the offset of this graph's container relative to the owner window.
+   *
+   * @return object
+   *         The { left, top } offset.
+   */
+  _getContainerOffset: function() {
+    let node = this._canvas;
+    let x = 0;
+    let y = 0;
+
+    while ((node = node.offsetParent)) {
+      x += node.offsetLeft;
+      y += node.offsetTop;
+    }
+
+    return { left: x, top: y };
+  },
+
+  /**
+   * Listener for the "resize" event on the graph's parent node.
+   */
+  _onResize: function() {
+    if (this.hasData()) {
+      setNamedTimeout(this._uid, GRAPH_RESIZE_EVENTS_DRAIN, this.refresh);
+    }
+  }
+};
+
+const FLAME_GRAPH_BLOCK_HEIGHT = 12; // px
+
+const PALLETTE_SIZE = 10;
+const PALLETTE_HUE_OFFSET = Math.random() * 90;
+const PALLETTE_HUE_RANGE = 270;
+const PALLETTE_SATURATION = 60;
+const PALLETTE_BRIGHTNESS = 75;
+const PALLETTE_OPACITY = 0.7;
+
+const COLOR_PALLETTE = Array.from(Array(PALLETTE_SIZE)).map((_, i) => "hsla" +
+  "(" + ((PALLETTE_HUE_OFFSET + (i / PALLETTE_SIZE * PALLETTE_HUE_RANGE))|0 % 360) +
+  "," + PALLETTE_SATURATION + "%" +
+  "," + PALLETTE_BRIGHTNESS + "%" +
+  "," + PALLETTE_OPACITY +
+  ")"
+);
+
+/**
+ * A collection of utility functions converting various data sources
+ * into a format drawable by the FlameGraph.
+ */
+let FlameGraphUtils = {
+  /**
+   * Converts a list of samples from the profiler data to something that's
+   * drawable by a FlameGraph widget.
+   *
+   * @param array samples
+   *        A list of { time, frames: [{ location }] } objects.
+   * @param array out [optional]
+   *        An output storage to reuse for storing the flame graph data.
+   * @return array
+   *         The flame graph data.
+   */
+  createFlameGraphDataFromSamples: function(samples, out = []) {
+    // 1. Create a map of colors to arrays, representing buckets of
+    // blocks inside the flame graph pyramid sharing the same style.
+
+    let buckets = new Map();
+
+    for (let color of COLOR_PALLETTE) {
+      buckets.set(color, []);
+    }
+
+    // 2. Populate the buckets by iterating over every frame in every sample.
+
+    let prevTime = 0;
+    let prevFrames = [];
+
+    for (let { frames, time } of samples) {
+      let frameIndex = 0;
+
+      for (let { location } of frames) {
+        let prevFrame = prevFrames[frameIndex];
+
+        // Frames at the same location and the same depth will be reused.
+        // If there is a block already created, change its width.
+        if (prevFrame && prevFrame.srcData.rawLocation == location) {
+          prevFrame.width = (time - prevFrame.srcData.startTime);
+        }
+        // Otherwise, create a new block for this frame at this depth,
+        // using a simple location based salt for picking a color.
+        else {
+          let hash = this._getStringHash(location);
+          let color = COLOR_PALLETTE[hash % PALLETTE_SIZE];
+          let bucket = buckets.get(color);
+
+          bucket.push(prevFrames[frameIndex] = {
+            srcData: { startTime: prevTime, rawLocation: location },
+            x: prevTime,
+            y: frameIndex * FLAME_GRAPH_BLOCK_HEIGHT,
+            width: time - prevTime,
+            height: FLAME_GRAPH_BLOCK_HEIGHT,
+            text: location
+          });
+        }
+
+        frameIndex++;
+      }
+
+      // Previous frames at stack depths greater than the current sample's
+      // maximum need to be nullified. It's nonsensical to reuse them.
+      prevFrames.length = frameIndex;
+      prevTime = time;
+    }
+
+    // 3. Convert the buckets into a data source usable by the FlameGraph.
+    // This is a simple conversion from a Map to an Array.
+
+    for (let [color, blocks] of buckets) {
+      out.push({ color, blocks });
+    }
+
+    return out;
+  },
+
+  /**
+   * Very dumb hashing of a string. Used to pick colors from a pallette.
+   *
+   * @param string input
+   * @return number
+   */
+  _getStringHash: function(input) {
+    const STRING_HASH_PRIME1 = 7;
+    const STRING_HASH_PRIME2 = 31;
+
+    let hash = STRING_HASH_PRIME1;
+
+    for (let i = 0, len = input.length; i < len; i++) {
+      hash *= STRING_HASH_PRIME2;
+      hash += input.charCodeAt(i);
+    }
+
+    return hash;
+  }
+};
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -2,20 +2,24 @@
  * 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 Cu = Components.utils;
 
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
-const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 
 this.EXPORTED_SYMBOLS = [
+  "GraphCursor",
+  "GraphSelection",
+  "GraphSelectionDragger",
+  "GraphSelectionResizer",
   "AbstractCanvasGraph",
   "LineGraphWidget",
   "BarGraphWidget",
   "CanvasGraphUtils"
 ];
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
@@ -88,38 +92,33 @@ const BAR_GRAPH_REGION_STRIPES_COLOR = "
 const BAR_GRAPH_HIGHLIGHTS_MASK_BACKGROUND = "rgba(255,255,255,0.75)";
 const BAR_GRAPH_HIGHLIGHTS_MASK_STRIPES = "rgba(255,255,255,0.5)";
 
 const BAR_GRAPH_LEGEND_MOUSEOVER_DEBOUNCE = 50; // ms
 
 /**
  * Small data primitives for all graphs.
  */
-this.GraphCursor = function() {};
-this.GraphSelection = function() {};
-this.GraphSelectionDragger = function() {};
-this.GraphSelectionResizer = function() {};
-
-GraphCursor.prototype = {
-  x: null,
-  y: null
+this.GraphCursor = function() {
+  this.x = null;
+  this.y = null;
 };
 
-GraphSelection.prototype = {
-  start: null,
-  end: null
+this.GraphSelection = function() {
+  this.start = null;
+  this.end = null;
 };
 
-GraphSelectionDragger.prototype = {
-  origin: null,
-  anchor: new GraphSelection()
+this.GraphSelectionDragger = function() {
+  this.origin = null;
+  this.anchor = new GraphSelection();
 };
 
-GraphSelectionResizer.prototype = {
-  margin: null
+this.GraphSelectionResizer = function() {
+  this.margin = null;
 };
 
 /**
  * Base class for all graphs using a canvas to render the data source. Handles
  * frame creation, data source, selection bounds, cursor position, etc.
  *
  * Language:
  *   - The "data" represents the values used when building the graph.
@@ -240,16 +239,21 @@ AbstractCanvasGraph.prototype = {
     container.removeEventListener("mouseout", this._onMouseOut);
 
     let ownerWindow = this._parent.ownerDocument.defaultView;
     ownerWindow.removeEventListener("resize", this._onResize);
 
     this._window.cancelAnimationFrame(this._animationId);
     this._iframe.remove();
 
+    this._cursor = null;
+    this._selection = null;
+    this._selectionDragger = null;
+    this._selectionResizer = null;
+
     this._data = null;
     this._mask = null;
     this._maskArgs = null;
     this._regions = null;
 
     this._cachedBackgroundImage = null;
     this._cachedGraphImage = null;
     this._cachedMaskImage = null;
@@ -887,16 +891,19 @@ AbstractCanvasGraph.prototype = {
    * @return boolean
    */
   _isHoveringRegion: function() {
     return !!this.getHoveredRegion();
   },
 
   /**
    * Gets the offset of this graph's container relative to the owner window.
+   *
+   * @return object
+   *         The { left, top } offset.
    */
   _getContainerOffset: function() {
     let node = this._canvas;
     let x = 0;
     let y = 0;
 
     while (node = node.offsetParent) {
       x += node.offsetLeft;
@@ -1444,17 +1451,17 @@ LineGraphWidget.prototype = Heritage.ext
     this._maxTooltip.setAttribute("with-arrows", this.withTooltipArrows);
     this._avgTooltip.setAttribute("with-arrows", this.withTooltipArrows);
     this._minTooltip.setAttribute("with-arrows", this.withTooltipArrows);
 
     let distanceMinMax = Math.abs(maxTooltipTop - minTooltipTop);
     this._maxTooltip.hidden = this._showMax === false || !totalTicks || distanceMinMax < LINE_GRAPH_MIN_MAX_TOOLTIP_DISTANCE;
     this._avgTooltip.hidden = this._showAvg === false || !totalTicks;
     this._minTooltip.hidden = this._showMin === false || !totalTicks;
-    this._gutter.hidden = (this._showMin === false && this._showMax === false) || !totalTicks || !this.withTooltipArrows;
+    this._gutter.hidden = (this._showMin === false && this._showAvg === false && this._showMax === false) || !totalTicks;
 
     this._maxGutterLine.hidden = this._showMax === false;
     this._avgGutterLine.hidden = this._showAvg === false;
     this._minGutterLine.hidden = this._showMin === false;
   },
 
   /**
    * Creates the gutter node when constructing this graph.
--- a/browser/devtools/timeline/widgets/markers-overview.js
+++ b/browser/devtools/timeline/widgets/markers-overview.js
@@ -16,19 +16,18 @@ Cu.import("resource:///modules/devtools/
 
 const { colorUtils: { setAlpha }} = require("devtools/css-color");
 const { getColor } = require("devtools/shared/theme");
 
 loader.lazyRequireGetter(this, "L10N",
   "devtools/timeline/global", true);
 
 const OVERVIEW_HEADER_HEIGHT = 14; // px
-const OVERVIEW_ROW_HEIGHT = 11; // row height
+const OVERVIEW_ROW_HEIGHT = 11; // px
 
-const OVERVIEW_BODY_HEIGHT = 55; // 11px * 5 groups
 const OVERVIEW_SELECTION_LINE_COLOR = "#666";
 const OVERVIEW_CLIPHEAD_LINE_COLOR = "#555";
 
 const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms
 const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px
 const OVERVIEW_HEADER_SAFE_BOUNDS = 50; // px
 const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
 const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -372,17 +372,16 @@
 @RESPATH@/browser/components/WebContentConverter.js
 @RESPATH@/browser/components/BrowserComponents.manifest
 @RESPATH@/browser/components/nsBrowserContentHandler.js
 @RESPATH@/browser/components/nsBrowserGlue.js
 @RESPATH@/browser/components/nsSetDefaultBrowser.manifest
 @RESPATH@/browser/components/nsSetDefaultBrowser.js
 @RESPATH@/browser/components/BrowserDownloads.manifest
 @RESPATH@/browser/components/DownloadsStartup.js
-@RESPATH@/browser/components/DownloadsUI.js
 @RESPATH@/browser/components/BrowserPlaces.manifest
 @RESPATH@/browser/components/devtools-clhandler.manifest
 @RESPATH@/browser/components/devtools-clhandler.js
 @RESPATH@/browser/components/webideCli.js
 @RESPATH@/browser/components/webideComponents.manifest
 @RESPATH@/browser/components/Experiments.manifest
 @RESPATH@/browser/components/ExperimentsService.js
 @RESPATH@/browser/components/translation.manifest
@@ -408,18 +407,16 @@
 @RESPATH@/components/WebVTT.manifest
 @RESPATH@/components/WebVTTParserWrapper.js
 #ifdef MOZ_GTK
 @RESPATH@/components/nsFilePicker.manifest
 @RESPATH@/components/nsFilePicker.js
 #endif
 @RESPATH@/components/nsHelperAppDlg.manifest
 @RESPATH@/components/nsHelperAppDlg.js
-@RESPATH@/components/nsDownloadManagerUI.manifest
-@RESPATH@/components/nsDownloadManagerUI.js
 @RESPATH@/components/NetworkGeolocationProvider.manifest
 @RESPATH@/components/NetworkGeolocationProvider.js
 @RESPATH@/browser/components/nsSidebar.manifest
 @RESPATH@/browser/components/nsSidebar.js
 @RESPATH@/components/extensions.manifest
 @RESPATH@/components/addonManager.js
 @RESPATH@/components/amContentHandler.js
 @RESPATH@/components/amInstallTrigger.js
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -417,16 +417,21 @@ These should match what Safari and other
 <!ENTITY searchFocus.commandkey2      "e">
 <!ENTITY searchFocusUnix.commandkey   "j">
 
 <!-- LOCALIZATION NOTE (searchFor.label, searchWith.label):
      These two strings are used to build the header above the list of one-click
      search providers:  "Search for <used typed keywords> with:" -->
 <!ENTITY searchFor.label              "Search for ">
 <!ENTITY searchWith.label             " with:">
+<!-- LOCALIZATION NOTE (searchWithHeader.label):
+     The wording of this string should be as close as possible to
+     searchFor.label and searchWith.label. This string will be used instead of
+     them when the user has not typed any keyword. -->
+<!ENTITY searchWithHeader.label       "Search with:">
 <!ENTITY changeSearchSettings.button  "Change Search Settings">
 
 <!ENTITY tabView.commandkey           "e">
 
 <!ENTITY openLinkCmdInTab.label       "Open Link in New Tab">
 <!ENTITY openLinkCmdInTab.accesskey   "T">
 <!ENTITY openLinkCmd.label            "Open Link in New Window">
 <!ENTITY openLinkCmd.accesskey        "W">
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/animationinspector.dtd
@@ -0,0 +1,24 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Animations panel strings.
+  - The Animations panel is part of the Inspector sidebar -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+  - keep it in English, or another language commonly spoken among web developers.
+  - You want to make that choice consistent across the developer tools.
+  - A good criteria is the language in which you'd find the best
+  - documentation on web development on the web. -->
+
+<!-- LOCALIZATION NOTE (title): This is the label shown in the sidebar tab -->
+<!ENTITY title                  "Animations">
+
+<!-- LOCALIZATION NOTE (invalidElement): This is the label shown in the panel
+  - when an invalid node is currently selected in the inspector. -->
+<!ENTITY invalidElement         "No animations were found for the current element.">
+
+<!-- LOCALIZATION NOTE (selectElement): This is the label shown in the panel
+  - when an invalid node is currently selected in the inspector, to invite the
+  - user to select a new node by clicking on the element-picker icon. -->
+<!ENTITY selectElement         "Pick another element from the page.">
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/animationinspector.properties
@@ -0,0 +1,43 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Animation inspector
+# which is available as a sidebar panel in the Inspector.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (player.animationNameLabel):
+# This string is displayed in each animation player widget. It is the label
+# displayed before the animation name.
+player.animationNameLabel=Animation:
+
+# LOCALIZATION NOTE (player.transitionNameLabel):
+# This string is displayed in each animation player widget. It is the label
+# displayed in the header, when the element is animated by mean of a css
+# transition
+player.transitionNameLabel=Transition
+
+# LOCALIZATION NOTE (player.animationDurationLabel):
+# This string is displayed in each animation player widget. It is the label
+# displayed before the animation duration.
+player.animationDurationLabel=Duration:
+
+# LOCALIZATION NOTE (player.animationIterationCountLabel):
+# This string is displayed in each animation player widget. It is the label
+# displayed before the number of times the animation is set to repeat.
+player.animationIterationCountLabel=Repeats:
+
+# LOCALIZATION NOTE (player.infiniteIterationCount):
+# In case the animation repeats infinitely, this string is displayed next to the
+# player.animationIterationCountLabel string, instead of a number.
+player.infiniteIterationCount=&#8734;
+
+# LOCALIZATION NOTE (player.timeLabel):
+# This string is displayed in each animation player widget, to indicate either
+# how long (in seconds) the animation lasts, or what is the animation's current
+# time (in seconds too);
+player.timeLabel=%Ss
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
@@ -69,16 +69,20 @@
 <!-- LOCALIZATION NOTE (debuggerUI.tab.timings): This is the label displayed
   -  in the network details pane identifying the timings tab. -->
 <!ENTITY netmonitorUI.tab.timings         "Timings">
 
 <!-- LOCALIZATION NOTE (debuggerUI.tab.preview): This is the label displayed
   -  in the network details pane identifying the preview tab. -->
 <!ENTITY netmonitorUI.tab.preview         "Preview">
 
+<!-- LOCALIZATION NOTE (netmonitorUI.tab.security): This is the label displayed
+  -  in the network details pane identifying the security tab. -->
+<!ENTITY netmonitorUI.tab.security        "Security">
+
 <!-- LOCALIZATION NOTE (debuggerUI.footer.filterAll): This is the label displayed
   -  in the network details footer for the "All" filtering button. -->
 <!ENTITY netmonitorUI.footer.filterAll    "All">
 
 <!-- LOCALIZATION NOTE (debuggerUI.footer.filterHTML): This is the label displayed
   -  in the network details footer for the "HTML" filtering button. -->
 <!ENTITY netmonitorUI.footer.filterHTML   "HTML">
 
@@ -187,16 +191,45 @@
   -  in a "wait" state. -->
 <!ENTITY netmonitorUI.timings.wait        "Waiting:">
 
 <!-- LOCALIZATION NOTE (debuggerUI.timings.receive): This is the label displayed
   -  in the network details timings tab identifying the amount of time spent
   -  in a "receive" state. -->
 <!ENTITY netmonitorUI.timings.receive     "Receiving:">
 
+<!-- LOCALIZATION NOTE (netmonitorUI.security.error): This is the label displayed
+  -  in the security tab if a security error prevented the connection. -->
+<!ENTITY netmonitorUI.security.error      "An error occured:">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.security.protocolVersion): This is the label displayed
+  -  in the security tab describing TLS/SSL protocol version. -->
+<!ENTITY netmonitorUI.security.protocolVersion "Protocol version:">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.security.cipherSuite): This is the label displayed
+  -  in the security tab describing the cipher suite used to secure this connection. -->
+<!ENTITY netmonitorUI.security.cipherSuite "Cipher suite:">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.security.hsts): This is the label displayed
+  -  in the security tab describing the usage of HTTP Strict Transport Security. -->
+<!ENTITY netmonitorUI.security.hsts "HTTP Strict Transport Security:">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.security.hpkp): This is the label displayed
+  -  in the security tab describing the usage of Public Key Pinning. -->
+<!ENTITY netmonitorUI.security.hpkp "Public Key Pinning:">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.security.connection): This is the label displayed
+  -  in the security tab describing the section containing information related to
+  -  the secure connection. -->
+<!ENTITY netmonitorUI.security.connection "Connection:">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.security.certificate): This is the label displayed
+  -  in the security tab describing the server certificate section. -->
+<!ENTITY netmonitorUI.security.certificate "Certificate:">
+
 <!-- LOCALIZATION NOTE (netmonitorUI.context.perfTools): This is the label displayed
   -  on the context menu that shows the performance analysis tools -->
 <!ENTITY netmonitorUI.context.perfTools   "Start Performance Analysis…">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.context.perfTools.accesskey): This is the access key
   -  for the performance analysis menu item displayed in the context menu for a request -->
 <!ENTITY netmonitorUI.context.perfTools.accesskey "S">
 
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.properties
@@ -24,16 +24,56 @@ netmonitor.panelLabel=Network Panel
 netmonitor.commandkey=Q
 netmonitor.accesskey=N
 
 # LOCALIZATION NOTE (netmonitor.tooltip):
 # This string is displayed in the tooltip of the tab when the Network Monitor is
 # displayed inside the developer tools window.
 netmonitor.tooltip=Network Monitor
 
+# LOCALIZATION NOTE (netmonitor.security.state.secure)
+# This string is used as an tooltip for request that was performed over secure
+# channel i.e. the connection was encrypted.
+netmonitor.security.state.secure=The connection used to fetch this resource was secure.
+
+# LOCALIZATION NOTE (netmonitor.security.state.insecure)
+# This string is used as an tooltip for request that was performed over insecure
+# channel i.e. the connection was not encrypted.
+netmonitor.security.state.insecure=The connection used to fetch this resource was not encrypted.
+
+# LOCALIZATION NOTE (netmonitor.security.state.broken)
+# This string is used as an tooltip for request that failed due to security
+# issues.
+netmonitor.security.state.broken=A security error prevented the resource from being loaded.
+
+# LOCALIZATION NOTE (netmonitor.security.enabled):
+# This string is used to indicate that a specific security feature is used by
+# a connection in the security details tab.
+# For example: "HTTP Strict Transport Security: Enabled"
+netmonitor.security.enabled=Enabled
+
+# LOCALIZATION NOTE (netmonitor.security.disabled):
+# This string is used to indicate that a specific security feature is not used by
+# a connection in the security details tab.
+# For example: "HTTP Strict Transport Security: Disabled"
+netmonitor.security.disabled=Disabled
+
+# LOCALIZATION NOTE (netmonitor.security.hostHeader):
+# This string is used as a header for section containing security information
+# related to the remote host. %S is replaced with the domain name of the remote
+# host. For example: Host example.com
+netmonitor.security.hostHeader=Host %S:
+
+# LOCALIZATION NOTE (netmonitor.security.notAvailable):
+# This string is used to indicate that a certain piece of information is not
+# available to be displayd. For example a certificate that has no organization
+# defined:
+#   Organization: <Not Available>
+netmonitor.security.notAvailable=<Not Available>
+
 # LOCALIZATION NOTE (collapseDetailsPane): This is the tooltip for the button
 # that collapses the network details pane in the UI.
 collapseDetailsPane=Hide request details
 
 # LOCALIZATION NOTE (expandDetailsPane): This is the tooltip for the button
 # that expands the network details pane in the UI.
 expandDetailsPane=Show request details
 
--- a/browser/locales/en-US/chrome/browser/devtools/profiler.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/profiler.properties
@@ -59,16 +59,22 @@ recordingsList.saveLabel=Save
 profile.tab=%1$S ms → %2$S ms
 
 # LOCALIZATION NOTE (graphs.fps):
 # This string is displayed in the framerate graph of the Profiler,
 # as the unit used to measure frames per second. This label should be kept
 # AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
 graphs.fps=fps
 
+# LOCALIZATION NOTE (graphs.ms):
+# This string is displayed in the flamegraph of the Profiler,
+# as the unit used to measure time (in milliseconds). This label should be kept
+# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
+graphs.ms=ms
+
 # LOCALIZATION NOTE (category.*):
 # These strings are displayed in the categories graph of the Profiler,
 # as the legend for each block in every bar. These labels should be kept
 # AS SHORT AS POSSIBLE so they don't obstruct important parts of the graph.
 category.other=Gecko
 category.css=Styles
 category.js=JIT
 category.gc=GC
@@ -97,8 +103,9 @@ recordingsList.saveDialogTitle=Save profile…
 
 # LOCALIZATION NOTE (recordingsList.saveDialogJSONFilter):
 # This string is displayed as a filter for saving a recording to disk.
 recordingsList.saveDialogJSONFilter=JSON Files
 
 # LOCALIZATION NOTE (recordingsList.saveDialogAllFilter):
 # This string is displayed as a filter for saving a recording to disk.
 recordingsList.saveDialogAllFilter=All Files
+
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -22,16 +22,18 @@
     locale/browser/syncProgress.dtd                (%chrome/browser/syncProgress.dtd)
     locale/browser/syncCustomize.dtd               (%chrome/browser/syncCustomize.dtd)
     locale/browser/aboutSyncTabs.dtd               (%chrome/browser/aboutSyncTabs.dtd)
 #endif
     locale/browser/browser.dtd                     (%chrome/browser/browser.dtd)
     locale/browser/baseMenuOverlay.dtd             (%chrome/browser/baseMenuOverlay.dtd)
     locale/browser/browser.properties              (%chrome/browser/browser.properties)
     locale/browser/customizableui/customizableWidgets.properties (%chrome/browser/customizableui/customizableWidgets.properties)
+    locale/browser/devtools/animationinspector.dtd    (%chrome/browser/devtools/animationinspector.dtd)
+    locale/browser/devtools/animationinspector.properties (%chrome/browser/devtools/animationinspector.properties)
     locale/browser/devtools/appcacheutils.properties  (%chrome/browser/devtools/appcacheutils.properties)
     locale/browser/devtools/debugger.dtd              (%chrome/browser/devtools/debugger.dtd)
     locale/browser/devtools/debugger.properties       (%chrome/browser/devtools/debugger.properties)
     locale/browser/devtools/netmonitor.dtd            (%chrome/browser/devtools/netmonitor.dtd)
     locale/browser/devtools/netmonitor.properties     (%chrome/browser/devtools/netmonitor.properties)
     locale/browser/devtools/shadereditor.dtd          (%chrome/browser/devtools/shadereditor.dtd)
     locale/browser/devtools/shadereditor.properties   (%chrome/browser/devtools/shadereditor.properties)
     locale/browser/devtools/canvasdebugger.dtd        (%chrome/browser/devtools/canvasdebugger.dtd)
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -551,17 +551,17 @@ this.BrowserUITelemetry = {
         visibleTabs.push(visibleTabsNum);
         hiddenTabs.push(someWin.gBrowser.tabs.length - visibleTabsNum);
       }
     }
     result.visibleTabs = visibleTabs;
     result.hiddenTabs = hiddenTabs;
 
     if (Components.isSuccessCode(searchResult)) {
-      result.currentSearchEngine = Services.search.currentEngine;
+      result.currentSearchEngine = Services.search.currentEngine.name;
     }
 
     return result;
   },
 
   getToolbarMeasures: function() {
     let result = this._firstWindowMeasurements || {};
     result.countableEvents = this._countableEvents;
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -120,16 +120,20 @@
 
 /* Places toolbar */
 toolbarbutton.bookmark-item:not(.subviewbutton),
 #personal-bookmarks[cui-areatype="toolbar"]:not([overflowedItem=true]) > #bookmarks-toolbar-placeholder {
   margin: 0;
   padding: 2px 3px;
 }
 
+toolbarbutton.bookmark-item:not(.subviewbutton):not(:hover):not(:active):not([open]) {
+  color: inherit;
+}
+
 toolbarbutton.bookmark-item:not(.subviewbutton):hover:active,
 toolbarbutton.bookmark-item[open="true"] {
   padding-top: 3px;
   padding-bottom: 1px;
   -moz-padding-start: 4px;
   -moz-padding-end: 2px;
 }
 
--- a/browser/themes/linux/devtools/netmonitor.css
+++ b/browser/themes/linux/devtools/netmonitor.css
@@ -11,17 +11,17 @@
 #toggle-raw-headers {
   padding: 4px;
 }
 
 .requests-menu-status-and-method {
   width: 9em;
 }
 
-.requests-menu-domain {
+.requests-menu-security-and-domain {
   width: 16vw;
 }
 
 .requests-menu-size {
   width: 6em;
 }
 
 /* Responsive sidebar */
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -263,16 +263,17 @@ browser.jar:
   skin/classic/browser/devtools/markup-view.css       (../shared/devtools/markup-view.css)
   skin/classic/browser/devtools/editor-error.png       (../shared/devtools/images/editor-error.png)
   skin/classic/browser/devtools/editor-breakpoint.png  (../shared/devtools/images/editor-breakpoint.png)
   skin/classic/browser/devtools/editor-debug-location.png (../shared/devtools/images/editor-debug-location.png)
   skin/classic/browser/devtools/editor-debug-location@2x.png (../shared/devtools/images/editor-debug-location@2x.png)
   skin/classic/browser/devtools/breadcrumbs-divider@2x.png      (../shared/devtools/images/breadcrumbs-divider@2x.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton.png    (../shared/devtools/images/breadcrumbs-scrollbutton.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
+  skin/classic/browser/devtools/animationinspector.css          (../shared/devtools/animationinspector.css)
 * skin/classic/browser/devtools/canvasdebugger.css    (devtools/canvasdebugger.css)
 * skin/classic/browser/devtools/debugger.css          (devtools/debugger.css)
   skin/classic/browser/devtools/eyedropper.css        (../shared/devtools/eyedropper.css)
 * skin/classic/browser/devtools/netmonitor.css        (devtools/netmonitor.css)
 * skin/classic/browser/devtools/profiler.css          (devtools/profiler.css)
 * skin/classic/browser/devtools/performance.css       (devtools/performance.css)
 * skin/classic/browser/devtools/timeline.css          (devtools/timeline.css)
   skin/classic/browser/devtools/timeline-filter.svg   (../shared/devtools/images/timeline-filter.svg)
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -394,16 +394,17 @@ browser.jar:
   skin/classic/browser/devtools/editor-debug-location@2x.png    (../shared/devtools/images/editor-debug-location@2x.png)
 * skin/classic/browser/devtools/webconsole.css                  (devtools/webconsole.css)
   skin/classic/browser/devtools/webconsole_networkpanel.css     (devtools/webconsole_networkpanel.css)
   skin/classic/browser/devtools/webconsole.png                  (../shared/devtools/images/webconsole.png)
   skin/classic/browser/devtools/webconsole@2x.png               (../shared/devtools/images/webconsole@2x.png)
   skin/classic/browser/devtools/breadcrumbs-divider@2x.png      (../shared/devtools/images/breadcrumbs-divider@2x.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton.png    (../shared/devtools/images/breadcrumbs-scrollbutton.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
+  skin/classic/browser/devtools/animationinspector.css          (../shared/devtools/animationinspector.css)
 * skin/classic/browser/devtools/canvasdebugger.css          (devtools/canvasdebugger.css)
 * skin/classic/browser/devtools/debugger.css                (devtools/debugger.css)
   skin/classic/browser/devtools/eyedropper.css              (../shared/devtools/eyedropper.css)
 * skin/classic/browser/devtools/netmonitor.css              (devtools/netmonitor.css)
 * skin/classic/browser/devtools/profiler.css                (devtools/profiler.css)
 * skin/classic/browser/devtools/performance.css             (devtools/performance.css)
 * skin/classic/browser/devtools/timeline.css                (devtools/timeline.css)
   skin/classic/browser/devtools/timeline-filter.svg         (../shared/devtools/images/timeline-filter.svg)
--- a/browser/themes/shared/devedition.inc.css
+++ b/browser/themes/shared/devedition.inc.css
@@ -245,17 +245,17 @@ window:not([chromehidden~="toolbar"]) #u
   border: none;
 }
 
 /* Use smaller back button icon */
 #back-button {
   -moz-image-region: rect(0, 54px, 18px, 36px);
 }
 
-.search-go-button {
+searchbar:not([oneoffui]) .search-go-button {
    /* !important is needed because searchbar.css is loaded after this */
   -moz-image-region: auto !important;
   list-style-image: var(--search-button-image);
 }
 
 .tab-background {
   visibility: hidden;
 }
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/devtools/animationinspector.css
@@ -0,0 +1,149 @@
+body {
+  margin: 0;
+  padding: 0;
+}
+
+/* The error message, shown when an invalid/unanimated element is selected */
+
+#error-message {
+  margin-top: 10%;
+  text-align: center;
+
+  /* The error message is hidden by default */
+  display: none;
+}
+
+/* Element picker button */
+
+#element-picker {
+  position: relative;
+}
+
+#element-picker::before {
+  content: "";
+  display: block;
+  width: 16px;
+  height: 16px;
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  margin: -8px 0 0 -8px;
+  background-image: url("chrome://browser/skin/devtools/command-pick.png");
+}
+
+#element-picker[checked]::before {
+  background-position: -48px 0;
+  filter: none; /* Icon is blue when checked, don't invert for light theme */
+}
+
+@media (min-resolution: 2dppx) {
+  #element-picker::before {
+    background-image: url("chrome://browser/skin/devtools/command-pick@2x.png");
+    background-size: 64px;
+  }
+}
+
+/* Animation title gutter, contains the name, duration, iteration */
+
+.animation-title {
+  background-color: var(--theme-toolbar-background);
+  color: var(--theme-content-color3);
+  border-bottom: 1px solid var(--theme-splitter-color);
+  padding: 1px 4px;
+  word-wrap: break-word;
+  overflow: auto;
+}
+
+.animation-title .meta-data {
+  float: right;
+}
+
+.animation-title strong {
+  margin: 0 .5em;
+}
+
+/* Timeline wiget */
+
+.timeline {
+  height: 20px;
+  width: 100%;
+  display: flex;
+  flex-direction: row;
+  border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.timeline .playback-controls {
+  width: 50px;
+  display: flex;
+  flex-direction: row;
+}
+
+/* Playback control buttons */
+
+.timeline .playback-controls button {
+  flex-grow: 1;
+  border-width: 0 1px 0 0;
+}
+
+/* Play/pause button */
+
+.timeline .toggle::before {
+  background-image: url(debugger-pause.png);
+}
+
+.paused .timeline .toggle::before {
+  background-image: url(debugger-play.png);
+}
+
+@media (min-resolution: 2dppx) {
+  .timeline .toggle::before {
+    background-image: url(debugger-pause@2x.png);
+  }
+
+  .paused .timeline .toggle::before {
+    background-image: url(debugger-play@2x.png);
+  }
+}
+
+/* Slider (input type range) container */
+
+.timeline .sliders-container {
+  flex-grow: 1;
+  height: 100%;
+  position: relative;
+  border-width: 1px 0;
+}
+
+.timeline .sliders-container .current-time {
+  position: absolute;
+  padding: 0;
+  margin: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.timeline .sliders-container .current-time::-moz-range-thumb {
+  height: 100%;
+  width: 4px;
+  border-radius: 0;
+  border: none;
+  background: var(--theme-highlight-blue);
+}
+
+.timeline .sliders-container .current-time::-moz-range-track {
+  width: 100%;
+  height: 50px;
+  background: transparent;
+}
+
+/* Current time label */
+
+.timeline .time-display {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 50px;
+  border-left: 1px solid var(--theme-splitter-color);
+  background: var(--theme-toolbar-background);
+}
--- a/browser/themes/shared/devtools/images/performance-icons.svg
+++ b/browser/themes/shared/devtools/images/performance-icons.svg
@@ -2,40 +2,41 @@
 <style>
   g {
     fill: #edf0f1;
   }
   g:not(:target) {
     display: none;
   }
 </style>
-<g id="call-tree">
-  <rect x="1px" y="3.5px" width="14px" height="2px" rx="1" ry="1"/>
-  <rect x="1px" y="7.5px" width="7px" height="2px" rx="1" ry="1"/>
-  <rect x="11px" y="7.5px" width="4px" height="2px" rx="1" ry="1"/>
-  <rect x="4px" y="11.5px" width="4px" height="2px" rx="1" ry="1"/>
+<g id="overview-markers">
+  <rect x="0px" y="3px" width="5px" height="2.5px" rx="1" ry="1"/>
+  <rect x="7px" y="3px" width="9px" height="2.5px" rx="1" ry="1"/>
+  <rect x="0px" y="7px" width="9px" height="2.5px" rx="1" ry="1"/>
+  <rect x="10px" y="7px" width="6px" height="2.5px" rx="1" ry="1"/>
+  <rect x="4px" y="11px" width="5px" height="2.5px" rx="1" ry="1"/>
+  <rect x="12px" y="11px" width="4px" height="2.5px" rx="1" ry="1"/>
 </g>
-<g id="flamechart">
-  <rect x="1px" y="3px" width="14px" height="4px" style="shape-rendering: crispEdges"/>
-  <rect x="1px" y="5px" width="3px" height="5px" rx="1" ry="1"/>
-  <rect x="4px" y="5px" width="3px" height="10px" rx="1" ry="1"/>
-  <rect x="7px" y="5px" width="5px" height="3px" rx="1" ry="1"/>
-  <rect x="12px" y="5px" width="3px" height="7px" rx="1" ry="1"/>
+<g id="overview-frames">
+  <rect x="1px" y="4px" width="2px" height="12px" rx="1" ry="1"/>
+  <rect x="5px" y="12px" width="2px" height="4px" rx="1" ry="1"/>
+  <rect x="9px" y="9px" width="2px" height="7px" rx="1" ry="1"/>
+  <rect x="13px" y="7px" width="2px" height="9px" rx="1" ry="1"/>
 </g>
-<g id="frame">
-  <rect x="1px" y="4px" width="2px" height="13px" rx="1" ry="1"/>
-  <rect x="5px" y="12px" width="2px" height="5px" rx="1" ry="1"/>
-  <rect x="9px" y="9px" width="2px" height="8px" rx="1" ry="1"/>
-  <rect x="13px" y="7px" width="2px" height="10px" rx="1" ry="1"/>
+<g id="details-waterfall">
+  <rect x="0px" y="3px" width="9px" height="2.5px" rx="1" ry="1"/>
+  <rect x="5px" y="7px" width="8px" height="2.5px" rx="1" ry="1"/>
+  <rect x="7px" y="11px" width="9px" height="2.5px" rx="1" ry="1"/>
 </g>
-<g id="markers">
-  <path d="m2.1,2.1h9.6c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-9.6c-.6,0-1.1-.5-1.1-1.1 .1-.6 .5-1.1 1.1-1.1z"/>
-  <path d="m7.4,5.3h7.4c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-7.4c-.5-.1-1-.5-1-1.1 0-.6 .5-1.1 1-1.1z"/>
-  <path d="m5.3,8.5h3.2c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-3.2c-.6,0-1.1-.5-1.1-1.1 .1-.6 .5-1.1 1.1-1.1z"/>
-  <path d="m4.3,11.7h2.1c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-2.1c-.6,0-1.1-.5-1.1-1.1 0-.6 .5-1.1 1.1-1.1z"/>
-  <path d="m4.3,11.7h2.1c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-2.1c-.6,0-1.1-.5-1.1-1.1 0-.6 .5-1.1 1.1-1.1z" style="transform: translateX(7px)"/>
+<g id="details-call-tree">
+  <rect x="0px" y="3px" width="16px" height="2px"/>
+  <rect x="3px" y="6px" width="7px" height="2px"/>
+  <rect x="6px" y="9px" width="6px" height="2px"/>
+  <rect x="9px" y="12px" width="5px" height="2px"/>
 </g>
-<g id="waterfall">
-  <rect x="1px" y="3px" width="8px" height="2.5px" rx="1" ry="1"/>
-  <rect x="5px" y="7px" width="8px" height="2.5px" rx="1" ry="1"/>
-  <rect x="7px" y="11.5px" width="8px" height="2.5px" rx="1" ry="1"/>
+<g id="details-flamegraph">
+  <rect x="0px" y="3px" width="16px" height="2px"/>
+  <rect x="0px" y="6px" width="8px" height="2px"/>
+  <rect x="10px" y="6px" width="6px" height="2px"/>
+  <rect x="2px" y="9px" width="6px" height="2px"/>
+  <rect x="5px" y="12px" width="3px" height="2px"/>
 </g>
-</svg>
\ No newline at end of file
+</svg>
--- a/browser/themes/shared/devtools/netmonitor.inc.css
+++ b/browser/themes/shared/devtools/netmonitor.inc.css
@@ -146,21 +146,48 @@
 .theme-light .requests-menu-icon {
   outline: 1px solid @table_itemLightStartBorder@;
 }
 
 .requests-menu-file {
   text-align: start;
 }
 
-.requests-menu-domain {
+.requests-menu-security-and-domain {
   width: 14vw;
   min-width: 10em;
 }
 
+.requests-security-state-icon {
+  -moz-margin-end: 4px;
+  -moz-image-region:rect(0px, 16px, 16px, 0px);
+}
+
+.requests-security-state-icon:hover {
+  -moz-image-region: rect(0px, 32px, 16px, 16px);
+}
+
+.requests-security-state-icon:active {
+  -moz-image-region: rect(0px, 48px, 16px, 32px);
+}
+
+.security-state-insecure {
+  list-style-image: url(chrome://browser/skin/identity-icons-generic.png);
+}
+
+.security-state-secure {
+  cursor: pointer;
+  list-style-image: url(chrome://browser/skin/identity-icons-https.png);
+}
+
+.security-state-broken {
+  cursor: pointer;
+  list-style-image: url(chrome://browser/skin/identity-icons-https-mixed-active.png);
+}
+
 .requests-menu-type {
   text-align: center;
   width: 4em;
 }
 
 .requests-menu-size {
   text-align: center;
   width: 8em;
@@ -528,16 +555,29 @@ label.requests-menu-status-code {
   border: none;
   min-width: 1px;
 }
 
 #timings-tabpanel .requests-menu-timings-total {
   transition: transform 0.2s ease-out;
 }
 
+/* Security tabpanel */
+.security-info-section {
+  -moz-padding-start: 1em;
+}
+
+#security-tabpanel {
+  overflow: auto;
+}
+
+#security-error-message {
+  white-space: pre-wrap;
+}
+
 /* Custom request form */
 
 #custom-pane {
   padding: 0.6em 0.5em;
 }
 
 .custom-header {
   font-size: 1.1em;
@@ -773,17 +813,17 @@ label.requests-menu-status-code {
   .requests-menu-status-and-method {
     width: 16vw;
   }
 
   .requests-menu-icon-and-file {
     width: 30vw;
   }
 
-  .requests-menu-domain {
+  .requests-menu-security-and-domain {
     width: 30vw;
   }
 
   .requests-menu-type {
     width: 8vw;
   }
 
   .requests-menu-size {
--- a/browser/themes/shared/devtools/performance.inc.css
+++ b/browser/themes/shared/devtools/performance.inc.css
@@ -37,23 +37,26 @@
 
 #record-button[locked] {
   pointer-events: none;
 }
 
 /* Details Panel */
 
 #select-waterfall-view {
-  list-style-image: url(performance-icons.svg#waterfall);
+  list-style-image: url(performance-icons.svg#details-waterfall);
 }
 
 #select-calltree-view {
-  list-style-image: url(performance-icons.svg#call-tree);
+  list-style-image: url(performance-icons.svg#details-call-tree);
 }
 
+#select-flamegraph-view {
+  list-style-image: url(performance-icons.svg#details-flamegraph);
+}
 
 /* Profile call tree */
 
 .call-tree-cells-container {
   /* Hack: force hardware acceleration */
   transform: translateZ(1px);
   overflow: auto;
 }
@@ -326,31 +329,28 @@
 .waterfall-marker-location:hover,
 .waterfall-marker-location:focus {
    text-decoration: underline;
 }
 
 #waterfall-details {
   -moz-padding-start: 8px;
   -moz-padding-end: 8px;
-  padding-top: 8vh;
+  padding-top: 2vh;
   overflow: auto;
 }
 
 .marker-details-bullet {
   width: 8px;
   height: 8px;
-  margin: 0 8px;
   border: 1px solid;
   border-radius: 1px;
 }
 
-.marker-details-start,
-.marker-details-end,
-.marker-details-duration {
+#waterfall-details > * {
   padding-top: 3px;
 }
 
 .marker-details-labelname {
   -moz-padding-end: 4px;
 }
 
 .marker-details-type {
--- a/browser/themes/shared/devtools/timeline.inc.css
+++ b/browser/themes/shared/devtools/timeline.inc.css
@@ -212,30 +212,28 @@
 .waterfall-marker-location:hover,
 .waterfall-marker-location:focus {
    text-decoration: underline;
 }
 
 #timeline-waterfall-details {
   -moz-padding-start: 8px;
   -moz-padding-end: 8px;
-  padding-top: 8vh;
+  padding-top: 2vh;
   overflow: auto;
 }
 
 .marker-details-bullet {
   width: 8px;
   height: 8px;
   border: 1px solid;
   border-radius: 1px;
 }
 
-.marker-details-start,
-.marker-details-end,
-.marker-details-duration {
+#timeline-waterfall-details > * {
   padding-top: 3px;
 }
 
 .marker-details-labelname {
   -moz-padding-end: 4px;
 }
 
 .marker-details-type {
--- a/browser/themes/shared/devtools/toolbars.inc.css
+++ b/browser/themes/shared/devtools/toolbars.inc.css
@@ -239,16 +239,77 @@
 .devtools-toolbarbutton-group + .devtools-toolbarbutton {
   -moz-margin-start: 3px;
 }
 
 .devtools-separator + .devtools-toolbarbutton {
   -moz-margin-start: 1px;
 }
 
+/* HTML buttons, similar to toolbar buttons, but work in HTML documents */
+
+.devtools-button {
+  border: 0 solid var(--theme-splitter-color);
+  background: var(--theme-toolbar-background);
+  margin: 0;
+  padding: 0;
+  min-width: 32px;
+  min-height: 18px;
+  /* The icon is absolutely positioned in the button using ::before */
+  position: relative;
+}
+
+.devtools-button[standalone] {
+  min-height: 32px;
+  border-width: 1px;
+}
+
+/* Button States */
+.theme-dark .devtools-button:not([disabled]):hover {
+  background: rgba(0, 0, 0, .3); /* Splitters */
+}
+.theme-light .devtools-button:not([disabled]):hover {
+  background: rgba(170, 170, 170, .3); /* Splitters */
+}
+
+.theme-dark .devtools-button:not([disabled]):hover:active {
+  background: rgba(0, 0, 0, .4); /* Splitters */
+}
+.theme-light .devtools-button:not([disabled]):hover:active {
+  background: rgba(170, 170, 170, .4); /* Splitters */
+}
+
+/* Menu type buttons and checked states */
+.theme-dark .devtools-button[checked] {
+  background: rgba(29, 79, 115, .7) !important; /* Select highlight blue */
+  color: var(--theme-selection-color);
+}
+
+.theme-light .devtools-button[checked] {
+  background: rgba(76, 158, 217, .2) !important; /* Select highlight blue */
+}
+
+.devtools-button::before {
+  content: "";
+  display: block;
+  width: 16px;
+  height: 16px;
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  margin: -8px 0 0 -8px;
+  background-repeat: no-repeat;
+}
+
+@media (min-resolution: 2dppx) {
+  .devtools-button::before {
+    background-size: 32px;
+  }
+}
+
 /* Text input */
 
 .devtools-textinput,
 .devtools-searchinput {
   -moz-appearance: none;
   margin: 0 3px;
   border: 1px solid;
 %ifdef XP_MACOSX
@@ -817,17 +878,18 @@
 .theme-light #breadcrumb-separator-normal,
 .theme-light .scrollbutton-up > .toolbarbutton-icon,
 .theme-light .scrollbutton-down > .toolbarbutton-icon,
 .theme-light #black-boxed-message-button .button-icon,
 .theme-light #profiling-notice-button .button-icon,
 .theme-light #canvas-debugging-empty-notice-button .button-icon,
 .theme-light #requests-menu-perf-notice-button .button-icon,
 .theme-light #requests-menu-network-summary-button .button-icon,
-.theme-light .event-tooltip-debugger-icon {
+.theme-light .event-tooltip-debugger-icon,
+.theme-light .devtools-button::before {
   filter: url(filters.svg#invert);
 }
 
 /* Since selected backgrounds are blue, we want to use the normal
  * (light) icons. */
 .theme-light .command-button-invertable[checked=true]:not(:active) > image,
 .theme-light .devtools-tab[icon-invertable][selected] > image,
 .theme-light .devtools-tab[icon-invertable][highlighted] > image,
--- a/browser/themes/shared/devtools/widgets.inc.css
+++ b/browser/themes/shared/devtools/widgets.inc.css
@@ -930,36 +930,24 @@
 }
 
 .line-graph-widget-tooltip[arrow=end][with-arrows=true]::before {
   -moz-border-start: 3px solid rgba(255,255,255,0.75);
   right: -3px;
 }
 
 .line-graph-widget-tooltip[type=maximum] {
-  left: -1px;
+  left: 14px;
 }
 
 .line-graph-widget-tooltip[type=minimum] {
-  left: -1px;
+  left: 14px;
 }
 
 .line-graph-widget-tooltip[type=average] {
-  right: -1px;
-}
-
-.line-graph-widget-tooltip[type=maximum][with-arrows=true] {
-  left: 14px;
-}
-
-.line-graph-widget-tooltip[type=minimum][with-arrows=true] {
-  left: 14px;
-}
-
-.line-graph-widget-tooltip[type=average][with-arrows=true] {
   right: 4px;
 }
 
 .line-graph-widget-tooltip > [text=info] {
   color: #18191a;
 }
 
 .line-graph-widget-tooltip > [text=value] {
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -301,16 +301,17 @@ browser.jar:
         skin/classic/browser/devtools/editor-debug-location@2x.png     (../shared/devtools/images/editor-debug-location@2x.png)
 *       skin/classic/browser/devtools/webconsole.css                (devtools/webconsole.css)
         skin/classic/browser/devtools/webconsole_networkpanel.css   (devtools/webconsole_networkpanel.css)
         skin/classic/browser/devtools/webconsole.png                (../shared/devtools/images/webconsole.png)
         skin/classic/browser/devtools/webconsole@2x.png             (../shared/devtools/images/webconsole@2x.png)
         skin/classic/browser/devtools/breadcrumbs-divider@2x.png    (../shared/devtools/images/breadcrumbs-divider@2x.png)
         skin/classic/browser/devtools/breadcrumbs-scrollbutton.png  (../shared/devtools/images/breadcrumbs-scrollbutton.png)
         skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
+        skin/classic/browser/devtools/animationinspector.css        (../shared/devtools/animationinspector.css)
         skin/classic/browser/devtools/eyedropper.css                (../shared/devtools/eyedropper.css)
 *       skin/classic/browser/devtools/canvasdebugger.css            (devtools/canvasdebugger.css)
 *       skin/classic/browser/devtools/debugger.css                  (devtools/debugger.css)
 *       skin/classic/browser/devtools/netmonitor.css                (devtools/netmonitor.css)
 *       skin/classic/browser/devtools/profiler.css                  (devtools/profiler.css)
 *       skin/classic/browser/devtools/performance.css               (devtools/performance.css)
 *       skin/classic/browser/devtools/timeline.css                  (devtools/timeline.css)
         skin/classic/browser/devtools/timeline-filter.svg           (../shared/devtools/images/timeline-filter.svg)
@@ -762,16 +763,17 @@ browser.jar:
         skin/classic/aero/browser/devtools/editor-debug-location@2x.png  (../shared/devtools/images/editor-debug-location@2x.png)
 *       skin/classic/aero/browser/devtools/webconsole.css                  (devtools/webconsole.css)
         skin/classic/aero/browser/devtools/webconsole_networkpanel.css     (devtools/webconsole_networkpanel.css)
         skin/classic/aero/browser/devtools/webconsole.png                  (../shared/devtools/images/webconsole.png)
         skin/classic/aero/browser/devtools/webconsole@2x.png                  (../shared/devtools/images/webconsole@2x.png)
         skin/classic/aero/browser/devtools/breadcrumbs-divider@2x.png      (../shared/devtools/images/breadcrumbs-divider@2x.png)
         skin/classic/aero/browser/devtools/breadcrumbs-scrollbutton.png    (../shared/devtools/images/breadcrumbs-scrollbutton.png)
         skin/classic/aero/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
+        skin/classic/aero/browser/devtools/animationinspector.css    (../shared/devtools/animationinspector.css)
 *       skin/classic/aero/browser/devtools/canvasdebugger.css        (devtools/canvasdebugger.css)
 *       skin/classic/aero/browser/devtools/debugger.css              (devtools/debugger.css)
         skin/classic/aero/browser/devtools/eyedropper.css            (../shared/devtools/eyedropper.css)
 *       skin/classic/aero/browser/devtools/netmonitor.css            (devtools/netmonitor.css)
 *       skin/classic/aero/browser/devtools/profiler.css              (devtools/profiler.css)
 *       skin/classic/aero/browser/devtools/performance.css           (devtools/performance.css)
 *       skin/classic/aero/browser/devtools/timeline.css              (devtools/timeline.css)
         skin/classic/aero/browser/devtools/timeline-filter.svg       (../shared/devtools/images/timeline-filter.svg)
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -10743,33 +10743,34 @@ FactoryOp::UnblockQuotaManager()
 
 nsresult
 FactoryOp::CheckPermission(ContentParent* aContentParent,
                            PermissionRequestBase::PermissionValue* aPermission)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(mState == State_Initial || mState == State_PermissionRetry);
 
-  if (NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) {
+  const PrincipalInfo& principalInfo = mCommonParams.principalInfo();
+  if (principalInfo.type() != PrincipalInfo::TSystemPrincipalInfo &&
+      NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) {
     if (aContentParent) {
       // The DOM in the other process should have kept us from receiving any
       // indexedDB messages so assume that the child is misbehaving.
       aContentParent->KillHard();
     }
     return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR;
   }
 
   if (NS_WARN_IF(mCommonParams.privateBrowsingMode())) {
     // XXX This is only temporary.
     return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR;
   }
 
   PersistenceType persistenceType = mCommonParams.metadata().persistenceType();
 
-  const PrincipalInfo& principalInfo = mCommonParams.principalInfo();
   MOZ_ASSERT(principalInfo.type() != PrincipalInfo::TNullPrincipalInfo);
 
   if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
     MOZ_ASSERT(mState == State_Initial);
     MOZ_ASSERT(persistenceType == PERSISTENCE_TYPE_PERSISTENT);
 
     if (aContentParent) {
       // Check to make sure that the child process has access to the database it
--- a/dom/indexedDB/IDBFactory.cpp
+++ b/dom/indexedDB/IDBFactory.cpp
@@ -124,23 +124,25 @@ nsresult
 IDBFactory::CreateForWindow(nsPIDOMWindow* aWindow,
                             IDBFactory** aFactory)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aWindow);
   MOZ_ASSERT(aWindow->IsInnerWindow());
   MOZ_ASSERT(aFactory);
 
-  if (NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) {
+  nsCOMPtr<nsIPrincipal> principal;
+  nsresult rv = AllowedForWindowInternal(aWindow, getter_AddRefs(principal));
+
+  if (!(NS_SUCCEEDED(rv) && nsContentUtils::IsSystemPrincipal(principal)) &&
+      NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) {
     *aFactory = nullptr;
     return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR;
   }
 
-  nsCOMPtr<nsIPrincipal> principal;
-  nsresult rv = AllowedForWindowInternal(aWindow, getter_AddRefs(principal));
   if (rv == NS_ERROR_DOM_NOT_SUPPORTED_ERR) {
     NS_WARNING("IndexedDB is not permitted in a third-party window.");
     *aFactory = nullptr;
     return NS_OK;
   }
 
   if (NS_WARN_IF(NS_FAILED(rv))) {
     if (rv == NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR) {
@@ -256,18 +258,20 @@ IDBFactory::CreateForWorker(JSContext* a
 nsresult
 IDBFactory::CreateForMainThreadJSInternal(
                                        JSContext* aCx,
                                        JS::Handle<JSObject*> aOwningObject,
                                        nsAutoPtr<PrincipalInfo>& aPrincipalInfo,
                                        IDBFactory** aFactory)
 {
   MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aPrincipalInfo);
 
-  if (NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) {
+  if (aPrincipalInfo->type() != PrincipalInfo::TSystemPrincipalInfo &&
+      NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) {
     *aFactory = nullptr;
     return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR;
   }
 
   IndexedDatabaseManager* mgr = IndexedDatabaseManager::GetOrCreate();
   if (NS_WARN_IF(!mgr)) {
     IDB_REPORT_INTERNAL_ERR();
     return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
--- a/dom/plugins/base/nsPluginStreamListenerPeer.cpp
+++ b/dom/plugins/base/nsPluginStreamListenerPeer.cpp
@@ -1,22 +1,25 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 #include "nsPluginStreamListenerPeer.h"
+#include "nsIContentPolicy.h"
+#include "nsContentPolicyUtils.h"
 #include "nsIDOMElement.h"
 #include "nsIStreamConverterService.h"
 #include "nsIHttpChannel.h"
 #include "nsIHttpChannelInternal.h"
 #include "nsIFileChannel.h"
 #include "nsMimeTypes.h"
 #include "nsISupportsPrimitives.h"
 #include "nsNetCID.h"
+#include "nsPluginInstanceOwner.h"
 #include "nsPluginLogging.h"
 #include "nsIURI.h"
 #include "nsIURL.h"
 #include "nsPluginHost.h"
 #include "nsIByteRangeRequest.h"
 #include "nsIMultiPartChannel.h"
 #include "nsIInputStreamTee.h"
 #include "nsPrintfCString.h"
@@ -472,16 +475,47 @@ nsPluginStreamListenerPeer::OnStartReque
 
       if (!wantsAllNetworkStreams) {
         mRequestFailed = true;
         return NS_ERROR_FAILURE;
       }
     }
   }
 
+  nsAutoCString contentType;
+  rv = channel->GetContentType(contentType);
+  if (NS_FAILED(rv))
+    return rv;
+
+  // Check ShouldProcess with content policy
+  nsRefPtr<nsPluginInstanceOwner> owner;
+  if (mPluginInstance) {
+    owner = mPluginInstance->GetOwner();
+  }
+  nsCOMPtr<nsIDOMElement> element;
+  nsCOMPtr<nsIDocument> doc;
+  if (owner) {
+    owner->GetDOMElement(getter_AddRefs(element));
+    owner->GetDocument(getter_AddRefs(doc));
+  }
+  nsCOMPtr<nsIPrincipal> principal = doc ? doc->NodePrincipal() : nullptr;
+
+  int16_t shouldLoad = nsIContentPolicy::ACCEPT;
+  rv = NS_CheckContentProcessPolicy(nsIContentPolicy::TYPE_OBJECT_SUBREQUEST,
+                                    mURL,
+                                    principal,
+                                    element,
+                                    contentType,
+                                    nullptr,
+                                    &shouldLoad);
+  if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) {
+    mRequestFailed = true;
+    return NS_ERROR_CONTENT_BLOCKED;
+  }
+
   // Get the notification callbacks from the channel and save it as
   // week ref we'll use it in nsPluginStreamInfo::RequestRead() when
   // we'll create channel for byte range request.
   nsCOMPtr<nsIInterfaceRequestor> callbacks;
   channel->GetNotificationCallbacks(getter_AddRefs(callbacks));
   if (callbacks)
     mWeakPtrChannelCallbacks = do_GetWeakReference(callbacks);
 
@@ -504,35 +538,30 @@ nsPluginStreamListenerPeer::OnStartReque
       return NS_ERROR_FAILURE;
     }
     mLength = 0;
   }
   else {
     mLength = uint32_t(length);
   }
 
-  nsAutoCString aContentType; // XXX but we already got the type above!
-  rv = channel->GetContentType(aContentType);
-  if (NS_FAILED(rv))
-    return rv;
-
   nsCOMPtr<nsIURI> aURL;
   rv = channel->GetURI(getter_AddRefs(aURL));
   if (NS_FAILED(rv))
     return rv;
 
   aURL->GetSpec(mURLSpec);
 
-  if (!aContentType.IsEmpty())
-    mContentType = aContentType;
+  if (!contentType.IsEmpty())
+    mContentType = contentType;
 
 #ifdef PLUGIN_LOGGING
   PR_LOG(nsPluginLogging::gPluginLog, PLUGIN_LOG_NOISY,
          ("nsPluginStreamListenerPeer::OnStartRequest this=%p request=%p mime=%s, url=%s\n",
-          this, request, aContentType.get(), mURLSpec.get()));
+          this, request, contentType.get(), mURLSpec.get()));
 
   PR_LogFlush();
 #endif
 
   // Set up the stream listener...
   rv = SetUpStreamListener(request, aURL);
   if (NS_FAILED(rv)) {
     return rv;
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -659,19 +659,28 @@ pref("ui.scrolling.min_scrollable_distan
 pref("ui.scrolling.axis_lock_mode", "standard");
 // Negate scrollY, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
 pref("ui.scrolling.negate_wheel_scrollY", true);
 // Determine the dead zone for gamepad joysticks. Higher values result in larger dead zones; use a negative value to
 // auto-detect based on reported hardware values
 pref("ui.scrolling.gamepad_dead_zone", 115);
 
 // Prefs for fling acceleration
-pref("ui.scrolling.fling_accel_interval", 500);
-pref("ui.scrolling.fling_accel_base_multiplier", "1.0");
-pref("ui.scrolling.fling_accel_supplemental_multiplier", "1.0");
+pref("ui.scrolling.fling_accel_interval", -1);
+pref("ui.scrolling.fling_accel_base_multiplier", -1);
+pref("ui.scrolling.fling_accel_supplemental_multiplier", -1);
+
+// Prefs for fling curving
+pref("ui.scrolling.fling_curve_function_x1", -1);
+pref("ui.scrolling.fling_curve_function_y1", -1);
+pref("ui.scrolling.fling_curve_function_x2", -1);
+pref("ui.scrolling.fling_curve_function_y2", -1);
+pref("ui.scrolling.fling_curve_threshold_velocity", -1);
+pref("ui.scrolling.fling_curve_max_velocity", -1);
+pref("ui.scrolling.fling_curve_newton_iterations", -1);
 
 // Enable accessibility mode if platform accessibility is enabled.
 pref("accessibility.accessfu.activate", 2);
 pref("accessibility.accessfu.quicknav_modes", "Link,Heading,FormElement,Landmark,ListItem");
 // Active quicknav mode, index value of list from quicknav_modes
 pref("accessibility.accessfu.quicknav_index", 0);
 // Setting for an utterance order (0 - description first, 1 - description last).
 pref("accessibility.accessfu.utterance", 1);
--- a/mobile/android/base/FindInPageBar.java
+++ b/mobile/android/base/FindInPageBar.java
@@ -91,16 +91,19 @@ public class FindInPageBar extends Linea
         mFindText.requestFocus();
 
         // handleMessage() receives response message and determines initial state of softInput
         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:Get", REQUEST_ID));
         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FindInPage:Opened", null));
     }
 
     public void hide() {
+        // Always clear the Find string, primarily for privacy.
+        mFindText.setText("");
+
         setVisibility(GONE);
         getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FindInPage:Closed", null));
     }
 
     private InputMethodManager getInputMethodManager(View view) {
         Context context = view.getContext();
         return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
--- a/mobile/android/base/gfx/Axis.java
+++ b/mobile/android/base/gfx/Axis.java
@@ -29,16 +29,23 @@ abstract class Axis {
     private static final String PREF_SCROLLING_FRICTION_FAST = "ui.scrolling.friction_fast";
     private static final String PREF_SCROLLING_MAX_EVENT_ACCELERATION = "ui.scrolling.max_event_acceleration";
     private static final String PREF_SCROLLING_OVERSCROLL_DECEL_RATE = "ui.scrolling.overscroll_decel_rate";
     private static final String PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT = "ui.scrolling.overscroll_snap_limit";
     private static final String PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE = "ui.scrolling.min_scrollable_distance";
     private static final String PREF_FLING_ACCEL_INTERVAL = "ui.scrolling.fling_accel_interval";
     private static final String PREF_FLING_ACCEL_BASE_MULTIPLIER = "ui.scrolling.fling_accel_base_multiplier";
     private static final String PREF_FLING_ACCEL_SUPPLEMENTAL_MULTIPLIER = "ui.scrolling.fling_accel_supplemental_multiplier";
+    private static final String PREF_FLING_CURVE_FUNCTION_X1 = "ui.scrolling.fling_curve_function_x1";
+    private static final String PREF_FLING_CURVE_FUNCTION_Y1 = "ui.scrolling.fling_curve_function_y1";
+    private static final String PREF_FLING_CURVE_FUNCTION_X2 = "ui.scrolling.fling_curve_function_x2";
+    private static final String PREF_FLING_CURVE_FUNCTION_Y2 = "ui.scrolling.fling_curve_function_y2";
+    private static final String PREF_FLING_CURVE_THRESHOLD_VELOCITY = "ui.scrolling.fling_curve_threshold_velocity";
+    private static final String PREF_FLING_CURVE_MAXIMUM_VELOCITY = "ui.scrolling.fling_curve_max_velocity";
+    private static final String PREF_FLING_CURVE_NEWTON_ITERATIONS = "ui.scrolling.fling_curve_newton_iterations";
 
     // This fraction of velocity remains after every animation frame when the velocity is low.
     private static float FRICTION_SLOW;
     // This fraction of velocity remains after every animation frame when the velocity is high.
     private static float FRICTION_FAST;
     // Below this velocity (in pixels per frame), the friction starts increasing from FRICTION_FAST
     // to FRICTION_SLOW.
     private static float VELOCITY_THRESHOLD;
@@ -59,16 +66,37 @@ abstract class Axis {
     private static long FLING_ACCEL_INTERVAL;
 
     // The multiplication constant of the base velocity in case of accelerated scrolling.
     private static float FLING_ACCEL_BASE_MULTIPLIER;
 
     // The multiplication constant of the supplemental velocity in case of accelerated scrolling.
     private static float FLING_ACCEL_SUPPLEMENTAL_MULTIPLIER;
 
+    // x co-ordinate of the second bezier control point
+    private static float FLING_CURVE_FUNCTION_X1;
+
+    // y co-ordinate of the second bezier control point
+    private static float FLING_CURVE_FUNCTION_Y1;
+
+    // x co-ordinate of the third bezier control point
+    private static float FLING_CURVE_FUNCTION_X2;
+
+    // y co-ordinate of the third bezier control point
+    private static float FLING_CURVE_FUNCTION_Y2;
+
+    // Minimum velocity for curve to be implemented i.e fling curving
+    private static float FLING_CURVE_THRESHOLD_VELOCITY;
+
+    // Maximum permitted velocity
+    private static float FLING_CURVE_MAXIMUM_VELOCITY;
+
+    // Number of iterations in the Newton-Raphson method
+    private static int FLING_CURVE_NEWTON_ITERATIONS;
+
     private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
         Integer value = (prefs == null ? null : prefs.get(prefName));
         return (value == null || value < 0 ? defaultValue : value) / 1000f;
     }
 
     private static int getIntPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
         Integer value = (prefs == null ? null : prefs.get(prefName));
         return (value == null || value < 0 ? defaultValue : value);
@@ -78,17 +106,24 @@ abstract class Axis {
         final String[] prefs = { PREF_SCROLLING_FRICTION_FAST,
                                  PREF_SCROLLING_FRICTION_SLOW,
                                  PREF_SCROLLING_MAX_EVENT_ACCELERATION,
                                  PREF_SCROLLING_OVERSCROLL_DECEL_RATE,
                                  PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT,
                                  PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE,
                                  PREF_FLING_ACCEL_INTERVAL,
                                  PREF_FLING_ACCEL_BASE_MULTIPLIER,
-                                 PREF_FLING_ACCEL_SUPPLEMENTAL_MULTIPLIER };
+                                 PREF_FLING_ACCEL_SUPPLEMENTAL_MULTIPLIER,
+                                 PREF_FLING_CURVE_FUNCTION_X1,
+                                 PREF_FLING_CURVE_FUNCTION_Y1,
+                                 PREF_FLING_CURVE_FUNCTION_X2,
+                                 PREF_FLING_CURVE_FUNCTION_Y2,
+                                 PREF_FLING_CURVE_THRESHOLD_VELOCITY,
+                                 PREF_FLING_CURVE_MAXIMUM_VELOCITY,
+                                 PREF_FLING_CURVE_NEWTON_ITERATIONS };
 
         PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
             Map<String, Integer> mPrefs = new HashMap<String, Integer>();
 
             @Override public void prefValue(String name, int value) {
                 mPrefs.put(name, value);
             }
 
@@ -115,16 +150,24 @@ abstract class Axis {
         VELOCITY_THRESHOLD = 10 / FRAMERATE_MULTIPLIER;
         MAX_EVENT_ACCELERATION = getFloatPref(prefs, PREF_SCROLLING_MAX_EVENT_ACCELERATION, GeckoAppShell.getDpi() > 300 ? 100 : 40);
         OVERSCROLL_DECEL_RATE = getFloatPref(prefs, PREF_SCROLLING_OVERSCROLL_DECEL_RATE, 40);
         SNAP_LIMIT = getFloatPref(prefs, PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT, 300);
         MIN_SCROLLABLE_DISTANCE = getFloatPref(prefs, PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE, 500);
         FLING_ACCEL_INTERVAL = getIntPref(prefs, PREF_FLING_ACCEL_INTERVAL, 500);
         FLING_ACCEL_BASE_MULTIPLIER = getFloatPref(prefs, PREF_FLING_ACCEL_BASE_MULTIPLIER, 1000);
         FLING_ACCEL_SUPPLEMENTAL_MULTIPLIER = getFloatPref(prefs, PREF_FLING_ACCEL_SUPPLEMENTAL_MULTIPLIER, 1000);
+        FLING_CURVE_FUNCTION_X1 = getFloatPref(prefs, PREF_FLING_CURVE_FUNCTION_X1, 410);
+        FLING_CURVE_FUNCTION_Y1 = getFloatPref(prefs, PREF_FLING_CURVE_FUNCTION_Y1, 0);
+        FLING_CURVE_FUNCTION_X2 = getFloatPref(prefs, PREF_FLING_CURVE_FUNCTION_X2, 800);
+        FLING_CURVE_FUNCTION_Y2 = getFloatPref(prefs, PREF_FLING_CURVE_FUNCTION_Y2, 1000);
+        FLING_CURVE_THRESHOLD_VELOCITY = getFloatPref(prefs, PREF_FLING_CURVE_THRESHOLD_VELOCITY, 30);
+        FLING_CURVE_MAXIMUM_VELOCITY = getFloatPref(prefs, PREF_FLING_CURVE_MAXIMUM_VELOCITY, 70);
+        FLING_CURVE_NEWTON_ITERATIONS = getIntPref(prefs, PREF_FLING_CURVE_NEWTON_ITERATIONS, 5);
+
         Log.i(LOGTAG, "Prefs: " + FRICTION_SLOW + "," + FRICTION_FAST + "," + VELOCITY_THRESHOLD + ","
                 + MAX_EVENT_ACCELERATION + "," + OVERSCROLL_DECEL_RATE + "," + SNAP_LIMIT + "," + MIN_SCROLLABLE_DISTANCE);
     }
 
     static {
         // set the scrolling parameters to default values on startup
         setPrefs(null);
     }
@@ -207,19 +250,68 @@ abstract class Axis {
     void setScrollingDisabled(boolean disabled) {
         mScrollingDisabled = disabled;
     }
 
     void saveTouchPos() {
         mLastTouchPos = mTouchPos;
     }
 
+    // Calculates and return the slope of the curve at given parameter t
+    float getSlope(float t) {
+        float y1 = FLING_CURVE_FUNCTION_Y1;
+        float y2 = FLING_CURVE_FUNCTION_Y2;
+
+        return (3 * y1)
+             + t * (6 * y2 - 12 * y1)
+             + t * t * (9 * y1 - 9 * y2 + 3);
+    }
+
+    // Calculates and returns the value of the bezier curve with the given parameter t and control points p1 and p2
+    float cubicBezier(float p1, float p2, float t) {
+        return (3 * t * (1-t) * (1-t) * p1)
+             + (3 * t * t * (1-t) * p2)
+             + (t * t * t);
+    }
+
+    // Responsible for mapping the physical velocity to a the velocity obtained after applying bezier curve (with control points (X1,Y1) and (X2,Y2))
+    float flingCurve(float By) {
+        int ni = FLING_CURVE_NEWTON_ITERATIONS;
+        float[] guess = new float[ni];
+        float y1 = FLING_CURVE_FUNCTION_Y1;
+        float y2 = FLING_CURVE_FUNCTION_Y2;
+        guess[0] = By;
+
+        for (int i = 1; i < ni; i++) {
+            guess[i] = guess[i-1] - (cubicBezier(y1, y2, guess[i-1]) - By) / getSlope(guess[i-1]);
+        }
+        // guess[4] is the final approximate root the cubic equation.
+        float t = guess[4];
+
+        float x1 = FLING_CURVE_FUNCTION_X1;
+        float x2 = FLING_CURVE_FUNCTION_X2;
+        return cubicBezier(x1, x2, t);
+    }
+
     void updateWithTouchAt(float pos, float timeDelta) {
+        float curveVelocityThreshold = FLING_CURVE_THRESHOLD_VELOCITY * GeckoAppShell.getDpi() * MS_PER_FRAME;
+        float maxVelocity = FLING_CURVE_MAXIMUM_VELOCITY * GeckoAppShell.getDpi() * MS_PER_FRAME;
+
         float newVelocity = (mTouchPos - pos) / timeDelta * MS_PER_FRAME;
 
+        if (Math.abs(newVelocity) > curveVelocityThreshold && Math.abs(newVelocity) < maxVelocity) {
+            float sign = Math.signum(newVelocity);
+            newVelocity = newVelocity * sign;
+            float scale = maxVelocity - curveVelocityThreshold;
+            float functInp = (newVelocity - curveVelocityThreshold) / scale;
+            float functOut = flingCurve(functInp);
+            newVelocity = functOut * scale + curveVelocityThreshold;
+            newVelocity = newVelocity * sign;
+        }
+
         mRecentVelocities[mRecentVelocityCount % FLING_VELOCITY_POINTS] = newVelocity;
         mRecentVelocityCount++;
 
         // If there's a direction change, or current velocity is very low,
         // allow setting of the velocity outright. Otherwise, use the current
         // velocity and a maximum change factor to set the new velocity.
         boolean curVelocityIsLow = Math.abs(mVelocity) < 1.0f / FRAMERATE_MULTIPLIER;
         boolean directionChange = (mVelocity > 0) != (newVelocity > 0);
--- a/mobile/android/base/gfx/ImmutableViewportMetrics.java
+++ b/mobile/android/base/gfx/ImmutableViewportMetrics.java
@@ -228,22 +228,22 @@ public class ImmutableViewportMetrics {
 
     public ImmutableViewportMetrics offsetViewportBy(float dx, float dy) {
         return setViewportOrigin(viewportRectLeft + dx, viewportRectTop + dy);
     }
 
     public ImmutableViewportMetrics offsetViewportByAndClamp(float dx, float dy) {
         if (isRTL) {
             return setViewportOrigin(
-                Math.min(pageRectRight - getWidthWithoutMargins(), Math.max(viewportRectLeft + dx, pageRectLeft)),
-                Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeightWithoutMargins())));
+                Math.min(pageRectRight - getWidth(), Math.max(viewportRectLeft + dx, pageRectLeft)),
+                Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
         }
         return setViewportOrigin(
-            Math.max(pageRectLeft, Math.min(viewportRectLeft + dx, pageRectRight - getWidthWithoutMargins())),
-            Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeightWithoutMargins())));
+            Math.max(pageRectLeft, Math.min(viewportRectLeft + dx, pageRectRight - getWidth())),
+            Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
     }
 
     public ImmutableViewportMetrics setPageRect(RectF pageRect, RectF cssPageRect) {
         return new ImmutableViewportMetrics(
             pageRect.left, pageRect.top, pageRect.right, pageRect.bottom,
             cssPageRect.left, cssPageRect.top, cssPageRect.right, cssPageRect.bottom,
             viewportRectLeft, viewportRectTop, viewportRectRight, viewportRectBottom,
             marginLeft, marginTop, marginRight, marginBottom,
--- a/mobile/android/chrome/content/about.js
+++ b/mobile/android/chrome/content/about.js
@@ -93,16 +93,20 @@ function init() {
 
   let updateLink = document.getElementById("updateLink");
   let checkingSpan = document.getElementById("update-message-checking");
   let noneSpan = document.getElementById("update-message-none");
   let foundSpan = document.getElementById("update-message-found");
   let downloadingSpan = document.getElementById("update-message-downloading");
   let downloadedSpan = document.getElementById("update-message-downloaded");
 
+  updateLink.onclick = checkForUpdates;
+  foundSpan.onclick = downloadUpdate;
+  downloadedSpan.onclick = installUpdate;
+
   function showCheckAction() {
     checkingSpan.style.display = "none";
     noneSpan.style.display = "none";
     foundSpan.style.display = "none";
     downloadingSpan.style.display = "none";
     downloadedSpan.style.display = "none";
     updateLink.style.display = "block";
   }
--- a/mobile/android/chrome/content/about.xhtml
+++ b/mobile/android/chrome/content/about.xhtml
@@ -26,22 +26,22 @@
     <div id="wordmark"></div>
 #expand <p id="version">__MOZ_APP_VERSION__</p>
   </div>
 
   <div id="banner">
     <div id="logo"/>
 #ifdef MOZ_UPDATER
     <div id="updateBox">
-      <a id="updateLink" href="" onclick="checkForUpdates();">&aboutPage.checkForUpdates.link;</a>
+      <a id="updateLink" href="">&aboutPage.checkForUpdates.link;</a>
       <span id="update-message-checking">&aboutPage.checkForUpdates.checking;</span>
       <span id="update-message-none">&aboutPage.checkForUpdates.none;</span>
-      <span id="update-message-found" onclick="downloadUpdate()">&aboutPage.checkForUpdates.available2;</span>
+      <span id="update-message-found">&aboutPage.checkForUpdates.available2;</span>
       <span id="update-message-downloading">&aboutPage.checkForUpdates.downloading;</span>
-      <span id="update-message-downloaded" onclick="installUpdate()">&aboutPage.checkForUpdates.downloaded2;</span>
+      <span id="update-message-downloaded">&aboutPage.checkForUpdates.downloaded2;</span>
     </div>
 #endif
 
     <div id="messages">
       <p id="distributionAbout" hidden="true"/>
       <p id="distributionID" hidden="true"/>
       <p id="telemetry" hidden="true">
         &aboutPage.warningVersion;
--- a/testing/config/mozharness/android_arm_config.py
+++ b/testing/config/mozharness/android_arm_config.py
@@ -100,15 +100,15 @@ config = {
         "xpcshell": {
             "run_filename": "remotexpcshelltests.py",
             "testsdir": "xpcshell",
             "options": ["--deviceIP=%(device_ip)s", "--devicePort=%(device_port)s",
                 "--xre-path=%(xre_path)s", "--testing-modules-dir=%(modules_dir)s",
                 "--apk=%(installer_path)s", "--no-logfiles",
                 "--symbols-path=%(symbols_path)s",
                 "--manifest=tests/xpcshell.ini",
+                "--log-raw=%(raw_log_file)s",
                 # Bug 1064002 - Land once mozharness changes land
-                #"--log-raw=%(raw_log_file)s",
                 #"--total-chunks=3",
             ],
         },
     }, # end suite_definitions
 }
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5891,16 +5891,21 @@
     "kind": "boolean",
     "description": "How many times has the devtool's Layout View been opened?"
   },
   "DEVTOOLS_FONTINSPECTOR_OPENED_BOOLEAN": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "How many times has the devtool's Font Inspector been opened?"
   },
+  "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_BOOLEAN": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "How many times has the devtool's Animation Inspector been opened?"
+  },
   "DEVTOOLS_JSDEBUGGER_OPENED_BOOLEAN": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "How many times has the devtool's Debugger been opened?"
   },
   "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_BOOLEAN": {
     "expires_in_version": "never",
     "kind": "boolean",
@@ -6031,16 +6036,21 @@
     "kind": "flag",
     "description": "How many users have opened the devtool's Layout View?"
   },
   "DEVTOOLS_FONTINSPECTOR_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "How many users have opened the devtool's Font Inspector?"
   },
+  "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_PER_USER_FLAG": {
+    "expires_in_version": "never",
+    "kind": "flag",
+    "description": "How many users have opened the devtool's Animation Inspector?"
+  },
   "DEVTOOLS_JSDEBUGGER_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "How many users have opened the devtool's Debugger?"
   },
   "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
@@ -6189,16 +6199,23 @@
   },
   "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000000",
     "n_buckets": 100,
     "description": "How long has the font inspector been active (seconds)"
   },
+  "DEVTOOLS_ANIMATIONINSPECTOR_TIME_ACTIVE_SECONDS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": "10000000",
+    "n_buckets": 100,
+    "description": "How long has the animation inspector been active (seconds)"
+  },
   "DEVTOOLS_JSDEBUGGER_TIME_ACTIVE_SECONDS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000000",
     "n_buckets": 100,
     "description": "How long has the JS debugger been active (seconds)"
   },
   "DEVTOOLS_JSBROWSERDEBUGGER_TIME_ACTIVE_SECONDS": {
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -1750,16 +1750,30 @@ NetworkEventActor.prototype =
     return {
       from: this.actorID,
       postData: this._request.postData,
       postDataDiscarded: this._discardRequestBody,
     };
   },
 
   /**
+   * The "getSecurityInfo" packet type handler.
+   *
+   * @return object
+   *         The response packet - connection security information.
+   */
+  onGetSecurityInfo: function NEA_onGetSecurityInfo()
+  {
+    return {
+      from: this.actorID,
+      securityInfo: this._securityInfo,
+    };
+  },
+
+  /**
    * The "getResponseHeaders" packet type handler.
    *
    * @return object
    *         The response packet - network response headers.
    */
   onGetResponseHeaders: function NEA_onGetResponseHeaders()
   {
     return {
@@ -1905,16 +1919,36 @@ NetworkEventActor.prototype =
       updateType: "responseStart",
       response: aInfo,
     };
 
     this.conn.send(packet);
   },
 
   /**
+   * Add connection security information.
+   *
+   * @param object info
+   *        The object containing security information.
+   */
+  addSecurityInfo: function NEA_addSecurityInfo(info)
+  {
+    this._securityInfo = info;
+
+    let packet = {
+      from: this.actorID,
+      type: "networkEventUpdate",
+      updateType: "securityInfo",
+      state: info.state,
+    };
+
+    this.conn.send(packet);
+  },
+
+  /**
    * Add network response headers.
    *
    * @param array aHeaders
    *        The response headers array.
    */
   addResponseHeaders: function NEA_addResponseHeaders(aHeaders)
   {
     this._response.headers = aHeaders;
@@ -2027,9 +2061,10 @@ NetworkEventActor.prototype.requestTypes
   "release": NetworkEventActor.prototype.onRelease,
   "getRequestHeaders": NetworkEventActor.prototype.onGetRequestHeaders,
   "getRequestCookies": NetworkEventActor.prototype.onGetRequestCookies,
   "getRequestPostData": NetworkEventActor.prototype.onGetRequestPostData,
   "getResponseHeaders": NetworkEventActor.prototype.onGetResponseHeaders,
   "getResponseCookies": NetworkEventActor.prototype.onGetResponseCookies,
   "getResponseContent": NetworkEventActor.prototype.onGetResponseContent,
   "getEventTimings": NetworkEventActor.prototype.onGetEventTimings,
+  "getSecurityInfo": NetworkEventActor.prototype.onGetSecurityInfo,
 };
--- a/toolkit/devtools/server/tests/browser/browser_animation_actors_03.js
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_03.js
@@ -38,42 +38,42 @@ function* playerHasAnInitialState(walker
   ok("duration" in player.initialState, "Player's state has duration");
   ok("iterationCount" in player.initialState, "Player's state has iterationCount");
   ok("isRunningOnCompositor" in player.initialState, "Player's state has isRunningOnCompositor");
 }
 
 function* playerStateIsCorrect(walker, front) {
   info("Checking the state of the simple animation");
 
-  let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
-  let [player] = yield front.getAnimationPlayersForNode(node);
-  let state = player.initialState;
-
+  let state = yield getAnimationStateForNode(walker, front, ".simple-animation", 0);
   is(state.name, "move", "Name is correct");
   is(state.duration, 2000, "Duration is correct");
   // null = infinite count
   is(state.iterationCount, null, "Iteration count is correct");
   is(state.playState, "running", "PlayState is correct");
 
   info("Checking the state of the transition");
 
-  node = yield walker.querySelector(walker.rootNode, ".transition");
-  [player] = yield front.getAnimationPlayersForNode(node);
-  state = player.initialState;
-
+  state = yield getAnimationStateForNode(walker, front, ".transition", 0);
   is(state.name, "", "Transition has no name");
   is(state.duration, 5000, "Transition duration is correct");
   // transitions run only once
   is(state.iterationCount, 1, "Transition iteration count is correct");
   is(state.playState, "running", "Transition playState is correct");
 
   info("Checking the state of one of multiple animations on a node");
 
-  node = yield walker.querySelector(walker.rootNode, ".multiple-animations");
   // Checking the 2nd player
-  [, player] = yield front.getAnimationPlayersForNode(node);
-  state = player.initialState;
-
+  state = yield getAnimationStateForNode(walker, front, ".multiple-animations", 1);
   is(state.name, "glow", "The 2nd animation's name is correct");
   is(state.duration, 1000, "The 2nd animation's duration is correct");
   is(state.iterationCount, 5, "The 2nd animation's iteration count is correct");
   is(state.playState, "running", "The 2nd animation's playState is correct");
 }
+
+function* getAnimationStateForNode(walker, front, nodeSelector, playerIndex) {
+  let node = yield walker.querySelector(walker.rootNode, nodeSelector);
+  let players = yield front.getAnimationPlayersForNode(node);
+  let player = players[playerIndex];
+  yield player.ready();
+  let state = yield player.getCurrentState();
+  return state;
+}
--- a/toolkit/devtools/webconsole/client.js
+++ b/toolkit/devtools/webconsole/client.js
@@ -362,16 +362,33 @@ WebConsoleClient.prototype = {
     let packet = {
       to: aActor,
       type: "getEventTimings",
     };
     this._client.request(packet, aOnResponse);
   },
 
   /**
+   * Retrieve the security information for the given NetworkEventActor.
+   *
+   * @param string aActor
+   *        The NetworkEventActor ID.
+   * @param function aOnResponse
+   *        The function invoked when the response is received.
+   */
+  getSecurityInfo: function WCC_getSecurityInfo(aActor, aOnResponse)
+  {
+    let packet = {
+      to: aActor,
+      type: "getSecurityInfo",
+    };
+    this._client.request(packet, aOnResponse);
+  },
+
+  /**
    * Send a HTTP request with the given data.
    *
    * @param string aData
    *        The details of the HTTP request.
    * @param function aOnResponse
    *        The function invoked when the response is received.
    */
   sendHTTPRequest: function WCC_sendHTTPRequest(aData, aOnResponse) {
--- a/toolkit/devtools/webconsole/network-helper.js
+++ b/toolkit/devtools/webconsole/network-helper.js
@@ -51,16 +51,17 @@
  *  Steven Roussey (AppCenter Inc, Network54)
  *  Mihai Sucan (Mozilla Corp.)
  */
 
 "use strict";
 
 const {components, Cc, Ci, Cu} = require("chrome");
 loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
+loader.lazyImporter(this, "DevToolsUtils", "resource://gre/modules/devtools/DevToolsUtils.jsm");
 
 /**
  * Helper object for networking stuff.
  *
  * Most of the following functions have been taken from the Firebug source. They
  * have been modified to match the Firefox coding rules.
  */
 let NetworkHelper = {
@@ -475,13 +476,214 @@ let NetworkHelper = {
       case "svg":
       case "xml":
         return true;
 
       default:
         return false;
     }
   },
+
+  /**
+   * Takes a securityInfo object of nsIRequest, the nsIRequest itself and
+   * extracts security information from them.
+   *
+   * @param object securityInfo
+   *        The securityInfo object of a request. If null channel is assumed
+   *        to be insecure.
+   * @param nsIRequest request
+   *        The nsIRequest object for the request used to dig more information
+   *        about this request.
+   *
+   * @return object
+   *         Returns an object containing following members:
+   *          - state: The security of the connection used to fetch this
+   *                   request. Has one of following string values:
+   *                    * "insecure": the connection was not secure (only http)
+   *                    * "broken": secure connection failed (e.g. expired cert)
+   *                    * "secure": the connection was properly secured.
+   *          If state == broken:
+   *            - errorMessage: full error message from nsITransportSecurityInfo.
+   *          If state == secure:
+   *            - protocolVersion: one of SSLv3, TLSv1, TLSv1.1, TLSv1.2.
+   *            - cipherSuite: the cipher suite used in this connection.
+   *            - cert: information about certificate used in this connection.
+   *                    See parseCertificateInfo for the contents.
+   *            - hsts: true if host uses Strict Transport Security, false otherwise
+   *            - hpkp: true if host uses Public Key Pinning, false otherwise
+   */
+  parseSecurityInfo: function NH_parseSecurityInfo(securityInfo, request) {
+    const info = {
+      state: "insecure",
+    };
+
+    // The request did not contain any security info.
+    if (!securityInfo) {
+      return info;
+    }
+
+    /**
+     * Different scenarios to consider here and how they are handled:
+     * - request is HTTP, the connection is not secure
+     *   => securityInfo is null
+     *      => state === "insecure"
+     *
+     * - request is HTTPS, the connection is secure
+     *   => .securityState has STATE_IS_SECURE flag
+     *      => state === "secure"
+     *
+     * - request is HTTPS, the connection has security issues
+     *   => .securityState has STATE_IS_INSECURE flag
+     *   => .errorCode is an NSS error code.
+     *      => state === "broken"
+     *
+     * - request is HTTPS, the connection was terminated before the security
+     *   could be validated
+     *   => .securityState has STATE_IS_INSECURE flag
+     *   => .errorCode is NOT an NSS error code.
+     *   => .errorMessage is not available.
+     *      => state === "insecure"
+     *
+     * - request is HTTPS but it uses a weak cipher or old protocol, see
+     *   http://hg.mozilla.org/mozilla-central/annotate/def6ed9d1c1a/
+     *   security/manager/ssl/src/nsNSSCallbacks.cpp#l1233
+     * - request is mixed content (which makes no sense whatsoever)
+     *   => .securityState has STATE_IS_BROKEN flag
+     *   => .errorCode is NOT an NSS error code
+     *   => .errorMessage is not available
+     *      => state === "insecure"
+     */
+
+    securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+    securityInfo.QueryInterface(Ci.nsISSLStatusProvider);
+
+    const wpl = Ci.nsIWebProgressListener;
+    const NSSErrorsService = Cc['@mozilla.org/nss_errors_service;1']
+                               .getService(Ci.nsINSSErrorsService);
+    const SSLStatus = securityInfo.SSLStatus;
+
+    if (securityInfo.securityState & wpl.STATE_IS_SECURE) {
+      // The connection is secure.
+      info.state = "secure";
+
+      // Cipher suite.
+      info.cipherSuite = SSLStatus.cipherName;
+
+      // Protocol version.
+      info.protocolVersion = this.formatSecurityProtocol(SSLStatus.protocolVersion);
+
+      // Certificate.
+      info.cert = this.parseCertificateInfo(SSLStatus.serverCert);
+
+      // HSTS and HPKP if available.
+      if (request.URI) {
+        const sss = Cc["@mozilla.org/ssservice;1"]
+                      .getService(Ci.nsISiteSecurityService);
+
+        request.QueryInterface(Ci.nsIPrivateBrowsingChannel);
+
+        // SiteSecurityService uses different storage if the channel is
+        // private. Thus we must give isSecureHost correct flags or we
+        // might get incorrect results.
+        let flags = (request.isChannelPrivate) ?
+                      Ci.nsISocketProvider.NO_PERMANENT_STORAGE : 0;
+
+        let host = request.URI.host;
+
+        info.hsts = sss.isSecureHost(sss.HEADER_HSTS, host, flags);
+        info.hpkp = sss.isSecureHost(sss.HEADER_HPKP, host, flags);
+      } else {
+        DevToolsUtils.reportException("NetworkHelper.parseSecurityInfo",
+          "Could not get HSTS/HPKP status as request.URI not available.");
+        info.hsts = false;
+        info.hpkp = false;
+      }
+
+    } else if (NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) {
+      // The connection failed.
+      info.state = "broken";
+      info.errorMessage = securityInfo.errorMessage;
+    } else {
+      // Connection has securityInfo, it is not secure and there's no problems
+      // to report. Mark the request as insecure.
+      return info;
+    }
+
+    return info;
+  },
+
+  /**
+   * Takes an nsIX509Cert and returns an object with certificate information.
+   *
+   * @param nsIX509Cert cert
+   *        The certificate to extract the information from.
+   * @return object
+   *         An object with following format:
+   *           {
+   *             subject: { commonName, organization, organizationalUnit },
+   *             issuer: { commonName, organization, organizationUnit },
+   *             validity: { start, end },
+   *             fingerprint: { sha1, sha256 }
+   *           }
+   */
+  parseCertificateInfo: function NH_parseCertifificateInfo(cert) {
+    let info = {};
+    if (cert) {
+      info.subject = {
+        commonName: cert.commonName,
+        organization: cert.organization,
+        organizationalUnit: cert.organizationalUnit,
+      };
+
+      info.issuer = {
+        commonName: cert.issuerCommonName,
+        organization: cert.issuerOrganization,
+        organizationUnit: cert.issuerOrganizationUnit,
+      };
+
+      info.validity = {
+        start: cert.validity.notBeforeLocalDay,
+        end: cert.validity.notAfterLocalDay,
+      };
+
+      info.fingerprint = {
+        sha1: cert.sha1Fingerprint,
+        sha256: cert.sha256Fingerprint,
+      };
+    } else {
+      DevToolsUtils.reportException("NetworkHelper.parseCertificateInfo",
+        "Secure connection established without certificate.");
+    }
+
+    return info;
+  },
+
+  /**
+   * Takes protocolVersion of SSLStatus object and returns human readable
+   * description.
+   *
+   * @param Number version
+   *        One of nsISSLStatus version constants.
+   * @return string
+   *         One of SSLv3, TLSv1, TLSv1.1, TLSv1.2 if @param version is valid,
+   *         Unknown otherwise.
+   */
+  formatSecurityProtocol: function NH_formatSecurityProtocol(version) {
+    switch (version) {
+      case Ci.nsISSLStatus.SSL_VERSION_3:
+        return "SSLv3";
+      case Ci.nsISSLStatus.TLS_VERSION_1:
+        return "TLSv1";
+      case Ci.nsISSLStatus.TLS_VERSION_1_1:
+        return "TLSv1.1";
+      case Ci.nsISSLStatus.TLS_VERSION_1_2:
+        return "TLSv1.2";
+      default:
+        DevToolsUtils.reportException("NetworkHelper.formatSecurityProtocol",
+          "protocolVersion " + version + " is unknown.");
+        return "Unknown";
+    }
+  },
 };
 
 for (let prop of Object.getOwnPropertyNames(NetworkHelper)) {
   exports[prop] = NetworkHelper[prop];
 }
--- a/toolkit/devtools/webconsole/network-monitor.js
+++ b/toolkit/devtools/webconsole/network-monitor.js
@@ -155,22 +155,37 @@ NetworkResponseListener.prototype = {
    * https://developer.mozilla.org/En/NsIRequestObserver
    *
    * @param nsIRequest aRequest
    * @param nsISupports aContext
    */
   onStartRequest: function NRL_onStartRequest(aRequest)
   {
     this.request = aRequest;
+    this._getSecurityInfo();
     this._findOpenResponse();
     // Asynchronously wait for the data coming from the request.
     this.setAsyncListener(this.sink.inputStream, this);
   },
 
   /**
+   * Parse security state of this request and report it to the client.
+   */
+  _getSecurityInfo: DevToolsUtils.makeInfallible(function NRL_getSecurityInfo() {
+    // Take the security information from the original nsIHTTPChannel instead of
+    // the nsIRequest received in onStartRequest. If response to this request
+    // was a redirect from http to https, the request object seems to contain
+    // security info for the https request after redirect.
+    let secinfo = this.httpActivity.channel.securityInfo;
+    let info = NetworkHelper.parseSecurityInfo(secinfo, this.request);
+
+    this.httpActivity.owner.addSecurityInfo(info);
+  }),
+
+  /**
    * Handle the onStopRequest by closing the sink output stream.
    *
    * For more documentation about nsIRequestObserver go to:
    * https://developer.mozilla.org/En/NsIRequestObserver
    */
   onStopRequest: function NRL_onStopRequest()
   {
     this._findOpenResponse();
@@ -1155,18 +1170,18 @@ NetworkEventActorProxy.prototype = {
     });
     return this;
   }),
 };
 
 (function() {
   // Listeners for new network event data coming from the NetworkMonitor.
   let methods = ["addRequestHeaders", "addRequestCookies", "addRequestPostData",
-                 "addResponseStart", "addResponseHeaders", "addResponseCookies",
-                 "addResponseContent", "addEventTimings"];
+                 "addResponseStart", "addSecurityInfo", "addResponseHeaders",
+                 "addResponseCookies", "addResponseContent", "addEventTimings"];
   let factory = NetworkEventActorProxy.methodFactory;
   for (let method of methods) {
     NetworkEventActorProxy.prototype[method] = factory(method);
   }
 })();
 
 
 /**
--- a/toolkit/devtools/webconsole/test/chrome.ini
+++ b/toolkit/devtools/webconsole/test/chrome.ini
@@ -12,15 +12,17 @@ support-files =
 [test_consoleapi.html]
 [test_consoleapi_innerID.html]
 [test_file_uri.html]
 [test_reflow.html]
 [test_jsterm.html]
 [test_network_get.html]
 [test_network_longstring.html]
 [test_network_post.html]
+[test_network_security-hpkp.html]
+[test_network_security-hsts.html]
 [test_nsiconsolemessage.html]
 [test_object_actor.html]
 [test_object_actor_native_getters.html]
 [test_object_actor_native_getters_lenient_this.html]
 [test_page_errors.html]
 [test_throw.html]
 [test_jsterm_cd_iframe.html]
--- a/toolkit/devtools/webconsole/test/test_network_get.html
+++ b/toolkit/devtools/webconsole/test/test_network_get.html
@@ -86,16 +86,21 @@ function onNetworkEventUpdate(aState, aT
           httpVersion: /^HTTP\/\d\.\d$/,
           status: 200,
           statusText: "OK",
           headersSize: /^\d+$/,
           discardResponseBody: true,
         },
       };
       break;
+    case "securityInfo":
+      expectedPacket = {
+        state: "insecure",
+      };
+      break;
     case "responseCookies":
       expectedPacket = {
         cookies: 0,
       };
       break;
     case "responseContent":
       expectedPacket = {
         mimeType: "application/json",
--- a/toolkit/devtools/webconsole/test/test_network_longstring.html
+++ b/toolkit/devtools/webconsole/test/test_network_longstring.html
@@ -109,16 +109,21 @@ function onNetworkEventUpdate(aState, aT
           httpVersion: /^HTTP\/\d\.\d$/,
           status: 200,
           statusText: "OK",
           headersSize: /^\d+$/,
           discardResponseBody: false,
         },
       };
       break;
+    case "securityInfo":
+      expectedPacket = {
+        state: "insecure",
+      };
+      break;
     case "responseCookies":
       expectedPacket = {
         cookies: 0,
       };
       break;
     case "responseContent":
       expectedPacket = {
         mimeType: "application/json",
--- a/toolkit/devtools/webconsole/test/test_network_post.html
+++ b/toolkit/devtools/webconsole/test/test_network_post.html
@@ -102,16 +102,21 @@ function onNetworkEventUpdate(aState, aT
           httpVersion: /^HTTP\/\d\.\d$/,
           status: 200,
           statusText: "OK",
           headersSize: /^\d+$/,
           discardResponseBody: false,
         },
       };
       break;
+    case "securityInfo":
+      expectedPacket = {
+        state: "insecure",
+      };
+      break;
     case "responseCookies":
       expectedPacket = {
         cookies: 0,
       };
       break;
     case "responseContent":
       expectedPacket = {
         mimeType: "application/json",
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/test_network_security-hpkp.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+  <meta charset="utf8">
+  <title>Test for the network actor (HPKP detection)</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript;version=1.8" src="common.js"></script>
+  <!-- Any copyright is dedicated to the Public Domain.
+     - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the network actor (HPKP detection)</p>
+
+<iframe src="https://example.com/chrome/toolkit/devtools/webconsole/test/network_requests_iframe.html"></iframe>
+
+<script class="testbody" type="text/javascript;version=1.8">
+SimpleTest.waitForExplicitFinish();
+
+let gCurrentTestCase = -1;
+const HPKP_PREF = "security.cert_pinning.process_headers_from_non_builtin_roots";
+
+// Static pins tested by unit/test_security-info-static-hpkp.js.
+const TEST_CASES = [
+  {
+    desc: "no Public Key Pinning",
+    url: "https://example.com",
+    usesPinning: false,
+  },
+  {
+    desc: "dynamic Public Key Pinning with this request",
+    url: "https://include-subdomains.pinning-dynamic.example.com/" +
+         "browser/browser/base/content/test/general/pinning_headers.sjs",
+    usesPinning: true,
+  },
+  {
+    desc: "dynamic Public Key Pinning with previous request",
+    url: "https://include-subdomains.pinning-dynamic.example.com/",
+    usesPinning: true,
+  }
+];
+
+function startTest()
+{
+  // Need to enable this pref or pinning headers are rejected due test
+  // certificate.
+  Services.prefs.setBoolPref(HPKP_PREF, true);
+  SimpleTest.registerCleanupFunction(() => {
+    Services.prefs.setBoolPref(HPKP_PREF, false);
+
+    // Reset pinning state.
+    let gSSService = Cc["@mozilla.org/ssservice;1"]
+                       .getService(Ci.nsISiteSecurityService);
+
+    let gIOService = Cc["@mozilla.org/network/io-service;1"]
+                       .getService(Ci.nsIIOService);
+    for (let {url} of TEST_CASES) {
+      let uri = gIOService.newURI(url, null, null);
+      gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HPKP, uri, 0);
+    }
+  });
+
+  info("Test detection of Public Key Pinning.");
+  removeEventListener("load", startTest);
+  attachConsole(["NetworkActivity"], onAttach, true);
+}
+
+function onAttach(aState, aResponse)
+{
+  onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState);
+  aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate);
+
+  runNextCase(aState);
+}
+
+function runNextCase(aState) {
+  gCurrentTestCase++;
+  if (gCurrentTestCase === TEST_CASES.length) {
+    info("Tests ran. Cleaning up.");
+    closeDebugger(aState, SimpleTest.finish);
+    return;
+  }
+
+  let { desc, url } = TEST_CASES[gCurrentTestCase];
+  info("Testing site with " + desc);
+
+  let iframe = document.querySelector("iframe").contentWindow;
+  iframe.wrappedJSObject.makeXhrCallback("GET", url);
+}
+
+function onNetworkEventUpdate(aState, aType, aPacket)
+{
+  function onSecurityInfo(packet) {
+    let data = TEST_CASES[gCurrentTestCase];
+    is(packet.securityInfo.hpkp, data.usesPinning,
+      "Public Key Pinning detected correctly.");
+
+    runNextCase(aState);
+  }
+
+  if (aPacket.updateType === "securityInfo") {
+    aState.client.getSecurityInfo(aPacket.from, onSecurityInfo);
+  }
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/test_network_security-hsts.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+  <meta charset="utf8">
+  <title>Test for the network actor (HSTS detection)</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript;version=1.8" src="common.js"></script>
+  <!-- Any copyright is dedicated to the Public Domain.
+     - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the network actor (HSTS detection)</p>
+
+<iframe src="https://example.com/chrome/toolkit/devtools/webconsole/test/network_requests_iframe.html"></iframe>
+
+<script class="testbody" type="text/javascript;version=1.8">
+SimpleTest.waitForExplicitFinish();
+
+let gCurrentTestCase = -1;
+const TEST_CASES = [
+  {
+    desc: "no HSTS",
+    url: "https://example.com",
+    usesHSTS: false,
+  },
+  {
+    desc: "HSTS from this response",
+    url: "https://example.com/"+
+         "browser/browser/base/content/test/general/browser_star_hsts.sjs",
+    usesHSTS: true,
+  },
+  {
+    desc: "stored HSTS from previous response",
+    url: "https://example.com/",
+    usesHSTS: true,
+  }
+];
+
+function startTest()
+{
+
+  SimpleTest.registerCleanupFunction(() => {
+    // Reset HSTS state.
+    let gSSService = Cc["@mozilla.org/ssservice;1"]
+                       .getService(Ci.nsISiteSecurityService);
+
+    let gIOService = Cc["@mozilla.org/network/io-service;1"]
+                       .getService(Ci.nsIIOService);
+
+    let uri = gIOService.newURI(TEST_CASES[0].url, null, null);
+    gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0);
+  });
+
+  info("Test detection of HTTP Strict Transport Security.");
+  removeEventListener("load", startTest);
+  attachConsole(["NetworkActivity"], onAttach, true);
+}
+
+function onAttach(aState, aResponse)
+{
+  onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState);
+  aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate);
+
+  runNextCase(aState);
+}
+
+function runNextCase(aState) {
+  gCurrentTestCase++;
+  if (gCurrentTestCase === TEST_CASES.length) {
+    info("Tests ran. Cleaning up.");
+    closeDebugger(aState, SimpleTest.finish);
+    return;
+  }
+
+  let { desc, url } = TEST_CASES[gCurrentTestCase];
+  info("Testing site with " + desc);
+
+  let iframe = document.querySelector("iframe").contentWindow;
+  iframe.wrappedJSObject.makeXhrCallback("GET", url);
+}
+
+function onNetworkEventUpdate(aState, aType, aPacket)
+{
+  function onSecurityInfo(packet) {
+    let data = TEST_CASES[gCurrentTestCase];
+    is(packet.securityInfo.hsts, data.usesHSTS,
+      "Strict Transport Security detected correctly.");
+
+    runNextCase(aState);
+  }
+
+  if (aPacket.updateType === "securityInfo") {
+    aState.client.getSecurityInfo(aPacket.from, onSecurityInfo);
+  }
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/unit/test_security-info-certificate.js
@@ -0,0 +1,68 @@
+/* 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 NetworkHelper.parseCertificateInfo parses certificate information
+// correctly.
+
+const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+
+Object.defineProperty(this, "NetworkHelper", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/network-helper");
+  },
+  configurable: true,
+  writeable: false,
+  enumerable: true
+});
+
+const Ci = Components.interfaces;
+const DUMMY_CERT = {
+  commonName: "cn",
+  organization: "o",
+  organizationalUnit: "ou",
+  issuerCommonName: "issuerCN",
+  issuerOrganization: "issuerO",
+  issuerOrganizationUnit: "issuerOU",
+  sha256Fingerprint: "qwertyuiopoiuytrewq",
+  sha1Fingerprint: "qwertyuiop",
+  validity: {
+    notBeforeLocalDay: "yesterday",
+    notAfterLocalDay: "tomorrow",
+  }
+};
+
+function run_test() {
+  do_print("Testing NetworkHelper.parseCertificateInfo.");
+
+  let result = NetworkHelper.parseCertificateInfo(DUMMY_CERT);
+
+  // Subject
+  equal(result.subject.commonName, DUMMY_CERT.commonName,
+    "Common name is correct.");
+  equal(result.subject.organization, DUMMY_CERT.organization,
+    "Organization is correct.");
+  equal(result.subject.organizationalUnit, DUMMY_CERT.organizationalUnit,
+    "Organizational unit is correct.");
+
+  // Issuer
+  equal(result.issuer.commonName, DUMMY_CERT.issuerCommonName,
+    "Common name of the issuer is correct.");
+  equal(result.issuer.organization, DUMMY_CERT.issuerOrganization,
+    "Organization of the issuer is correct.");
+  equal(result.issuer.organizationalUnit, DUMMY_CERT.issuerOrganizationalUnit,
+    "Organizational unit of the issuer is correct.");
+
+  // Validity
+  equal(result.validity.start, DUMMY_CERT.validity.notBeforeLocalDay,
+    "Start of the validity period is correct.");
+  equal(result.validity.end, DUMMY_CERT.validity.notAfterLocalDay,
+    "End of the validity period is correct.");
+
+  // Fingerprints
+  equal(result.fingerprint.sha1, DUMMY_CERT.sha1Fingerprint,
+    "Certificate SHA1 fingerprint is correct.");
+  equal(result.fingerprint.sha256, DUMMY_CERT.sha256Fingerprint,
+    "Certificate SHA256 fingerprint is correct.");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/unit/test_security-info-parser.js
@@ -0,0 +1,64 @@
+/* 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";
+
+// Test that NetworkHelper.parseSecurityInfo returns correctly formatted object.
+
+const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Object.defineProperty(this, "NetworkHelper", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/network-helper");
+  },
+  configurable: true,
+  writeable: false,
+  enumerable: true
+});
+
+const Ci = Components.interfaces;
+const wpl = Ci.nsIWebProgressListener;
+const MockCertificate = {
+  commonName: "cn",
+  organization: "o",
+  organizationalUnit: "ou",
+  issuerCommonName: "issuerCN",
+  issuerOrganization: "issuerO",
+  issuerOrganizationUnit: "issuerOU",
+  sha256Fingerprint: "qwertyuiopoiuytrewq",
+  sha1Fingerprint: "qwertyuiop",
+  validity: {
+    notBeforeLocalDay: "yesterday",
+    notAfterLocalDay: "tomorrow",
+  }
+};
+
+const MockSecurityInfo = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsITransportSecurityInfo,
+                                         Ci.nsISSLStatusProvider]),
+  securityState: wpl.STATE_IS_SECURE,
+  errorCode: 0,
+  SSLStatus: {
+    cipherSuite: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+    protocolVersion: 3, // TLS_VERSION_1_2
+    serverCert: MockCertificate,
+  }
+};
+
+function run_test() {
+  let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
+
+  equal(result.state, "secure", "State is correct.");
+
+  equal(result.cipherSuite, MockSecurityInfo.cipherSuite,
+    "Cipher suite is correct.");
+
+  equal(result.protocolVersion, "TLSv1.2", "Protocol version is correct.");
+
+  deepEqual(result.cert, NetworkHelper.parseCertificateInfo(MockCertificate),
+    "Certificate information is correct.");
+
+  equal(result.hpkp, false, "HPKP is false when URI is not available.");
+  equal(result.hsts, false, "HSTS is false when URI is not available.");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/unit/test_security-info-protocol-version.js
@@ -0,0 +1,54 @@
+/* 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 NetworkHelper.formatSecurityProtocol returns correct
+// protocol version strings.
+
+const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+
+Object.defineProperty(this, "NetworkHelper", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/network-helper");
+  },
+  configurable: true,
+  writeable: false,
+  enumerable: true
+});
+
+const Ci = Components.interfaces;
+const TEST_CASES = [
+  {
+    description: "SSL_VERSION_3",
+    input: 0,
+    expected: "SSLv3"
+  }, {
+    description: "TLS_VERSION_1",
+    input: 1,
+    expected: "TLSv1"
+  }, {
+    description: "TLS_VERSION_1.1",
+    input: 2,
+    expected: "TLSv1.1"
+  }, {
+    description: "TLS_VERSION_1.2",
+    input: 3,
+    expected: "TLSv1.2"
+  }, {
+    description: "invalid version",
+    input: -1,
+    expected: "Unknown"
+  },
+];
+
+function run_test() {
+  do_print("Testing NetworkHelper.formatSecurityProtocol.");
+
+  for (let {description, input, expected} of TEST_CASES) {
+    do_print("Testing " + description);
+
+    equal(NetworkHelper.formatSecurityProtocol(input), expected,
+      "Got the expected protocol string.");
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/unit/test_security-info-state.js
@@ -0,0 +1,100 @@
+/* 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 security info parser gives correct general security state for
+// different cases.
+
+const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Object.defineProperty(this, "NetworkHelper", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/network-helper");
+  },
+  configurable: true,
+  writeable: false,
+  enumerable: true
+});
+
+const Ci = Components.interfaces;
+const wpl = Ci.nsIWebProgressListener;
+const MockSecurityInfo = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsITransportSecurityInfo,
+                                         Ci.nsISSLStatusProvider]),
+  securityState: wpl.STATE_IS_BROKEN,
+  errorCode: 0,
+  SSLStatus: {
+    protocolVersion: 3, // nsISSLStatus.TLS_VERSION_1_2
+    cipherSuite: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+  }
+};
+
+function run_test() {
+  test_nullSecurityInfo();
+  test_insecureSecurityInfoWithNSSError();
+  test_insecureSecurityInfoWithoutNSSError();
+  test_brokenSecurityInfo();
+  test_secureSecurityInfo();
+}
+
+/**
+ * Test that undefined security information is returns "insecure".
+ */
+function test_nullSecurityInfo() {
+  let result = NetworkHelper.parseSecurityInfo(null, {});
+  equal(result.state, "insecure",
+    "state == 'insecure' when securityInfo was undefined");
+}
+
+/**
+ * Test that STATE_IS_INSECURE with NSSError returns "broken"
+ */
+function test_insecureSecurityInfoWithNSSError() {
+  MockSecurityInfo.securityState = wpl.STATE_IS_INSECURE;
+
+  // Taken from security/manager/ssl/tests/unit/head_psm.js.
+  MockSecurityInfo.errorCode = -8180;
+
+  let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
+  equal(result.state, "broken",
+    "state == 'broken' if securityState contains STATE_IS_INSECURE flag AND " +
+    "errorCode is NSS error.");
+
+  MockSecurityInfo.errorCode = 0;
+}
+
+/**
+ * Test that STATE_IS_INSECURE without NSSError returns "insecure"
+ */
+function test_insecureSecurityInfoWithoutNSSError() {
+  MockSecurityInfo.securityState = wpl.STATE_IS_INSECURE;
+
+  let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
+  equal(result.state, "insecure",
+    "state == 'insecure' if securityState contains STATE_IS_INSECURE flag BUT " +
+    "errorCode is not NSS error.");
+}
+
+/**
+ * Test that STATE_IS_SECURE returns "secure"
+ */
+function test_secureSecurityInfo() {
+  MockSecurityInfo.securityState = wpl.STATE_IS_SECURE;
+
+  let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
+  equal(result.state, "secure",
+    "state == 'secure' if securityState contains STATE_IS_SECURE flag");
+}
+
+/**
+ * Test that STATE_IS_BROKEN returns "insecure"
+ */
+function test_brokenSecurityInfo() {
+  MockSecurityInfo.securityState = wpl.STATE_IS_BROKEN;
+
+  let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
+  equal(result.state, "insecure",
+    "state == 'insecure' if securityState contains STATE_IS_BROKEN flag");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/unit/test_security-info-static-hpkp.js
@@ -0,0 +1,47 @@
+/* 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";
+
+// Test that NetworkHelper.parseSecurityInfo correctly detects static hpkp pins
+
+const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Object.defineProperty(this, "NetworkHelper", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/network-helper");
+  },
+  configurable: true,
+  writeable: false,
+  enumerable: true
+});
+
+const Ci = Components.interfaces;
+const wpl = Ci.nsIWebProgressListener;
+
+const MockSecurityInfo = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsITransportSecurityInfo,
+                                         Ci.nsISSLStatusProvider]),
+  securityState: wpl.STATE_IS_SECURE,
+  errorCode: 0,
+  SSLStatus: {
+    cipherSuite: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+    protocolVersion: 3, // TLS_VERSION_1_2
+    serverCert: {
+      validity: {}
+    },
+  }
+};
+
+const MockRequest = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivateBrowsingChannel]),
+  URI: {
+    host: "include-subdomains.pinning.example.com"
+  }
+};
+
+function run_test() {
+  let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, MockRequest);
+  equal(result.hpkp, true, "Static HPKP detected.");
+}
--- a/toolkit/devtools/webconsole/test/unit/xpcshell.ini
+++ b/toolkit/devtools/webconsole/test/unit/xpcshell.ini
@@ -1,8 +1,13 @@
 [DEFAULT]
 head =
 tail =
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 support-files =
 
 [test_js_property_provider.js]
 [test_network_helper.js]
+[test_security-info-certificate.js]
+[test_security-info-parser.js]
+[test_security-info-protocol-version.js]
+[test_security-info-state.js]
+[test_security-info-static-hpkp.js]