merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 18 Jun 2015 15:08:51 +0200
changeset 267527 1fb3ebf68777aa480667874eb7b0bcc39e63ebcb
parent 267505 66b2f328a16fcf76c09dfc2179c3f811237042d8 (current diff)
parent 267526 844efb38a131a2a7320c8f837413a69302c9c4fb (diff)
child 267616 efe86609e77669a4780509a38b9d0ed04525c179
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-esr52@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone41.0a1
merge fx-team to mozilla-central a=merge
browser/base/content/test/general/browser_trackingUI.js
browser/devtools/performance/test/browser_perf-jit-model-01.js
browser/devtools/performance/test/browser_perf-jit-model-02.js
browser/devtools/shared/widgets/Graphs.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1457,17 +1457,17 @@ pref("devtools.performance.profiler.samp
 pref("devtools.performance.ui.invert-call-tree", true);
 pref("devtools.performance.ui.invert-flame-graph", false);
 pref("devtools.performance.ui.flatten-tree-recursion", true);
 pref("devtools.performance.ui.show-platform-data", false);
 pref("devtools.performance.ui.show-idle-blocks", true);
 pref("devtools.performance.ui.enable-memory", false);
 pref("devtools.performance.ui.enable-allocations", false);
 pref("devtools.performance.ui.enable-framerate", true);
-pref("devtools.performance.ui.show-jit-optimizations", false);
+pref("devtools.performance.ui.enable-jit-optimizations", false);
 
 // Enable experimental options in the UI only in Nightly
 #if defined(NIGHTLY_BUILD)
 pref("devtools.performance.ui.experimental", true);
 #else
 pref("devtools.performance.ui.experimental", false);
 #endif
 
--- a/browser/base/content/newtab/customize.js
+++ b/browser/base/content/newtab/customize.js
@@ -17,80 +17,105 @@ let gCustomize = {
 
   _nodes: {},
 
   init: function() {
     for (let idSuffix of this._nodeIDSuffixes) {
       this._nodes[idSuffix] = document.getElementById("newtab-customize-" + idSuffix);
     }
 
-    this._nodes.button.addEventListener("click", e => this.showPanel());
-    this._nodes.blank.addEventListener("click", e => {
-      gAllPages.enabled = false;
-    });
-    this._nodes.classic.addEventListener("click", e => {
-      gAllPages.enabled = true;
-
-      if (this._nodes.enhanced.getAttribute("selected")) {
-        gAllPages.enhanced = true;
-      } else {
-        gAllPages.enhanced = false;
-      }
-    });
-    this._nodes.enhanced.addEventListener("click", e => {
-      if (!gAllPages.enabled) {
-        gAllPages.enabled = true;
-        return;
-      }
-      gAllPages.enhanced = !gAllPages.enhanced;
-    });
-    this._nodes.learn.addEventListener("click", e => {
-      window.open(TILES_INTRO_LINK,'new_window');
-      this._onHidden();
-    });
+    this._nodes.button.addEventListener("click", e => this.showPanel(e));
+    this._nodes.blank.addEventListener("click", this);
+    this._nodes.classic.addEventListener("click", this);
+    this._nodes.enhanced.addEventListener("click", this);
+    this._nodes.learn.addEventListener("click", this);
 
     this.updateSelected();
   },
 
-  _onHidden: function() {
-    let nodes = gCustomize._nodes;
-    nodes.overlay.addEventListener("transitionend", function onTransitionEnd() {
-      nodes.overlay.removeEventListener("transitionend", onTransitionEnd);
-      nodes.overlay.style.display = "none";
+  hidePanel: function() {
+    this._nodes.overlay.addEventListener("transitionend", function onTransitionEnd() {
+      gCustomize._nodes.overlay.removeEventListener("transitionend", onTransitionEnd);
+      gCustomize._nodes.overlay.style.display = "none";
     });
-    nodes.overlay.style.opacity = 0;
-    nodes.panel.removeEventListener("popuphidden", gCustomize._onHidden);
-    nodes.panel.hidden = true;
-    nodes.button.removeAttribute("active");
+    this._nodes.overlay.style.opacity = 0;
+    this._nodes.button.removeAttribute("active");
+    this._nodes.panel.removeAttribute("open");
+    document.removeEventListener("click", this);
+    document.removeEventListener("keydown", this);
   },
 
-  showPanel: function() {
-    this._nodes.overlay.style.display = "block";
+  showPanel: function(event) {
+    if (this._nodes.panel.getAttribute("open") == "true") {
+      return;
+    }
+
+    let {panel, button, overlay} = this._nodes;
+    overlay.style.display = "block";
+    panel.setAttribute("open", "true");
+    button.setAttribute("active", "true");
     setTimeout(() => {
       // Wait for display update to take place, then animate.
-      this._nodes.overlay.style.opacity = 0.8;
+      overlay.style.opacity = 0.8;
     }, 0);
 
-    let nodes = this._nodes;
-    let {button, panel} = nodes;
-    if (button.hasAttribute("active")) {
-      return Promise.resolve(nodes);
+    document.addEventListener("click", this);
+    document.addEventListener("keydown", this);
+
+    // Stop the event propogation to prevent panel from immediately closing
+    // via the document click event that we just added.
+    event.stopPropagation();
+  },
+
+  handleEvent: function(event) {
+    switch (event.type) {
+      case "click":
+        this.onClick(event);
+        break;
+      case "keydown":
+        this.onKeyDown(event);
+        break;
     }
+  },
 
-    panel.hidden = false;
-    panel.openPopup(button);
-    button.setAttribute("active", true);
-    panel.addEventListener("popuphidden", this._onHidden);
+  onClick: function(event) {
+    if (event.currentTarget == document) {
+      if (!this._nodes.panel.contains(event.target)) {
+        this.hidePanel();
+      }
+    }
+    switch (event.currentTarget.id) {
+      case "newtab-customize-blank":
+        sendAsyncMessage("NewTab:Customize", {enabled: false, enhanced: false});
+        break;
+      case "newtab-customize-classic":
+        if (this._nodes.enhanced.getAttribute("selected")){
+          sendAsyncMessage("NewTab:Customize", {enabled: true, enhanced: true});
+        } else {
+          sendAsyncMessage("NewTab:Customize", {enabled: true, enhanced: false});
+        }
+        break;
+      case "newtab-customize-enhanced":
+        sendAsyncMessage("NewTab:Customize", {enabled: true, enhanced: !gAllPages.enhanced});
+        break;
+      case "newtab-customize-learn":
+        this.showLearn();
+        break;
+    }
+  },
 
-    return new Promise(resolve => {
-      panel.addEventListener("popupshown", function onShown() {
-        panel.removeEventListener("popupshown", onShown);
-        resolve(nodes);
-      });
-    });
+  onKeyDown: function(event) {
+    if (event.keyCode == event.DOM_VK_ESCAPE) {
+      this.hidePanel();
+    }
+  },
+
+  showLearn: function() {
+    window.open(TILES_INTRO_LINK, 'new_window');
+    this.hidePanel();
   },
 
   updateSelected: function() {
     let {enabled, enhanced} = gAllPages;
     let selected = enabled ? enhanced ? "enhanced" : "classic" : "blank";
     ["enhanced", "classic", "blank"].forEach(id => {
       let node = this._nodes[id];
       if (id == selected) {
--- a/browser/base/content/newtab/newTab.css
+++ b/browser/base/content/newtab/newTab.css
@@ -476,34 +476,63 @@ input[type=button] {
   width: 100%;
   height: 100%;
   background: #F9F9F9;
   z-index: 100;
   position: fixed;
   transition: opacity .07s linear;
 }
 
+.newtab-customize-panel-container {
+  position: absolute;
+  margin-right: 40px;
+}
+
 #newtab-customize-panel {
-  z-index: 101;
-  margin-top: -5px;
+  z-index: 999;
+  margin-top: 55px;
   min-width: 270px;
+  position: absolute;
+  top: 100%;
+  right: -25px;
+  background-color: white;
+  border-radius: 6px;
+  filter: drop-shadow(0 0 1px rgba(0,0,0,0.4)) drop-shadow(0 3px 4px rgba(0,0,0,0.4));
+  transition: all 200ms ease-in-out;
+  transform-origin: top right;
+  transform: translate(-30px, -20px) scale(0) translate(30px, 20px);
+}
+
+#newtab-customize-panel[open="true"] {
+  transform: translate(-30px, -20px) scale(1) translate(30px, 20px);
+}
+
+#newtab-customize-panel-anchor {
+  width: 18px;
+  height: 18px;
+  background-color: white;
+  transform: rotate(45deg);
+  position: absolute;
+  top: -6px;
+  right: 15px;
 }
 
 #newtab-customize-title {
   color: #7A7A7A;
   font-size: 14px;
   background-color: #FFFFFF;
   line-height: 25px;
   padding: 15px;
   font-weight: 600;
   cursor: default;
   border-radius: 5px 5px 0px 0px;
   max-width: 300px;
   overflow: hidden;
   display: table-cell;
+  border-top: none;
 }
 
 #newtab-customize-title > label {
   cursor: default;
 }
 
 #newtab-customize-panel > .panel-arrowcontainer > .panel-arrowcontent,
 #newtab-search-panel > .panel-arrowcontainer > .panel-arrowcontent {
@@ -513,16 +542,17 @@ input[type=button] {
 .newtab-customize-panel-item,
 .newtab-search-panel-engine,
 #newtab-search-manage {
   line-height: 25px;
   padding: 15px;
   -moz-padding-start: 40px;
   font-size: 14px;
   cursor: pointer;
+  max-width: 300px;
 }
 
 .newtab-customize-panel-item:not(:first-child),
 .newtab-search-panel-engine {
   border-top: 1px solid threedshadow;
 }
 
 .newtab-search-panel-engine > image {
@@ -539,20 +569,18 @@ input[type=button] {
 .newtab-customize-complex-option {
   padding: 0;
   margin: 0;
   cursor: pointer;
 }
 
 .newtab-customize-panel-item,
 .newtab-customize-complex-option {
-  width: 100%;
   display: block;
   text-align: start;
-  max-width: 300px;
   background-color: #F9F9F9;
 }
 
 .newtab-customize-panel-item[selected]:-moz-locale-dir(rtl) {
   background-position: right 15px center;
 }
 
 .newtab-customize-complex-option:hover > .selectable:not([selected]):-moz-locale-dir(rtl),
@@ -610,16 +638,17 @@ input[type=button] {
   color: #333333;
 }
 
 .newtab-customize-panel-subitem {
   font-size: 12px;
   padding: 0px 15px 15px 15px;
   -moz-padding-start: 40px;
   display: block;
+  max-width: 300px;
 }
 
 .newtab-customize-panel-subitem > label {
   padding: 0px 10px;
   line-height: 20px;
   vertical-align: middle;
   max-width: 225px;
 }
@@ -783,28 +812,28 @@ input[type=button] {
   left: 0px;
   right: auto;
   float: left;
   margin-right: 40px;
 }
 
 .newtab-intro-image-customize {
   box-shadow: 3px 3px 5px #888;
-  margin: 0 !important;
+  margin-top: 0px;
   background-color: #FFF;
   float: left;
   z-index: 101;
   margin-top: -5px;
   min-width: 270px;
   padding: 0;
 }
 
 .newtab-intro-image-customize #newtab-customize-title {
   display: block;
-  max-height: 72px;
+  max-height: 40px;
 }
 
 .newtab-intro-image-customize .newtab-customize-panel-item:not([selected]):hover {
   background-color: inherit;
   color: #7A7A7A;
   background: none;
 }
 
--- a/browser/base/content/newtab/newTab.xul
+++ b/browser/base/content/newtab/newTab.xul
@@ -17,46 +17,49 @@
   <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
   %browserDTD;
 ]>
 
 <xul:window id="newtab-window" xmlns="http://www.w3.org/1999/xhtml"
             xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
             title="&newtab.pageTitle;">
 
-  <div id="newtab-customize-overlay"></div>
-
   <xul:panel id="newtab-search-panel" orient="vertical" type="arrow"
              noautohide="true" hidden="true">
     <xul:hbox id="newtab-search-manage">
       <xul:label>&changeSearchSettings.button;</xul:label>
     </xul:hbox>
   </xul:panel>
 
-  <xul:panel id="newtab-customize-panel" orient="vertical" type="arrow"
-             noautohide="true" hidden="true">
-    <xul:hbox id="newtab-customize-title" class="newtab-customize-panel-item">
-      <xul:label>&newtab.customize.cog.title2;</xul:label>
-    </xul:hbox>
-    <xul:vbox class="newtab-customize-complex-option">
-      <xul:hbox id="newtab-customize-classic" class="newtab-customize-panel-superitem newtab-customize-panel-item selectable">
-        <xul:label>&newtab.customize.classic;</xul:label>
-      </xul:hbox>
-      <xul:hbox id="newtab-customize-enhanced" class="newtab-customize-panel-subitem">
-        <xul:label class="checkbox"></xul:label>
-        <xul:label>&newtab.customize.cog.enhanced;</xul:label>
-      </xul:hbox>
-    </xul:vbox>
-    <xul:hbox id="newtab-customize-blank" class="newtab-customize-panel-item selectable">
-      <xul:label>&newtab.customize.blank2;</xul:label>
-    </xul:hbox>
-    <xul:hbox id="newtab-customize-learn" class="newtab-customize-panel-item">
-      <xul:label>&newtab.customize.cog.learn;</xul:label>
-    </xul:hbox>
-  </xul:panel>
+  <div class="newtab-customize-panel-container">
+    <div id="newtab-customize-panel" orient="vertical">
+        <div id="newtab-customize-panel-anchor"></div>
+        <div id="newtab-customize-title" class="newtab-customize-panel-item">
+            <label>&newtab.customize.cog.title2;</label>
+        </div>
+
+        <div class="newtab-customize-complex-option">
+            <div id="newtab-customize-classic" class="newtab-customize-panel-superitem newtab-customize-panel-item selectable">
+                <label>&newtab.customize.classic;</label>
+            </div>
+            <div id="newtab-customize-enhanced" class="newtab-customize-panel-subitem">
+                <label class="checkbox"></label>
+                <label>&newtab.customize.cog.enhanced;</label>
+            </div>
+        </div>
+        <div id="newtab-customize-blank" class="newtab-customize-panel-item selectable">
+            <label>&newtab.customize.blank2;</label>
+        </div>
+        <div id="newtab-customize-learn" class="newtab-customize-panel-item">
+            <label>&newtab.customize.cog.learn;</label>
+        </div>
+    </div>
+  </div>
+
+  <div id="newtab-customize-overlay"></div>
 
   <div id="newtab-intro-mask">
     <div id="newtab-intro-modal">
       <div id="newtab-intro-progress">
         <div id="newtab-intro-numerical-progress"/>
         <div id="newtab-intro-graphical-progress">
           <span id="indicator"/>
         </div>
@@ -121,17 +124,16 @@
         </div>
 
         <div class="newtab-side-margin"/>
       </div>
 
       <div id="newtab-margin-bottom"/>
 
     </div>
-
     <input id="newtab-customize-button" type="button" title="&newtab.customize.title;"/>
   </div>
 
   <xul:script type="text/javascript;version=1.8"
               src="chrome://browser/content/searchSuggestionUI.js"/>
   <xul:script type="text/javascript;version=1.8"
               src="chrome://browser/content/newtab/newTab.js"/>
 </xul:window>
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -346,16 +346,17 @@ skip-if = buildapp == 'mulet'
 [browser_popup_blocker.js]
 skip-if = (os == 'linux') || (e10s && debug) # Frequent bug 1081925 and bug 1125520 failures
 [browser_printpreview.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1101973 - breaks the next test in e10s, and may be responsible for later timeout after logging "Error: Channel closing: too late to send/recv, messages will be lost"
 [browser_private_browsing_window.js]
 skip-if = buildapp == 'mulet'
 [browser_private_no_prompt.js]
 skip-if = buildapp == 'mulet'
+[browser_PageMetaData_pushstate.js]
 [browser_relatedTabs.js]
 [browser_remoteTroubleshoot.js]
 support-files =
   test_remoteTroubleshoot.html
 [browser_removeTabsToTheEnd.js]
 [browser_removeUnsafeProtocolsFromURLBarPaste.js]
 [browser_restore_isAppTab.js]
 [browser_sanitize-passwordDisabledHosts.js]
@@ -413,17 +414,21 @@ skip-if = e10s
 support-files =
   close_beforeunload_opens_second_tab.html
   close_beforeunload.html
 [browser_tabs_isActive.js]
 skip-if = e10s # Bug 1100664 - test relies on linkedBrowser.docShell
 [browser_tabs_owner.js]
 [browser_testOpenNewRemoteTabsFromNonRemoteBrowsers.js]
 run-if = e10s
-[browser_trackingUI.js]
+[browser_trackingUI_1.js]
+support-files =
+  trackingPage.html
+  benignPage.html
+[browser_trackingUI_2.js]
 support-files =
   trackingPage.html
   benignPage.html
 [browser_typeAheadFind.js]
 skip-if = buildapp == 'mulet'
 [browser_unknownContentType_title.js]
 [browser_unloaddialogs.js]
 skip-if = e10s # Bug 1100700 - test relies on unload event firing on closed tabs, which it doesn't
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_PageMetaData_pushstate.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(function* () {
+  let rooturi = "https://example.com/browser/toolkit/modules/tests/browser/";
+  yield BrowserTestUtils.openNewForegroundTab(gBrowser, rooturi + "metadata_simple.html");
+  let result = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+    return PageMetadata.getData(content.document);
+  });
+  // result should have description
+  is(result.url, rooturi + "metadata_simple.html", "metadata url is correct");
+  is(result.title, "Test Title", "metadata title is correct");
+  is(result.description, "A very simple test page", "description is correct");
+
+  result = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+    content.history.pushState({}, "2", "2.html");
+    return PageMetadata.getData(content.document);
+  });
+  // result should not have description
+  is(result.url, rooturi + "2.html", "metadata url is correct");
+  is(result.title, "Test Title", "metadata title is correct");
+  ok(!result.description, "description is undefined");
+
+  let documentURI = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+    return content.document.documentURI;
+  });
+  is(gBrowser.currentURI.spec, rooturi + "2.html", "gBrowser has correct url");
+  is(documentURI, rooturi + "2.html", "content.document has correct url");
+
+  gBrowser.removeTab(gBrowser.selectedTab);
+});
rename from browser/base/content/test/general/browser_trackingUI.js
rename to browser/base/content/test/general/browser_trackingUI_1.js
--- a/browser/base/content/test/general/browser_trackingUI.js
+++ b/browser/base/content/test/general/browser_trackingUI_1.js
@@ -1,106 +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/. */
 
 // Test that the Tracking Protection Doorhanger appears
 // and has the correct state when tracking content is blocked (Bug 1043801)
 
 var PREF = "privacy.trackingprotection.enabled";
-var TABLE = "urlclassifier.trackingTable";
-
-// Update tracking database
-function doUpdate() {
-  // Add some URLs to the tracking database (to be blocked)
-  var testData = "tracking.example.com/";
-  var testUpdate =
-    "n:1000\ni:test-track-simple\nad:1\n" +
-    "a:524:32:" + testData.length + "\n" +
-    testData;
-
-  var dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
-                  .getService(Ci.nsIUrlClassifierDBService);
-
-  let deferred = Promise.defer();
-
-  var listener = {
-    QueryInterface: function(iid)
-    {
-      if (iid.equals(Ci.nsISupports) ||
-          iid.equals(Ci.nsIUrlClassifierUpdateObserver))
-        return this;
-
-      throw Cr.NS_ERROR_NO_INTERFACE;
-    },
-    updateUrlRequested: function(url) { },
-    streamFinished: function(status) { },
-    updateError: function(errorCode) {
-      ok(false, "Couldn't update classifier.");
-      deferred.resolve();
-    },
-    updateSuccess: function(requestedTimeout) {
-      deferred.resolve();
-    }
-  };
-
-  dbService.beginUpdate(listener, "test-track-simple", "");
-  dbService.beginStream("", "");
-  dbService.updateStream(testUpdate);
-  dbService.finishStream();
-  dbService.finishUpdate();
-
-  return deferred.promise;
-}
+var BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/benignPage.html";
+var TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html";
 
 function testBenignPage(gTestBrowser)
 {
   // Make sure the doorhanger does NOT appear
   var notification = PopupNotifications.getNotification("bad-content", gTestBrowser);
   is(notification, null, "Tracking Content Doorhanger did NOT appear when protection was ON and tracking was NOT present");
 }
 
 function* testTrackingPage(gTestBrowser)
 {
   // Make sure the doorhanger appears
   var notification = PopupNotifications.getNotification("bad-content", gTestBrowser);
   isnot(notification, null, "Tracking Content Doorhanger did appear when protection was ON and tracking was present");
   notification.reshow();
+  var notificationElement = PopupNotifications.panel.firstChild;
 
   // Wait for the method to be attached after showing the popup
   yield promiseWaitForCondition(() => {
-    return PopupNotifications.panel.firstChild.disableTrackingContentProtection;
+    return notificationElement.disableTrackingContentProtection;
   });
 
-
   // Make sure the state of the doorhanger includes blocking tracking elements
-  is(PopupNotifications.panel.firstChild.isTrackingContentBlocked,
-     Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
+  ok(notificationElement.isTrackingContentBlocked,
      "Tracking Content is being blocked");
 
   // Make sure the notification has no trackingblockdisabled attribute
-  ok(!PopupNotifications.panel.firstChild.hasAttribute("trackingblockdisabled"),
+  ok(!notificationElement.hasAttribute("trackingblockdisabled"),
     "Doorhanger must have no trackingblockdisabled attribute");
-
-  // Disable Tracking Content Protection for the page (which reloads the page)
-  PopupNotifications.panel.firstChild.disableTrackingContentProtection();
 }
 
-function testTrackingPageWhitelisted(gTestBrowser)
+function* testTrackingPageWhitelisted(gTestBrowser)
 {
   // Make sure the doorhanger appears
   var notification = PopupNotifications.getNotification("bad-content", gTestBrowser);
   isnot(notification, null, "Tracking Content Doorhanger did appear when protection was ON and tracking was present but white-listed");
   notification.reshow();
+  var notificationElement = PopupNotifications.panel.firstChild;
+
+  // Wait for the method to be attached after showing the popup
+  yield promiseWaitForCondition(() => {
+    return notificationElement.disableTrackingContentProtection;
+  });
+
+  var notificationElement = PopupNotifications.panel.firstChild;
+
   // Make sure the state of the doorhanger does NOT include blocking tracking elements
-  is(PopupNotifications.panel.firstChild.isTrackingContentBlocked, 0,
+  ok(!notificationElement.isTrackingContentBlocked,
     "Tracking Content is NOT being blocked");
 
   // Make sure the notification has the trackingblockdisabled attribute set to true
-  is(PopupNotifications.panel.firstChild.getAttribute("trackingblockdisabled"), "true",
+  is(notificationElement.getAttribute("trackingblockdisabled"), "true",
     "Doorhanger must have [trackingblockdisabled='true'] attribute");
 }
 
 function testTrackingPageOFF(gTestBrowser)
 {
   // Make sure the doorhanger does NOT appear
   var notification = PopupNotifications.getNotification("bad-content", gTestBrowser);
   is(notification, null, "Tracking Content Doorhanger did NOT appear when protection was OFF and tracking was present");
@@ -111,45 +74,46 @@ function testBenignPageOFF(gTestBrowser)
   // Make sure the doorhanger does NOT appear
   var notification = PopupNotifications.getNotification("bad-content", gTestBrowser);
   is(notification, null, "Tracking Content Doorhanger did NOT appear when protection was OFF and tracking was NOT present");
 }
 
 add_task(function* () {
   registerCleanupFunction(function() {
     Services.prefs.clearUserPref(PREF);
-    Services.prefs.clearUserPref(TABLE);
     gBrowser.removeCurrentTab();
   });
 
-  // Populate and use 'test-track-simple' for tracking protection lookups
-  Services.prefs.setCharPref(TABLE, "test-track-simple");
-  yield doUpdate();
+  yield updateTrackingProtectionDatabase();
 
   let tab = gBrowser.selectedTab = gBrowser.addTab();
 
   // Enable Tracking Protection
   Services.prefs.setBoolPref(PREF, true);
 
   // Point tab to a test page NOT containing tracking elements
-  yield promiseTabLoadEvent(tab, "http://tracking.example.org/browser/browser/base/content/test/general/benignPage.html");
+  yield promiseTabLoadEvent(tab, BENIGN_PAGE);
   testBenignPage(gBrowser.getBrowserForTab(tab));
 
   // Point tab to a test page containing tracking elements
-  yield promiseTabLoadEvent(tab, "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html");
+  yield promiseTabLoadEvent(tab, TRACKING_PAGE);
+
+  // Tracking content must be blocked
   yield testTrackingPage(gBrowser.getBrowserForTab(tab));
 
+  // Disable Tracking Content Protection for the page (which reloads the page)
+  PopupNotifications.panel.firstChild.disableTrackingContentProtection();
+
   // Wait for tab to reload following tracking-protection page white-listing
   yield promiseTabLoadEvent(tab);
-  // Tracking content must be white-listed (NOT blocked)
-  testTrackingPageWhitelisted(gBrowser.getBrowserForTab(tab));
 
-  // Disable Tracking Protection
-  Services.prefs.setBoolPref(PREF, false);
+  // Tracking content must be white-listed (NOT blocked)
+  yield testTrackingPageWhitelisted(gBrowser.getBrowserForTab(tab));
+
+  // Re-enable Tracking Content Protection for the page (which reloads the page)
+  PopupNotifications.panel.firstChild.enableTrackingContentProtection();
 
-  // Point tab to a test page containing tracking elements
-  yield promiseTabLoadEvent(tab, "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html");
-  testTrackingPageOFF(gBrowser.getBrowserForTab(tab));
+  // Wait for tab to reload following tracking-protection page white-listing
+  yield promiseTabLoadEvent(tab);
 
-  // Point tab to a test page NOT containing tracking elements
-  yield promiseTabLoadEvent(tab, "http://tracking.example.org/browser/browser/base/content/test/general/benignPage.html");
-  testBenignPageOFF(gBrowser.getBrowserForTab(tab));
+  // Tracking content must be blocked
+  yield testTrackingPage(gBrowser.getBrowserForTab(tab));
 });
copy from browser/base/content/test/general/browser_trackingUI.js
copy to browser/base/content/test/general/browser_trackingUI_2.js
--- a/browser/base/content/test/general/browser_trackingUI.js
+++ b/browser/base/content/test/general/browser_trackingUI_2.js
@@ -1,108 +1,18 @@
 /* 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/. */
 
-// Test that the Tracking Protection Doorhanger appears
-// and has the correct state when tracking content is blocked (Bug 1043801)
+// Test that the Tracking Protection Doorhanger does not ever appear
+// when the feature is off (Bug 1043801)
 
 var PREF = "privacy.trackingprotection.enabled";
-var TABLE = "urlclassifier.trackingTable";
-
-// Update tracking database
-function doUpdate() {
-  // Add some URLs to the tracking database (to be blocked)
-  var testData = "tracking.example.com/";
-  var testUpdate =
-    "n:1000\ni:test-track-simple\nad:1\n" +
-    "a:524:32:" + testData.length + "\n" +
-    testData;
-
-  var dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
-                  .getService(Ci.nsIUrlClassifierDBService);
-
-  let deferred = Promise.defer();
-
-  var listener = {
-    QueryInterface: function(iid)
-    {
-      if (iid.equals(Ci.nsISupports) ||
-          iid.equals(Ci.nsIUrlClassifierUpdateObserver))
-        return this;
-
-      throw Cr.NS_ERROR_NO_INTERFACE;
-    },
-    updateUrlRequested: function(url) { },
-    streamFinished: function(status) { },
-    updateError: function(errorCode) {
-      ok(false, "Couldn't update classifier.");
-      deferred.resolve();
-    },
-    updateSuccess: function(requestedTimeout) {
-      deferred.resolve();
-    }
-  };
-
-  dbService.beginUpdate(listener, "test-track-simple", "");
-  dbService.beginStream("", "");
-  dbService.updateStream(testUpdate);
-  dbService.finishStream();
-  dbService.finishUpdate();
-
-  return deferred.promise;
-}
-
-function testBenignPage(gTestBrowser)
-{
-  // Make sure the doorhanger does NOT appear
-  var notification = PopupNotifications.getNotification("bad-content", gTestBrowser);
-  is(notification, null, "Tracking Content Doorhanger did NOT appear when protection was ON and tracking was NOT present");
-}
-
-function* testTrackingPage(gTestBrowser)
-{
-  // Make sure the doorhanger appears
-  var notification = PopupNotifications.getNotification("bad-content", gTestBrowser);
-  isnot(notification, null, "Tracking Content Doorhanger did appear when protection was ON and tracking was present");
-  notification.reshow();
-
-  // Wait for the method to be attached after showing the popup
-  yield promiseWaitForCondition(() => {
-    return PopupNotifications.panel.firstChild.disableTrackingContentProtection;
-  });
-
-
-  // Make sure the state of the doorhanger includes blocking tracking elements
-  is(PopupNotifications.panel.firstChild.isTrackingContentBlocked,
-     Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
-     "Tracking Content is being blocked");
-
-  // Make sure the notification has no trackingblockdisabled attribute
-  ok(!PopupNotifications.panel.firstChild.hasAttribute("trackingblockdisabled"),
-    "Doorhanger must have no trackingblockdisabled attribute");
-
-  // Disable Tracking Content Protection for the page (which reloads the page)
-  PopupNotifications.panel.firstChild.disableTrackingContentProtection();
-}
-
-function testTrackingPageWhitelisted(gTestBrowser)
-{
-  // Make sure the doorhanger appears
-  var notification = PopupNotifications.getNotification("bad-content", gTestBrowser);
-  isnot(notification, null, "Tracking Content Doorhanger did appear when protection was ON and tracking was present but white-listed");
-  notification.reshow();
-  // Make sure the state of the doorhanger does NOT include blocking tracking elements
-  is(PopupNotifications.panel.firstChild.isTrackingContentBlocked, 0,
-    "Tracking Content is NOT being blocked");
-
-  // Make sure the notification has the trackingblockdisabled attribute set to true
-  is(PopupNotifications.panel.firstChild.getAttribute("trackingblockdisabled"), "true",
-    "Doorhanger must have [trackingblockdisabled='true'] attribute");
-}
+var BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/benignPage.html";
+var TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html";
 
 function testTrackingPageOFF(gTestBrowser)
 {
   // Make sure the doorhanger does NOT appear
   var notification = PopupNotifications.getNotification("bad-content", gTestBrowser);
   is(notification, null, "Tracking Content Doorhanger did NOT appear when protection was OFF and tracking was present");
 }
 
@@ -111,45 +21,26 @@ function testBenignPageOFF(gTestBrowser)
   // Make sure the doorhanger does NOT appear
   var notification = PopupNotifications.getNotification("bad-content", gTestBrowser);
   is(notification, null, "Tracking Content Doorhanger did NOT appear when protection was OFF and tracking was NOT present");
 }
 
 add_task(function* () {
   registerCleanupFunction(function() {
     Services.prefs.clearUserPref(PREF);
-    Services.prefs.clearUserPref(TABLE);
     gBrowser.removeCurrentTab();
   });
 
-  // Populate and use 'test-track-simple' for tracking protection lookups
-  Services.prefs.setCharPref(TABLE, "test-track-simple");
-  yield doUpdate();
+  yield updateTrackingProtectionDatabase();
 
   let tab = gBrowser.selectedTab = gBrowser.addTab();
 
-  // Enable Tracking Protection
-  Services.prefs.setBoolPref(PREF, true);
-
-  // Point tab to a test page NOT containing tracking elements
-  yield promiseTabLoadEvent(tab, "http://tracking.example.org/browser/browser/base/content/test/general/benignPage.html");
-  testBenignPage(gBrowser.getBrowserForTab(tab));
-
-  // Point tab to a test page containing tracking elements
-  yield promiseTabLoadEvent(tab, "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html");
-  yield testTrackingPage(gBrowser.getBrowserForTab(tab));
-
-  // Wait for tab to reload following tracking-protection page white-listing
-  yield promiseTabLoadEvent(tab);
-  // Tracking content must be white-listed (NOT blocked)
-  testTrackingPageWhitelisted(gBrowser.getBrowserForTab(tab));
-
   // Disable Tracking Protection
   Services.prefs.setBoolPref(PREF, false);
 
   // Point tab to a test page containing tracking elements
-  yield promiseTabLoadEvent(tab, "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html");
+  yield promiseTabLoadEvent(tab, TRACKING_PAGE);
   testTrackingPageOFF(gBrowser.getBrowserForTab(tab));
 
   // Point tab to a test page NOT containing tracking elements
-  yield promiseTabLoadEvent(tab, "http://tracking.example.org/browser/browser/base/content/test/general/benignPage.html");
+  yield promiseTabLoadEvent(tab, BENIGN_PAGE);
   testBenignPageOFF(gBrowser.getBrowserForTab(tab));
 });
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -655,16 +655,65 @@ function promiseWindow(url) {
 function promiseIndicatorWindow() {
   // We don't show the indicator window on Mac.
   if ("nsISystemStatusBar" in Ci)
     return Promise.resolve();
 
   return promiseWindow("chrome://browser/content/webrtcIndicator.xul");
 }
 
+/**
+ * Add some entries to a test tracking protection database, and reset
+ * back to the default database after the test ends.
+ */
+function updateTrackingProtectionDatabase() {
+  let TABLE = "urlclassifier.trackingTable";
+  Services.prefs.setCharPref(TABLE, "test-track-simple");
+
+  registerCleanupFunction(function() {
+    Services.prefs.clearUserPref(TABLE);
+  });
+
+  // Add some URLs to the tracking database (to be blocked)
+  let testData = "tracking.example.com/";
+  let testUpdate =
+    "n:1000\ni:test-track-simple\nad:1\n" +
+    "a:524:32:" + testData.length + "\n" +
+    testData;
+
+  return new Promise((resolve, reject) => {
+    let dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
+                    .getService(Ci.nsIUrlClassifierDBService);
+    let listener = {
+      QueryInterface: iid => {
+        if (iid.equals(Ci.nsISupports) ||
+            iid.equals(Ci.nsIUrlClassifierUpdateObserver))
+          return listener;
+
+        throw Cr.NS_ERROR_NO_INTERFACE;
+      },
+      updateUrlRequested: url => { },
+      streamFinished: status => { },
+      updateError: errorCode => {
+        ok(false, "Couldn't update classifier.");
+        resolve();
+      },
+      updateSuccess: requestedTimeout => {
+        resolve();
+      }
+    };
+
+    dbService.beginUpdate(listener, "test-track-simple", "");
+    dbService.beginStream("", "");
+    dbService.updateStream(testUpdate);
+    dbService.finishStream();
+    dbService.finishUpdate();
+  });
+}
+
 function assertWebRTCIndicatorStatus(expected) {
   let ui = Cu.import("resource:///modules/webrtcUI.jsm", {}).webrtcUI;
   let expectedState = expected ? "visible" : "hidden";
   let msg = "WebRTC indicator " + expectedState;
   if (!expected && ui.showGlobalIndicator) {
     // It seems the global indicator is not always removed synchronously
     // in some cases.
     info("waiting for the global indicator to be hidden");
--- a/browser/base/content/test/newtab/head.js
+++ b/browser/base/content/test/newtab/head.js
@@ -737,31 +737,41 @@ function whenSearchInitDone() {
 
 /**
  * Changes the newtab customization option and waits for the panel to open and close
  *
  * @param {string} aTheme
  *        Can be any of("blank"|"classic"|"enhanced")
  */
 function customizeNewTabPage(aTheme) {
-  let document = getContentDocument();
-  let panel = document.getElementById("newtab-customize-panel");
-  let customizeButton = document.getElementById("newtab-customize-button");
+  let promise = ContentTask.spawn(gBrowser.selectedBrowser, aTheme, function*(aTheme) {
+
+    let document = content.document;
+    let panel = document.getElementById("newtab-customize-panel");
+    let customizeButton = document.getElementById("newtab-customize-button");
 
-  // Attache onShown the listener on panel
-  panel.addEventListener("popupshown", function onShown() {
-    panel.removeEventListener("popupshown", onShown);
+    function panelOpened(opened) {
+      return new Promise( (resolve) => {
+        let options = {attributes: true, oldValue: true};
+        let observer = new content.MutationObserver(function(mutations) {
+          mutations.forEach(function(mutation) {
+            document.getElementById("newtab-customize-" + aTheme).click();
+            observer.disconnect();
+            if (opened == panel.hasAttribute("open")) {
+              resolve();
+            }
+          });
+        });
+        observer.observe(panel, options);
+      });
+    }
 
-    // Get the element for the specific option and click on it,
-    // then trigger an escape to close the panel
-    document.getElementById("newtab-customize-" + aTheme).click();
-    executeSoon(() => { panel.hidePopup(); });
+    let opened = panelOpened(true);
+    customizeButton.click();
+    yield opened;
+
+    let closed = panelOpened(false);
+    customizeButton.click();
+    yield closed;
   });
 
-  // Attache the listener for panel closing, this will resolve the promise
-  panel.addEventListener("popuphidden", function onHidden() {
-    panel.removeEventListener("popuphidden", onHidden);
-    executeSoon(TestRunner.next);
-  });
-
-  // Click on the customize button to display the panel
-  customizeButton.click();
+  promise.then(TestRunner.next);
 }
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -2298,18 +2298,18 @@ file, You can obtain one at http://mozil
           "trackingContentAction.block");
       </field>
       <field name="_trackingContentHelpLink">
         document.getAnonymousElementByAttribute(this, "anonid",
           "trackingContent.helplink")
       </field>
       <property name="isTrackingContentBlocked" readonly="true">
         <getter><![CDATA[
-          return this.notification.options.state &
-            Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT;
+          return !!(this.notification.options.state &
+            Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT);
         ]]></getter>
       </property>
       <constructor><![CDATA[
         // default title
         _doorhangerTitle.value =
           gNavigatorBundle.getFormattedString(
             "badContentBlocked.notblocked.message", [this._brandShortName]);
         if (this.notification.options.state &
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -480,19 +480,19 @@ loop.contacts = (function(_, mozL10n) {
         case "edit":
           this.props.startForm("contacts_edit", contact);
           break;
         case "remove":
           navigator.mozLoop.confirm({
             message: mozL10n.get("confirm_delete_contact_alert"),
             okButton: mozL10n.get("confirm_delete_contact_remove_button"),
             cancelButton: mozL10n.get("confirm_delete_contact_cancel_button")
-          }, (err, result) => {
-            if (err) {
-              throw err;
+          }, (error, result) => {
+            if (error) {
+              throw error;
             }
 
             if (!result) {
               return;
             }
 
             navigator.mozLoop.contacts.remove(contact._guid, err => {
               if (err) {
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -87,19 +87,20 @@ loop.roomViews = (function(mozL10n) {
 
       this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());
     },
 
     handleProviderClick: function(event) {
       event.preventDefault();
 
       var origin = event.currentTarget.dataset.provider;
-      var provider = this.props.socialShareProviders.filter(function(provider) {
-        return provider.origin == origin;
-      })[0];
+      var provider = this.props.socialShareProviders
+                         .filter(function(socialProvider) {
+                           return socialProvider.origin == origin;
+                         })[0];
 
       this.props.dispatcher.dispatch(new sharedActions.ShareRoomUrl({
         provider: provider,
         roomUrl: this.props.roomUrl,
         previews: []
       }));
     },
 
@@ -299,22 +300,22 @@ loop.roomViews = (function(mozL10n) {
         newState.editMode = nextProps.editMode;
         // If we're switching to edit mode, fetch the metadata of the current tab.
         // But _only_ if there's no context currently attached to the room; the
         // checkbox will be disabled in that case.
         if (nextProps.editMode) {
           this.props.mozLoop.getSelectedTabMetadata(function(metadata) {
             var previewImage = metadata.favicon || "";
             var description = metadata.title || metadata.description;
-            var url = metadata.url;
+            var metaUrl = metadata.url;
             this.setState({
               availableContext: {
                 previewImage: previewImage,
                 description: description,
-                url: url
+                url: metaUrl
               }
            });
           }.bind(this));
         }
       }
       // When we receive an update for the `roomData` property, make sure that
       // the current form fields reflect reality. This is necessary, because the
       // form state is maintained in the components' state.
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -386,24 +386,24 @@ loop.shared.views = (function(_, l10n) {
       var outgoing = this.getDOMNode().querySelector(".local");
 
       // XXX move this into its StreamingVideo component?
       this.publisher = this.props.sdk.initPublisher(
         outgoing, this.getDefaultPublisherConfig({publishVideo: this.props.video.enabled}));
 
       // Suppress OT GuM custom dialog, see bug 1018875
       this.listenTo(this.publisher, "accessDialogOpened accessDenied",
-                    function(event) {
-                      event.preventDefault();
+                    function(ev) {
+                      ev.preventDefault();
                     });
 
-      this.listenTo(this.publisher, "streamCreated", function(event) {
+      this.listenTo(this.publisher, "streamCreated", function(ev) {
         this.setState({
-          audio: {enabled: event.stream.hasAudio},
-          video: {enabled: event.stream.hasVideo}
+          audio: {enabled: ev.stream.hasAudio},
+          video: {enabled: ev.stream.hasVideo}
         });
       }.bind(this));
 
       this.listenTo(this.publisher, "streamDestroyed", function() {
         this.setState({
           audio: {enabled: false},
           video: {enabled: false}
         });
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -4,19 +4,22 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/.  -->
 <html>
   <head>
     <meta charset="utf-8">
     <title>Loop UI Components Showcase</title>
     <link rel="stylesheet" type="text/css" href="../content/shared/css/reset.css">
     <link rel="stylesheet" type="text/css" href="../content/shared/css/common.css">
     <link rel="stylesheet" type="text/css" href="../content/shared/css/conversation.css">
-    <link rel="stylesheet" type="text/css" href="../content/css/panel.css">
-    <link rel="stylesheet" type="text/css" href="../content/css/contacts.css">
-    <link rel="stylesheet" type="text/css" href="../content/css/webapp.css">
+    <link class="fx-embedded-panel" rel="stylesheet" type="text/css"
+          href="../content/css/panel.css">
+    <link class="fx-embedded-panel" rel="stylesheet" type="text/css"
+          href="../content/css/contacts.css">
+    <link class="standalone" rel="stylesheet" type="text/css"
+          href="../content/css/webapp.css">
     <link rel="stylesheet" type="text/css" href="ui-showcase.css">
  </head>
   <body>
     <script>
       var uncaughtError;
       window.addEventListener("error", function(error) {
         uncaughtError = error;
       });
--- a/browser/components/loop/ui/react-frame-component.js
+++ b/browser/components/loop/ui/react-frame-component.js
@@ -24,64 +24,88 @@ window.queuedFrames = [];
  * @type {ReactComponentFactory<P>}
  */
 window.Frame = React.createClass({
   propTypes: {
     style: React.PropTypes.object,
     head: React.PropTypes.node,
     width: React.PropTypes.number,
     height: React.PropTypes.number,
-    onContentsRendered: React.PropTypes.func
+    onContentsRendered: React.PropTypes.func,
+    className: React.PropTypes.string,
+    /* By default, <link rel="stylesheet> nodes from the containing frame's
+       head will be cloned into this iframe.  However, if the link also has
+       a "class" attribute, we only clone it if that class attribute is the
+       same as cssClass.  This allows us to avoid injecting stylesheets that
+       aren't intended for this rendering of this component. */
+    cssClass: React.PropTypes.string
   },
   render: function() {
     return React.createElement("iframe", {
       style: this.props.style,
       head: this.props.head,
       width: this.props.width,
-      height: this.props.height
+      height: this.props.height,
+      className: this.props.className
     });
   },
   componentDidMount: function() {
     this.renderFrameContents();
   },
   renderFrameContents: function() {
-    var doc = this.getDOMNode().contentDocument;
-    if (doc && doc.readyState === "complete") {
+    function isStyleSheet(node) {
+      return node.tagName.toLowerCase() === "link" &&
+        node.getAttribute("rel") === "stylesheet";
+    }
+
+    var childDoc = this.getDOMNode().contentDocument;
+    if (childDoc && childDoc.readyState === "complete") {
       // Remove this from the queue.
       window.queuedFrames.splice(window.queuedFrames.indexOf(this), 1);
 
-      var iframeHead = doc.querySelector("head");
+      var iframeHead = childDoc.querySelector("head");
       var parentHeadChildren = document.querySelector("head").children;
 
       [].forEach.call(parentHeadChildren, function(parentHeadNode) {
+
+        // if this node is a CSS stylesheet...
+        if (isStyleSheet(parentHeadNode)) {
+          // and it has a class different from the one that this frame does,
+          // return immediately instead of appending it.
+          if (parentHeadNode.hasAttribute("class") && this.props.cssClass &&
+            parentHeadNode.getAttribute("class") !== this.props.cssClass) {
+            return;
+          }
+        }
+
         iframeHead.appendChild(parentHeadNode.cloneNode(true));
-      });
+      }.bind(this));
 
       var contents = React.createElement("div",
         undefined,
         this.props.head,
         this.props.children
       );
 
-      React.render(contents, doc.body, this.fireOnContentsRendered.bind(this));
+      React.render(contents, childDoc.body, this.fireOnContentsRendered);
 
       // Set the RTL mode. We assume for now that rtl is the only query parameter.
       //
       // See also "ShowCase" in ui-showcase.jsx
       if (document.location.search === "?rtl=1") {
-        doc.documentElement.setAttribute("lang", "ar");
-        doc.documentElement.setAttribute("dir", "rtl");
+        childDoc.documentElement.setAttribute("lang", "ar");
+        childDoc.documentElement.setAttribute("dir", "rtl");
       }
     } else {
       // Queue it, only if it isn't already. We do need to set the timeout
       // regardless, as this function can get re-entered several times.
       if (window.queuedFrames.indexOf(this) === -1) {
         window.queuedFrames.push(this);
       }
-      setTimeout(this.renderFrameContents.bind(this), 0);
+      setTimeout(this.renderFrameContents, 0);
     }
   },
   /**
    * Fires the onContentsRendered callback passed in via this.props,
    * with the first argument set to the window global used by the iframe.
    * This is useful in extracting things specific to that iframe (such as
    * the matchMedia function) for use by code running in that iframe.  Once
    * React gets a more complete "context" feature:
--- a/browser/components/loop/ui/ui-showcase.css
+++ b/browser/components/loop/ui/ui-showcase.css
@@ -52,17 +52,19 @@ body {
   margin: 1em 0;
   border-bottom: 1px solid #aaa;
 }
 
 .showcase > section .comp {
   margin: 0 auto; /* width is usually set programmatically */
 }
 
-.showcase > section .comp.dashed {
+.showcase > section .comp.dashed,
+.showcase > section .comp > iframe.dashed
+{
   border: 1px dashed #ccc;
 }
 
 .showcase > section > .example {
   margin-bottom: 6em;
 }
 
 .showcase > section > h2 {
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -95,19 +95,19 @@
   /**
    * Every view that uses an activeRoomStore needs its own; if they shared
    * an active store, they'd interfere with each other.
    *
    * @param options
    * @returns {loop.store.ActiveRoomStore}
    */
   function makeActiveRoomStore(options) {
-    var dispatcher = new loop.Dispatcher();
+    var roomDispatcher = new loop.Dispatcher();
 
-    var store = new loop.store.ActiveRoomStore(dispatcher, {
+    var store = new loop.store.ActiveRoomStore(roomDispatcher, {
       mozLoop: navigator.mozLoop,
       sdkDriver: mockSDK
     });
 
     if (!("remoteVideoEnabled" in options)) {
       options.remoteVideoEnabled = true;
     }
 
@@ -365,35 +365,47 @@
       );
     }
   });
 
   var FramedExample = React.createClass({displayName: "FramedExample",
     propTypes: {
       width: React.PropTypes.number,
       height: React.PropTypes.number,
-      onContentsRendered: React.PropTypes.func
+      onContentsRendered: React.PropTypes.func,
+      dashed: React.PropTypes.bool,
+      cssClass: React.PropTypes.string
     },
 
     makeId: function(prefix) {
       return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
     },
 
     render: function() {
+      var height = this.props.height;
+      var width = this.props.width;
+
+      // make room for a 1-pixel border on each edge
+      if (this.props.dashed) {
+        height += 2;
+        width += 2;
+      }
+
       var cx = React.addons.classSet;
       return (
         React.createElement("div", {className: "example"}, 
           React.createElement("h3", {id: this.makeId()}, 
             this.props.summary, 
             React.createElement("a", {href: this.makeId("#")}, " ¶")
           ), 
-          React.createElement("div", {className: cx({comp: true, dashed: this.props.dashed}), 
-               style: this.props.style}, 
-            React.createElement(Frame, {width: this.props.width, height: this.props.height, 
-                   onContentsRendered: this.props.onContentsRendered}, 
+          React.createElement("div", {className: "comp"}, 
+            React.createElement(Frame, {width: width, height: height, 
+                   onContentsRendered: this.props.onContentsRendered, 
+                   className: cx({dashed: this.props.dashed}), 
+                   cssClass: this.props.cssClass}, 
               this.props.children
             )
           )
         )
       );
     }
   });
 
@@ -810,155 +822,165 @@
                   dispatcher: dispatcher, 
                   mozLoop: navigator.mozLoop, 
                   localPosterUrl: "sample-img/video-screen-local.png"})
               )
             )
           ), 
 
           React.createElement(Section, {name: "StandaloneRoomView"}, 
-            React.createElement(FramedExample, {width: 644, height: 483, 
-              summary: "Standalone room conversation (ready)"}, 
+            React.createElement(FramedExample, {width: 644, height: 483, dashed: true, 
+                           cssClass: "standalone", 
+                           summary: "Standalone room conversation (ready)"}, 
               React.createElement("div", {className: "standalone"}, 
                 React.createElement(StandaloneRoomView, {
                   dispatcher: dispatcher, 
                   activeRoomStore: readyRoomStore, 
                   roomState: ROOM_STATES.READY, 
                   isFirefox: true})
               )
             ), 
 
-            React.createElement(FramedExample, {width: 644, height: 483, 
+            React.createElement(FramedExample, {width: 644, height: 483, dashed: true, 
               summary: "Standalone room conversation (joined)", 
+              cssClass: "standalone", 
               onContentsRendered: joinedRoomStore.forcedUpdate}, 
               React.createElement("div", {className: "standalone"}, 
                 React.createElement(StandaloneRoomView, {
                   dispatcher: dispatcher, 
                   activeRoomStore: joinedRoomStore, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   isFirefox: true})
               )
             ), 
 
-            React.createElement(FramedExample, {width: 644, height: 483, 
+            React.createElement(FramedExample, {width: 644, height: 483, dashed: true, 
+                           cssClass: "standalone", 
                            onContentsRendered: updatingActiveRoomStore.forcedUpdate, 
                            summary: "Standalone room conversation (has-participants, 644x483)"}, 
                 React.createElement("div", {className: "standalone"}, 
                   React.createElement(StandaloneRoomView, {
                     dispatcher: dispatcher, 
                     activeRoomStore: updatingActiveRoomStore, 
                     roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                     isFirefox: true, 
                     localPosterUrl: "sample-img/video-screen-local.png", 
                     remotePosterUrl: "sample-img/video-screen-remote.png"})
                 )
             ), 
 
-            React.createElement(FramedExample, {width: 644, height: 483, 
+            React.createElement(FramedExample, {width: 644, height: 483, dashed: true, 
+                           cssClass: "standalone", 
                            onContentsRendered: localFaceMuteRoomStore.forcedUpdate, 
                            summary: "Standalone room conversation (local face mute, has-participants, 644x483)"}, 
               React.createElement("div", {className: "standalone"}, 
                 React.createElement(StandaloneRoomView, {
                   dispatcher: dispatcher, 
                   activeRoomStore: localFaceMuteRoomStore, 
                   isFirefox: true, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   remotePosterUrl: "sample-img/video-screen-remote.png"})
               )
             ), 
 
-            React.createElement(FramedExample, {width: 644, height: 483, 
+            React.createElement(FramedExample, {width: 644, height: 483, dashed: true, 
+                           cssClass: "standalone", 
                            onContentsRendered: remoteFaceMuteRoomStore.forcedUpdate, 
                            summary: "Standalone room conversation (remote face mute, has-participants, 644x483)"}, 
               React.createElement("div", {className: "standalone"}, 
                 React.createElement(StandaloneRoomView, {
                   dispatcher: dispatcher, 
                   activeRoomStore: remoteFaceMuteRoomStore, 
                   isFirefox: true, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   remotePosterUrl: "sample-img/video-screen-remote.png"})
               )
             ), 
 
-            React.createElement(FramedExample, {width: 800, height: 660, 
+            React.createElement(FramedExample, {width: 800, height: 660, dashed: true, 
+                           cssClass: "standalone", 
                            onContentsRendered: updatingSharingRoomStore.forcedUpdate, 
               summary: "Standalone room convo (has-participants, receivingScreenShare, 800x660)"}, 
                 React.createElement("div", {className: "standalone"}, 
                   React.createElement(StandaloneRoomView, {
                     dispatcher: dispatcher, 
                     activeRoomStore: updatingSharingRoomStore, 
                     roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                     isFirefox: true, 
                     localPosterUrl: "sample-img/video-screen-local.png", 
                     remotePosterUrl: "sample-img/video-screen-remote.png", 
                     screenSharePosterUrl: "sample-img/video-screen-terminal.png"}
                   )
                 )
             ), 
 
-            React.createElement(FramedExample, {width: 644, height: 483, 
+            React.createElement(FramedExample, {width: 644, height: 483, dashed: true, 
+                           cssClass: "standalone", 
                            summary: "Standalone room conversation (full - FFx user)"}, 
               React.createElement("div", {className: "standalone"}, 
                 React.createElement(StandaloneRoomView, {
                   dispatcher: dispatcher, 
                   activeRoomStore: fullActiveRoomStore, 
                   isFirefox: true})
               )
             ), 
 
-            React.createElement(FramedExample, {width: 644, height: 483, 
-              summary: "Standalone room conversation (full - non FFx user)"}, 
+            React.createElement(FramedExample, {width: 644, height: 483, dashed: true, 
+                           cssClass: "standalone", 
+                           summary: "Standalone room conversation (full - non FFx user)"}, 
               React.createElement("div", {className: "standalone"}, 
                 React.createElement(StandaloneRoomView, {
                   dispatcher: dispatcher, 
                   activeRoomStore: fullActiveRoomStore, 
                   isFirefox: false})
               )
             ), 
 
-            React.createElement(FramedExample, {width: 644, height: 483, 
-              summary: "Standalone room conversation (feedback)"}, 
+            React.createElement(FramedExample, {width: 644, height: 483, dashed: true, 
+                           cssClass: "standalone", 
+                           summary: "Standalone room conversation (feedback)"}, 
               React.createElement("div", {className: "standalone"}, 
                 React.createElement(StandaloneRoomView, {
                   dispatcher: dispatcher, 
                   activeRoomStore: endedRoomStore, 
                   feedbackStore: feedbackStore, 
                   isFirefox: false})
               )
             ), 
 
-            React.createElement(FramedExample, {width: 644, height: 483, 
+            React.createElement(FramedExample, {width: 644, height: 483, dashed: true, 
+                           cssClass: "standalone", 
                            summary: "Standalone room conversation (failed)"}, 
               React.createElement("div", {className: "standalone"}, 
                 React.createElement(StandaloneRoomView, {
                   dispatcher: dispatcher, 
                   activeRoomStore: failedRoomStore, 
                   isFirefox: false})
               )
             )
           ), 
 
           React.createElement(Section, {name: "StandaloneRoomView (Mobile)"}, 
-            React.createElement(FramedExample, {width: 600, height: 480, 
+            React.createElement(FramedExample, {width: 600, height: 480, cssClass: "standalone", 
                            onContentsRendered: updatingActiveRoomStore.forcedUpdate, 
                            summary: "Standalone room conversation (has-participants, 600x480)"}, 
                 React.createElement("div", {className: "standalone"}, 
                   React.createElement(StandaloneRoomView, {
                     dispatcher: dispatcher, 
                     activeRoomStore: updatingActiveRoomStore, 
                     roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                     isFirefox: true, 
                     localPosterUrl: "sample-img/video-screen-local.png", 
                     remotePosterUrl: "sample-img/video-screen-remote.png"})
                 )
             )
           ), 
 
           React.createElement(Section, {name: "TextChatView (standalone)"}, 
-            React.createElement(FramedExample, {width: 200, height: 400, 
+            React.createElement(FramedExample, {width: 200, height: 400, cssClass: "standalone", 
                           summary: "Standalone Text Chat conversation (200 x 400)"}, 
               React.createElement("div", {className: "standalone text-chat-example"}, 
                 React.createElement(TextChatView, {
                   dispatcher: dispatcher, 
                   showAlways: true, 
                   showRoomName: true})
               )
             )
@@ -990,17 +1012,17 @@
       }
     } catch(err) {
       console.error(err);
       uncaughtError = err;
     }
 
     // Wait until all the FramedExamples have been fully loaded.
     setTimeout(function waitForQueuedFrames() {
-      if (window.queuedFrames.length != 0) {
+      if (window.queuedFrames.length !== 0) {
         setTimeout(waitForQueuedFrames, 500);
         return;
       }
       // Put the title back, in case views changed it.
       document.title = "Loop UI Components Showcase";
 
       // This simulates the mocha layout for errors which means we can run
       // this alongside our other unit tests but use the same harness.
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -365,35 +365,47 @@
       );
     }
   });
 
   var FramedExample = React.createClass({
     propTypes: {
       width: React.PropTypes.number,
       height: React.PropTypes.number,
-      onContentsRendered: React.PropTypes.func
+      onContentsRendered: React.PropTypes.func,
+      dashed: React.PropTypes.bool,
+      cssClass: React.PropTypes.string
     },
 
     makeId: function(prefix) {
       return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
     },
 
     render: function() {
+      var height = this.props.height;
+      var width = this.props.width;
+
+      // make room for a 1-pixel border on each edge
+      if (this.props.dashed) {
+        height += 2;
+        width += 2;
+      }
+
       var cx = React.addons.classSet;
       return (
         <div className="example">
           <h3 id={this.makeId()}>
             {this.props.summary}
             <a href={this.makeId("#")}>&nbsp;¶</a>
           </h3>
-          <div className={cx({comp: true, dashed: this.props.dashed})}
-               style={this.props.style}>
-            <Frame width={this.props.width} height={this.props.height}
-                   onContentsRendered={this.props.onContentsRendered}>
+          <div className="comp">
+            <Frame width={width} height={height}
+                   onContentsRendered={this.props.onContentsRendered}
+                   className={cx({dashed: this.props.dashed})}
+                   cssClass={this.props.cssClass}>
               {this.props.children}
             </Frame>
           </div>
         </div>
       );
     }
   });
 
@@ -810,155 +822,165 @@
                   dispatcher={dispatcher}
                   mozLoop={navigator.mozLoop}
                   localPosterUrl="sample-img/video-screen-local.png" />
               </div>
             </FramedExample>
           </Section>
 
           <Section name="StandaloneRoomView">
-            <FramedExample width={644} height={483}
-              summary="Standalone room conversation (ready)">
+            <FramedExample width={644} height={483} dashed={true}
+                           cssClass="standalone"
+                           summary="Standalone room conversation (ready)">
               <div className="standalone">
                 <StandaloneRoomView
                   dispatcher={dispatcher}
                   activeRoomStore={readyRoomStore}
                   roomState={ROOM_STATES.READY}
                   isFirefox={true} />
               </div>
             </FramedExample>
 
-            <FramedExample width={644} height={483}
+            <FramedExample width={644} height={483} dashed={true}
               summary="Standalone room conversation (joined)"
+              cssClass="standalone"
               onContentsRendered={joinedRoomStore.forcedUpdate}>
               <div className="standalone">
                 <StandaloneRoomView
                   dispatcher={dispatcher}
                   activeRoomStore={joinedRoomStore}
                   localPosterUrl="sample-img/video-screen-local.png"
                   isFirefox={true} />
               </div>
             </FramedExample>
 
-            <FramedExample width={644} height={483}
+            <FramedExample width={644} height={483} dashed={true}
+                           cssClass="standalone"
                            onContentsRendered={updatingActiveRoomStore.forcedUpdate}
                            summary="Standalone room conversation (has-participants, 644x483)">
                 <div className="standalone">
                   <StandaloneRoomView
                     dispatcher={dispatcher}
                     activeRoomStore={updatingActiveRoomStore}
                     roomState={ROOM_STATES.HAS_PARTICIPANTS}
                     isFirefox={true}
                     localPosterUrl="sample-img/video-screen-local.png"
                     remotePosterUrl="sample-img/video-screen-remote.png" />
                 </div>
             </FramedExample>
 
-            <FramedExample width={644} height={483}
+            <FramedExample width={644} height={483} dashed={true}
+                           cssClass="standalone"
                            onContentsRendered={localFaceMuteRoomStore.forcedUpdate}
                            summary="Standalone room conversation (local face mute, has-participants, 644x483)">
               <div className="standalone">
                 <StandaloneRoomView
                   dispatcher={dispatcher}
                   activeRoomStore={localFaceMuteRoomStore}
                   isFirefox={true}
                   localPosterUrl="sample-img/video-screen-local.png"
                   remotePosterUrl="sample-img/video-screen-remote.png" />
               </div>
             </FramedExample>
 
-            <FramedExample width={644} height={483}
+            <FramedExample width={644} height={483} dashed={true}
+                           cssClass="standalone"
                            onContentsRendered={remoteFaceMuteRoomStore.forcedUpdate}
                            summary="Standalone room conversation (remote face mute, has-participants, 644x483)">
               <div className="standalone">
                 <StandaloneRoomView
                   dispatcher={dispatcher}
                   activeRoomStore={remoteFaceMuteRoomStore}
                   isFirefox={true}
                   localPosterUrl="sample-img/video-screen-local.png"
                   remotePosterUrl="sample-img/video-screen-remote.png" />
               </div>
             </FramedExample>
 
-            <FramedExample width={800} height={660}
+            <FramedExample width={800} height={660} dashed={true}
+                           cssClass="standalone"
                            onContentsRendered={updatingSharingRoomStore.forcedUpdate}
               summary="Standalone room convo (has-participants, receivingScreenShare, 800x660)">
                 <div className="standalone">
                   <StandaloneRoomView
                     dispatcher={dispatcher}
                     activeRoomStore={updatingSharingRoomStore}
                     roomState={ROOM_STATES.HAS_PARTICIPANTS}
                     isFirefox={true}
                     localPosterUrl="sample-img/video-screen-local.png"
                     remotePosterUrl="sample-img/video-screen-remote.png"
                     screenSharePosterUrl="sample-img/video-screen-terminal.png"
                   />
                 </div>
             </FramedExample>
 
-            <FramedExample width={644} height={483}
+            <FramedExample width={644} height={483} dashed={true}
+                           cssClass="standalone"
                            summary="Standalone room conversation (full - FFx user)">
               <div className="standalone">
                 <StandaloneRoomView
                   dispatcher={dispatcher}
                   activeRoomStore={fullActiveRoomStore}
                   isFirefox={true} />
               </div>
             </FramedExample>
 
-            <FramedExample width={644} height={483}
-              summary="Standalone room conversation (full - non FFx user)">
+            <FramedExample width={644} height={483} dashed={true}
+                           cssClass="standalone"
+                           summary="Standalone room conversation (full - non FFx user)">
               <div className="standalone">
                 <StandaloneRoomView
                   dispatcher={dispatcher}
                   activeRoomStore={fullActiveRoomStore}
                   isFirefox={false} />
               </div>
             </FramedExample>
 
-            <FramedExample width={644} height={483}
-              summary="Standalone room conversation (feedback)">
+            <FramedExample width={644} height={483} dashed={true}
+                           cssClass="standalone"
+                           summary="Standalone room conversation (feedback)">
               <div className="standalone">
                 <StandaloneRoomView
                   dispatcher={dispatcher}
                   activeRoomStore={endedRoomStore}
                   feedbackStore={feedbackStore}
                   isFirefox={false} />
               </div>
             </FramedExample>
 
-            <FramedExample width={644} height={483}
+            <FramedExample width={644} height={483} dashed={true}
+                           cssClass="standalone"
                            summary="Standalone room conversation (failed)">
               <div className="standalone">
                 <StandaloneRoomView
                   dispatcher={dispatcher}
                   activeRoomStore={failedRoomStore}
                   isFirefox={false} />
               </div>
             </FramedExample>
           </Section>
 
           <Section name="StandaloneRoomView (Mobile)">
-            <FramedExample width={600} height={480}
+            <FramedExample width={600} height={480} cssClass="standalone"
                            onContentsRendered={updatingActiveRoomStore.forcedUpdate}
                            summary="Standalone room conversation (has-participants, 600x480)">
                 <div className="standalone">
                   <StandaloneRoomView
                     dispatcher={dispatcher}
                     activeRoomStore={updatingActiveRoomStore}
                     roomState={ROOM_STATES.HAS_PARTICIPANTS}
                     isFirefox={true}
                     localPosterUrl="sample-img/video-screen-local.png"
                     remotePosterUrl="sample-img/video-screen-remote.png" />
                 </div>
             </FramedExample>
           </Section>
 
           <Section name="TextChatView (standalone)">
-            <FramedExample width={200} height={400}
+            <FramedExample width={200} height={400} cssClass="standalone"
                           summary="Standalone Text Chat conversation (200 x 400)">
               <div className="standalone text-chat-example">
                 <TextChatView
                   dispatcher={dispatcher}
                   showAlways={true}
                   showRoomName={true} />
               </div>
             </FramedExample>
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -12,16 +12,19 @@ const XULNS = "http://www.mozilla.org/ke
 const POLARIS_ENABLED = "browser.polaris.enabled";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AboutHome",
                                   "resource:///modules/AboutHome.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "AboutNewTab",
+                                  "resource:///modules/AboutNewTab.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "UITour",
                                   "resource:///modules/UITour.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentClick",
                                   "resource:///modules/ContentClick.jsm");
@@ -685,16 +688,17 @@ BrowserGlue.prototype = {
     NewTabUtils.links.addProvider(DirectoryLinksProvider);
 #ifdef NIGHTLY_BUILD
     if (Services.prefs.getBoolPref("dom.identity.enabled")) {
       SignInToWebsiteUX.init();
     }
 #endif
     webrtcUI.init();
     AboutHome.init();
+    AboutNewTab.init();
     SessionStore.init();
     BrowserUITelemetry.init();
     ContentSearch.init();
     FormValidationHandler.init();
 
     ContentClick.init();
     RemotePrompt.init();
     ContentPrefServiceParent.init();
@@ -1010,16 +1014,17 @@ BrowserGlue.prototype = {
     } catch (e) {
       Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e);
     }
 
     SelfSupportBackend.uninit();
 
     CustomizationTabPreloader.uninit();
     WebappManager.uninit();
+    AboutNewTab.uninit();
 #ifdef NIGHTLY_BUILD
     if (Services.prefs.getBoolPref("dom.identity.enabled")) {
       SignInToWebsiteUX.uninit();
     }
 #endif
     webrtcUI.uninit();
     FormValidationHandler.uninit();
 #ifdef NIGHTLY_BUILD
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -2063,17 +2063,17 @@ Toolbox.prototype = {
     yield this.performance.open();
     // Emit an event when connected, but don't wait on startup for this.
     this.emit("profiler-connected");
 
     return this.performance;
   }),
 
   /**
-   * Disconnects the underlying Performance Actor Connection. If the connection
+   * Disconnects the underlying Performance actor. If the connection
    * has not finished initializing, as opening a toolbox does not wait,
    * the performance connection destroy method will wait for it on its own.
    */
   destroyPerformance: Task.async(function*() {
     if (!this.performance) {
       return;
     }
     yield this.performance.destroy();
--- a/browser/devtools/performance/modules/logic/actors.js
+++ b/browser/devtools/performance/modules/logic/actors.js
@@ -244,17 +244,17 @@ TimelineFrontFacade.prototype = {
    */
   destroy: Task.async(function *() {
     this.EVENTS.forEach(type => this._actor.off(type, this[`_on${type}`]));
     yield this._actor.destroy();
   }),
 
   /**
    * An aggregate of all events (markers, frames, memory, ticks) and exposes
-   * to PerformanceActorsConnection as a single event.
+   * to PerformanceFront as a single event.
    */
   _onTimelineData: function (type, ...data) {
     this.emit("timeline-data", type, ...data);
   },
 
   toString: () => "[object TimelineFrontFacade]"
 };
 
--- a/browser/devtools/performance/modules/logic/frame-utils.js
+++ b/browser/devtools/performance/modules/logic/frame-utils.js
@@ -254,23 +254,25 @@ function getOrAddInflatedFrame(cache, in
  *
  * @param number index
  * @param object frameTable
  * @param object stringTable
  * @param object allocationsTable
  */
 function InflatedFrame(index, frameTable, stringTable, allocationsTable) {
   const LOCATION_SLOT = frameTable.schema.location;
+  const IMPLEMENTATION_SLOT = frameTable.schema.implementation;
   const OPTIMIZATIONS_SLOT = frameTable.schema.optimizations;
   const LINE_SLOT = frameTable.schema.line;
   const CATEGORY_SLOT = frameTable.schema.category;
 
   let frame = frameTable.data[index];
   let category = frame[CATEGORY_SLOT];
   this.location = stringTable[frame[LOCATION_SLOT]];
+  this.implementation = frame[IMPLEMENTATION_SLOT];
   this.optimizations = frame[OPTIMIZATIONS_SLOT];
   this.line = frame[LINE_SLOT];
   this.column = undefined;
   this.allocations = allocationsTable ? allocationsTable[index] : 0;
   this.category = category;
   this.isContent = false;
 
   // Attempt to compute if this frame is a content frame, and if not,
--- a/browser/devtools/performance/modules/logic/front.js
+++ b/browser/devtools/performance/modules/logic/front.js
@@ -30,18 +30,18 @@ loader.lazyImporter(this, "gDevTools",
 let PerformanceFronts = new WeakMap();
 
 /**
  * Instantiates a shared PerformanceFront for the specified target.
  * Consumers must yield on `open` to make sure the connection is established.
  *
  * @param Target target
  *        The target owning this connection.
- * @return PerformanceActorsConnection
- *         The shared connection for the specified target.
+ * @return PerformanceFront
+ *         The pseudofront for all the underlying actors.
  */
 PerformanceFronts.forTarget = function(target) {
   if (this.has(target)) {
     return this.get(target);
   }
 
   let instance = new PerformanceFront(target);
   this.set(target, instance);
@@ -345,17 +345,17 @@ PerformanceFront.prototype = {
    * Manually ends the recording session for the corresponding RecordingModel.
    *
    * @param RecordingModel model
    *        The corresponding RecordingModel that belongs to the recording session wished to stop.
    * @return RecordingModel
    *         Returns the same model, populated with the profiling data.
    */
   stopRecording: Task.async(function*(model) {
-    // If model isn't in the PerformanceActorsConnections internal store,
+    // If model isn't in the PerformanceFront internal store,
     // then do nothing.
     if (this._recordings.indexOf(model) === -1) {
       return;
     }
 
     // Flag the recording as no longer recording, so that `model.isRecording()`
     // is false. Do this before we fetch all the data, and then subsequently
     // the recording can be considered "completed".
@@ -455,15 +455,16 @@ PerformanceFront.prototype = {
  * Creates an object of configurations based off of preferences for a RecordingModel.
  */
 function getRecordingModelPrefs () {
   return {
     withMarkers: true,
     withMemory: Services.prefs.getBoolPref("devtools.performance.ui.enable-memory"),
     withTicks: Services.prefs.getBoolPref("devtools.performance.ui.enable-framerate"),
     withAllocations: Services.prefs.getBoolPref("devtools.performance.ui.enable-allocations"),
+    withJITOptimizations: Services.prefs.getBoolPref("devtools.performance.ui.enable-jit-optimizations"),
     allocationsSampleProbability: +Services.prefs.getCharPref("devtools.performance.memory.sample-probability"),
     allocationsMaxLogLength: Services.prefs.getIntPref("devtools.performance.memory.max-log-length")
   };
 }
 
 exports.getPerformanceFront = t => PerformanceFronts.forTarget(t);
 exports.PerformanceFront = PerformanceFront;
--- a/browser/devtools/performance/modules/logic/jit.js
+++ b/browser/devtools/performance/modules/logic/jit.js
@@ -202,17 +202,17 @@ const JITOptimizations = function (rawSi
       }),
 
       propertyName: maybeString(stringTable, data.propertyName),
       line: data.line,
       column: data.column
     };
   }
 
-  this.optimizationSites = sites.sort((a, b) => b.samples - a.samples);;
+  this.optimizationSites = sites.sort((a, b) => b.samples - a.samples);
 };
 
 /**
  * Make JITOptimizations iterable.
  */
 JITOptimizations.prototype = {
   [Symbol.iterator]: function *() {
     yield* this.optimizationSites;
@@ -247,10 +247,99 @@ function maybeTypeset(typeset, stringTab
       keyedBy: maybeString(stringTable, ty.keyedBy),
       name: maybeString(stringTable, ty.name),
       location: maybeString(stringTable, ty.location),
       line: ty.line
     };
   });
 }
 
+// Map of optimization implementation names to an enum.
+const IMPLEMENTATION_MAP = {
+  "interpreter": 0,
+  "baseline": 1,
+  "ion": 2
+};
+const IMPLEMENTATION_NAMES = Object.keys(IMPLEMENTATION_MAP);
+
+/**
+ * Takes data from a FrameNode and computes rendering positions for
+ * a stacked mountain graph, to visualize JIT optimization tiers over time.
+ *
+ * @param {FrameNode} frameNode
+ *                    The FrameNode who's optimizations we're iterating.
+ * @param {Array<number>} sampleTimes
+ *                        An array of every sample time within the range we're counting.
+ *                        From a ThreadNode's `sampleTimes` property.
+ * @param {number} op.startTime
+ *                 The start time of the first sample.
+ * @param {number} op.endTime
+ *                 The end time of the last sample.
+ * @param {number} op.resolution
+ *                 The maximum amount of possible data points returned.
+ *                 Also determines the size in milliseconds of each bucket
+ *                 via `(endTime - startTime) / resolution`
+ * @return {?Array<object>}
+ */
+function createTierGraphDataFromFrameNode (frameNode, sampleTimes, { startTime, endTime, resolution }) {
+  if (!frameNode.hasOptimizations()) {
+    return;
+  }
+
+  let tierData = frameNode.getOptimizationTierData();
+  let duration = endTime - startTime;
+  let stringTable = frameNode._stringTable;
+  let output = [];
+  let implEnum;
+
+  let tierDataIndex = 0;
+  let nextOptSample = tierData[tierDataIndex];
+
+  // Bucket data
+  let samplesInCurrentBucket = 0;
+  let currentBucketStartTime = sampleTimes[0];
+  let bucket = [];
+  // Size of each bucket in milliseconds
+  let bucketSize = Math.ceil(duration / resolution);
+
+  // Iterate one after the samples, so we can finalize the last bucket
+  for (let i = 0; i <= sampleTimes.length; i++) {
+    let sampleTime = sampleTimes[i];
+
+    // If this sample is in the next bucket, or we're done
+    // checking sampleTimes and on the last iteration, finalize previous bucket
+    if (sampleTime >= (currentBucketStartTime + bucketSize) ||
+        i >= sampleTimes.length) {
+
+      let dataPoint = {};
+      dataPoint.ys = [];
+      dataPoint.x = currentBucketStartTime;
+
+      // Map the opt site counts as a normalized percentage (0-1)
+      // of its count in context of total samples this bucket
+      for (let j = 0; j < IMPLEMENTATION_NAMES.length; j++) {
+        dataPoint.ys[j] = (bucket[j] || 0) / (samplesInCurrentBucket || 1);
+      }
+      output.push(dataPoint);
+
+      // Set the new start time of this bucket and reset its count
+      currentBucketStartTime += bucketSize;
+      samplesInCurrentBucket = 0;
+      bucket = [];
+    }
+
+    // If this sample observed an optimization in this frame, record it
+    if (nextOptSample && nextOptSample.time === sampleTime) {
+      // If no implementation defined, it was the "interpreter".
+      implEnum = IMPLEMENTATION_MAP[stringTable[nextOptSample.implementation] || "interpreter"];
+      bucket[implEnum] = (bucket[implEnum] || 0) + 1;
+      nextOptSample = tierData[++tierDataIndex];
+    }
+
+    samplesInCurrentBucket++;
+  }
+
+  return output;
+}
+
+exports.createTierGraphDataFromFrameNode = createTierGraphDataFromFrameNode;
 exports.OptimizationSite = OptimizationSite;
 exports.JITOptimizations = JITOptimizations;
--- a/browser/devtools/performance/modules/logic/recording-model.js
+++ b/browser/devtools/performance/modules/logic/recording-model.js
@@ -20,16 +20,17 @@ const RecordingModel = function (options
   this._label = options.label || "";
   this._console = options.console || false;
 
   this._configuration = {
     withMarkers: options.withMarkers || false,
     withTicks: options.withTicks || false,
     withMemory: options.withMemory || false,
     withAllocations: options.withAllocations || false,
+    withJITOptimizations: options.withJITOptimizations || false,
     allocationsSampleProbability: options.allocationsSampleProbability || 0,
     allocationsMaxLogLength: options.allocationsMaxLogLength || 0,
     bufferSize: options.bufferSize || 0,
     sampleFrequency: options.sampleFrequency || 1
   };
 };
 
 RecordingModel.prototype = {
@@ -83,18 +84,18 @@ RecordingModel.prototype = {
    *        The file to stream the data into.
    */
   exportRecording: Task.async(function *(file) {
     let recordingData = this.getAllData();
     yield PerformanceIO.saveRecordingToFile(recordingData, file);
   }),
 
   /**
-   * Sets up the instance with data from the SharedPerformanceConnection when
-   * starting a recording. Should only be called by SharedPerformanceConnection.
+   * Sets up the instance with data from the PerformanceFront when
+   * starting a recording. Should only be called by PerformanceFront.
    */
   _populate: function (info) {
     // Times must come from the actor in order to be self-consistent.
     // However, we also want to update the view with the elapsed time
     // even when the actor is not generating data. To do this we get
     // the local time and use it to compute a reasonable elapsed time.
     this._localStartTime = Date.now();
 
@@ -125,18 +126,18 @@ RecordingModel.prototype = {
    * all the data is fetched.
    */
   _onStoppingRecording: function (endTime) {
     this._duration = endTime - this._localStartTime;
     this._recording = false;
   },
 
   /**
-   * Sets results available from stopping a recording from SharedPerformanceConnection.
-   * Should only be called by SharedPerformanceConnection.
+   * Sets results available from stopping a recording from PerformanceFront.
+   * Should only be called by PerformanceFront.
    */
   _onStopRecording: Task.async(function *({ profilerEndTime, profile }) {
     // Update the duration with the accurate profilerEndTime, so we don't have
     // samples outside of the approximate duration set in `_onStoppingRecording`.
     this._duration = profilerEndTime - this._profilerStartTime;
     this._profile = profile;
     this._completed = true;
 
@@ -179,17 +180,18 @@ RecordingModel.prototype = {
       return Date.now() - this._localStartTime;
     } else {
       return this._duration;
     }
   },
 
   /**
    * Returns configuration object of specifying whether the recording
-   * was started withTicks, withMemory and withAllocations.
+   * was started withTicks, withMemory and withAllocations and other
+   * recording options.
    * @return object
    */
   getConfiguration: function () {
     return this._configuration;
   },
 
   /**
    * Gets the accumulated markers in the current recording.
--- a/browser/devtools/performance/modules/logic/recording-utils.js
+++ b/browser/devtools/performance/modules/logic/recording-utils.js
@@ -313,16 +313,30 @@ function deflateMarkers(markers, uniqueS
  * Deflate a thread.
  *
  * @param object thread
  *               The profile thread.
  * @param UniqueStacks uniqueStacks
  * @return object
  */
 function deflateThread(thread, uniqueStacks) {
+  // Some extra threads in a profile come stringified as a full profile (so
+  // it has nested threads itself) so the top level "thread" does not have markers
+  // or samples. We don't use this anyway so just make this safe to deflate.
+  // can be a string rather than an object on import. Bug 1173695
+  if (typeof thread === "string") {
+    thread = JSON.parse(thread);
+  }
+  if (!thread.samples) {
+    thread.samples = [];
+  }
+  if (!thread.markers) {
+    thread.markers = [];
+  }
+
   return {
     name: thread.name,
     tid: thread.tid,
     samples: deflateSamples(thread.samples, uniqueStacks),
     markers: deflateMarkers(thread.markers, uniqueStacks),
     stackTable: uniqueStacks.getStackTableWithSchema(),
     frameTable: uniqueStacks.getFrameTableWithSchema(),
     stringTable: uniqueStacks.getStringTable()
--- a/browser/devtools/performance/modules/logic/tree-model.js
+++ b/browser/devtools/performance/modules/logic/tree-model.js
@@ -30,16 +30,17 @@ loader.lazyRequireGetter(this, "FrameUti
  *          - boolean invertTree [optional]
  *          - boolean flattenRecursion [optional]
  */
 function ThreadNode(thread, options = {}) {
   if (options.endTime == void 0 || options.startTime == void 0) {
     throw new Error("ThreadNode requires both `startTime` and `endTime`.");
   }
   this.samples = 0;
+  this.sampleTimes = [];
   this.youngestFrameSamples = 0;
   this.calls = [];
   this.duration = options.endTime - options.startTime;
 
   let { samples, stackTable, frameTable, stringTable, allocationsTable } = thread;
 
   // Nothing to do if there are no samples.
   if (samples.data.length === 0) {
@@ -126,42 +127,34 @@ ThreadNode.prototype = {
     // Caches.
     let inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable);
     let leafTable = Object.create(null);
 
     let startTime = options.startTime;
     let endTime = options.endTime;
     let flattenRecursion = options.flattenRecursion;
 
-    // Take the timestamp of the first sample as prevSampleTime. 0 is
-    // incorrect due to circular buffer wraparound. If wraparound happens,
-    // then the first sample will have an incorrect, large duration.
-    let prevSampleTime = samplesData[0][SAMPLE_TIME_SLOT];
-
     // Reused options object passed to InflatedFrame.prototype.getFrameKey.
     let mutableFrameKeyOptions = {
       contentOnly: options.contentOnly,
       isRoot: false,
       isLeaf: false,
       isMetaCategoryOut: false
     };
 
-    // Start iteration at the second sample, as we use the first sample to
-    // compute prevSampleTime.
-    for (let i = 1; i < samplesData.length; i++) {
+    for (let i = 0; i < samplesData.length; i++) {
       let sample = samplesData[i];
       let sampleTime = sample[SAMPLE_TIME_SLOT];
 
       // A sample's end time is considered to be its time of sampling. Its
       // start time is the sampling time of the previous sample.
       //
       // Thus, we compare sampleTime <= start instead of < to filter out
       // samples that end exactly at the start time.
       if (!sampleTime || sampleTime <= startTime || sampleTime > endTime) {
-        prevSampleTime = sampleTime;
         continue;
       }
 
       let stackIndex = sample[SAMPLE_STACK_SLOT];
       let calls = this.calls;
       let prevCalls = this.calls;
       let prevFrameKey;
       let isLeaf = mutableFrameKeyOptions.isLeaf = true;
@@ -230,26 +223,30 @@ ThreadNode.prototype = {
           calls = prevCalls;
         }
 
         let frameNode = getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame,
                                           mutableFrameKeyOptions.isMetaCategoryOut,
                                           leafTable);
         if (isLeaf) {
           frameNode.youngestFrameSamples++;
-          frameNode._addOptimizations(inflatedFrame.optimizations, stringTable);
+          if (inflatedFrame.optimizations) {
+            frameNode._addOptimizations(inflatedFrame.optimizations, inflatedFrame.implementation,
+                                        sampleTime, stringTable);
+          }
         }
         frameNode.samples++;
 
         prevFrameKey = frameKey;
         prevCalls = frameNode.calls;
         isLeaf = mutableFrameKeyOptions.isLeaf = false;
       }
 
       this.samples++;
+      this.sampleTimes.push(sampleTime);
     }
   },
 
   /**
    * Uninverts the call tree after its having been built.
    */
   _uninvert: function uninvert() {
     function mergeOrAddFrameNode(calls, node) {
@@ -367,41 +364,53 @@ function FrameNode(frameKey, { location,
   this.location = location;
   this.line = line;
   this.allocations = allocations;
   this.youngestFrameSamples = 0;
   this.samples = 0;
   this.calls = [];
   this.isContent = !!isContent;
   this._optimizations = null;
+  this._tierData = null;
   this._stringTable = null;
   this.isMetaCategory = !!isMetaCategory;
   this.category = category;
 }
 
 FrameNode.prototype = {
   /**
    * Take optimization data observed for this frame.
    *
    * @param object optimizationSite
    *               Any JIT optimization information attached to the current
    *               sample. Lazily inflated via stringTable.
+   * @param number implementation
+   *               JIT implementation used for this observed frame (interpreter,
+   *               baseline, ion);
+   * @param number time
+   *               The time this optimization occurred.
    * @param object stringTable
    *               The string table used to inflate the optimizationSite.
    */
-  _addOptimizations: function (optimizationSite, stringTable) {
+  _addOptimizations: function (site, implementation, time, stringTable) {
     // Simply accumulate optimization sites for now. Processing is done lazily
     // by JITOptimizations, if optimization information is actually displayed.
-    if (optimizationSite) {
+    if (site) {
       let opts = this._optimizations;
       if (opts === null) {
         opts = this._optimizations = [];
         this._stringTable = stringTable;
       }
-      opts.push(optimizationSite);
+      opts.push(site);
+
+      if (this._tierData === null) {
+        this._tierData = [];
+      }
+      // Record type of implementation used and the sample time
+      this._tierData.push({ implementation, time });
     }
   },
 
   _clone: function () {
     let newNode = new FrameNode(this.key, this, this.isMetaCategory);
     newNode._merge(this);
     return newNode;
   },
@@ -470,12 +479,24 @@ FrameNode.prototype = {
    * @return {JITOptimizations|null}
    */
   getOptimizations: function () {
     if (!this._optimizations) {
       return null;
     }
     return new JITOptimizations(this._optimizations, this._stringTable);
   },
+
+  /**
+   * Returns the optimization tiers used overtime.
+   *
+   * @return {?Array<object>}
+   */
+  getOptimizationTierData: function () {
+    if (!this._tierData) {
+      return null;
+    }
+    return this._tierData;
+  }
 };
 
 exports.ThreadNode = ThreadNode;
 exports.FrameNode = FrameNode;
--- a/browser/devtools/performance/modules/widgets/graphs.js
+++ b/browser/devtools/performance/modules/widgets/graphs.js
@@ -4,20 +4,20 @@
 "use strict";
 
 /**
  * This file contains the base line graph that all Performance line graphs use.
  */
 
 const { Cc, Ci, Cu, Cr } = require("chrome");
 const { Task } = require("resource://gre/modules/Task.jsm");
-const { LineGraphWidget } = require("resource:///modules/devtools/Graphs.jsm");
-const { BarGraphWidget } = require("resource:///modules/devtools/Graphs.jsm");
-const { CanvasGraphUtils } = require("resource:///modules/devtools/Graphs.jsm");
 const { Heritage } = require("resource:///modules/devtools/ViewHelpers.jsm");
+const { LineGraphWidget } = require("devtools/shared/widgets/Graphs");
+const { BarGraphWidget } = require("devtools/shared/widgets/Graphs");
+const { CanvasGraphUtils } = require("devtools/shared/widgets/Graphs");
 
 loader.lazyRequireGetter(this, "promise");
 loader.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 
 loader.lazyRequireGetter(this, "colorUtils",
   "devtools/css-color", true);
 loader.lazyRequireGetter(this, "getColor",
@@ -320,17 +320,17 @@ GraphsController.prototype = {
    * for keeping the graphs' selections in sync.
    */
   setMappedSelection: function (selection, { mapStart, mapEnd }) {
     return this._getPrimaryLink().setMappedSelection(selection, { mapStart, mapEnd });
   },
 
   /**
    * Fetches the currently mapped selection. If graphs are not yet rendered,
-   * (which throws in Graphs.jsm), return null.
+   * (which throws in Graphs.js), return null.
    */
   getMappedSelection: function ({ mapStart, mapEnd }) {
     let primary = this._getPrimaryLink();
     if (primary && primary.hasData()) {
       return primary.getMappedSelection({ mapStart, mapEnd });
     } else {
       return null;
     }
--- a/browser/devtools/performance/modules/widgets/markers-overview.js
+++ b/browser/devtools/performance/modules/widgets/markers-overview.js
@@ -5,18 +5,18 @@
 
 /**
  * This file contains the "markers overview" graph, which is a minimap of all
  * the timeline data. Regions inside it may be selected, determining which
  * markers are visible in the "waterfall".
  */
 
 const { Cc, Ci, Cu, Cr } = require("chrome");
-const { AbstractCanvasGraph } = require("resource:///modules/devtools/Graphs.jsm");
 const { Heritage } = require("resource:///modules/devtools/ViewHelpers.jsm");
+const { AbstractCanvasGraph } = require("devtools/shared/widgets/Graphs");
 
 loader.lazyRequireGetter(this, "colorUtils",
   "devtools/css-color", true);
 loader.lazyRequireGetter(this, "getColor",
   "devtools/shared/theme", true);
 loader.lazyRequireGetter(this, "L10N",
   "devtools/performance/global", true);
 loader.lazyRequireGetter(this, "TickUtils",
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -290,16 +290,17 @@ let PerformanceController = {
    * Starts recording with the PerformanceFront. Emits `EVENTS.RECORDING_STARTED`
    * when the front has started to record.
    */
   startRecording: Task.async(function *() {
     let options = {
       withMarkers: true,
       withMemory: this.getOption("enable-memory"),
       withTicks: this.getOption("enable-framerate"),
+      withJITOptimizations: this.getOption("enable-jit-optimizations"),
       withAllocations: this.getOption("enable-allocations"),
       allocationsSampleProbability: this.getPref("memory-sample-probability"),
       allocationsMaxLogLength: this.getPref("memory-max-log-length"),
       bufferSize: this.getPref("profiler-buffer-size"),
       sampleFrequency: this.getPref("profiler-sample-frequency")
     };
 
     yield gFront.startRecording(options);
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -64,22 +64,22 @@
                 data-pref="invert-flame-graph"
                 label="&profilerUI.invertFlameGraph;"
                 tooltiptext="&profilerUI.invertFlameGraph.tooltiptext;"/>
       <menuitem id="option-flatten-tree-recursion"
                 type="checkbox"
                 data-pref="flatten-tree-recursion"
                 label="&profilerUI.flattenTreeRecursion;"
                 tooltiptext="&profilerUI.flattenTreeRecursion.tooltiptext;"/>
-      <menuitem id="option-show-jit-optimizations"
+      <menuitem id="option-enable-jit-optimizations"
                 class="experimental-option"
                 type="checkbox"
-                data-pref="show-jit-optimizations"
-                label="&profilerUI.showJITOptimizations;"
-                tooltiptext="&profilerUI.showJITOptimizations.tooltiptext;"/>
+                data-pref="enable-jit-optimizations"
+                label="&profilerUI.enableJITOptimizations;"
+                tooltiptext="&profilerUI.enableJITOptimizations.tooltiptext;"/>
     </menupopup>
   </popupset>
 
   <hbox class="theme-body" flex="1">
 
     <!-- Sidebar: controls and recording list -->
     <vbox id="recordings-pane">
       <toolbar id="recordings-toolbar"
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -63,18 +63,16 @@ support-files =
 [browser_perf-front-profiler-04.js]
 #[browser_perf-front-profiler-05.js] bug 1077464
 #[browser_perf-front-profiler-06.js]
 [browser_perf-front-01.js]
 [browser_perf-front-02.js]
 [browser_perf-highlighted.js]
 [browser_perf-jit-view-01.js]
 [browser_perf-jit-view-02.js]
-[browser_perf-jit-model-01.js]
-[browser_perf-jit-model-02.js]
 [browser_perf-loading-01.js]
 [browser_perf-loading-02.js]
 [browser_perf-marker-details-01.js]
 [browser_perf-options-01.js]
 [browser_perf-options-02.js]
 [browser_perf-options-03.js]
 [browser_perf-options-invert-call-tree-01.js]
 [browser_perf-options-invert-call-tree-02.js]
@@ -83,16 +81,17 @@ support-files =
 [browser_perf-options-flatten-tree-recursion-01.js]
 [browser_perf-options-flatten-tree-recursion-02.js]
 [browser_perf-options-show-platform-data-01.js]
 [browser_perf-options-show-platform-data-02.js]
 [browser_perf-options-show-idle-blocks-01.js]
 [browser_perf-options-show-idle-blocks-02.js]
 [browser_perf-options-enable-memory-01.js]
 [browser_perf-options-enable-memory-02.js]
+[browser_perf-options-enable-optimizations.js]
 [browser_perf-options-enable-framerate.js]
 [browser_perf-options-allocations.js]
 [browser_perf-options-profiler.js]
 [browser_perf-overview-render-01.js]
 [browser_perf-overview-render-02.js]
 [browser_perf-overview-render-03.js]
 [browser_perf-overview-render-04.js]
 [browser_perf-overview-selection-01.js]
@@ -111,16 +110,17 @@ support-files =
 [browser_perf-recording-notices-03.js]
 [browser_perf-recording-notices-04.js]
 [browser_perf-recording-notices-05.js]
 [browser_perf_recordings-io-01.js]
 [browser_perf_recordings-io-02.js]
 [browser_perf_recordings-io-03.js]
 [browser_perf_recordings-io-04.js]
 [browser_perf_recordings-io-05.js]
+[browser_perf_recordings-io-06.js]
 [browser_perf-range-changed-render.js]
 [browser_perf-recording-selected-01.js]
 [browser_perf-recording-selected-02.js]
 [browser_perf-recording-selected-03.js]
 [browser_perf-recording-selected-04.js]
 [browser_perf-theme-toggle-01.js]
 [browser_profiler_tree-abstract-01.js]
 [browser_profiler_tree-abstract-02.js]
--- a/browser/devtools/performance/test/browser_perf-jit-view-01.js
+++ b/browser/devtools/performance/test/browser_perf-jit-view-01.js
@@ -12,17 +12,19 @@ Services.prefs.setBoolPref(INVERT_PREF, 
 
 function* spawnTest() {
   let { panel } = yield initPerformance(SIMPLE_URL);
   let { EVENTS, $, $$, window, PerformanceController } = panel.panelWin;
   let { OverviewView, DetailsView, JITOptimizationsView, JsCallTreeView, RecordingsView } = panel.panelWin;
 
   let profilerData = { threads: [gThread] }
 
-  is(Services.prefs.getBoolPref(JIT_PREF), false, "show JIT Optimizations pref off by default");
+  is(Services.prefs.getBoolPref(JIT_PREF), false, "record JIT Optimizations pref off by default");
+  Services.prefs.setBoolPref(JIT_PREF, true);
+  is(Services.prefs.getBoolPref(JIT_PREF), true, "toggle on record JIT Optimizations");
 
   // Make two recordings, so we have one to switch to later, as the
   // second one will have fake sample data
   yield startRecording(panel);
   yield stopRecording(panel);
 
   yield startRecording(panel);
   yield stopRecording(panel);
@@ -51,37 +53,30 @@ function* spawnTest() {
   finish();
 
   function *injectAndRenderProfilerData() {
     // Get current recording and inject our mock data
     info("Injecting mock profile data");
     let recording = PerformanceController.getCurrentRecording();
     recording._profile = profilerData;
 
-    is($("#jit-optimizations-view").hidden, true, "JIT Optimizations panel is hidden when pref off.");
-
     // Force a rerender
     let rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
     JsCallTreeView.render(OverviewView.getTimeInterval());
     yield rendered;
 
-    is($("#jit-optimizations-view").hidden, true, "JIT Optimizations panel still hidden when rerendered");
-    Services.prefs.setBoolPref(JIT_PREF, true);
     is($("#jit-optimizations-view").hidden, false, "JIT Optimizations should be visible when pref is on");
     ok($("#jit-optimizations-view").classList.contains("empty"),
       "JIT Optimizations view has empty message when no frames selected.");
-
-     Services.prefs.setBoolPref(JIT_PREF, false);
   }
 
   function *checkFrame (frameIndex, expectedOpts=[]) {
     // Click the frame
     let rendered = once(JITOptimizationsView, EVENTS.OPTIMIZATIONS_RENDERED);
     mousedown(window, $$(".call-tree-item")[frameIndex]);
-    Services.prefs.setBoolPref(JIT_PREF, true);
     yield rendered;
     ok(true, "JITOptimizationsView rendered when enabling with the current frame node selected");
 
     let isEmpty = $("#jit-optimizations-view").classList.contains("empty");
     if (expectedOpts.length === 0) {
       ok(isEmpty, "JIT Optimizations view has an empty message when selecting a frame without opt data.");
       return;
     } else {
--- a/browser/devtools/performance/test/browser_perf-jit-view-02.js
+++ b/browser/devtools/performance/test/browser_perf-jit-view-02.js
@@ -14,31 +14,30 @@ Services.prefs.setBoolPref(PLATFORM_DATA
 
 function* spawnTest() {
   let { panel } = yield initPerformance(SIMPLE_URL);
   let { EVENTS, $, $$, window, PerformanceController } = panel.panelWin;
   let { OverviewView, DetailsView, JITOptimizationsView, JsCallTreeView, RecordingsView } = panel.panelWin;
 
   let profilerData = { threads: [gThread] };
 
-  is(Services.prefs.getBoolPref(JIT_PREF), false, "show JIT Optimizations pref off by default");
+  Services.prefs.setBoolPref(JIT_PREF, true);
 
   // Make two recordings, so we have one to switch to later, as the
   // second one will have fake sample data
   yield startRecording(panel);
   yield stopRecording(panel);
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   yield DetailsView.selectView("js-calltree");
 
   yield injectAndRenderProfilerData();
 
-  Services.prefs.setBoolPref(JIT_PREF, true);
   // Click the frame
   let rendered = once(JITOptimizationsView, EVENTS.OPTIMIZATIONS_RENDERED);
   mousedown(window, $$(".call-tree-item")[2]);
   yield rendered;
 
   ok($("#jit-optimizations-view").classList.contains("empty"),
     "platform meta frame shows as empty");
 
@@ -59,21 +58,18 @@ function* spawnTest() {
     let recording = PerformanceController.getCurrentRecording();
     recording._profile = profilerData;
 
     // Force a rerender
     let rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
     JsCallTreeView.render(OverviewView.getTimeInterval());
     yield rendered;
 
-    Services.prefs.setBoolPref(JIT_PREF, true);
     ok($("#jit-optimizations-view").classList.contains("empty"),
       "JIT Optimizations view has empty message when no frames selected.");
-
-     Services.prefs.setBoolPref(JIT_PREF, false);
   }
 }
 
 let gUniqueStacks = new RecordingUtils.UniqueStacks();
 
 function uniqStr(s) {
   return gUniqueStacks.getOrAddStringIndex(s);
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-options-enable-optimizations.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that `enable-jit-optimizations` sets the recording to subsequently
+ * display optimizations info.
+ */
+function* spawnTest() {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, PerformanceController, $, DetailsView, JsCallTreeView } = panel.panelWin;
+  Services.prefs.setBoolPref(JIT_PREF, true);
+
+
+  yield startRecording(panel);
+  let rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
+  yield stopRecording(panel);
+
+  yield DetailsView.selectView("js-calltree");
+  ok(DetailsView.isViewSelected(JsCallTreeView), "The call tree is now selected.");
+  yield rendered;
+
+  let recording = PerformanceController.getCurrentRecording();
+  is(recording.getConfiguration().withJITOptimizations, true, "recording model has withJITOptimizations as true");
+
+  // Set back to false, should not affect display of first recording
+  info("Disabling enable-jit-optimizations");
+  Services.prefs.setBoolPref(JIT_PREF, false);
+  is($("#jit-optimizations-view").hidden, false, "JIT Optimizations panel is displayed when feature enabled.");
+
+  yield startRecording(panel);
+  rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
+  yield stopRecording(panel);
+
+  yield DetailsView.selectView("js-calltree");
+  ok(DetailsView.isViewSelected(JsCallTreeView), "The call tree is now selected.");
+  yield rendered;
+
+  recording = PerformanceController.getCurrentRecording();
+  is(recording.getConfiguration().withJITOptimizations, false, "recording model has withJITOptimizations as false");
+  is($("#jit-optimizations-view").hidden, true, "JIT Optimizations panel is hidden when feature disabled");
+
+  yield teardown(panel);
+  finish();
+}
--- a/browser/devtools/performance/test/browser_perf-shared-connection-02.js
+++ b/browser/devtools/performance/test/browser_perf-shared-connection-02.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Tests if the shared PerformanceActorsConnection is only opened once.
+ * Tests if the shared PerformanceFront is only opened once.
  */
 
 let gProfilerConnectionsOpened = 0;
 Services.obs.addObserver(profilerConnectionObserver, "performance-tools-connection-opened", false);
 
 function* spawnTest() {
   let { target, panel } = yield initPerformance(SIMPLE_URL);
 
--- a/browser/devtools/performance/test/browser_perf-shared-connection-03.js
+++ b/browser/devtools/performance/test/browser_perf-shared-connection-03.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Tests if the shared PerformanceActorsConnection can properly send requests.
+ * Tests if the shared PerformanceFront can properly send requests.
  */
 
 function* spawnTest() {
   let { panel } = yield initPerformance(SIMPLE_URL);
   let front = panel.panelWin.gFront;
 
   loadFrameScripts();
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf_recordings-io-06.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the performance tool can import profiler data when Profiler is v2
+ * and requires deflating, and has an extra thread that's a string. Not sure
+ * what causes this.
+ */
+let RecordingUtils = devtools.require("devtools/performance/recording-utils");
+
+let STRINGED_THREAD = (function () {
+  let thread = {};
+
+  thread.libs = [{
+    start: 123,
+    end: 456,
+    offset: 0,
+    name: "",
+    breakpadId: ""
+  }];
+  thread.meta = { version: 2, interval: 1, stackwalk: 0, processType: 1, startTime: 0 };
+  thread.threads = [{
+    name: "Plugin",
+    tid: 4197,
+    samples: [],
+    markers: [],
+  }];
+
+  return JSON.stringify(thread);
+})();
+
+let PROFILER_DATA = (function () {
+  let data = {};
+  let threads = data.threads = [];
+  let thread = {};
+  threads.push(thread);
+  threads.push(STRINGED_THREAD);
+  thread.name = "Content";
+  thread.samples = [{
+    time: 5,
+    frames: [
+      { location: "(root)" },
+      { location: "A" },
+      { location: "B" },
+      { location: "C" }
+    ]
+  }, {
+    time: 5 + 6,
+    frames: [
+      { location: "(root)" },
+      { location: "A" },
+      { location: "B" },
+      { location: "D" }
+    ]
+  }, {
+    time: 5 + 6 + 7,
+    frames: [
+      { location: "(root)" },
+      { location: "A" },
+      { location: "E" },
+      { location: "F" }
+    ]
+  }, {
+    time: 20,
+    frames: [
+      { location: "(root)" },
+      { location: "A" },
+      { location: "B" },
+      { location: "C" },
+      { location: "D" },
+      { location: "E" },
+      { location: "F" },
+      { location: "G" }
+    ]
+  }];
+
+  // Handled in other deflating tests
+  thread.markers = [];
+
+  let meta = data.meta = {};
+  meta.version = 2;
+  meta.interval = 1;
+  meta.stackwalk = 0;
+  meta.product = "Firefox";
+  return data;
+})();
+
+let test = Task.async(function*() {
+  let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+  let { $, EVENTS, PerformanceController, DetailsView, JsCallTreeView } = panel.panelWin;
+
+  let profilerData = {
+    profile: PROFILER_DATA,
+    duration: 10000,
+    configuration: {},
+    fileType: "Recorded Performance Data",
+    version: 2
+  };
+
+  let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+  file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+  yield asyncCopy(profilerData, file);
+
+  // Import recording.
+
+  let calltreeRendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
+  let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+  yield PerformanceController.importRecording("", file);
+
+  yield imported;
+  ok(true, "The profiler data appears to have been successfully imported.");
+
+  yield calltreeRendered;
+  ok(true, "The imported data was re-rendered.");
+
+  yield teardown(panel);
+  finish();
+});
+
+function getUnicodeConverter() {
+  let className = "@mozilla.org/intl/scriptableunicodeconverter";
+  let converter = Cc[className].createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  return converter;
+}
+
+function asyncCopy(data, file) {
+  let deferred = Promise.defer();
+
+  let string = JSON.stringify(data);
+  let inputStream = getUnicodeConverter().convertToInputStream(string);
+  let outputStream = FileUtils.openSafeFileOutputStream(file);
+
+  NetUtil.asyncCopy(inputStream, outputStream, status => {
+    if (!Components.isSuccessCode(status)) {
+      deferred.reject(new Error("Could not save data to file."));
+    }
+    deferred.resolve();
+  });
+
+  return deferred.promise;
+}
--- a/browser/devtools/performance/test/head.js
+++ b/browser/devtools/performance/test/head.js
@@ -34,34 +34,34 @@ const FRAMERATE_PREF = "devtools.perform
 const MEMORY_PREF = "devtools.performance.ui.enable-memory";
 const ALLOCATIONS_PREF = "devtools.performance.ui.enable-allocations";
 
 const PLATFORM_DATA_PREF = "devtools.performance.ui.show-platform-data";
 const IDLE_PREF = "devtools.performance.ui.show-idle-blocks";
 const INVERT_PREF = "devtools.performance.ui.invert-call-tree";
 const INVERT_FLAME_PREF = "devtools.performance.ui.invert-flame-graph";
 const FLATTEN_PREF = "devtools.performance.ui.flatten-tree-recursion";
-const JIT_PREF = "devtools.performance.ui.show-jit-optimizations";
+const JIT_PREF = "devtools.performance.ui.enable-jit-optimizations";
 const EXPERIMENTAL_PREF = "devtools.performance.ui.experimental";
 
 // All tests are asynchronous.
 waitForExplicitFinish();
 
 gDevTools.testing = true;
 
 let DEFAULT_PREFS = [
   "devtools.debugger.log",
   "devtools.performance.ui.invert-call-tree",
   "devtools.performance.ui.flatten-tree-recursion",
   "devtools.performance.ui.show-platform-data",
   "devtools.performance.ui.show-idle-blocks",
   "devtools.performance.ui.enable-memory",
   "devtools.performance.ui.enable-allocations",
   "devtools.performance.ui.enable-framerate",
-  "devtools.performance.ui.show-jit-optimizations",
+  "devtools.performance.ui.enable-jit-optimizations",
   "devtools.performance.memory.sample-probability",
   "devtools.performance.memory.max-log-length",
   "devtools.performance.profiler.buffer-size",
   "devtools.performance.profiler.sample-frequency-khz",
   "devtools.performance.ui.experimental",
   "devtools.performance.timeline.hidden-markers",
 ].reduce((prefs, pref) => {
   prefs[pref] = Preferences.get(pref);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/unit/test_jit-graph-data.js
@@ -0,0 +1,186 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Unit test for `createTierGraphDataFromFrameNode` function.
+ */
+
+function run_test() {
+  run_next_test();
+}
+
+const SAMPLE_COUNT = 1000;
+const RESOLUTION = 50;
+const TIME_PER_SAMPLE = 5;
+
+// Offset needed since ThreadNode requires the first sample to be strictly
+// greater than its start time. This lets us still have pretty numbers
+// in this test to keep it (more) simple, which it sorely needs.
+const TIME_OFFSET = 5;
+
+add_task(function test() {
+  let { ThreadNode } = devtools.require("devtools/performance/tree-model");
+  let { createTierGraphDataFromFrameNode } = devtools.require("devtools/performance/jit");
+
+  // Select the second half of the set of samples
+  let startTime = (SAMPLE_COUNT / 2 * TIME_PER_SAMPLE) - TIME_OFFSET;
+  let endTime = (SAMPLE_COUNT * TIME_PER_SAMPLE) - TIME_OFFSET;
+  let invertTree = true;
+
+  let root = new ThreadNode(gThread, { invertTree, startTime, endTime });
+
+  equal(root.samples, SAMPLE_COUNT / 2, "root has correct amount of samples");
+  equal(root.sampleTimes.length, SAMPLE_COUNT / 2, "root has correct amount of sample times");
+  // Add time offset since the first sample begins TIME_OFFSET after startTime
+  equal(root.sampleTimes[0], startTime + TIME_OFFSET, "root recorded first sample time in scope");
+  equal(root.sampleTimes[root.sampleTimes.length - 1], endTime, "root recorded last sample time in scope");
+
+  let frame = getFrameNodePath(root, "X");
+  let data = createTierGraphDataFromFrameNode(frame, root.sampleTimes, { startTime, endTime, resolution: RESOLUTION });
+
+  let TIME_PER_WINDOW = SAMPLE_COUNT / 2 / RESOLUTION * TIME_PER_SAMPLE;
+
+  for (let i = 0; i < 10; i++) {
+    equal(data[i].x, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "first window has correct x");
+    equal(data[i].ys[0], 0.2, "first window has 2 frames in interpreter");
+    equal(data[i].ys[1], 0.2, "first window has 2 frames in baseline");
+    equal(data[i].ys[2], 0.2, "first window has 2 frames in ion");
+  }
+  for (let i = 10; i < 20; i++) {
+    equal(data[i].x, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "second window has correct x");
+    equal(data[i].ys[0], 0, "second window observed no optimizations");
+    equal(data[i].ys[1], 0, "second window observed no optimizations");
+    equal(data[i].ys[2], 0, "second window observed no optimizations");
+  }
+  for (let i = 20; i < 30; i++) {
+    equal(data[i].x, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "third window has correct x");
+    equal(data[i].ys[0], 0.3, "third window has 3 frames in interpreter");
+    equal(data[i].ys[1], 0, "third window has 0 frames in baseline");
+    equal(data[i].ys[2], 0, "third window has 0 frames in ion");
+  }
+});
+
+let gUniqueStacks = new RecordingUtils.UniqueStacks();
+
+function uniqStr(s) {
+  return gUniqueStacks.getOrAddStringIndex(s);
+}
+
+const TIER_PATTERNS = [
+  // 0-99
+  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+  // 100-199
+  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+  // 200-299
+  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+  // 300-399
+  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+  // 400-499
+  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+
+  // 500-599
+  // Test current frames in all opts, including that
+  // the same frame with no opts does not get counted
+  ["X", "X", "A", "A", "X_1", "X_2", "X_1", "X_2", "X_0", "X_0"],
+
+  // 600-699
+  // Nothing for current frame
+  ["A", "B", "A", "B", "A", "B", "A", "B", "A", "B"],
+
+  // 700-799
+  // A few frames where the frame is not the leaf node
+  ["X_2 -> Y", "X_2 -> Y", "X_2 -> Y", "X_0", "X_0", "X_0", "A", "A", "A", "A"],
+
+  // 800-899
+  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+  // 900-999
+  ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
+];
+
+function createSample (i, frames) {
+  let sample = {};
+  sample.time = i * TIME_PER_SAMPLE;
+  sample.frames = [{ location: "(root)" }];
+  if (i === 0) {
+    return sample;
+  }
+  if (frames) {
+    frames.split(" -> ").forEach(frame => sample.frames.push({ location: frame }));
+  }
+  return sample;
+}
+
+let SAMPLES = (function () {
+  let samples = [];
+
+  for (let i = 0; i < SAMPLE_COUNT;) {
+    let pattern = TIER_PATTERNS[Math.floor(i/100)];
+    for (let j = 0; j < pattern.length; j++) {
+      samples.push(createSample(i+j, pattern[j]));
+    }
+    i += 10;
+  }
+
+  return samples;
+})();
+
+let gThread = RecordingUtils.deflateThread({ samples: SAMPLES, markers: [] }, gUniqueStacks);
+
+let gRawSite1 = {
+  line: 12,
+  column: 2,
+  types: [{
+    mirType: uniqStr("Object"),
+    site: uniqStr("B (http://foo/bar:10)"),
+    typeset: [{
+        keyedBy: uniqStr("constructor"),
+        name: uniqStr("Foo"),
+        location: uniqStr("B (http://foo/bar:10)")
+    }, {
+        keyedBy: uniqStr("primitive"),
+        location: uniqStr("self-hosted")
+    }]
+  }],
+  attempts: {
+    schema: {
+      outcome: 0,
+      strategy: 1
+    },
+    data: [
+      [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+      [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+      [uniqStr("Inlined"), uniqStr("SomeGetter3")]
+    ]
+  }
+};
+
+function serialize (x) {
+  return JSON.parse(JSON.stringify(x));
+}
+
+gThread.frameTable.data.forEach((frame) => {
+  const LOCATION_SLOT = gThread.frameTable.schema.location;
+  const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
+  const IMPLEMENTATION_SLOT = gThread.frameTable.schema.implementation;
+
+  let l = gThread.stringTable[frame[LOCATION_SLOT]];
+  switch (l) {
+  // Rename some of the location sites so we can register different
+  // frames with different opt sites
+  case "X_0":
+    frame[LOCATION_SLOT] = uniqStr("X");
+    frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+    frame[IMPLEMENTATION_SLOT] = null;
+    break;
+  case "X_1":
+    frame[LOCATION_SLOT] = uniqStr("X");
+    frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+    frame[IMPLEMENTATION_SLOT] = uniqStr("baseline");
+    break;
+  case "X_2":
+    frame[LOCATION_SLOT] = uniqStr("X");
+    frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+    frame[IMPLEMENTATION_SLOT] = uniqStr("ion");
+    break;
+  }
+});
rename from browser/devtools/performance/test/browser_perf-jit-model-01.js
rename to browser/devtools/performance/test/unit/test_jit-model-01.js
--- a/browser/devtools/performance/test/browser_perf-jit-model-01.js
+++ b/browser/devtools/performance/test/unit/test_jit-model-01.js
@@ -2,46 +2,46 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests that JITOptimizations track optimization sites and create
  * an OptimizationSiteProfile when adding optimization sites, like from the
  * FrameNode, and the returning of that data is as expected.
  */
 
-const RecordingUtils = devtools.require("devtools/performance/recording-utils");
+function run_test() {
+  run_next_test();
+}
 
-function test() {
+add_task(function test() {
   let { JITOptimizations } = devtools.require("devtools/performance/jit");
 
   let rawSites = [];
   rawSites.push(gRawSite2);
   rawSites.push(gRawSite2);
   rawSites.push(gRawSite1);
   rawSites.push(gRawSite1);
   rawSites.push(gRawSite2);
   rawSites.push(gRawSite3);
 
   let jit = new JITOptimizations(rawSites, gStringTable.stringTable);
   let sites = jit.optimizationSites;
 
   let [first, second, third] = sites;
 
-  is(first.id, 0, "site id is array index");
-  is(first.samples, 3, "first OptimizationSiteProfile has correct sample count");
-  is(first.data.line, 34, "includes OptimizationSite as reference under `data`");
-  is(second.id, 1, "site id is array index");
-  is(second.samples, 2, "second OptimizationSiteProfile has correct sample count");
-  is(second.data.line, 12, "includes OptimizationSite as reference under `data`");
-  is(third.id, 2, "site id is array index");
-  is(third.samples, 1, "third OptimizationSiteProfile has correct sample count");
-  is(third.data.line, 78, "includes OptimizationSite as reference under `data`");
-
-  finish();
-}
+  equal(first.id, 0, "site id is array index");
+  equal(first.samples, 3, "first OptimizationSiteProfile has correct sample count");
+  equal(first.data.line, 34, "includes OptimizationSite as reference under `data`");
+  equal(second.id, 1, "site id is array index");
+  equal(second.samples, 2, "second OptimizationSiteProfile has correct sample count");
+  equal(second.data.line, 12, "includes OptimizationSite as reference under `data`");
+  equal(third.id, 2, "site id is array index");
+  equal(third.samples, 1, "third OptimizationSiteProfile has correct sample count");
+  equal(third.data.line, 78, "includes OptimizationSite as reference under `data`");
+});
 
 let gStringTable = new RecordingUtils.UniqueStrings();
 
 function uniqStr(s) {
   return gStringTable.getOrAddStringIndex(s);
 }
 
 let gRawSite1 = {
rename from browser/devtools/performance/test/browser_perf-jit-model-02.js
rename to browser/devtools/performance/test/unit/test_jit-model-02.js
--- a/browser/devtools/performance/test/browser_perf-jit-model-02.js
+++ b/browser/devtools/performance/test/unit/test_jit-model-02.js
@@ -1,51 +1,51 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests that JITOptimizations create OptimizationSites, and the underlying
  * OptimizationSites methods work as expected.
  */
 
-const RecordingUtils = devtools.require("devtools/performance/recording-utils");
+function run_test() {
+  run_next_test();
+}
 
-function test() {
+add_task(function test() {
   let { JITOptimizations, OptimizationSite } = devtools.require("devtools/performance/jit");
 
   let rawSites = [];
   rawSites.push(gRawSite2);
   rawSites.push(gRawSite2);
   rawSites.push(gRawSite1);
   rawSites.push(gRawSite1);
   rawSites.push(gRawSite2);
   rawSites.push(gRawSite3);
 
   let jit = new JITOptimizations(rawSites, gStringTable.stringTable);
   let sites = jit.optimizationSites;
 
   let [first, second, third] = sites;
 
   /* hasSuccessfulOutcome */
-  is(first.hasSuccessfulOutcome(), false, "optSite.hasSuccessfulOutcome() returns expected (1)");
-  is(second.hasSuccessfulOutcome(), true, "optSite.hasSuccessfulOutcome() returns expected (2)");
-  is(third.hasSuccessfulOutcome(), true, "optSite.hasSuccessfulOutcome() returns expected (3)");
+  equal(first.hasSuccessfulOutcome(), false, "optSite.hasSuccessfulOutcome() returns expected (1)");
+  equal(second.hasSuccessfulOutcome(), true, "optSite.hasSuccessfulOutcome() returns expected (2)");
+  equal(third.hasSuccessfulOutcome(), true, "optSite.hasSuccessfulOutcome() returns expected (3)");
 
   /* getAttempts */
-  is(first.getAttempts().length, 2, "optSite.getAttempts() has the correct amount of attempts (1)");
-  is(second.getAttempts().length, 5, "optSite.getAttempts() has the correct amount of attempts (2)");
-  is(third.getAttempts().length, 3, "optSite.getAttempts() has the correct amount of attempts (3)");
+  equal(first.getAttempts().length, 2, "optSite.getAttempts() has the correct amount of attempts (1)");
+  equal(second.getAttempts().length, 5, "optSite.getAttempts() has the correct amount of attempts (2)");
+  equal(third.getAttempts().length, 3, "optSite.getAttempts() has the correct amount of attempts (3)");
 
   /* getIonTypes */
-  is(first.getIonTypes().length, 1, "optSite.getIonTypes() has the correct amount of IonTypes (1)");
-  is(second.getIonTypes().length, 2, "optSite.getIonTypes() has the correct amount of IonTypes (2)");
-  is(third.getIonTypes().length, 1, "optSite.getIonTypes() has the correct amount of IonTypes (3)");
-
-  finish();
-}
+  equal(first.getIonTypes().length, 1, "optSite.getIonTypes() has the correct amount of IonTypes (1)");
+  equal(second.getIonTypes().length, 2, "optSite.getIonTypes() has the correct amount of IonTypes (2)");
+  equal(third.getIonTypes().length, 1, "optSite.getIonTypes() has the correct amount of IonTypes (3)");
+});
 
 
 let gStringTable = new RecordingUtils.UniqueStrings();
 
 function uniqStr(s) {
   return gStringTable.getOrAddStringIndex(s);
 }
 
--- a/browser/devtools/performance/test/unit/xpcshell.ini
+++ b/browser/devtools/performance/test/unit/xpcshell.ini
@@ -5,16 +5,19 @@ tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_frame-utils-01.js]
 [test_frame-utils-02.js]
 [test_marker-blueprint.js]
 [test_marker-utils.js]
 [test_profiler-categories.js]
+[test_jit-graph-data.js] 
+[test_jit-model-01.js]
+[test_jit-model-02.js]
 [test_tree-model-01.js]
 [test_tree-model-02.js]
 [test_tree-model-03.js]
 [test_tree-model-04.js]
 [test_tree-model-05.js]
 [test_tree-model-06.js]
 [test_tree-model-07.js]
 [test_tree-model-08.js]
--- a/browser/devtools/performance/views/details-js-call-tree.js
+++ b/browser/devtools/performance/views/details-js-call-tree.js
@@ -49,16 +49,17 @@ let JsCallTreeView = Heritage.extend(Det
       contentOnly: !PerformanceController.getOption("show-platform-data"),
       invertTree: PerformanceController.getOption("invert-call-tree"),
       flattenRecursion: PerformanceController.getOption("flatten-tree-recursion")
     };
     let recording = PerformanceController.getCurrentRecording();
     let profile = recording.getProfile();
     let threadNode = this._prepareCallTree(profile, interval, options);
     this._populateCallTree(threadNode, options);
+    this._toggleJITOptimizationsView(recording);
     this.emit(EVENTS.JS_CALL_TREE_RENDERED);
   },
 
   /**
    * Fired on the "link" event for the call tree in this container.
    */
   _onLink: function (_, treeItem) {
     let { url, line } = treeItem.frame.getInfo();
@@ -124,12 +125,27 @@ let JsCallTreeView = Heritage.extend(Det
     // When platform data isn't shown, hide the cateogry labels, since they're
     // only available for C++ frames. Pass *false* to make them invisible.
     root.toggleCategories(!options.contentOnly);
 
     // Return the CallView for tests
     return root;
   },
 
+  /**
+   * Displays or hides the optimizations view based on the recordings
+   * optimizations feature.
+   *
+   * @param {RecordingModel} recording
+   */
+  _toggleJITOptimizationsView: function (recording) {
+    if (recording && recording.getConfiguration().withJITOptimizations) {
+      JITOptimizationsView.show();
+      JITOptimizationsView.render();
+    } else {
+      JITOptimizationsView.hide();
+    }
+  },
+
   toString: () => "[object JsCallTreeView]"
 });
 
 EventEmitter.decorate(JsCallTreeView);
--- a/browser/devtools/performance/views/jit-optimizations.js
+++ b/browser/devtools/performance/views/jit-optimizations.js
@@ -20,46 +20,41 @@ let JITOptimizationsView = {
   _currentFrame: null,
 
   /**
    * Initialization function called when the tool starts up.
    */
   initialize: function () {
     this.reset = this.reset.bind(this);
     this._onFocusFrame = this._onFocusFrame.bind(this);
-    this._toggleVisibility = this._toggleVisibility.bind(this);
 
     this.el = $("#jit-optimizations-view");
     this.$headerName = $("#jit-optimizations-header .header-function-name");
     this.$headerFile = $("#jit-optimizations-header .header-file");
     this.$headerLine = $("#jit-optimizations-header .header-line");
 
     this.tree = new TreeWidget($("#jit-optimizations-raw-view"), {
       sorted: false,
       emptyText: JIT_EMPTY_TEXT
     });
 
     // Start the tree by resetting.
     this.reset();
 
-    this._toggleVisibility();
-
     PerformanceController.on(EVENTS.RECORDING_SELECTED, this.reset);
-    PerformanceController.on(EVENTS.PREF_CHANGED, this._toggleVisibility);
     JsCallTreeView.on("focus", this._onFocusFrame);
   },
 
   /**
    * Destruction function called when the tool cleans up.
    */
   destroy: function () {
     this.tree = null;
     this.$headerName = this.$headerFile = this.$headerLine = this.el = null;
     PerformanceController.off(EVENTS.RECORDING_SELECTED, this.reset);
-    PerformanceController.off(EVENTS.PREF_CHANGED, this._toggleVisibility);
     JsCallTreeView.off("focus", this._onFocusFrame);
   },
 
   /**
    * Takes a FrameNode, with corresponding optimization data to be displayed
    * in the view.
    *
    * @param {FrameNode} frameNode
@@ -93,21 +88,30 @@ let JITOptimizationsView = {
 
   /**
    * Clears out data in the tree.
    */
   clear: function () {
     this.tree.clear();
   },
 
+  show: function () {
+    this.el.hidden = false;
+  },
+
+  hide: function () {
+    this.el.hidden = true;
+  },
+
   /**
    * Helper to determine whether or not this view should be enabled.
    */
   isEnabled: function () {
-    return PerformanceController.getOption("show-jit-optimizations");
+    let recording = PerformanceController.getCurrentRecording();
+    return !!(recording && recording.getConfiguration().withJITOptimizations);
   },
 
   /**
    * Takes a JITOptimizations object and builds a view containing all attempted
    * optimizations for this frame. This view is very verbose and meant for those
    * who understand JIT compilers.
    */
   render: function () {
@@ -377,32 +381,16 @@ let JITOptimizationsView = {
   _isLinkableURL: function (url) {
     return url && url.indexOf &&
        (url.indexOf("http") === 0 ||
         url.indexOf("resource://") === 0 ||
         url.indexOf("file://") === 0);
   },
 
   /**
-   * Toggles the visibility of the JITOptimizationsView based on the preference
-   * devtools.performance.ui.show-jit-optimizations.
-   */
-
-  _toggleVisibility: function () {
-    let enabled = this.isEnabled();
-    this.el.hidden = !enabled;
-
-    // If view is toggled on, and there's a frame node selected,
-    // attempt to render it
-    if (enabled) {
-      this.render();
-    }
-  },
-
-  /**
    * Called when the JSCallTreeView focuses on a frame.
    */
 
   _onFocusFrame: function (_, view) {
     if (!view.frame) {
       return;
     }
 
--- a/browser/devtools/performance/views/overview.js
+++ b/browser/devtools/performance/views/overview.js
@@ -173,17 +173,17 @@ let OverviewView = {
     }
     return { startTime: selection.min, endTime: selection.max };
   },
 
   /**
    * Method for handling all the set up for rendering the overview graphs.
    *
    * @param number resolution
-   *        The fps graph resolution. @see Graphs.jsm
+   *        The fps graph resolution. @see Graphs.js
    */
   render: Task.async(function *(resolution) {
     if (this.isDisabled()) {
       return;
     }
 
     let recording = PerformanceController.getCurrentRecording();
     yield this.graphs.render(recording.getAllData(), resolution);
--- a/browser/devtools/performance/views/toolbar.js
+++ b/browser/devtools/performance/views/toolbar.js
@@ -88,17 +88,17 @@ let ToolbarView = {
    * Fired when `devtools.performance.ui.experimental` is changed, or
    * during init. Toggles the visibility of experimental performance tool options
    * in the UI options.
    *
    * Sets or removes "experimental-enabled" on the menu and main elements,
    * hiding or showing all elements with class "experimental-option".
    *
    * TODO re-enable "#option-enable-memory" permanently once stable in bug 1163350
-   * TODO re-enable "#option-show-jit-optimizations" permanently once stable in bug 1163351
+   * TODO re-enable "#option-enable-jit-optimizations" permanently once stable in bug 1163351
    *
    * @param {boolean} isEnabled
    */
   _toggleExperimentalUI: function (isEnabled) {
     if (isEnabled) {
       $(".theme-body").classList.add("experimental-enabled");
       this._popup.classList.add("experimental-enabled");
     } else {
--- a/browser/devtools/shared/moz.build
+++ b/browser/devtools/shared/moz.build
@@ -16,17 +16,16 @@ EXTRA_JS_MODULES.devtools += [
     'Parser.jsm',
     'SplitView.jsm',
 ]
 
 EXTRA_JS_MODULES.devtools += [
     'widgets/AbstractTreeItem.jsm',
     'widgets/BreadcrumbsWidget.jsm',
     'widgets/Chart.jsm',
-    'widgets/Graphs.jsm',
     'widgets/GraphsWorker.js',
     'widgets/SideMenuWidget.jsm',
     'widgets/SimpleListWidget.jsm',
     'widgets/VariablesView.jsm',
     'widgets/VariablesViewController.jsm',
     'widgets/ViewHelpers.jsm',
 ]
 
@@ -49,14 +48,15 @@ EXTRA_JS_MODULES.devtools.shared += [
 ]
 
 EXTRA_JS_MODULES.devtools.shared.widgets += [
     'widgets/CubicBezierPresets.js',
     'widgets/CubicBezierWidget.js',
     'widgets/FastListWidget.js',
     'widgets/FilterWidget.js',
     'widgets/FlameGraph.js',
+    'widgets/Graphs.js',
     'widgets/MdnDocsWidget.js',
     'widgets/Spectrum.js',
     'widgets/TableWidget.js',
     'widgets/Tooltip.js',
     'widgets/TreeWidget.js',
 ]
--- a/browser/devtools/shared/test/browser_graphs-01.js
+++ b/browser/devtools/shared/test/browser_graphs-01.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets works properly.
 
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
   finish();
 });
--- a/browser/devtools/shared/test/browser_graphs-02.js
+++ b/browser/devtools/shared/test/browser_graphs-02.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets can properly add data, regions and highlights.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-03.js
+++ b/browser/devtools/shared/test/browser_graphs-03.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets can handle clients getting/setting the
 // selection or cursor.
 
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-04.js
+++ b/browser/devtools/shared/test/browser_graphs-04.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets can correctly compare selections and cursors.
 
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-05.js
+++ b/browser/devtools/shared/test/browser_graphs-05.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets can correctly determine which regions are hovered.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-06.js
+++ b/browser/devtools/shared/test/browser_graphs-06.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if clicking on regions adds a selection spanning that region.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-07a.js
+++ b/browser/devtools/shared/test/browser_graphs-07a.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if selecting, resizing, moving selections and zooming in/out works.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-07b.js
+++ b/browser/devtools/shared/test/browser_graphs-07b.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if selections can't be added via clicking, while not allowed.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-07c.js
+++ b/browser/devtools/shared/test/browser_graphs-07c.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if movement via event dispatching using screenX / screenY
 // works.  All of the other tests directly use the graph's mouse event
 // callbacks with textX / testY for convenience.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-07d.js
+++ b/browser/devtools/shared/test/browser_graphs-07d.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that selections are drawn onto the canvas.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-07e.js
+++ b/browser/devtools/shared/test/browser_graphs-07e.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that selections are drawn onto the canvas.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 let CURRENT_ZOOM = 1;
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
--- a/browser/devtools/shared/test/browser_graphs-08.js
+++ b/browser/devtools/shared/test/browser_graphs-08.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if a selection is dropped when clicking outside of it.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-09a.js
+++ b/browser/devtools/shared/test/browser_graphs-09a.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that line graphs properly create the gutter and tooltips.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-09b.js
+++ b/browser/devtools/shared/test/browser_graphs-09b.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that line graphs properly use the tooltips configuration properties.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-09c.js
+++ b/browser/devtools/shared/test/browser_graphs-09c.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that line graphs hide the tooltips when there's no data available.
 
 const TEST_DATA = [];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-09d.js
+++ b/browser/devtools/shared/test/browser_graphs-09d.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that line graphs hide the 'max' tooltip when the distance between
 // the 'min' and 'max' tooltip is too small.
 
 const TEST_DATA = [{ delta: 100, value: 60 }, { delta: 200, value: 59.9 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-09e.js
+++ b/browser/devtools/shared/test/browser_graphs-09e.js
@@ -2,17 +2,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that line graphs hide the gutter and tooltips when there's no data,
 // but show them when there is.
 
 const NO_DATA = [];
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-09f.js
+++ b/browser/devtools/shared/test/browser_graphs-09f.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests the constructor options for `min`, `max` and `avg` on displaying the
 // gutter/tooltips and lines.
 
 const TEST_DATA = [{ delta: 100, value: 60 }, { delta: 200, value: 1 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-10a.js
+++ b/browser/devtools/shared/test/browser_graphs-10a.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graphs properly handle resizing.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-10b.js
+++ b/browser/devtools/shared/test/browser_graphs-10b.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graphs aren't refreshed when the owner window resizes but
 // the graph dimensions stay the same.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-10c.js
+++ b/browser/devtools/shared/test/browser_graphs-10c.js
@@ -1,13 +1,13 @@
 
 // Tests that graphs properly handle resizing.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-11a.js
+++ b/browser/devtools/shared/test/browser_graphs-11a.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that bar graph create a legend as expected.
 
-let {BarGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {BarGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 const CATEGORIES = [
   { color: "#46afe3", label: "Foo" },
   { color: "#eb5368", label: "Bar" },
   { color: "#70bf53", label: "Baz" }
 ];
 
--- a/browser/devtools/shared/test/browser_graphs-11b.js
+++ b/browser/devtools/shared/test/browser_graphs-11b.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that bar graph's legend items handle mouseover/mouseout.
 
-let {BarGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {BarGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 const CATEGORIES = [
   { color: "#46afe3", label: "Foo" },
   { color: "#eb5368", label: "Bar" },
   { color: "#70bf53", label: "Baz" }
 ];
 
--- a/browser/devtools/shared/test/browser_graphs-12.js
+++ b/browser/devtools/shared/test/browser_graphs-12.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that canvas graphs can have their selection linked.
 
-let {LineGraphWidget, BarGraphWidget, CanvasGraphUtils} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget,BarGraphWidget,CanvasGraphUtils} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-13.js
+++ b/browser/devtools/shared/test/browser_graphs-13.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets may have a fixed width or height.
 
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-14.js
+++ b/browser/devtools/shared/test/browser_graphs-14.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets correctly emit mouse input events.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_graphs-15.js
+++ b/browser/devtools/shared/test/browser_graphs-15.js
@@ -14,17 +14,17 @@ let t = 0;
 for (let frameRate of FRAMES) {
   for (let i = 0; i < frameRate; i++) {
     let delta = Math.floor(1000 / frameRate); // Duration between frames at this rate
     t += delta;
     TEST_DATA.push(t);
   }
 }
 
-let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {LineGraphWidget} = devtools.require("devtools/shared/widgets/Graphs");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/widgets/FlameGraph.js
+++ b/browser/devtools/shared/widgets/FlameGraph.js
@@ -14,22 +14,22 @@ loader.lazyRequireGetter(this, "EventEmi
 loader.lazyRequireGetter(this, "getColor",
   "devtools/shared/theme", true);
 
 loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
   "devtools/performance/global", true);
 loader.lazyRequireGetter(this, "FrameUtils",
   "devtools/performance/frame-utils");
 
-loader.lazyImporter(this, "AbstractCanvasGraph",
-  "resource:///modules/devtools/Graphs.jsm");
-loader.lazyImporter(this, "GraphArea",
-  "resource:///modules/devtools/Graphs.jsm");
-loader.lazyImporter(this, "GraphAreaDragger",
-  "resource:///modules/devtools/Graphs.jsm");
+loader.lazyRequireGetter(this, "AbstractCanvasGraph",
+  "devtools/shared/widgets/Graphs", true);
+loader.lazyRequireGetter(this, "GraphArea",
+  "devtools/shared/widgets/Graphs", true);
+loader.lazyRequireGetter(this, "GraphAreaDragger",
+  "devtools/shared/widgets/Graphs", true);
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
 
 const L10N = new ViewHelpers.L10N();
 
 const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
 
rename from browser/devtools/shared/widgets/Graphs.jsm
rename to browser/devtools/shared/widgets/Graphs.js
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.js
@@ -1,36 +1,32 @@
 /* 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;
+const { Cc, Ci, Cu, Cr } = require("chrome");
 
-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 {DevToolsWorker} = Cu.import("resource://gre/modules/devtools/shared/worker.js", {});
-const {LayoutHelpers} = Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", {});
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+const { ViewHelpers } = require("resource:///modules/devtools/ViewHelpers.jsm");
+const { Heritage, setNamedTimeout, clearNamedTimeout } = require("resource:///modules/devtools/ViewHelpers.jsm");
 
-this.EXPORTED_SYMBOLS = [
-  "GraphCursor",
-  "GraphArea",
-  "GraphAreaDragger",
-  "GraphAreaResizer",
-  "AbstractCanvasGraph",
-  "LineGraphWidget",
-  "BarGraphWidget",
-  "CanvasGraphUtils"
-];
+loader.lazyRequireGetter(this, "promise");
+loader.lazyRequireGetter(this, "EventEmitter",
+  "devtools/toolkit/event-emitter");
+
+loader.lazyImporter(this, "DevToolsWorker",
+  "resource://gre/modules/devtools/shared/worker.js");
+loader.lazyImporter(this, "LayoutHelpers",
+  "resource://gre/modules/devtools/LayoutHelpers.jsm");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
 const WORKER_URL = "resource:///modules/devtools/GraphsWorker.js";
+
 const L10N = new ViewHelpers.L10N();
 
 // Generic constants.
 
 const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
 const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00075;
 const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.1;
 const GRAPH_WHEEL_MIN_SELECTION_WIDTH = 10; // px
@@ -2220,16 +2216,25 @@ function distSquared(x0, y0, x1, y1) {
  */
 function findFirst(array, predicate) {
   for (let i = 0, len = array.length; i < len; i++) {
     let element = array[i];
     if (predicate(element)) return element;
   }
 }
 
+exports.GraphCursor = GraphCursor;
+exports.GraphArea = GraphArea;
+exports.GraphAreaDragger = GraphAreaDragger;
+exports.GraphAreaResizer = GraphAreaResizer;
+exports.AbstractCanvasGraph = AbstractCanvasGraph;
+exports.LineGraphWidget = LineGraphWidget;
+exports.BarGraphWidget = BarGraphWidget;
+exports.CanvasGraphUtils = CanvasGraphUtils;
+
 /**
  * Finds the last element in an array that validates a predicate.
  * @param array
  * @param function predicate
  * @return number
  */
 function findLast(array, predicate) {
   for (let i = array.length - 1; i >= 0; i--) {
--- a/browser/devtools/shared/widgets/GraphsWorker.js
+++ b/browser/devtools/shared/widgets/GraphsWorker.js
@@ -5,17 +5,17 @@
 
 /**
  * Import `createTask` to communicate with `devtools/toolkit/shared/worker`.
  */
 importScripts("resource://gre/modules/workers/require.js");
 const { createTask } = require("resource://gre/modules/devtools/shared/worker-helper");
 
 /**
- * @see LineGraphWidget.prototype.setDataFromTimestamps in Graphs.jsm
+ * @see LineGraphWidget.prototype.setDataFromTimestamps in Graphs.js
  * @param number id
  * @param array timestamps
  * @param number interval
  * @param number duration
  */
 createTask(self, "plotTimestampsGraph", function ({ timestamps, interval, duration }) {
   let plottedData = plotTimestamps(timestamps, interval);
   let plottedMinMaxSum = getMinMaxAvg(plottedData, timestamps, duration);
--- a/browser/devtools/webaudioeditor/includes.js
+++ b/browser/devtools/webaudioeditor/includes.js
@@ -18,18 +18,19 @@ let { EventTarget } = require("sdk/event
 
 const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 const { Class } = require("sdk/core/heritage");
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const STRINGS_URI = "chrome://browser/locale/devtools/webaudioeditor.properties"
 const L10N = new ViewHelpers.L10N(STRINGS_URI);
 const Telemetry = require("devtools/shared/telemetry");
 const telemetry = new Telemetry();
-devtools.lazyImporter(this, "LineGraphWidget",
-  "resource:///modules/devtools/Graphs.jsm");
+
+devtools.lazyRequireGetter(this, "LineGraphWidget",
+  "devtools/shared/widgets/Graphs", true);
 
 // `AUDIO_NODE_DEFINITION` defined in the controller's initialization,
 // which describes all the properties of an AudioNode
 let AUDIO_NODE_DEFINITION;
 
 // Override DOM promises with Promise.jsm helpers
 const { defer, all } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 
--- a/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
@@ -137,21 +137,21 @@
 <!ENTITY profilerUI.enableAllocations.tooltiptext "Record Object allocations while profiling.">
 
 <!-- LOCALIZATION NOTE (profilerUI.enableFramerate): This string
   -  is displayed next to a checkbox determining whether or not framerate
   -  is recorded. -->
 <!ENTITY profilerUI.enableFramerate             "Record Framerate">
 <!ENTITY profilerUI.enableFramerate.tooltiptext "Record framerate while profiling.">
 
-<!-- LOCALIZATION NOTE (profilerUI.showJITOptimizations): This string
+<!-- LOCALIZATION NOTE (profilerUI.enableJITOptimizations): This string
   -  is displayed next to a checkbox determining whether or not JIT optimization data
-  -  should be shown. -->
-<!ENTITY profilerUI.showJITOptimizations             "Show JIT Optimizations">
-<!ENTITY profilerUI.showJITOptimizations.tooltiptext "Show JIT optimization data sampled in each frame of the JS call tree.">
+  -  should be recorded. -->
+<!ENTITY profilerUI.enableJITOptimizations             "Record JIT Optimizations">
+<!ENTITY profilerUI.enableJITOptimizations.tooltiptext "Record JIT optimization data sampled in each JavaScript frame.">
 
 <!-- LOCALIZATION NOTE (profilerUI.JITOptimizationsTitle): This string
   -  is displayed as the title of the JIT Optimizations panel. -->
 <!ENTITY profilerUI.JITOptimizationsTitle "JIT Optimizations">
 
 <!-- LOCALIZATION NOTE (profilerUI.console.recordingNoticeStart/recordingNoticeEnd):
   -  This string is displayed when a recording is selected that started via console.profile.
   -  Wraps the command used to start, like "Currently recording via console.profile("label")" -->
new file mode 100644
--- /dev/null
+++ b/browser/modules/AboutNewTab.jsm
@@ -0,0 +1,39 @@
+/* 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";
+
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+let Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [ "AboutNewTab" ];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "RemotePages",
+  "resource://gre/modules/RemotePageManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
+  "resource://gre/modules/NewTabUtils.jsm");
+
+let AboutNewTab = {
+
+  pageListener: null,
+
+  init: function() {
+    this.pageListener = new RemotePages("about:newtab");
+    this.pageListener.addMessageListener("NewTab:Customize", this.customize.bind(this));
+  },
+
+  customize: function(message) {
+    NewTabUtils.allPages.enabled = message.data.enabled;
+    NewTabUtils.allPages.enhanced = message.data.enhanced;
+  },
+
+  uninit: function() {
+    this.pageListener.destroy();
+    this.pageListener = null;
+  },
+};
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -7,16 +7,17 @@
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 XPCSHELL_TESTS_MANIFESTS += [
     'test/unit/social/xpcshell.ini',
     'test/xpcshell/xpcshell.ini',
 ]
 
 EXTRA_JS_MODULES += [
     'AboutHome.jsm',
+    'AboutNewTab.jsm',
     'BrowserUITelemetry.jsm',
     'CastingApps.jsm',
     'Chat.jsm',
     'ContentClick.jsm',
     'ContentCrashReporters.jsm',
     'ContentLinkHandler.jsm',
     'ContentObservers.jsm',
     'ContentSearch.jsm',
--- a/build/mobile/robocop/AndroidManifest.xml.in
+++ b/build/mobile/robocop/AndroidManifest.xml.in
@@ -9,22 +9,26 @@
     android:versionName="1.0" >
 
     <uses-sdk android:minSdkVersion="@MOZ_ANDROID_MIN_SDK_VERSION@"
 #ifdef MOZ_ANDROID_MAX_SDK_VERSION
               android:maxSdkVersion="@MOZ_ANDROID_MAX_SDK_VERSION@"
 #endif
               android:targetSdkVersion="@ANDROID_TARGET_SDK@"/>
 
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
     <instrumentation
         android:name="org.mozilla.gecko.FennecInstrumentationTestRunner"
         android:targetPackage="@ANDROID_PACKAGE_NAME@" />
 
     <application
-        android:label="@string/app_name" >
+        android:label="@string/app_name"
+        android:debuggable="true">
+
         <uses-library android:name="android.test.runner" />
 
         <!-- Fake handlers to ensure that we have some share intents to show in our share handler list -->
         <activity android:name="org.mozilla.gecko.RobocopShare1"
                   android:label="Robocop fake activity">
 
             <intent-filter android:label="Fake robocop share handler 1">
                 <action android:name="android.intent.action.SEND" />
--- a/js/src/doc/Debugger/Conventions.md
+++ b/js/src/doc/Debugger/Conventions.md
@@ -133,18 +133,19 @@ If a function that would normally return
 how the debuggee should continue instead throws an exception, we never
 propagate such an exception to the debuggee; instead, we call the
 associated `Debugger` instance's `uncaughtExceptionHook` property, as
 described below.
 
 
 ## Timestamps
 
-Timestamps are expressed in units of microseconds since the epoch (midnight,
-January 1st, 1970).
+Timestamps are expressed in units of milliseconds since an arbitrary,
+but fixed, epoch.  The resolution of timestamps is generally greater
+than milliseconds, though no specific resolution is guaranteed.
 
 
 ## The `Debugger.DebuggeeWouldRun` Exception
 
 Some debugger operations that appear to simply inspect the debuggee's state
 may actually cause debuggee code to run. For example, reading a variable
 might run a getter function on the global or on a `with` expression's
 operand; and getting an object's property descriptor will run a handler
--- a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-14.js
+++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-14.js
@@ -1,28 +1,28 @@
 // Test that drainAllocationsLog returns some timestamps.
 
 load(libdir + 'asserts.js');
 
 var allocTimes = [];
 
-allocTimes.push(1000 * dateNow());
+allocTimes.push(dateNow());
 
 const root = newGlobal();
 const dbg = new Debugger(root);
 
 dbg.memory.trackingAllocationSites = true;
 root.eval("this.alloc1 = {}");
-allocTimes.push(1000 * dateNow());
+allocTimes.push(dateNow());
 root.eval("this.alloc2 = {}");
-allocTimes.push(1000 * dateNow());
+allocTimes.push(dateNow());
 root.eval("this.alloc3 = {}");
-allocTimes.push(1000 * dateNow());
+allocTimes.push(dateNow());
 root.eval("this.alloc4 = {}");
-allocTimes.push(1000 * dateNow());
+allocTimes.push(dateNow());
 
 allocs = dbg.memory.drainAllocationsLog();
 assertEq(allocs.length >= 4, true);
 assertEq(allocs[0].timestamp >= allocTimes[0], true);
 var seenAlloc = 0;
 var lastIndexSeenAllocIncremented = 0;
 for (i = 1; i < allocs.length; ++i) {
     assertEq(allocs[i].timestamp >= allocs[i - 1].timestamp, true);
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -333,17 +333,17 @@ IterPerformanceStats(JSContext* cx,
         }
 
         Set::AddPtr ptr = set.lookupForAdd(group);
         if (ptr) {
             // Don't report the same group twice.
             continue;
         }
 
-        if (!(*walker)(cx, group->data, closure)) {
+        if (!(*walker)(cx, group->data, group->uid, closure)) {
             // Issue in callback
             return false;
         }
         if (!set.add(ptr, group)) {
             // Memory issue
             return false;
         }
     }
@@ -658,16 +658,32 @@ JS_SetICUMemoryFunctions(JS_ICUAllocFn a
     UErrorCode status = U_ZERO_ERROR;
     u_setMemoryFunctions(/* context = */ nullptr, allocFn, reallocFn, freeFn, &status);
     return U_SUCCESS(status);
 #else
     return true;
 #endif
 }
 
+static JS_CurrentEmbedderTimeFunction currentEmbedderTimeFunction;
+
+JS_PUBLIC_API(void)
+JS_SetCurrentEmbedderTimeFunction(JS_CurrentEmbedderTimeFunction timeFn)
+{
+    currentEmbedderTimeFunction = timeFn;
+}
+
+JS_PUBLIC_API(double)
+JS_GetCurrentEmbedderTime()
+{
+    if (currentEmbedderTimeFunction)
+        return currentEmbedderTimeFunction();
+    return PRMJ_Now() / static_cast<double>(PRMJ_USEC_PER_MSEC);
+}
+
 JS_PUBLIC_API(void*)
 JS_GetRuntimePrivate(JSRuntime* rt)
 {
     return rt->data;
 }
 
 JS_PUBLIC_API(void)
 JS_SetRuntimePrivate(JSRuntime* rt, void* data)
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -1048,16 +1048,35 @@ typedef void* (*JS_ICUAllocFn)(const voi
 typedef void* (*JS_ICUReallocFn)(const void*, void* p, size_t size);
 typedef void (*JS_ICUFreeFn)(const void*, void* p);
 
 // This function can be used to track memory used by ICU.
 // Do not use it unless you know what you are doing!
 extern JS_PUBLIC_API(bool)
 JS_SetICUMemoryFunctions(JS_ICUAllocFn allocFn, JS_ICUReallocFn reallocFn, JS_ICUFreeFn freeFn);
 
+typedef double (*JS_CurrentEmbedderTimeFunction)();
+
+/*
+ * The embedding can specify a time function that will be used in some
+ * situations.  The function can return the time however it likes; but
+ * the norm is to return times in units of milliseconds since an
+ * arbitrary, but consistent, epoch.  If the time function is not set,
+ * a built-in default will be used.
+ */
+JS_PUBLIC_API(void)
+JS_SetCurrentEmbedderTimeFunction(JS_CurrentEmbedderTimeFunction timeFn);
+
+/*
+ * Return the time as computed using the current time function, or a
+ * suitable default if one has not been set.
+ */
+JS_PUBLIC_API(double)
+JS_GetCurrentEmbedderTime();
+
 JS_PUBLIC_API(void*)
 JS_GetRuntimePrivate(JSRuntime* rt);
 
 extern JS_PUBLIC_API(JSRuntime*)
 JS_GetRuntime(JSContext* cx);
 
 extern JS_PUBLIC_API(JSRuntime*)
 JS_GetParentRuntime(JSContext* cx);
@@ -5443,16 +5462,19 @@ struct PerformanceData {
 //
 // This class is refcounted by instances of `JSCompartment`.
 // Do not attempt to hold to a pointer to a `PerformanceGroup`.
 struct PerformanceGroup {
 
     // Performance data for this group.
     PerformanceData data;
 
+    // An id unique to this runtime.
+    const uint64_t uid;
+
     // `true` if an instance of `AutoStopwatch` is already monitoring
     // the performance of this performance group for this iteration
     // of the event loop, `false` otherwise.
     bool hasStopwatch(uint64_t iteration) const {
         return stopwatch_ != nullptr && iteration_ == iteration;
     }
 
     // Mark that an instance of `AutoStopwatch` is monitoring
@@ -5467,22 +5489,17 @@ struct PerformanceGroup {
     void releaseStopwatch(uint64_t iteration, const AutoStopwatch* stopwatch) {
         if (iteration_ != iteration)
             return;
 
         MOZ_ASSERT(stopwatch == stopwatch_ || stopwatch_ == nullptr);
         stopwatch_ = nullptr;
     }
 
-    explicit PerformanceGroup(void* key)
-      : stopwatch_(nullptr)
-      , iteration_(0)
-      , key_(key)
-      , refCount_(0)
-    { }
+    explicit PerformanceGroup(JSContext* cx, void* key);
     ~PerformanceGroup()
     {
         MOZ_ASSERT(refCount_ == 0);
     }
   private:
     PerformanceGroup& operator=(const PerformanceGroup&) = delete;
     PerformanceGroup(const PerformanceGroup&) = delete;
 
@@ -5584,17 +5601,17 @@ IsStopwatchActive(JSRuntime*);
 
 /**
  * Access the performance information stored in a compartment.
  */
 extern JS_PUBLIC_API(PerformanceData*)
 GetPerformanceData(JSRuntime*);
 
 typedef bool
-(PerformanceStatsWalker)(JSContext* cx, const PerformanceData& stats, void* closure);
+(PerformanceStatsWalker)(JSContext* cx, const PerformanceData& stats, uint64_t uid, void* closure);
 
 /**
  * Extract the performance statistics.
  *
  * Note that before calling `walker`, we enter the corresponding context.
  */
 extern JS_PUBLIC_API(bool)
 IterPerformanceStats(JSContext* cx, PerformanceStatsWalker* walker, js::PerformanceData* process, void* closure);
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -1647,17 +1647,17 @@ Debugger::slowPathOnNewGlobalObject(JSCo
                 break;
         }
     }
     MOZ_ASSERT(!cx->isExceptionPending());
 }
 
 /* static */ bool
 Debugger::slowPathOnLogAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame,
-                                      int64_t when, GlobalObject::DebuggerVector& dbgs)
+                                      double when, GlobalObject::DebuggerVector& dbgs)
 {
     MOZ_ASSERT(!dbgs.empty());
     mozilla::DebugOnly<Debugger**> begin = dbgs.begin();
 
     for (Debugger** dbgp = dbgs.begin(); dbgp < dbgs.end(); dbgp++) {
         // The set of debuggers had better not change while we're iterating,
         // such that the vector gets reallocated.
         MOZ_ASSERT(dbgs.begin() == begin);
@@ -1695,17 +1695,17 @@ Debugger::slowPathOnIonCompilation(JSCon
 bool
 Debugger::isDebuggee(const JSCompartment* compartment) const
 {
     MOZ_ASSERT(compartment);
     return compartment->isDebuggee() && debuggees.has(compartment->maybeGlobal());
 }
 
 /* static */ Debugger::AllocationSite*
-Debugger::AllocationSite::create(JSContext* cx, HandleObject frame, int64_t when, HandleObject obj)
+Debugger::AllocationSite::create(JSContext* cx, HandleObject frame, double when, HandleObject obj)
 {
     assertSameCompartment(cx, frame);
 
     RootedAtom ctorName(cx);
     {
         AutoCompartment ac(cx, obj);
         if (!obj->constructorDisplayAtom(cx, &ctorName))
             return nullptr;
@@ -1718,17 +1718,17 @@ Debugger::AllocationSite::create(JSConte
     allocSite->className = obj->getClass()->name;
     allocSite->ctorName = ctorName.get();
     return allocSite;
 }
 
 
 bool
 Debugger::appendAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame,
-                               int64_t when)
+                               double when)
 {
     MOZ_ASSERT(trackingAllocationSites);
 
     AutoCompartment ac(cx, object);
     RootedObject wrappedFrame(cx, frame);
     if (!cx->compartment()->wrap(cx, &wrappedFrame))
         return false;
 
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -269,47 +269,47 @@ class Debugger : private mozilla::Linked
     JSCList breakpoints;                /* Circular list of all js::Breakpoints in this debugger */
 
     // The set of GC numbers for which one or more of this Debugger's observed
     // debuggees participated in.
     js::HashSet<uint64_t> observedGCs;
 
     struct AllocationSite : public mozilla::LinkedListElement<AllocationSite>
     {
-        AllocationSite(HandleObject frame, int64_t when)
+        AllocationSite(HandleObject frame, double when)
             : frame(frame),
               when(when),
               className(nullptr),
               ctorName(nullptr)
         {
             MOZ_ASSERT_IF(frame, UncheckedUnwrap(frame)->is<SavedFrame>());
         };
 
-        static AllocationSite* create(JSContext* cx, HandleObject frame, int64_t when,
+        static AllocationSite* create(JSContext* cx, HandleObject frame, double when,
                                       HandleObject obj);
 
         RelocatablePtrObject frame;
-        int64_t when;
+        double when;
         const char* className;
         RelocatablePtrAtom ctorName;
     };
     typedef mozilla::LinkedList<AllocationSite> AllocationSiteList;
 
     bool allowUnobservedAsmJS;
     bool trackingAllocationSites;
     double allocationSamplingProbability;
     AllocationSiteList allocationsLog;
     size_t allocationsLogLength;
     size_t maxAllocationsLogLength;
     bool allocationsLogOverflowed;
 
     static const size_t DEFAULT_MAX_ALLOCATIONS_LOG_LENGTH = 5000;
 
     bool appendAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame,
-                              int64_t when);
+                              double when);
     void emptyAllocationsLog();
 
     /*
      * Return true if there is an existing object metadata callback for the
      * given global's compartment that will prevent our instrumentation of
      * allocations.
      */
     static bool cannotTrackAllocations(const GlobalObject& global);
@@ -545,17 +545,17 @@ class Debugger : private mozilla::Linked
 
     static JSTrapStatus slowPathOnEnterFrame(JSContext* cx, AbstractFramePtr frame);
     static bool slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, bool ok);
     static JSTrapStatus slowPathOnDebuggerStatement(JSContext* cx, AbstractFramePtr frame);
     static JSTrapStatus slowPathOnExceptionUnwind(JSContext* cx, AbstractFramePtr frame);
     static void slowPathOnNewScript(JSContext* cx, HandleScript script);
     static void slowPathOnNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global);
     static bool slowPathOnLogAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame,
-                                            int64_t when, GlobalObject::DebuggerVector& dbgs);
+                                            double when, GlobalObject::DebuggerVector& dbgs);
     static void slowPathPromiseHook(JSContext* cx, Hook hook, HandleObject promise);
     static void slowPathOnIonCompilation(JSContext* cx, AutoScriptVector& scripts, LSprinter& graph);
 
     template <typename HookIsEnabledFun /* bool (Debugger*) */,
               typename FireHookFun /* JSTrapStatus (Debugger*) */>
     static JSTrapStatus dispatchHook(JSContext* cx, HookIsEnabledFun hookIsEnabled,
                                      FireHookFun fireHook);
 
@@ -708,17 +708,17 @@ class Debugger : private mozilla::Linked
      * throw, or vice versa: we can redirect to a complete copy of the
      * alternative path, containing its own call to onLeaveFrame.)
      */
     static inline bool onLeaveFrame(JSContext* cx, AbstractFramePtr frame, bool ok);
 
     static inline void onNewScript(JSContext* cx, HandleScript script);
     static inline void onNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global);
     static inline bool onLogAllocationSite(JSContext* cx, JSObject* obj, HandleSavedFrame frame,
-                                           int64_t when);
+                                           double when);
     static inline bool observesIonCompilation(JSContext* cx);
     static inline void onIonCompilation(JSContext* cx, AutoScriptVector& scripts, LSprinter& graph);
     static JSTrapStatus onTrap(JSContext* cx, MutableHandleValue vp);
     static JSTrapStatus onSingleStep(JSContext* cx, MutableHandleValue vp);
     static bool handleBaselineOsr(JSContext* cx, InterpreterFrame* from, jit::BaselineFrame* to);
     static bool handleIonBailout(JSContext* cx, jit::RematerializedFrame* from, jit::BaselineFrame* to);
     static void handleUnrecoverableIonBailoutError(JSContext* cx, jit::RematerializedFrame* frame);
     static void propagateForcedReturn(JSContext* cx, AbstractFramePtr frame, HandleValue rval);
@@ -1011,17 +1011,17 @@ Debugger::onNewGlobalObject(JSContext* c
 #ifdef DEBUG
     global->compartment()->firedOnNewGlobalObject = true;
 #endif
     if (!JS_CLIST_IS_EMPTY(&cx->runtime()->onNewGlobalObjectWatchers))
         Debugger::slowPathOnNewGlobalObject(cx, global);
 }
 
 /* static */ bool
-Debugger::onLogAllocationSite(JSContext* cx, JSObject* obj, HandleSavedFrame frame, int64_t when)
+Debugger::onLogAllocationSite(JSContext* cx, JSObject* obj, HandleSavedFrame frame, double when)
 {
     GlobalObject::DebuggerVector* dbgs = cx->global()->getDebuggers();
     if (!dbgs || dbgs->empty())
         return true;
     RootedObject hobj(cx, obj);
     return Debugger::slowPathOnLogAllocationSite(cx, hobj, frame, when, *dbgs);
 }
 
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -950,28 +950,37 @@ js::PerformanceGroupHolder::getGroup(JSC
 
     void* key = getHashKey(cx);
     JSRuntime::Stopwatch::Groups::AddPtr ptr =
         runtime_->stopwatch.groups_.lookupForAdd(key);
     if (ptr) {
         group_ = ptr->value();
         MOZ_ASSERT(group_);
     } else {
-        group_ = runtime_->new_<PerformanceGroup>(key);
+        group_ = runtime_->new_<PerformanceGroup>(cx, key);
         runtime_->stopwatch.groups_.add(ptr, key, group_);
     }
 
     group_->incRefCount();
 
     return group_;
 }
 
 PerformanceData*
 js::GetPerformanceData(JSRuntime* rt)
 {
     return &rt->stopwatch.performance;
 }
 
+js::PerformanceGroup::PerformanceGroup(JSContext* cx, void* key)
+  : uid(cx->runtime()->stopwatch.uniqueId())
+  , stopwatch_(nullptr)
+  , iteration_(0)
+  , key_(key)
+  , refCount_(0)
+{
+}
+
 void
 JS_SetCurrentPerfGroupCallback(JSRuntime *rt, JSCurrentPerfGroupCallback cb)
 {
     rt->stopwatch.currentPerfGroupCallback = cb;
 }
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -1512,16 +1512,17 @@ struct JSRuntime : public JS::shadow::Ru
         JSCurrentPerfGroupCallback currentPerfGroupCallback;
 
         Stopwatch()
           : iteration(0)
           , isEmpty(true)
           , currentPerfGroupCallback(nullptr)
           , isMonitoringJank_(false)
           , isMonitoringCPOW_(false)
+          , idCounter_(0)
         { }
 
         /**
          * Reset the stopwatch.
          *
          * This method is meant to be called whenever we start processing
          * an event, to ensure that stop any ongoing measurement that would
          * otherwise provide irrelevant results.
@@ -1565,16 +1566,23 @@ struct JSRuntime : public JS::shadow::Ru
             isMonitoringCPOW_ = value;
             return true;
         }
 
         bool isMonitoringCPOW() const {
             return isMonitoringCPOW_;
         }
 
+        /**
+         * Return a identifier for a group, unique to the runtime.
+         */
+        uint64_t uniqueId() {
+            return idCounter_++;
+        }
+
         // Some systems have non-monotonic clocks. While we cannot
         // improve the precision, we can make sure that our measures
         // are monotonic nevertheless. We do this by storing the
         // result of the latest call to the clock and making sure
         // that the next timestamp is greater or equal.
         struct MonotonicTimeStamp {
             MonotonicTimeStamp()
               : latestGood_(0)
@@ -1615,16 +1623,21 @@ struct JSRuntime : public JS::shadow::Ru
         Groups groups_;
         friend struct js::PerformanceGroupHolder;
 
         /**
          * `true` if stopwatch monitoring is active, `false` otherwise.
          */
         bool isMonitoringJank_;
         bool isMonitoringCPOW_;
+
+        /**
+         * A counter used to generate unique identifiers for groups.
+         */
+        uint64_t idCounter_;
     };
     Stopwatch stopwatch;
 };
 
 namespace js {
 
 // When entering JIT code, the calling JSContext* is stored into the thread's
 // PerThreadData. This function retrieves the JSContext with the pre-condition
--- a/js/src/vm/SavedStacks.cpp
+++ b/js/src/vm/SavedStacks.cpp
@@ -1216,17 +1216,17 @@ SavedStacksMetadataCallback(JSContext* c
         stacks.allocationSkipCount = std::floor(std::log(random_nextDouble(&stacks.rngState)) /
                                                 std::log(notSamplingProb));
     }
 
     RootedSavedFrame frame(cx);
     if (!stacks.saveCurrentStack(cx, &frame))
         CrashAtUnhandlableOOM("SavedStacksMetadataCallback");
 
-    if (!Debugger::onLogAllocationSite(cx, obj, frame, PRMJ_Now()))
+    if (!Debugger::onLogAllocationSite(cx, obj, frame, JS_GetCurrentEmbedderTime()))
         CrashAtUnhandlableOOM("SavedStacksMetadataCallback");
 
     MOZ_ASSERT_IF(frame, !frame->is<WrapperObject>());
     return frame;
 }
 
 JS_FRIEND_API(JSPrincipals*)
 GetSavedFramePrincipals(HandleObject savedFrame)
--- a/mobile/android/app/ua-update.json.in
+++ b/mobile/android/app/ua-update.json.in
@@ -1,7 +1,7 @@
 #filter slashslash
 // Everything after the first // on a line will be removed by the preproccesor.
 // Send these sites a custom user-agent. Bugs should be included with an entry.
 {
-  // bug 788422, youtube.com
-  "youtube.com": "Android; Tablet;#Android; Mobile;"
+  // bug 1174784, youtube.com
+  "youtube.com": "Android\\s\\d.+?;#Android;"
 }
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -20,17 +20,21 @@ import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
 import org.mozilla.gecko.firstrun.FirstrunPane;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.Engaged;
+import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.gfx.LayerMarginsAnimator;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.health.BrowserHealthRecorder;
 import org.mozilla.gecko.health.BrowserHealthReporter;
 import org.mozilla.gecko.health.HealthRecorder;
 import org.mozilla.gecko.health.SessionInformation;
@@ -47,16 +51,17 @@ import org.mozilla.gecko.menu.GeckoMenuI
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.preferences.ClearOnShutdownPref;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.prompts.Prompt;
 import org.mozilla.gecko.prompts.PromptListItem;
+import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
 import org.mozilla.gecko.sync.setup.SyncAccounts;
 import org.mozilla.gecko.tabqueue.TabQueueHelper;
 import org.mozilla.gecko.tabqueue.TabQueuePrompt;
 import org.mozilla.gecko.tabs.TabHistoryController;
 import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
 import org.mozilla.gecko.tabs.TabHistoryFragment;
 import org.mozilla.gecko.tabs.TabHistoryPage;
@@ -814,16 +819,17 @@ public class BrowserApp extends GeckoApp
             "Menu:Update",
             "LightweightTheme:Update",
             "Search:Keyword",
             "Prompt:ShowTop",
             "Accounts:Exist");
 
         EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this,
             "Accounts:Create",
+            "Accounts:CreateFirefoxAccountFromJSON",
             "CharEncoding:Data",
             "CharEncoding:State",
             "Favicon:CacheLoad",
             "Feedback:LastUrl",
             "Feedback:MaybeLater",
             "Feedback:OpenPlayStore",
             "Menu:Add",
             "Menu:Remove",
@@ -1011,27 +1017,27 @@ public class BrowserApp extends GeckoApp
         // NOTE: OnResume is called twice sometimes when showing on the lock screen.
         final boolean enableGuestSession = GuestSession.shouldUse(this, args);
         final boolean inGuestSession = GeckoProfile.get(this).inGuestMode();
         if (enableGuestSession != inGuestSession) {
             doRestart(getIntent());
             return;
         }
 
-        EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener)this,
+        EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
             "Prompt:ShowTop");
 
         processTabQueue();
     }
 
     @Override
     public void onPause() {
         super.onPause();
         // Register for Prompt:ShowTop so we can foreground this activity even if it's hidden.
-        EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener)this,
+        EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this,
             "Prompt:ShowTop");
     }
 
     @Override
     public void onStart() {
         super.onStart();
 
         // Queue this work so that the first launch of the activity doesn't
@@ -1379,26 +1385,27 @@ public class BrowserApp extends GeckoApp
         if (mReadingListHelper != null) {
             mReadingListHelper.uninit();
             mReadingListHelper = null;
         }
         if (mZoomedView != null) {
             mZoomedView.destroy();
         }
 
-        EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener)this,
+        EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
             "Menu:Open",
             "Menu:Update",
             "LightweightTheme:Update",
             "Search:Keyword",
             "Prompt:ShowTop",
             "Accounts:Exist");
 
-        EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener)this,
+        EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this,
             "Accounts:Create",
+            "Accounts:CreateFirefoxAccountFromJSON",
             "CharEncoding:Data",
             "CharEncoding:State",
             "Favicon:CacheLoad",
             "Feedback:LastUrl",
             "Feedback:MaybeLater",
             "Feedback:OpenPlayStore",
             "Menu:Add",
             "Menu:Remove",
@@ -1657,17 +1664,46 @@ public class BrowserApp extends GeckoApp
         }
 
         mBrowserToolbar.refresh();
     }
 
     @Override
     public void handleMessage(final String event, final NativeJSObject message,
                               final EventCallback callback) {
-        if ("Accounts:Create".equals(event)) {
+        if ("Accounts:CreateFirefoxAccountFromJSON".equals(event)) {
+            AndroidFxAccount fxAccount = null;
+            try {
+                final NativeJSObject json = message.getObject("json");
+                final String email = json.getString("email");
+                final String uid = json.getString("uid");
+                final boolean verified = json.optBoolean("verified", false);
+                final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey"));
+                final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken"));
+                final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken"));
+                // TODO: handle choose what to Sync.
+                State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken);
+                fxAccount = AndroidFxAccount.addAndroidAccount(this,
+                        email,
+                        getProfile().getName(),
+                        FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT,
+                        FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT,
+                        state,
+                        AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP);
+            } catch (Exception e) {
+                Log.w(LOGTAG, "Got exception creating Firefox Account from JSON; ignoring.", e);
+                if (callback == null) {
+                    callback.sendError("Could not create Firefox Account from JSON: " + e.toString());
+                }
+            }
+            if (callback != null) {
+                callback.sendSuccess(fxAccount != null);
+            }
+
+        } else if ("Accounts:Create".equals(event)) {
             // Do exactly the same thing as if you tapped 'Sync' in Settings.
             final Intent intent = new Intent(getContext(), FxAccountGetStartedActivity.class);
             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             final NativeJSObject extras = message.optObject("extras", null);
             if (extras != null) {
                 intent.putExtra("extras", extras.toString());
             }
             getContext().startActivity(intent);
--- a/mobile/android/modules/Accounts.jsm
+++ b/mobile/android/modules/Accounts.jsm
@@ -30,17 +30,17 @@ Cu.import("resource://gre/modules/Promis
  *       console.log("We failed so hard.");
  *     }
  *   );
  */
 let Accounts = Object.freeze({
   _accountsExist: function (kind) {
     return Messaging.sendRequestForResult({
       type: "Accounts:Exist",
-      kind: kind,
+      kind: kind
     }).then(data => data.exists);
   },
 
   firefoxAccountsExist: function () {
     return this._accountsExist("fxa");
   },
 
   syncAccountsExist: function () {
@@ -59,12 +59,28 @@ let Accounts = Object.freeze({
    * Account Getting Started activity in the extras bundle of the
    * activity launch intent, under the key "extras".
    *
    * There is no return value from this method.
    */
   launchSetup: function (extras) {
     Messaging.sendRequest({
       type: "Accounts:Create",
-      extras: extras,
+      extras: extras
     });
   },
+
+  /**
+   * Create a new Android Account corresponding to the given
+   * fxa-content-server "login" JSON datum.  The new account will be
+   * in the "Engaged" state, and will start syncing immediately.
+   *
+   * It is an error if an Android Account already exists.
+   *
+   * Returns a Promise that resolves to a boolean indicating success.
+   */
+  createFirefoxAccountFromJSON: function (json) {
+    return Messaging.sendRequestForResponse({
+      type: "Accounts:CreateFirefoxAccountFromJSON",
+      json: json
+    });
+  }
 });
--- a/mobile/android/tests/browser/robocop/robocop.ini
+++ b/mobile/android/tests/browser/robocop/robocop.ini
@@ -94,18 +94,18 @@ skip-if = android_version == "10" || and
 skip-if = android_version == "10" || android_version == "18"
 [testSessionOOMSave.java]
 # disabled on 2.3, bug 945395; on 4.3, bug 1144888
 skip-if = android_version == "10" || android_version == "18"
 [testSessionOOMRestore.java]
 # disabled on Android 2.3, bug 979600; on 4.3, bug 1145879
 skip-if = android_version == "10" || android_version == "18"
 [testSettingsMenuItems.java]
-# disabled on Android 2.3, bug 979552; on 4.3, bug 1144898
-skip-if = android_version == "10" || android_version == "18"
+# disabled on 4.3, bug 1144898
+skip-if = android_version == "18"
 # [testShareLink.java] # see bug 915897
 [testSystemPages.java]
 # disabled on 2.3, bug 979603; on 4.3, bug 1142811
 skip-if = android_version == "10" || android_version == "18"
 # [testThumbnails.java] # see bug 813107
 [testTitleBar.java]
 # disabled on Android 2.3, bug 979552; on 4.3, bug 1145881
 skip-if = android_version == "10" || android_version == "18"
--- a/mobile/android/tests/browser/robocop/testSettingsMenuItems.java
+++ b/mobile/android/tests/browser/robocop/testSettingsMenuItems.java
@@ -139,17 +139,17 @@ public class testSettingsMenuItems exten
         // Set special handling for Settings items that are conditionally built.
         updateConditionalSettings(settingsMenuItems);
 
         selectMenuItem(mStringHelper.SETTINGS_LABEL);
         mAsserter.ok(mSolo.waitForText(mStringHelper.SETTINGS_LABEL),
                 "The Settings menu did not load", mStringHelper.SETTINGS_LABEL);
 
         // Dismiss the Settings screen and verify that the view is returned to about:home page
-        mActions.sendSpecialKey(Actions.SpecialKey.BACK);
+        mSolo.goBack();
 
         // Waiting for page title to appear to be sure that is fully loaded before opening the menu
         mAsserter.ok(mSolo.waitForText(mStringHelper.TITLE_PLACE_HOLDER), "about:home did not load",
                 mStringHelper.TITLE_PLACE_HOLDER);
         verifyUrl(mStringHelper.ABOUT_HOME_URL);
 
         selectMenuItem(mStringHelper.SETTINGS_LABEL);
         mAsserter.ok(mSolo.waitForText(mStringHelper.SETTINGS_LABEL),
@@ -276,26 +276,24 @@ public class testSettingsMenuItems exten
                                      "The " + itemChoice + " choice is present in section " + section);
                     }
 
                     // Leave submenu after checking.
                     if (waitForText("^Cancel$")) {
                         mSolo.clickOnText("^Cancel$");
                     } else {
                         // Some submenus aren't dialogs, but are nested screens; exit using "back".
-                        mActions.sendSpecialKey(Actions.SpecialKey.BACK);
+                        mSolo.goBack();
                     }
                 }
             }
 
             // Navigate back if on a phone. Tablets shouldn't do this because they use headers and fragments.
             if (mDevice.type.equals("phone")) {
                 int menuDepth = menuPath.length;
                 while (menuDepth > 0) {
-                    mActions.sendSpecialKey(Actions.SpecialKey.BACK);
+                    mSolo.goBack();
                     menuDepth--;
-                    // Sleep so subsequent back actions aren't lost.
-                    mSolo.sleep(150);
                 }
             }
         }
     }
 }
--- a/toolkit/components/aboutperformance/content/aboutPerformance.js
+++ b/toolkit/components/aboutperformance/content/aboutPerformance.js
@@ -115,18 +115,17 @@ let State = {
    * - `deltaT`: the number of milliseconds elapsed since the data
    *   was last displayed.
    */
   update: Task.async(function*() {
     let snapshot = yield this._monitor.promiseSnapshot();
     let newData = new Map();
     let deltas = [];
     for (let componentNew of snapshot.componentsData) {
-      let {name, addonId, isSystem} = componentNew;
-      let key = JSON.stringify({name, addonId, isSystem});
+      let key = componentNew.groupId;
       let componentOld = State._componentsData.get(key);
       deltas.push(componentNew.subtract(componentOld));
       newData.set(key, componentNew);
     }
     State._componentsData = newData;
     let now = window.performance.now();
     let process = snapshot.processData.subtract(State._processData);
     let result = {
--- a/toolkit/components/perfmonitoring/PerformanceStats.jsm
+++ b/toolkit/components/perfmonitoring/PerformanceStats.jsm
@@ -37,17 +37,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
   Ci.nsIFinalizationWitnessService
 );
 
 
 // The topic used to notify that a PerformanceMonitor has been garbage-collected
 // and that we can release/close the probes it holds.
 const FINALIZATION_TOPIC = "performancemonitor-finalize";
 
-const PROPERTIES_META_IMMUTABLE = ["name", "addonId", "isSystem"];
+const PROPERTIES_META_IMMUTABLE = ["name", "addonId", "isSystem", "groupId"];
 const PROPERTIES_META = [...PROPERTIES_META_IMMUTABLE, "windowId", "title"];
 
 /**
  * Access to a low-level performance probe.
  *
  * Each probe is dedicated to some form of performance monitoring.
  * As each probe may have a performance impact, a probe is activated
  * only when a client has requested a PerformanceMonitor for this probe,
--- a/toolkit/components/perfmonitoring/nsIPerformanceStats.idl
+++ b/toolkit/components/perfmonitoring/nsIPerformanceStats.idl
@@ -17,19 +17,27 @@
 
 /**
  * Snapshot of the performance of a component, e.g. an add-on, a web
  * page, system built-ins, or the entire process itself.
  *
  * All values are monotonic and are updated only when
  * `nsIPerformanceStatsService.isStopwatchActive` is `true`.
  */
-[scriptable, uuid(b060d75d-55bc-4c82-a4ff-458fc5ab2a69)]
+[scriptable, uuid(47f8d36d-1d67-43cb-befd-d2f4720ac568)]
 interface nsIPerformanceStats: nsISupports {
   /**
+   * An identifier unique to the component.
+   *
+   * This identifier is somewhat human-readable to aid with debugging,
+   * but clients should not rely upon the format.
+   */
+  readonly attribute AString groupId;
+
+  /**
    * The name of the component:
    * - for the process itself, "<process>";
    * - for platform code, "<platform>";
    * - for an add-on, the identifier of the addon (e.g. "myaddon@foo.bar");
    * - for a webpage, the url of the page.
    */
   readonly attribute AString name;
 
--- a/toolkit/components/perfmonitoring/nsPerformanceStats.cpp
+++ b/toolkit/components/perfmonitoring/nsPerformanceStats.cpp
@@ -17,25 +17,33 @@
 #include "nsJSUtils.h"
 #include "xpcpublic.h"
 #include "jspubtd.h"
 #include "nsIJSRuntimeService.h"
 
 #include "nsIDOMWindow.h"
 #include "nsGlobalWindow.h"
 
+#if defined(XP_WIN)
+#include "Windows.h"
+#else
+#include <unistd.h>
+#endif
+
 class nsPerformanceStats: public nsIPerformanceStats {
 public:
   nsPerformanceStats(const nsAString& aName,
+                     const nsAString& aGroupId,
                      const nsAString& aAddonId,
                      const nsAString& aTitle,
                      const uint64_t aWindowId,
                      const bool aIsSystem,
                      const js::PerformanceData& aPerformanceData)
     : mName(aName)
+    , mGroupId(aGroupId)
     , mAddonId(aAddonId)
     , mTitle(aTitle)
     , mWindowId(aWindowId)
     , mIsSystem(aIsSystem)
     , mPerformanceData(aPerformanceData)
   {
   }
   explicit nsPerformanceStats() {}
@@ -43,17 +51,23 @@ public:
   NS_DECL_ISUPPORTS
 
   /* readonly attribute AString name; */
   NS_IMETHOD GetName(nsAString& aName) override {
     aName.Assign(mName);
     return NS_OK;
   };
 
-  /* readonly attribute AString addon id; */
+  /* readonly attribute AString groupId; */
+  NS_IMETHOD GetGroupId(nsAString& aGroupId) override {
+    aGroupId.Assign(mGroupId);
+    return NS_OK;
+  };
+
+  /* readonly attribute AString addonId; */
   NS_IMETHOD GetAddonId(nsAString& aAddonId) override {
     aAddonId.Assign(mAddonId);
     return NS_OK;
   };
 
   /* readonly attribute uint64_t windowId; */
   NS_IMETHOD GetWindowId(uint64_t *aWindowId) override {
     *aWindowId = mWindowId;
@@ -106,16 +120,17 @@ public:
     for (size_t i = 0; i < length; ++i) {
       (*aNumberOfOccurrences)[i] = mPerformanceData.durations[i];
     }
     return NS_OK;
   };
 
 private:
   nsString mName;
+  nsString mGroupId;
   nsString mAddonId;
   nsString mTitle;
   uint64_t mWindowId;
   bool mIsSystem;
 
   js::PerformanceData mPerformanceData;
 
   virtual ~nsPerformanceStats() {}
@@ -126,57 +141,60 @@ NS_IMPL_ISUPPORTS(nsPerformanceStats, ns
 
 class nsPerformanceSnapshot : public nsIPerformanceSnapshot
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIPERFORMANCESNAPSHOT
 
   nsPerformanceSnapshot();
-  nsresult Init(JSContext*);
+  nsresult Init(JSContext*, uint64_t processId);
 private:
   virtual ~nsPerformanceSnapshot();
 
   /**
    * Import a `js::PerformanceStats` as a `nsIPerformanceStats`.
    *
    * Precondition: this method assumes that we have entered the JSCompartment for which data `c`
    * has been collected.
    *
    * `cx` may be `nullptr` if we are importing the statistics for the
    * entire process, rather than the statistics for a specific set of
    * compartments.
    */
-  already_AddRefed<nsIPerformanceStats> ImportStats(JSContext* cx, const js::PerformanceData& data);
+  already_AddRefed<nsIPerformanceStats> ImportStats(JSContext* cx, const js::PerformanceData& data, uint64_t uid);
 
   /**
    * Callbacks for iterating through the `PerformanceStats` of a runtime.
    */
-  bool IterPerformanceStatsCallbackInternal(JSContext* cx, const js::PerformanceData& stats);
-  static bool IterPerformanceStatsCallback(JSContext* cx, const js::PerformanceData& stats, void* self);
+  bool IterPerformanceStatsCallbackInternal(JSContext* cx, const js::PerformanceData& stats, uint64_t uid);
+  static bool IterPerformanceStatsCallback(JSContext* cx, const js::PerformanceData& stats, uint64_t uid, void* self);
 
   // If the context represents a window, extract the title and window ID.
   // Otherwise, extract "" and 0.
   static void GetWindowData(JSContext*,
                             nsString& title,
                             uint64_t* windowId);
-
+  void GetGroupId(JSContext*,
+                  uint64_t uid,
+                  nsString& groupId);
   // If the context presents an add-on, extract the addon ID.
   // Otherwise, extract "".
   static void GetAddonId(JSContext*,
                          JS::Handle<JSObject*> global,
                          nsAString& addonId);
 
   // Determine whether a context is part of the system principals.
   static bool GetIsSystem(JSContext*,
                           JS::Handle<JSObject*> global);
 
 private:
   nsCOMArray<nsIPerformanceStats> mComponentsData;
   nsCOMPtr<nsIPerformanceStats> mProcessData;
+  uint64_t mProcessId;
 };
 
 NS_IMPL_ISUPPORTS(nsPerformanceSnapshot, nsIPerformanceSnapshot)
 
 nsPerformanceSnapshot::nsPerformanceSnapshot()
 {
 }
 
@@ -228,75 +246,96 @@ nsPerformanceSnapshot::GetAddonId(JSCont
 
   JSAddonId* jsid = AddonIdOfObject(global);
   if (!jsid) {
     return;
   }
   AssignJSFlatString(addonId, (JSFlatString*)jsid);
 }
 
+void
+nsPerformanceSnapshot::GetGroupId(JSContext* cx,
+                                  uint64_t uid,
+                                  nsString& groupId)
+{
+  JSRuntime* rt = JS_GetRuntime(cx);
+  uint64_t runtimeId = reinterpret_cast<uintptr_t>(rt);
+
+  groupId.AssignLiteral("process: ");
+  groupId.AppendInt(mProcessId);
+  groupId.AssignLiteral(", thread: ");
+  groupId.AppendInt(runtimeId);
+  groupId.AppendLiteral(", group: ");
+  groupId.AppendInt(uid);
+}
+
 /* static */ bool
 nsPerformanceSnapshot::GetIsSystem(JSContext*,
                                    JS::Handle<JSObject*> global)
 {
   return nsContentUtils::IsSystemPrincipal(nsContentUtils::ObjectPrincipal(global));
 }
 
 already_AddRefed<nsIPerformanceStats>
-nsPerformanceSnapshot::ImportStats(JSContext* cx, const js::PerformanceData& performance) {
+nsPerformanceSnapshot::ImportStats(JSContext* cx, const js::PerformanceData& performance, const uint64_t uid) {
   JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
 
   if (!global) {
     // While it is possible for a compartment to have no global
     // (e.g. atoms), this compartment is not very interesting for us.
     return nullptr;
   }
 
+  nsString groupId;
+  GetGroupId(cx, uid, groupId);
+
   nsString addonId;
   GetAddonId(cx, global, addonId);
 
   nsString title;
   uint64_t windowId;
   GetWindowData(cx, title, &windowId);
 
   nsAutoString name;
   nsAutoCString cname;
   xpc::GetCurrentCompartmentName(cx, cname);
   name.Assign(NS_ConvertUTF8toUTF16(cname));
 
   bool isSystem = GetIsSystem(cx, global);
 
   nsCOMPtr<nsIPerformanceStats> result =
-    new nsPerformanceStats(name, addonId, title, windowId, isSystem, performance);
+    new nsPerformanceStats(name, groupId, addonId, title, windowId, isSystem, performance);
   return result.forget();
 }
 
 /*static*/ bool
-nsPerformanceSnapshot::IterPerformanceStatsCallback(JSContext* cx, const js::PerformanceData& stats, void* self) {
-  return reinterpret_cast<nsPerformanceSnapshot*>(self)->IterPerformanceStatsCallbackInternal(cx, stats);
+nsPerformanceSnapshot::IterPerformanceStatsCallback(JSContext* cx, const js::PerformanceData& stats, const uint64_t uid, void* self) {
+  return reinterpret_cast<nsPerformanceSnapshot*>(self)->IterPerformanceStatsCallbackInternal(cx, stats, uid);
 }
 
 bool
-nsPerformanceSnapshot::IterPerformanceStatsCallbackInternal(JSContext* cx, const js::PerformanceData& stats) {
-  nsCOMPtr<nsIPerformanceStats> result = ImportStats(cx, stats);
+nsPerformanceSnapshot::IterPerformanceStatsCallbackInternal(JSContext* cx, const js::PerformanceData& stats, const uint64_t uid) {
+  nsCOMPtr<nsIPerformanceStats> result = ImportStats(cx, stats, uid);
   if (result) {
     mComponentsData.AppendElement(result);
   }
 
   return true;
 }
 
 nsresult
-nsPerformanceSnapshot::Init(JSContext* cx) {
+nsPerformanceSnapshot::Init(JSContext* cx, uint64_t processId) {
+  mProcessId = processId;
   js::PerformanceData processStats;
   if (!js::IterPerformanceStats(cx, nsPerformanceSnapshot::IterPerformanceStatsCallback, &processStats, this)) {
     return NS_ERROR_UNEXPECTED;
   }
 
   mProcessData = new nsPerformanceStats(NS_LITERAL_STRING("<process>"), // name
+                                        NS_LITERAL_STRING("<process:?>"), // group id
                                         NS_LITERAL_STRING(""),          // add-on id
                                         NS_LITERAL_STRING(""),          // title
                                         0,                              // window id
                                         true,                           // isSystem
                                         processStats);
   return NS_OK;
 }
 
@@ -321,16 +360,21 @@ NS_IMETHODIMP nsPerformanceSnapshot::Get
   NS_IF_ADDREF(*aProcess = mProcessData);
   return NS_OK;
 }
 
 
 NS_IMPL_ISUPPORTS(nsPerformanceStatsService, nsIPerformanceStatsService)
 
 nsPerformanceStatsService::nsPerformanceStatsService()
+#if defined(XP_WIN)
+  : mProcessId(GetCurrentProcessId())
+#else
+  : mProcessId(getpid())
+#endif
 {
 }
 
 nsPerformanceStatsService::~nsPerformanceStatsService()
 {
 }
 
 //[implicit_jscontext] attribute bool isMonitoringCPOW;
@@ -362,17 +406,17 @@ NS_IMETHODIMP nsPerformanceStatsService:
   }
   return NS_OK;
 }
 
 /* readonly attribute nsIPerformanceSnapshot snapshot; */
 NS_IMETHODIMP nsPerformanceStatsService::GetSnapshot(JSContext* cx, nsIPerformanceSnapshot * *aSnapshot)
 {
   nsRefPtr<nsPerformanceSnapshot> snapshot = new nsPerformanceSnapshot();
-  nsresult rv = snapshot->Init(cx);
+  nsresult rv = snapshot->Init(cx, mProcessId);
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   snapshot.forget(aSnapshot);
   return NS_OK;
 }
 
--- a/toolkit/components/perfmonitoring/nsPerformanceStats.h
+++ b/toolkit/components/perfmonitoring/nsPerformanceStats.h
@@ -14,12 +14,13 @@ public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIPERFORMANCESTATSSERVICE
 
   nsPerformanceStatsService();
 
 private:
   virtual ~nsPerformanceStatsService();
 
+  const uint64_t mProcessId;
 protected:
 };
 
 #endif
--- a/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js
@@ -98,18 +98,18 @@ function monotinicity_tester(source, tes
     processData: null,
     componentsMap: new Map(),
   };
 
   let sanityCheck = function(prev, next) {
     if (prev == null) {
       return;
     }
-    for (let k of ["name", "addonId", "isSystem"]) {
-      SilentAssert.equal(prev[k], next[k], `Sanity check (${testName}): ${k} hasn't changed.`);
+    for (let k of ["groupId", "addonId", "isSystem"]) {
+      SilentAssert.equal(prev[k], next[k], `Sanity check (${testName}): ${k} hasn't changed (${prev.name}).`);
     }
     for (let [probe, k] of [
       ["jank", "totalUserTime"],
       ["jank", "totalSystemTime"],
       ["cpow", "totalCPOWTime"],
       ["ticks", "ticks"]
     ]) {
       SilentAssert.equal(typeof next[probe][k], "number", `Sanity check (${testName}): ${k} is a number.`);
@@ -142,47 +142,35 @@ function monotinicity_tester(source, tes
     // Sanity check on the process data.
     sanityCheck(previous.processData, snapshot.processData);
     SilentAssert.equal(snapshot.processData.isSystem, true);
     SilentAssert.equal(snapshot.processData.name, "<process>");
     SilentAssert.equal(snapshot.processData.addonId, "");
     previous.procesData = snapshot.processData;
 
     // Sanity check on components data.
-    let set = new Set();
     let map = new Map();
     for (let item of snapshot.componentsData) {
-	 for (let [probe, k] of [
+      for (let [probe, k] of [
         ["jank", "totalUserTime"],
         ["jank", "totalSystemTime"],
         ["cpow", "totalCPOWTime"]
       ]) {
         SilentAssert.leq(item[probe][k], snapshot.processData[probe][k],
           `Sanity check (${testName}): component has a lower ${k} than process`);
       }
 
-      let key = `{name: ${item.name}, window: ${item.windowId}, addonId: ${item.addonId}, isSystem: ${item.isSystem}}`;
-      if (set.has(key)) {
-        // There are at least two components with the same name (e.g. about:blank).
-        // Don't perform sanity checks on that name until we know how to make
-        // the difference.
-        map.delete(key);
-        continue;
-      }
+      let key = item.groupId;
+      SilentAssert.ok(!map.has(key), "The component hasn't been seen yet.");
       map.set(key, item);
-      set.add(key);
     }
     for (let [key, item] of map) {
       sanityCheck(previous.componentsMap.get(key), item);
       previous.componentsMap.set(key, item);
     }
-    info(`Deactivating deduplication check (Bug 1150045)`);
-    if (false) {
-      SilentAssert.equal(set.size, snapshot.componentsData.length);
-    }
   });
   let interval = window.setInterval(frameCheck, 300);
   registerCleanupFunction(() => {
     window.clearInterval(interval);
   });
 }
 
 add_task(function* test() {
--- a/toolkit/devtools/server/tests/mochitest/test_memory_allocations_05.html
+++ b/toolkit/devtools/server/tests/mochitest/test_memory_allocations_05.html
@@ -65,17 +65,17 @@ window.onload = function() {
     for (var i = 0; i < 3; i++) {
       var timestamp = response.allocationsTimestamps[allocatorIndices[i]];
       info("timestamp", timestamp);
       ok(timestamp, "We should have a timestamp for the `allocator` allocation.");
 
       if (lastTimestamp) {
         var delta = timestamp - lastTimestamp;
         info("delta since last timestamp", delta);
-        ok(delta >= 1000 /* 1 ms */,
+        ok(delta >= 1 /* ms */,
            "The timestamp should be about 1 ms after the last timestamp.");
       }
 
       lastTimestamp = timestamp;
     }
 
     yield memory.detach();
     destroyServerAndFinish(client);
--- a/toolkit/modules/PageMetadata.jsm
+++ b/toolkit/modules/PageMetadata.jsm
@@ -45,16 +45,31 @@ this.PageMetadata = {
    */
   getData(document) {
     let result = {
       url: this._validateURL(document, document.documentURI),
       title: document.title,
       previews: [],
     };
 
+    // if pushState was used to change the url, most likely all meta data is
+    // invalid. This is the case with several major sites that rely on
+    // pushState. In that case, we'll only return uri and title. If document is
+    // via XHR or something, there is no view or history.
+    if (document.defaultView) {
+      let docshell = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+                                         .getInterface(Ci.nsIWebNavigation)
+                                         .QueryInterface(Ci.nsIDocShell);
+      let shentry = {};
+      if (docshell.getCurrentSHEntry(shentry) &&
+          shentry.value && shentry.value.URIWasModified) {
+        return result;
+      }
+    }
+
     this._getMetaData(document, result);
     this._getLinkData(document, result);
     this._getPageData(document, result);
     result.microdata = this.getMicrodata(document);
 
     return result;
   },
 
--- a/xpcom/build/XPCOMInit.cpp
+++ b/xpcom/build/XPCOMInit.cpp
@@ -467,16 +467,23 @@ private:
 };
 
 NS_IMPL_ISUPPORTS(NesteggReporter, nsIMemoryReporter)
 
 /* static */ template<> Atomic<size_t>
 CountingAllocatorBase<NesteggReporter>::sAmount(0);
 #endif /* MOZ_WEBM */
 
+static double
+TimeSinceProcessCreation()
+{
+  bool ignore;
+  return (TimeStamp::Now() - TimeStamp::ProcessCreation(ignore)).ToMilliseconds();
+}
+
 // Note that on OSX, aBinDirectory will point to .app/Contents/Resources/browser
 EXPORT_XPCOM_API(nsresult)
 NS_InitXPCOM2(nsIServiceManager** aResult,
               nsIFile* aBinDirectory,
               nsIDirectoryServiceProvider* aAppFileLocationProvider)
 {
   static bool sInitialized = false;
   if (sInitialized) {
@@ -484,16 +491,18 @@ NS_InitXPCOM2(nsIServiceManager** aResul
   }
 
   sInitialized = true;
 
   mozPoisonValueInit();
 
   NS_LogInit();
 
+  JS_SetCurrentEmbedderTimeFunction(TimeSinceProcessCreation);
+
   char aLocal;
   profiler_init(&aLocal);
   nsresult rv = NS_OK;
 
   // We are not shutting down
   gXPCOMShuttingDown = false;
 
   // Initialize the available memory tracker before other threads have had a