merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 18 Jun 2015 15:08:51 +0200
changeset 280205 1fb3ebf68777aa480667874eb7b0bcc39e63ebcb
parent 280183 66b2f328a16fcf76c09dfc2179c3f811237042d8 (current diff)
parent 280204 844efb38a131a2a7320c8f837413a69302c9c4fb (diff)
child 280294 efe86609e77669a4780509a38b9d0ed04525c179
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-beta@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to 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