merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Mon, 28 Sep 2015 14:10:50 +0200
changeset 264718 3323f5c35ed310823c2febe01bbe748c62055634
parent 264680 6b022a1c49b64fece41a83890561faf6b2615baa (current diff)
parent 264717 7e9fe56d9e96fc4a51c55aa50896506cb2c28bc6 (diff)
child 264719 031db40e2b558c7e4dd0b4c565db4a992c1627c8
push id65707
push usercbook@mozilla.com
push dateMon, 28 Sep 2015 12:18:34 +0000
treeherdermozilla-inbound@2c0e60a8f736 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone44.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
devtools/client/sourceeditor/codemirror/codemirror.css
devtools/client/sourceeditor/codemirror/codemirror.js
devtools/client/sourceeditor/codemirror/comment/comment.js
devtools/client/sourceeditor/codemirror/comment/continuecomment.js
devtools/client/sourceeditor/codemirror/dialog/dialog.css
devtools/client/sourceeditor/codemirror/dialog/dialog.js
devtools/client/sourceeditor/codemirror/edit/closebrackets.js
devtools/client/sourceeditor/codemirror/edit/closetag.js
devtools/client/sourceeditor/codemirror/edit/continuelist.js
devtools/client/sourceeditor/codemirror/edit/matchbrackets.js
devtools/client/sourceeditor/codemirror/edit/matchtags.js
devtools/client/sourceeditor/codemirror/edit/trailingspace.js
devtools/client/sourceeditor/codemirror/fold/brace-fold.js
devtools/client/sourceeditor/codemirror/fold/comment-fold.js
devtools/client/sourceeditor/codemirror/fold/foldcode.js
devtools/client/sourceeditor/codemirror/fold/foldgutter.css
devtools/client/sourceeditor/codemirror/fold/foldgutter.js
devtools/client/sourceeditor/codemirror/fold/indent-fold.js
devtools/client/sourceeditor/codemirror/fold/markdown-fold.js
devtools/client/sourceeditor/codemirror/fold/xml-fold.js
devtools/client/sourceeditor/codemirror/hint/show-hint.js
devtools/client/sourceeditor/codemirror/search/match-highlighter.js
devtools/client/sourceeditor/codemirror/search/search.js
devtools/client/sourceeditor/codemirror/search/searchcursor.js
devtools/client/sourceeditor/codemirror/selection/active-line.js
devtools/client/sourceeditor/codemirror/selection/mark-selection.js
devtools/client/sourceeditor/codemirror/tern/tern.css
devtools/client/sourceeditor/codemirror/tern/tern.js
devtools/client/sourceeditor/test/cm_comment_test.js
devtools/client/sourceeditor/test/cm_doc_test.js
devtools/client/sourceeditor/test/cm_driver.js
devtools/client/sourceeditor/test/cm_emacs_test.js
devtools/client/sourceeditor/test/cm_mode_javascript_test.js
devtools/client/sourceeditor/test/cm_mode_test.css
devtools/client/sourceeditor/test/cm_mode_test.js
devtools/client/sourceeditor/test/cm_multi_test.js
devtools/client/sourceeditor/test/cm_search_test.js
devtools/client/sourceeditor/test/cm_sublime_test.js
devtools/client/sourceeditor/test/cm_test.js
devtools/client/sourceeditor/test/cm_vim_test.js
devtools/client/sourceeditor/test/codemirror.html
devtools/client/sourceeditor/test/vimemacs.html
toolkit/locales/en-US/chrome/alerts/notificationNames.properties
--- a/browser/base/content/browser-trackingprotection.js
+++ b/browser/base/content/browser-trackingprotection.js
@@ -25,16 +25,17 @@ var TrackingProtection = {
     Services.prefs.addObserver(this.PREF_ENABLED_IN_PRIVATE_WINDOWS, this, false);
 
     this.activeTooltipText =
       gNavigatorBundle.getString("trackingProtection.icon.activeTooltip");
     this.disabledTooltipText =
       gNavigatorBundle.getString("trackingProtection.icon.disabledTooltip");
 
     this.enabledHistogramAdd(this.enabledGlobally);
+    this.disabledPBMHistogramAdd(!this.enabledInPrivateWindows);
   },
 
   uninit() {
     Services.prefs.removeObserver(this.PREF_ENABLED_GLOBALLY, this);
     Services.prefs.removeObserver(this.PREF_ENABLED_IN_PRIVATE_WINDOWS, this);
   },
 
   observe() {
@@ -57,16 +58,23 @@ var TrackingProtection = {
 
   enabledHistogramAdd(value) {
     if (PrivateBrowsingUtils.isWindowPrivate(window)) {
       return;
     }
     Services.telemetry.getHistogramById("TRACKING_PROTECTION_ENABLED").add(value);
   },
 
+  disabledPBMHistogramAdd(value) {
+    if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+      return;
+    }
+    Services.telemetry.getHistogramById("TRACKING_PROTECTION_PBM_DISABLED").add(value);
+  },
+
   eventsHistogramAdd(value) {
     if (PrivateBrowsingUtils.isWindowPrivate(window)) {
       return;
     }
     Services.telemetry.getHistogramById("TRACKING_PROTECTION_EVENTS").add(value);
   },
 
   shieldHistogramAdd(value) {
--- a/browser/base/content/newtab/grid.js
+++ b/browser/base/content/newtab/grid.js
@@ -47,17 +47,17 @@ var gGrid = {
    * Initializes the grid.
    * @param aSelector The query selector of the grid.
    */
   init: function Grid_init() {
     this._node = document.getElementById("newtab-grid");
     this._createSiteFragment();
 
     gLinks.populateCache(() => {
-      this.refresh();
+      this._refreshGrid();
       this._ready = true;
 
       // If fetching links took longer than loading the page itself then
       // we need to resize the grid as that was blocked until now.
       // We also want to resize now if the page was already loaded when
       // initializing the grid (the user toggled the page).
       this._resizeGrid();
 
@@ -104,19 +104,30 @@ var gGrid = {
   /**
    * Unlocks the grid to allow all pointer events.
    */
   unlock: function Grid_unlock() {
     this.node.removeAttribute("locked");
   },
 
   /**
+   * Renders and resizes the gird. _resizeGrid() call is needed to ensure
+   * that scrollbar disappears when the bottom row becomes empty following
+   * the block action, or tile display is turmed off via cog menu
+   */
+
+  refresh() {
+    this._refreshGrid();
+    this._resizeGrid();
+  },
+
+  /**
    * Renders the grid, including cells and sites.
    */
-  refresh() {
+  _refreshGrid() {
     let cell = document.createElementNS(HTML_NAMESPACE, "div");
     cell.classList.add("newtab-cell");
 
     // Creates all the cells up to the maximum
     let fragment = document.createDocumentFragment();
     for (let i = 0; i < gGridPrefs.gridColumns * gGridPrefs.gridRows; i++) {
       fragment.appendChild(cell.cloneNode(true));
     }
--- a/browser/base/content/newtab/sites.js
+++ b/browser/base/content/newtab/sites.js
@@ -45,23 +45,29 @@ Site.prototype = {
   get cell() {
     let parentNode = this.node.parentNode;
     return parentNode && parentNode._newtabCell;
   },
 
   /**
    * Pins the site on its current or a given index.
    * @param aIndex The pinned index (optional).
+   * @return true if link changed type after pin
    */
   pin: function Site_pin(aIndex) {
     if (typeof aIndex == "undefined")
       aIndex = this.cell.index;
 
     this._updateAttributes(true);
-    gPinnedLinks.pin(this._link, aIndex);
+    let changed = gPinnedLinks.pin(this._link, aIndex);
+    if (changed) {
+      // render site again to remove suggested/sponsored tags
+      this._render();
+    }
+    return changed;
   },
 
   /**
    * Unpins the site and calls the given callback when done.
    */
   unpin: function Site_unpin() {
     if (this.isPinned()) {
       this._updateAttributes(false);
@@ -175,16 +181,20 @@ Site.prototype = {
     this.node.setAttribute("type", this.link.type);
 
     let titleNode = this._querySelector(".newtab-title");
     titleNode.textContent = title;
     if (this.link.titleBgColor) {
       titleNode.style.backgroundColor = this.link.titleBgColor;
     }
 
+    // remove "suggested" attribute to avoid showing "suggested" tag
+    // after site was pinned or dropped
+    this.node.removeAttribute("suggested");
+
     if (this.link.targetedSite) {
       if (this.node.getAttribute("type") != "sponsored") {
         this._querySelector(".newtab-sponsored").textContent =
           newTabString("suggested.tag");
       }
 
       this.node.setAttribute("suggested", true);
       let explanation = this._getSuggestedTileExplanation();
@@ -365,17 +375,20 @@ Site.prototype = {
         this._toggleLegalText(".newtab-sponsored", ".sponsored-explain");
         action = "sponsored";
       }
       else if (pinned && target.classList.contains("newtab-control-pin")) {
         this.unpin();
         action = "unpin";
       }
       else if (!pinned && target.classList.contains("newtab-control-pin")) {
-        this.pin();
+        if (this.pin()) {
+          // suggested link has changed - update rest of the pages
+          gAllPages.update(gPage);
+        }
         action = "pin";
       }
     }
 
     // Report all link click actions
     if (action) {
       DirectoryLinksProvider.reportSitesAction(gGrid.sites, action, tileIndex);
     }
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -459,21 +459,21 @@ html[dir="rtl"] .settings-menu.dropdown-
 }
 
 .feedback-button-container {
   flex: 0 1 auto;
   margin: 30px;
   align-self: center;
 }
 
-.feedback-button-container button {
+.feedback-button-container > button {
   margin: 0 30px;
   padding: .5em 2em;
   border: none;
-  background: #4E92DF;
+  background: #00A9DC;
   color: #fff;
   cursor: pointer;
 }
 
 /*
  * For any audio-only streams, we want to display our own background
  */
 .avatar {
--- a/browser/components/loop/content/shared/img/helloicon.svg
+++ b/browser/components/loop/content/shared/img/helloicon.svg
@@ -1,1 +1,1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill-rule="evenodd" clip-rule="evenodd" fill="#4E92DF" d="M32 0C14.3 0 0 12.6 0 28.1c0 7.7 3.6 14.7 9.3 19.8-1 3.5-3 8.3-6.9 12.9.7 1.2 11.7-3 19.4-6.1 3.2.9 6.6 1.5 10.2 1.5 17.7 0 32-12.6 32-28.1S49.7 0 32 0zm9.6 16.9c2.3 0 4.2 1.9 4.2 4.2 0 2.3-1.9 4.2-4.2 4.2-2.3 0-4.2-1.9-4.2-4.2-.1-2.3 1.8-4.2 4.2-4.2zm-19.3 0c2.3 0 4.2 1.9 4.2 4.2 0 2.3-1.9 4.2-4.2 4.2-2.3 0-4.2-1.9-4.2-4.2-.1-2.3 1.8-4.2 4.2-4.2zM32 47.7h-.1-.1c-8.6 0-18.1-5.5-20.3-14.9 5.8 2.7 13.8 3.8 20.4 3.8 6.6 0 14.7-1.2 20.4-3.8-2.2 9.3-11.7 14.9-20.3 14.9z"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill-rule="evenodd" clip-rule="evenodd" fill="#00A9DC" d="M32 0C14.3 0 0 12.6 0 28.1c0 7.7 3.6 14.7 9.3 19.8-1 3.5-3 8.3-6.9 12.9.7 1.2 11.7-3 19.4-6.1 3.2.9 6.6 1.5 10.2 1.5 17.7 0 32-12.6 32-28.1S49.7 0 32 0zm9.6 16.9c2.3 0 4.2 1.9 4.2 4.2 0 2.3-1.9 4.2-4.2 4.2-2.3 0-4.2-1.9-4.2-4.2-.1-2.3 1.8-4.2 4.2-4.2zm-19.3 0c2.3 0 4.2 1.9 4.2 4.2 0 2.3-1.9 4.2-4.2 4.2-2.3 0-4.2-1.9-4.2-4.2-.1-2.3 1.8-4.2 4.2-4.2zM32 47.7h-.2c-8.6 0-18.1-5.5-20.3-14.9 5.8 2.7 13.8 3.8 20.4 3.8 6.6 0 14.7-1.2 20.4-3.8-2.2 9.3-11.7 14.9-20.3 14.9z"/></svg>
\ No newline at end of file
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -33,23 +33,19 @@ loop.OTSdkDriver = (function() {
       if (!options.mozLoop) {
         throw new Error("Missing option mozLoop");
       }
       this.mozLoop = options.mozLoop;
     }
 
     this.connections = {};
 
-    // Metrics object to keep track of the number of connections we have
+    // Setup the metrics object to keep track of the number of connections we have
     // and the amount of streams.
-    this._metrics = {
-      connections: 0,
-      sendStreams: 0,
-      recvStreams: 0
-    };
+    this._resetMetrics();
 
     this.dispatcher.register(this, [
       "setupStreamElements",
       "setMute"
     ]);
 
     // Set loop.debug.twoWayMediaTelemetry to true in the browser
     // by changing the hidden pref loop.debug.twoWayMediaTelemetry using
@@ -104,16 +100,27 @@ loop.OTSdkDriver = (function() {
           // always send on the publisher channel, and receive on the subscriber
           // channel.
           text: {}
         }
       };
     },
 
     /**
+     * Resets the metrics for the driver.
+     */
+    _resetMetrics: function() {
+      this._metrics = {
+        connections: 0,
+        sendStreams: 0,
+        recvStreams: 0
+      };
+    },
+
+    /**
      * Handles the setupStreamElements action. Saves the required data and
      * kicks off the initialising of the publisher.
      *
      * @param {sharedActions.SetupStreamElements} actionData The data associated
      *   with the action. See action.js.
      */
     setupStreamElements: function(actionData) {
       this.publisherConfig = actionData.publisherConfig;
@@ -288,16 +295,19 @@ loop.OTSdkDriver = (function() {
       }
       if (this.publisher) {
         this.publisher.off("accessAllowed accessDenied accessDialogOpened " +
                            "streamCreated streamDestroyed");
         this.publisher.destroy();
         delete this.publisher;
       }
 
+      // Now reset the metrics as well.
+      this._resetMetrics();
+
       this._noteConnectionLengthIfNeeded(this._getTwoWayMediaStartTime(), performance.now());
 
       // Also, tidy these variables ready for next time.
       delete this._sessionConnected;
       delete this._publisherReady;
       delete this._publishedLocalStream;
       delete this._subscribedRemoteStream;
       delete this._mockPublisherEl;
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -108,16 +108,29 @@ describe("loop.OTSdkDriver", function ()
       }).to.Throw(/dispatcher/);
     });
 
     it("should throw an error if the sdk is missing", function() {
       expect(function() {
         new loop.OTSdkDriver({dispatcher: dispatcher});
       }).to.Throw(/sdk/);
     });
+
+    it("should set the metrics to zero", function() {
+      driver = new loop.OTSdkDriver({
+        dispatcher: dispatcher,
+        sdk: sdk
+      });
+
+      expect(driver._metrics).eql({
+        connections: 0,
+        sendStreams: 0,
+        recvStreams: 0
+      });
+    });
   });
 
   describe("#setupStreamElements", function() {
     it("should call initPublisher", function() {
       driver.setupStreamElements(new sharedActions.SetupStreamElements({
         publisherConfig: publisherConfig
       }));
 
@@ -469,16 +482,32 @@ describe("loop.OTSdkDriver", function ()
         });
       });
 
       driver.disconnectSession();
 
       expect(subscribedEvents).eql([]);
     });
 
+    it("should reset the metrics to zero", function() {
+      driver._metrics = {
+        connections: 1,
+        sendStreams: 2,
+        recvStreams: 3
+      };
+
+      driver.disconnectSession();
+
+      expect(driver._metrics).eql({
+        connections: 0,
+        sendStreams: 0,
+        recvStreams: 0
+      });
+    });
+
     it("should dispatch a DataChannelsAvailable action with available = false", function() {
       driver.disconnectSession();
 
       sinon.assert.called(dispatcher.dispatch);
       sinon.assert.calledWithExactly(dispatcher.dispatch,
         new sharedActions.DataChannelsAvailable({
           available: false
         }));
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -22,16 +22,17 @@
   var ContactDropdown = loop.contacts.ContactDropdown;
   var ContactDetail = loop.contacts.ContactDetail;
   var GettingStartedView = loop.panel.GettingStartedView;
   // 1.2. Conversation Window
   var AcceptCallView = loop.conversationViews.AcceptCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var OngoingConversationView = loop.conversationViews.OngoingConversationView;
   var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
+  var DesktopRoomEditContextView = loop.roomViews.DesktopRoomEditContextView;
   var RoomFailureView = loop.roomViews.RoomFailureView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var StandaloneRoomView      = loop.standaloneRoomViews.StandaloneRoomView;
@@ -1390,16 +1391,33 @@
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
                   onCallTerminated: function(){}, 
                   roomState: ROOM_STATES.INIT, 
                   roomStore: invitationRoomStore})
               )
             ), 
 
+            React.createElement(FramedExample, {height: 278.6, 
+                           onContentsRendered: invitationRoomStore.activeRoomStore.forcedUpdate, 
+                           summary: "Desktop room Edit Context w/Error", 
+                           width: 298}, 
+              React.createElement("div", {className: "fx-embedded room-invitation-overlay"}, 
+                React.createElement(DesktopRoomEditContextView, {
+                  dispatcher: dispatcher, 
+                  error: {}, 
+                  mozLoop: navigator.mozLoop, 
+                  onClose: function(){}, 
+                  roomData: {}, 
+                  savingContext: false, 
+                  show: true}
+                  )
+              )
+            ), 
+
             React.createElement(FramedExample, {dashed: true, 
                            height: 394, 
                            onContentsRendered: desktopRoomStoreLoading.activeRoomStore.forcedUpdate, 
                            summary: "Desktop room conversation (loading)", 
                            width: 298}, 
               /* Hide scrollbars here. Rotating loading div overflows and causes
                scrollbars to appear */
               React.createElement("div", {className: "fx-embedded overflow-hidden"}, 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -22,16 +22,17 @@
   var ContactDropdown = loop.contacts.ContactDropdown;
   var ContactDetail = loop.contacts.ContactDetail;
   var GettingStartedView = loop.panel.GettingStartedView;
   // 1.2. Conversation Window
   var AcceptCallView = loop.conversationViews.AcceptCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var OngoingConversationView = loop.conversationViews.OngoingConversationView;
   var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
+  var DesktopRoomEditContextView = loop.roomViews.DesktopRoomEditContextView;
   var RoomFailureView = loop.roomViews.RoomFailureView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var StandaloneRoomView      = loop.standaloneRoomViews.StandaloneRoomView;
@@ -1390,16 +1391,33 @@
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
                   onCallTerminated={function(){}}
                   roomState={ROOM_STATES.INIT}
                   roomStore={invitationRoomStore} />
               </div>
             </FramedExample>
 
+            <FramedExample height={278.6}
+                           onContentsRendered={invitationRoomStore.activeRoomStore.forcedUpdate}
+                           summary="Desktop room Edit Context w/Error"
+                           width={298}>
+              <div className="fx-embedded room-invitation-overlay">
+                <DesktopRoomEditContextView
+                  dispatcher={dispatcher}
+                  error={{}}
+                  mozLoop={navigator.mozLoop}
+                  onClose={function(){}}
+                  roomData={{}}
+                  savingContext={false}
+                  show={true}
+                  />
+              </div>
+            </FramedExample>
+
             <FramedExample dashed={true}
                            height={394}
                            onContentsRendered={desktopRoomStoreLoading.activeRoomStore.forcedUpdate}
                            summary="Desktop room conversation (loading)"
                            width={298}>
               {/* Hide scrollbars here. Rotating loading div overflows and causes
                scrollbars to appear */}
               <div className="fx-embedded overflow-hidden">
--- a/browser/components/uitour/test/browser_UITour_heartbeat.js
+++ b/browser/components/uitour/test/browser_UITour_heartbeat.js
@@ -4,16 +4,17 @@
 "use strict";
 
 var gTestTab;
 var gContentAPI;
 var gContentWindow;
 
 function test() {
   UITourTest();
+  requestLongerTimeout(2);
 }
 
 function getHeartbeatNotification(aId, aChromeWindow = window) {
   let notificationBox = aChromeWindow.document.getElementById("high-priority-global-notificationbox");
   // UITour.jsm prefixes the notification box ID with "heartbeat-" to prevent collisions.
   return notificationBox.getNotificationWithValue("heartbeat-" + aId);
 }
 
--- a/browser/components/uitour/test/head.js
+++ b/browser/components/uitour/test/head.js
@@ -246,38 +246,41 @@ function UITourTest() {
       gBrowser.removeTab(gTestTab);
     delete window.gTestTab;
     Services.prefs.clearUserPref("browser.uitour.enabled", true);
     Services.perms.remove(testHttpsUri, "uitour");
     Services.perms.remove(testHttpUri, "uitour");
   });
 
   function done() {
+    info("== Done test, doing shared checks before teardown ==");
     executeSoon(() => {
       if (gTestTab)
         gBrowser.removeTab(gTestTab);
       gTestTab = null;
 
       let highlight = document.getElementById("UITourHighlightContainer");
       is_element_hidden(highlight, "Highlight should be closed/hidden after UITour tab is closed");
 
       let tooltip = document.getElementById("UITourTooltip");
       is_element_hidden(tooltip, "Tooltip should be closed/hidden after UITour tab is closed");
 
       ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up");
       ok(!PanelUI.panel.hasAttribute("panelopen"), "The panel shouldn't have @panelopen");
       isnot(PanelUI.panel.state, "open", "The panel shouldn't be open");
       is(document.getElementById("PanelUI-menu-button").hasAttribute("open"), false, "Menu button should know that the menu is closed");
 
+      info("Done shared checks");
       executeSoon(nextTest);
     });
   }
 
   function nextTest() {
     if (tests.length == 0) {
+      info("finished tests in this file");
       finish();
       return;
     }
     let test = tests.shift();
     info("Starting " + test.name);
     waitForFocus(function() {
       loadUITourTestPage(function() {
         test(done);
--- a/browser/locales/en-US/chrome/browser/devtools/performance.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/performance.dtd
@@ -30,17 +30,22 @@
   -  when running on a build that can run multiprocess Firefox, but just is not enabled. -->
 <!ENTITY performanceUI.disabledRealTime.disabledE10S "Enable multiprocess Firefox in preferences for rendering recording data in realtime.">
 
 <!-- LOCALIZATION NOTE (performanceUI.bufferStatusFull): This string
   -  is displayed when the profiler's circular buffer has started to overlap. -->
 <!ENTITY performanceUI.bufferStatusFull "The buffer is full. Older samples are now being overwritten.">
 
 <!-- LOCALIZATION NOTE (performanceUI.loadingNotice): This is the label shown
-  -  in the call list view while loading a profile. -->
+  -  in the details view while the profiler is unavailable, for example, while
+  -  in Private Browsing mode. -->
+<!ENTITY performanceUI.unavailableNoticePB "Recording a profile is currently unavailable. Please close all private browsing windows and try again.">
+
+<!-- LOCALIZATION NOTE (performanceUI.loadingNotice): This is the label shown
+  -  in the details view while loading a profile. -->
 <!ENTITY performanceUI.loadingNotice "Loading…">
 
 <!-- LOCALIZATION NOTE (performanceUI.recordButton): This string is displayed
   -  on a button that starts a new profile. -->
 <!ENTITY performanceUI.recordButton.tooltip "Toggle the recording state of a performance recording.">
 
 <!-- LOCALIZATION NOTE (performanceUI.importButton): This string is displayed
   -  on a button that opens a dialog to import a saved profile data file. -->
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -432,30 +432,28 @@
 .tabbrowser-tab::before {
   width: 1px;
   -moz-margin-start: -1px;
   background-image: linear-gradient(transparent 5px,
                                     currentColor 5px,
                                     currentColor calc(100% - 4px),
                                     transparent calc(100% - 4px));
   opacity: 0.2;
-  content: "";
-  display: -moz-box;
-  visibility: hidden;
 }
 
 #TabsToolbar[brighttext] > #tabbrowser-tabs > .tabbrowser-tab::before,
 #TabsToolbar[brighttext] > #tabbrowser-tabs > .tabbrowser-tab::after {
   opacity: 0.4;
 }
 
 /* Also show separators beside the selected tab when dragging it. */
 #tabbrowser-tabs[movingtab] > .tabbrowser-tab[beforeselected]:not([last-visible-tab])::after,
 .tabbrowser-tab:not([visuallyselected]):not([afterselected-visible]):not([afterhovered]):not([first-visible-tab]):not(:hover)::before,
 #tabbrowser-tabs:not([overflow]) > .tabbrowser-tab[last-visible-tab]:not([visuallyselected]):not([beforehovered]):not(:hover)::after {
-  visibility: visible;
+  content: "";
+  display: -moz-box;
 }
 
 /* New tab button */
 
 .tabs-newtab-button {
   width: calc(36px + @tabCurveWidth@);
 }
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -312,20 +312,38 @@ TabTarget.prototype = {
   get tab() {
     return this._tab;
   },
 
   get form() {
     return this._form;
   },
 
+  // Get a promise of the root form returned by a listTabs request. This promise
+  // is cached.
   get root() {
+    if (!this._root) {
+      this._root = this._getRoot();
+    }
     return this._root;
   },
 
+  _getRoot: function () {
+    return new Promise((resolve, reject) => {
+      this.client.listTabs(response => {
+        if (response.error) {
+          reject(new Error(response.error + ": " + response.message));
+          return;
+        }
+
+        resolve(response);
+      });
+    });
+  },
+
   get client() {
     return this._client;
   },
 
   // Tells us if we are debugging content document
   // or if we are debugging chrome stuff.
   // Allows to controls which features are available against
   // a chrome or a content document.
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -1,19 +1,28 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // This shared-head.js file is used for multiple directories in devtools.
+
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-const {gDevTools} = Cu.import("resource:///modules/devtools/client/framework/gDevTools.jsm", {});
-const {console} = Cu.import("resource://gre/modules/devtools/shared/Console.jsm", {});
-const {ScratchpadManager} = Cu.import("resource:///modules/devtools/client/scratchpad/scratchpad-manager.jsm", {});
-const {require} = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
+
+function scopedCuImport(path) {
+  const scope = {};
+  Cu.import(path, scope);
+  return scope;
+}
+
+const {Services} = scopedCuImport("resource://gre/modules/Services.jsm");
+const {gDevTools} = scopedCuImport("resource:///modules/devtools/client/framework/gDevTools.jsm");
+const {console} = scopedCuImport("resource://gre/modules/devtools/shared/Console.jsm");
+const {ScratchpadManager} = scopedCuImport("resource:///modules/devtools/client/scratchpad/scratchpad-manager.jsm");
+const {require} = scopedCuImport("resource://gre/modules/devtools/shared/Loader.jsm");
+
 const {TargetFactory} = require("devtools/client/framework/target");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const promise = require("promise");
 
 const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
 const CHROME_URL_ROOT = TEST_DIR + "/";
 const URL_ROOT = CHROME_URL_ROOT.replace("chrome://mochitests/content/", "http://example.com/");
 
@@ -64,16 +73,35 @@ function addTab(url) {
     gBrowser.selectedBrowser.removeEventListener("load", onload, true);
     info("URL '" + url + "' loading complete");
     def.resolve(tab);
   }, true);
 
   return def.promise;
 }
 
+/**
+ * Remove the given tab.
+ * @param {Object} tab The tab to be removed.
+ * @return Promise<undefined> resolved when the tab is successfully removed.
+ */
+function removeTab(tab) {
+  info("Removing tab.");
+  return new Promise(resolve => {
+    let tabContainer = gBrowser.tabContainer;
+    tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+      tabContainer.removeEventListener("TabClose", onClose, false);
+      info("Tab removed and finished closing.");
+      resolve();
+    }, false);
+
+    gBrowser.removeTab(tab);
+  });
+}
+
 function synthesizeKeyFromKeyTag(aKeyId, document) {
   let key = document.getElementById(aKeyId);
   isnot(key, null, "Successfully retrieved the <key> node");
 
   let modifiersAttr = key.getAttribute("modifiers");
 
   let name = null;
 
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -34,42 +34,42 @@ devtools.jar:
     content/layoutview/view.js (layoutview/view.js)
     content/layoutview/view.xhtml (layoutview/view.xhtml)
     content/fontinspector/font-inspector.js (fontinspector/font-inspector.js)
     content/fontinspector/font-inspector.xhtml (fontinspector/font-inspector.xhtml)
     content/fontinspector/font-inspector.css (fontinspector/font-inspector.css)
     content/animationinspector/animation-controller.js (animationinspector/animation-controller.js)
     content/animationinspector/animation-panel.js (animationinspector/animation-panel.js)
     content/animationinspector/animation-inspector.xhtml (animationinspector/animation-inspector.xhtml)
-    content/sourceeditor/codemirror/codemirror.js (sourceeditor/codemirror/codemirror.js)
-    content/sourceeditor/codemirror/codemirror.css (sourceeditor/codemirror/codemirror.css)
+    content/sourceeditor/codemirror/comment/comment.js (sourceeditor/codemirror/addon/comment/comment.js)
+    content/sourceeditor/codemirror/edit/trailingspace.js (sourceeditor/codemirror/addon/edit/trailingspace.js)
+    content/sourceeditor/codemirror/edit/matchbrackets.js (sourceeditor/codemirror/addon/edit/matchbrackets.js)
+    content/sourceeditor/codemirror/edit/closebrackets.js (sourceeditor/codemirror/addon/edit/closebrackets.js)
+    content/sourceeditor/codemirror/dialog/dialog.js (sourceeditor/codemirror/addon/dialog/dialog.js)
+    content/sourceeditor/codemirror/dialog/dialog.css (sourceeditor/codemirror/addon/dialog/dialog.css)
+    content/sourceeditor/codemirror/fold/foldcode.js (sourceeditor/codemirror/addon/fold/foldcode.js)
+    content/sourceeditor/codemirror/fold/brace-fold.js (sourceeditor/codemirror/addon/fold/brace-fold.js)
+    content/sourceeditor/codemirror/fold/comment-fold.js (sourceeditor/codemirror/addon/fold/comment-fold.js)
+    content/sourceeditor/codemirror/fold/xml-fold.js (sourceeditor/codemirror/addon/fold/xml-fold.js)
+    content/sourceeditor/codemirror/fold/foldgutter.js (sourceeditor/codemirror/addon/fold/foldgutter.js)
+    content/sourceeditor/codemirror/hint/show-hint.js (sourceeditor/codemirror/addon/hint/show-hint.js)
+    content/sourceeditor/codemirror/search/search.js (sourceeditor/codemirror/addon/search/search.js)
+    content/sourceeditor/codemirror/search/searchcursor.js (sourceeditor/codemirror/addon/search/searchcursor.js)
+    content/sourceeditor/codemirror/selection/active-line.js (sourceeditor/codemirror/addon/selection/active-line.js)
+    content/sourceeditor/codemirror/tern/tern.js (sourceeditor/codemirror/addon/tern/tern.js)
+    content/sourceeditor/codemirror/codemirror.js (sourceeditor/codemirror/lib/codemirror.js)
+    content/sourceeditor/codemirror/codemirror.css (sourceeditor/codemirror/lib/codemirror.css)
     content/sourceeditor/codemirror/mode/javascript.js (sourceeditor/codemirror/mode/javascript.js)
     content/sourceeditor/codemirror/mode/xml.js (sourceeditor/codemirror/mode/xml.js)
     content/sourceeditor/codemirror/mode/css.js (sourceeditor/codemirror/mode/css.js)
     content/sourceeditor/codemirror/mode/htmlmixed.js (sourceeditor/codemirror/mode/htmlmixed.js)
     content/sourceeditor/codemirror/mode/clike.js (sourceeditor/codemirror/mode/clike.js)
-    content/sourceeditor/codemirror/selection/active-line.js (sourceeditor/codemirror/selection/active-line.js)
-    content/sourceeditor/codemirror/edit/trailingspace.js (sourceeditor/codemirror/edit/trailingspace.js)
-    content/sourceeditor/codemirror/edit/matchbrackets.js (sourceeditor/codemirror/edit/matchbrackets.js)
-    content/sourceeditor/codemirror/edit/closebrackets.js (sourceeditor/codemirror/edit/closebrackets.js)
-    content/sourceeditor/codemirror/comment/comment.js (sourceeditor/codemirror/comment/comment.js)
-    content/sourceeditor/codemirror/search/searchcursor.js (sourceeditor/codemirror/search/searchcursor.js)
-    content/sourceeditor/codemirror/search/search.js (sourceeditor/codemirror/search/search.js)
-    content/sourceeditor/codemirror/dialog/dialog.js (sourceeditor/codemirror/dialog/dialog.js)
-    content/sourceeditor/codemirror/dialog/dialog.css (sourceeditor/codemirror/dialog/dialog.css)
     content/sourceeditor/codemirror/keymap/emacs.js (sourceeditor/codemirror/keymap/emacs.js)
     content/sourceeditor/codemirror/keymap/sublime.js (sourceeditor/codemirror/keymap/sublime.js)
     content/sourceeditor/codemirror/keymap/vim.js (sourceeditor/codemirror/keymap/vim.js)
-    content/sourceeditor/codemirror/fold/foldcode.js (sourceeditor/codemirror/fold/foldcode.js)
-    content/sourceeditor/codemirror/fold/brace-fold.js (sourceeditor/codemirror/fold/brace-fold.js)
-    content/sourceeditor/codemirror/fold/comment-fold.js (sourceeditor/codemirror/fold/comment-fold.js)
-    content/sourceeditor/codemirror/fold/xml-fold.js (sourceeditor/codemirror/fold/xml-fold.js)
-    content/sourceeditor/codemirror/fold/foldgutter.js (sourceeditor/codemirror/fold/foldgutter.js)
-    content/sourceeditor/codemirror/tern/tern.js (sourceeditor/codemirror/tern/tern.js)
-    content/sourceeditor/codemirror/hint/show-hint.js (sourceeditor/codemirror/hint/show-hint.js)
     content/sourceeditor/codemirror/mozilla.css (sourceeditor/codemirror/mozilla.css)
     content/debugger/debugger.xul (debugger/debugger.xul)
     content/debugger/debugger.css (debugger/debugger.css)
     content/debugger/debugger-controller.js (debugger/debugger-controller.js)
     content/debugger/debugger-view.js (debugger/debugger-view.js)
     content/debugger/views/workers-view.js (debugger/views/workers-view.js)
     content/debugger/views/sources-view.js (debugger/views/sources-view.js)
     content/debugger/views/variable-bubble-view.js (debugger/views/variable-bubble-view.js)
@@ -106,17 +106,17 @@ devtools.jar:
     content/performance/views/details-waterfall.js (performance/views/details-waterfall.js)
     content/performance/views/details-js-call-tree.js (performance/views/details-js-call-tree.js)
     content/performance/views/details-js-flamegraph.js (performance/views/details-js-flamegraph.js)
     content/performance/views/details-memory-call-tree.js (performance/views/details-memory-call-tree.js)
     content/performance/views/details-memory-flamegraph.js (performance/views/details-memory-flamegraph.js)
     content/performance/views/optimizations-list.js (performance/views/optimizations-list.js)
     content/performance/views/recordings.js (performance/views/recordings.js)
     content/memory/memory.xhtml (memory/memory.xhtml)
-    content/memory/controller.js (memory/controller.js)
+    content/memory/initializer.js (memory/initializer.js)
     content/promisedebugger/promise-controller.js (promisedebugger/promise-controller.js)
     content/promisedebugger/promise-panel.js (promisedebugger/promise-panel.js)
     content/promisedebugger/promise-debugger.xhtml (promisedebugger/promise-debugger.xhtml)
     content/commandline/commandline.css (commandline/commandline.css)
     content/commandline/commandlineoutput.xhtml (commandline/commandlineoutput.xhtml)
     content/commandline/commandlinetooltip.xhtml (commandline/commandlinetooltip.xhtml)
 *   content/framework/toolbox-window.xul (framework/toolbox-window.xul)
     content/framework/toolbox-options.xul (framework/toolbox-options.xul)
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/actions/moz.build
@@ -0,0 +1,8 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'snapshot.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/actions/snapshot.js
@@ -0,0 +1,14 @@
+/* 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 { PROMISE } = require("devtools/client/shared/redux/middleware/promise");
+const { actions } = require("../constants");
+
+const takeSnapshot = exports.takeSnapshot = function takeSnapshot (front) {
+  return {
+    type: actions.TAKE_SNAPSHOT,
+    [PROMISE]: front.saveHeapSnapshot()
+  };
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/constants.js
@@ -0,0 +1,9 @@
+/* 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 actions = exports.actions = {};
+
+// Fired by UI to request a snapshot from the actor.
+actions.TAKE_SNAPSHOT = "take-snapshot";
--- a/devtools/client/memory/controller.js
+++ b/devtools/client/memory/controller.js
@@ -1,28 +1,28 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
-const { loader, require } = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
-
 const { Task } = require("resource://gre/modules/Task.jsm");
-const { Heritage, ViewHelpers, WidgetMethods } = require("resource:///modules/devtools/client/shared/widgets/ViewHelpers.jsm");
+const Store = require("./store");
 
 /**
  * The current target, toolbox and MemoryFront, set by this tool's host.
  */
 var gToolbox, gTarget, gFront;
 
-/**
- * Initializes the profiler controller and views.
- */
-const MemoryController = {
-  initialize: Task.async(function *() {
+const REDUX_METHODS_TO_PIPE = ["dispatch", "subscribe", "getState"];
 
-  }),
+const MemoryController = exports.MemoryController = function ({ toolbox, target, front }) {
+  this.store = Store();
+  this.toolbox = toolbox;
+  this.target = target;
+  this.front = front;
+};
 
-  destroy: Task.async(function *() {
+REDUX_METHODS_TO_PIPE.map(m =>
+  MemoryController.prototype[m] = function (...args) { return this.store[m](...args); });
 
-  })
+MemoryController.prototype.destroy = function () {
+  this.store = this.toolbox = this.target = this.front = null;
 };
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/initializer.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+const { require } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const { Task } = require("resource://gre/modules/Task.jsm");
+const { MemoryController } = require("devtools/client/memory/controller");
+
+/**
+ * The current target, toolbox and MemoryFront, set by this tool's host.
+ */
+let gToolbox, gTarget, gFront;
+
+/**
+ * Initializes the profiler controller and views.
+ */
+var controller = null;
+function initialize () {
+  return Task.spawn(function *() {
+    controller = new MemoryController({ toolbox: gToolbox, target: gTarget, front: gFront });
+  });
+}
+
+function destroy () {
+  return Task.spawn(function *() {
+    controller.destroy();
+  });
+}
--- a/devtools/client/memory/memory.xhtml
+++ b/devtools/client/memory/memory.xhtml
@@ -7,33 +7,31 @@
 ]>
 
 <!-- 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/. -->
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <link rel="stylesheet" href="chrome://browser/skin/" type="text/css"/>
-    <link rel="stylesheet" href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"/>
-    <link rel="stylesheet" href="chrome://devtools/skin/themes/common.css" type="text/css"/>
-    <link rel="stylesheet" href="chrome://devtools/skin/themes/widgets.css" type="text/css"/>
-    <link rel="stylesheet" href="chrome://devtools/skin/themes/memory.css" type="text/css"/>
+    <link rel="stylesheet" href="chrome://browser/content/devtools/widgets.css" type="text/css"/>
+    <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
+    <link rel="stylesheet" href="chrome://browser/skin/devtools/widgets.css" type="text/css"/>
+    <link rel="stylesheet" href="chrome://browser/skin/devtools/memory.css" type="text/css"/>
 
     <script type="application/javascript;version=1.8"
-            src="chrome://devtools/content/shared/theme-switching.js"></script>
+            src="chrome://devtools/content/shared/theme-switching.js"/>
     <script type="application/javascript;version=1.8"
-            src="controller.js"></script>
+            src="initializer.js"></script>
   </head>
   <body class="theme-body">
-    <toolbar class="devtools-toolbar">
-      <toolbarbutton id="snapshot-button" class="devtools-toolbarbutton"
-                     tabindex="0"/>
-      <spacer flex="1"></spacer>
-    </toolbar>
-    <splitter class="devtools-horizontal-splitter"/>
+    <div class="devtools-toolbar">
+      <div id="snapshot-button" class="devtools-toolbarbutton" />
+    </div>
+    <div class="devtools-horizontal-splitter"></div>
     <div id="memory-content"
          class="devtools-responsive-container"
          flex="1">
       <toolbar class="devtools-toolbar">
         <spacer flex="1"></spacer>
       </toolbar>
       <hbox flex="1">
       </hbox>
--- a/devtools/client/memory/modules/census-view.js
+++ b/devtools/client/memory/modules/census-view.js
@@ -4,17 +4,16 @@
 "use strict";
 
 /**
  * This file contains the tree view, displaying all the samples and frames
  * received from the proviler in a tree-like structure.
  */
 
 const { Cc, Ci, Cu, Cr } = require("chrome");
-const { L10N } = require("devtools/client/performance/modules/global");
 const { Heritage } = require("resource:///modules/devtools/client/shared/widgets/ViewHelpers.jsm");
 const { AbstractTreeItem } = require("resource:///modules/devtools/client/shared/widgets/AbstractTreeItem.jsm");
 
 const INDENTATION = exports.INDENTATION = 16; // px
 const DEFAULT_AUTO_EXPAND_DEPTH = 2;
 const COURSE_TYPES = ["objects", "scripts", "strings", "other"];
 
 /**
--- a/devtools/client/memory/moz.build
+++ b/devtools/client/memory/moz.build
@@ -1,14 +1,22 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
+    'actions',
     'modules',
+    'reducers',
 ]
 
 DevToolsModules(
+    'constants.js',
+    'controller.js',
+    'initializer.js',
     'panel.js',
+    'reducers.js',
+    'store.js',
 )
 
 MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
--- a/devtools/client/memory/panel.js
+++ b/devtools/client/memory/panel.js
@@ -21,40 +21,47 @@ function MemoryPanel (iframeWindow, tool
 MemoryPanel.prototype = {
   open: Task.async(function *() {
     if (this._opening) {
       return this._opening;
     }
 
     this.panelWin.gToolbox = this._toolbox;
     this.panelWin.gTarget = this.target;
-    this.panelWin.gFront = new MemoryFront(this.target.client, this.target.form);
 
-    console.log(this.panelWin, this.panelWin.MemoryController);
-    return this._opening = this.panelWin.MemoryController.initialize().then(() => {
+    const rootForm = yield this.target.root;
+    this.panelWin.gFront = new MemoryFront(this.target.client,
+                                           this.target.form,
+                                           rootForm);
+
+    yield this.panelWin.gFront.attach();
+    return this._opening = this.panelWin.initialize().then(() => {
       this.isReady = true;
       this.emit("ready");
       return this;
     });
+    return this._opening;
   }),
 
   // DevToolPanel API
 
   get target() {
     return this._toolbox.target;
   },
 
-  destroy: function () {
+  destroy: Task.async(function *() {
     // Make sure this panel is not already destroyed.
     if (this._destroyer) {
       return this._destroyer;
     }
 
-    return this._destroyer = this.panelWin.MemoryController.destroy().then(() => {
+    yield this.panelWin.gFront.detach();
+    return this._destroyer = this.panelWin.destroy().then(() => {
       // Destroy front to ensure packet handler is removed from client
       this.panelWin.gFront.destroy();
+      this.panelWin = null;
       this.emit("destroyed");
       return this;
     });
-  }
+  })
 };
 
 exports.MemoryPanel = MemoryPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/reducers.js
@@ -0,0 +1,1 @@
+exports.snapshots = require("./reducers/snapshot");
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/reducers/moz.build
@@ -0,0 +1,8 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'snapshot.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/reducers/snapshot.js
@@ -0,0 +1,37 @@
+const { actions } = require("../constants");
+const { PROMISE } = require("devtools/client/shared/redux/middleware/promise");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+
+function handleTakeSnapshot (state, action) {
+  switch (action.status) {
+
+    case "start":
+      return [...state, {
+        id: action.seqId,
+        status: action.status
+      }];
+
+    case "done":
+      let snapshot = state.find(s => s.id === action.seqId);
+      if (!snapshot) {
+        DevToolsUtils.reportException(`No snapshot with id "${action.seqId}" for TAKE_SNAPSHOT`);
+        break;
+      }
+      snapshot.status = "done";
+      snapshot.snapshotId = action.value;
+      return [...state];
+
+    case "error":
+      DevToolsUtils.reportException(`No async state found for ${action.type}`);
+  }
+  return [...state];
+}
+
+module.exports = function (state=[], action) {
+  switch (action.type) {
+    case actions.TAKE_SNAPSHOT:
+      return handleTakeSnapshot(state, action);
+  }
+
+  return state;
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/store.js
@@ -0,0 +1,8 @@
+const { combineReducers } = require("../shared/vendor/redux");
+const createStore = require("../shared/redux/create-store");
+const reducers = require("./reducers");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+
+module.exports = function () {
+  return createStore({ log: DevToolsUtils.testing })(combineReducers(reducers), {});
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+  head.js
+
+[browser_memory_transferHeapSnapshot_e10s_01.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_transferHeapSnapshot_e10s_01.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can save a heap snapshot and transfer it over the RDP in e10s
+// where the child process is sandboxed and so we have to use
+// HeapSnapshotFileActor to get the heap snapshot file.
+
+"use strict";
+
+const TEST_URL = "data:text/html,<html><body></body></html>";
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+  const memoryFront = panel.panelWin.gFront;
+  ok(memoryFront, "Should get the MemoryFront");
+
+  const snapshotFilePath = yield memoryFront.saveHeapSnapshot({
+    // Force a copy so that we go through the HeapSnapshotFileActor's
+    // transferHeapSnapshot request and exercise this code path on e10s.
+    forceCopy: true
+  });
+
+  ok(!!(yield OS.File.stat(snapshotFilePath)),
+     "Should have the heap snapshot file");
+
+  const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath);
+  ok(snapshot instanceof HeapSnapshot,
+     "And we should be able to read a HeapSnapshot instance from the file");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/browser/head.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Load the shared test helpers into this compartment.
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+  this);
+
+Services.prefs.setBoolPref("devtools.memory.enabled", true);
+
+/**
+ * Open the memory panel for the given tab.
+ */
+this.openMemoryPanel = Task.async(function* (tab) {
+  info("Opening memory panel.");
+  const target = TargetFactory.forTab(tab);
+  const toolbox = yield gDevTools.showToolbox(target, "memory");
+  info("Memory panel shown successfully.");
+  let panel = toolbox.getCurrentPanel();
+  return { tab, panel };
+});
+
+/**
+ * Close the memory panel for the given tab.
+ */
+this.closeMemoryPanel = Task.async(function* (tab) {
+  info("Closing memory panel.");
+  const target = TargetFactory.forTab(tab);
+  const toolbox = gDevTools.getToolbox(target);
+  yield toolbox.destroy();
+  info("Closed memory panel successfully.");
+});
+
+/**
+ * Return a test function that adds a tab with the given url, opens the memory
+ * panel, runs the given generator, closes the memory panel, removes the tab,
+ * and finishes.
+ *
+ * Example usage:
+ *
+ *     this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ *         // Your tests go here...
+ *     });
+ */
+function makeMemoryTest(url, generator) {
+  return Task.async(function* () {
+    waitForExplicitFinish();
+
+    const tab = yield addTab(url);
+    const results = yield openMemoryPanel(tab);
+
+    try {
+      yield* generator(results);
+    } catch (err) {
+      ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
+    }
+
+    yield closeMemoryPanel(tab);
+    yield removeTab(tab);
+
+    finish();
+  });
+}
--- a/devtools/client/memory/test/mochitest/head.js
+++ b/devtools/client/memory/test/mochitest/head.js
@@ -4,9 +4,9 @@
 "use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 const CC = Components.Constructor;
 
-const { require } = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
+const { require } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
--- a/devtools/client/memory/test/mochitest/test_census-view-01.html
+++ b/devtools/client/memory/test/mochitest/test_census-view-01.html
@@ -2,21 +2,21 @@
 <html>
 <!--
 Bug 1067491 - Test taking a census over the RDP.
 -->
 <head>
   <meta charset="utf-8">
   <title>Census Tree 01</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <link href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css" />
-  <link href="chrome://devtools/skin/themes/light-theme.css" type="text/css" />
-  <link href="chrome://devtools/skin/themes/common.css" type="text/css" />
-  <link href="chrome://devtools/skin/themes/widgets.css" type="text/css" />
-  <link href="chrome://devtools/skin/themes/memory.css" type="text/css" />
+  <link href="chrome://browser/content/devtools/widgets.css" type="text/css" />
+  <link href="chrome://browser/skin/devtools/light-theme.css" type="text/css" />
+  <link href="chrome://browser/skin/devtools/common.css" type="text/css" />
+  <link href="chrome://browser/skin/devtools/widgets.css" type="text/css" />
+  <link href="chrome://browser/skin/devtools/memory.css" type="text/css" />
 </head>
 <body>
 <ul id="container" style="width:100%;height:300px;"></ul>
 <pre id="test">
 <script src="head.js" type="application/javascript;version=1.8"></script>
 <script>
 window.onload = function() {
   var { CensusTreeNode } = require("devtools/shared/heapsnapshot/census-tree-node");
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/head.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+var { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+var { gDevTools } = Cu.import("resource:///modules/devtools/client/framework/gDevTools.jsm", {});
+var { console } = Cu.import("resource://gre/modules/devtools/shared/Console.jsm", {});
+var { require } = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
+var { TargetFactory } = require("devtools/client/framework/target");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var promise = require("promise");
+var { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+var { MemoryController } = require("devtools/client/memory/controller");
+var { expectState } = require("devtools/server/actors/common");
+var HeapSnapshotFileUtils = require("devtools/shared/heapsnapshot/HeapSnapshotFileUtils");
+var { addDebuggerToGlobal } = require("resource://gre/modules/jsdebugger.jsm");
+var SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
+var { setTimeout } = require("sdk/timers");
+
+DevToolsUtils.testing = true;
+
+function initDebugger () {
+  let global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true });
+  addDebuggerToGlobal(global);
+  return new global.Debugger();
+}
+
+function StubbedMemoryFront () {
+  this.dbg = initDebugger();
+}
+
+StubbedMemoryFront.prototype.attach = Task.async(function *() {
+  this.state = "attached";
+});
+
+StubbedMemoryFront.prototype.detach = Task.async(function *() {
+  this.state = "detached";
+});
+
+StubbedMemoryFront.prototype.saveHeapSnapshot = expectState("attached", Task.async(function *() {
+  let path = ThreadSafeChromeUtils.saveHeapSnapshot({ debugger: this.dbg });
+  return HeapSnapshotFileUtils.getSnapshotIdFromPath(path);
+}), "saveHeapSnapshot");
+
+function waitUntilState (store, predicate) {
+  let deferred = promise.defer();
+  let unsubscribe = store.subscribe(() => {
+    if (predicate(store.getState())) {
+      unsubscribe();
+      deferred.resolve()
+    }
+  });
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-take-snapshot.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the async action creator `takeSnapshot(front)`
+ */
+
+let actions = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function *() {
+  let front = new StubbedMemoryFront();
+  yield front.attach();
+  let controller = new MemoryController({ toolbox: {}, target: {}, front });
+
+  let unsubscribe = controller.subscribe(checkState);
+
+  let foundPendingState = false;
+  let foundDoneState = false;
+
+  function checkState () {
+    let state = controller.getState();
+    if (state.snapshots.length === 1 && state.snapshots[0].status === "start") {
+      foundPendingState = true;
+      ok(foundPendingState, "Got state change for pending heap snapshot request");
+      ok(!(state.snapshots[0].snapshotId), "Snapshot does not yet have a snapshotId");
+    }
+    if (state.snapshots.length === 1 && state.snapshots[0].status === "done") {
+      foundDoneState = true;
+      ok(foundDoneState, "Got state change for completed heap snapshot request");
+      ok(state.snapshots[0].snapshotId, "Snapshot fetched with a snapshotId");
+    }
+    if (state.snapshots.lenght === 1 && state.snapshots[0].status === "error") {
+      ok(false, "takeSnapshot's promise returned with an error");
+    }
+  }
+
+  controller.dispatch(actions.takeSnapshot(front));
+  yield waitUntilState(controller, () => foundPendingState && foundDoneState);
+
+  unsubscribe();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android' || toolkit == 'gonk'
+
+[test_action-take-snapshot.js]
--- a/devtools/client/performance/events.js
+++ b/devtools/client/performance/events.js
@@ -25,16 +25,19 @@ module.exports = {
   // Emitted by the PerformanceView on import button click
   UI_IMPORT_RECORDING: "Performance:UI:ImportRecording",
   // Emitted by the RecordingsView on export button click
   UI_EXPORT_RECORDING: "Performance:UI:ExportRecording",
 
   // When a new recording is being tracked in the panel.
   NEW_RECORDING: "Performance:NewRecording",
 
+  // When a new recording can't be successfully created when started.
+  NEW_RECORDING_FAILED: "Performance:NewRecordingFailed",
+
   // When a recording is started or stopped or stopping via the PerformanceController
   RECORDING_STATE_CHANGE: "Performance:RecordingStateChange",
 
   // Emitted by the PerformanceController or RecordingView
   // when a recording model is selected
   RECORDING_SELECTED: "Performance:RecordingSelected",
 
   // When recordings have been cleared out
--- a/devtools/client/performance/performance-controller.js
+++ b/devtools/client/performance/performance-controller.js
@@ -84,24 +84,26 @@ const BRANCH_NAME = "devtools.performanc
 var gToolbox, gTarget, gFront;
 
 /**
  * Initializes the profiler controller and views.
  */
 var startupPerformance = Task.async(function*() {
   yield PerformanceController.initialize();
   yield PerformanceView.initialize();
+  PerformanceController.enableFrontEventListeners();
 });
 
 /**
  * Destroys the profiler controller and views.
  */
 var shutdownPerformance = Task.async(function*() {
   yield PerformanceController.destroy();
   yield PerformanceView.destroy();
+  PerformanceController.disableFrontEventListeners();
 });
 
 /**
  * Functions handling target-related lifetime events and
  * UI interaction.
  */
 var PerformanceController = {
   _recordings: [],
@@ -126,17 +128,16 @@ var PerformanceController = {
 
     // Store data regarding if e10s is enabled.
     this._e10s = Services.appinfo.browserTabsRemoteAutostart;
     this._setMultiprocessAttributes();
 
     this._prefs = require("devtools/client/performance/modules/global").PREFS;
     this._prefs.on("pref-changed", this._onPrefChanged);
 
-    gFront.on("*", this._onFrontEvent);
     ToolbarView.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
     PerformanceView.on(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
     RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     RecordingsView.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
     DetailsView.on(EVENTS.DETAILS_VIEW_SELECTED, this._pipe);
@@ -146,30 +147,50 @@ var PerformanceController = {
 
   /**
    * Remove events handled by the PerformanceController
    */
   destroy: function() {
     this._telemetry.destroy();
     this._prefs.off("pref-changed", this._onPrefChanged);
 
-    gFront.off("*", this._onFrontEvent);
     ToolbarView.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
     PerformanceView.off(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
     RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     RecordingsView.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
     DetailsView.off(EVENTS.DETAILS_VIEW_SELECTED, this._pipe);
 
     gDevTools.off("pref-changed", this._onThemeChanged);
   },
 
   /**
+   * Enables front event listeners.
+   *
+   * The rationale behind this is given by the async intialization of all the
+   * frontend components. Even though the panel is considered "open" only after
+   * both the controller and the view are created, and even though their
+   * initialization is sequential (controller, then view), the controller might
+   * start handling backend events before the view finishes if the event
+   * listeners are added too soon.
+   */
+  enableFrontEventListeners: function() {
+    gFront.on("*", this._onFrontEvent);
+  },
+
+  /**
+   * Disables front event listeners.
+   */
+  disableFrontEventListeners: function() {
+    gFront.off("*", this._onFrontEvent);
+  },
+
+  /**
    * Returns the current devtools theme.
    */
   getTheme: function () {
     return Services.prefs.getCharPref("devtools.theme");
   },
 
   /**
    * Get a boolean preference setting from `prefName` via the underlying
@@ -201,32 +222,59 @@ var PerformanceController = {
    * @param string prefName
    * @param any prefValue
    */
   setPref: function (prefName, prefValue) {
     this._prefs[prefName] = prefValue;
   },
 
   /**
+   * Checks whether or not a new recording is supported by the PerformanceFront.
+   * @return Promise:boolean
+   */
+  canCurrentlyRecord: Task.async(function*() {
+    // If we're testing the legacy front, the performance actor will exist,
+    // with `canCurrentlyRecord` method; this ensures we test the legacy path.
+    if (gFront.LEGACY_FRONT) {
+      return true;
+    }
+    let hasActor = yield gTarget.hasActor("performance");
+    if (!hasActor) {
+      return true;
+    }
+    let actorCanCheck = yield gTarget.actorHasMethod("performance", "canCurrentlyRecord");
+    if (!actorCanCheck) {
+      return true;
+    }
+    return (yield gFront.canCurrentlyRecord()).success;
+  }),
+
+  /**
    * Starts recording with the PerformanceFront.
    */
   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);
+    // In some cases, like when the target has a private browsing tab,
+    // recording is not currently supported because of the profiler module.
+    // Present a notification in this case alerting the user of this issue.
+    if (!(yield gFront.startRecording(options))) {
+      this.emit(EVENTS.NEW_RECORDING_FAILED);
+      PerformanceView.setState("unavailable");
+    }
   }),
 
   /**
    * Stops recording with the PerformanceFront.
    */
   stopRecording: Task.async(function *() {
     let recording = this.getLatestManualRecording();
     yield gFront.stopRecording(recording);
--- a/devtools/client/performance/performance-view.js
+++ b/devtools/client/performance/performance-view.js
@@ -9,37 +9,40 @@
 var PerformanceView = {
 
   _state: null,
 
   // Set to true if the front emits a "buffer-status" event, indicating
   // that the server has support for determining buffer status.
   _bufferStatusSupported: false,
 
-  // Mapping of state to selectors for different panes
-  // of the main profiler view. Used in `PerformanceView.setState()`
+  // Mapping of state to selectors for different properties and their values,
+  // from the main profiler view. Used in `PerformanceView.setState()`
   states: {
-    empty: [
-      { deck: "#performance-view", pane: "#empty-notice" }
+    "unavailable": [
+      { sel: "#performance-view", opt: "selectedPanel", val: () => $("#unavailable-notice") },
     ],
-    recording: [
-      { deck: "#performance-view", pane: "#performance-view-content" },
-      { deck: "#details-pane-container", pane: "#recording-notice" }
+    "empty": [
+      { sel: "#performance-view", opt: "selectedPanel", val: () => $("#empty-notice") }
+    ],
+    "recording": [
+      { sel: "#performance-view", opt: "selectedPanel", val: () => $("#performance-view-content") },
+      { sel: "#details-pane-container", opt: "selectedPanel", val: () => $("#recording-notice") }
     ],
     "console-recording": [
-      { deck: "#performance-view", pane: "#performance-view-content" },
-      { deck: "#details-pane-container", pane: "#console-recording-notice" }
+      { sel: "#performance-view", opt: "selectedPanel", val: () => $("#performance-view-content") },
+      { sel: "#details-pane-container", opt: "selectedPanel", val: () => $("#console-recording-notice") }
     ],
-    recorded: [
-      { deck: "#performance-view", pane: "#performance-view-content" },
-      { deck: "#details-pane-container", pane: "#details-pane" }
+    "recorded": [
+      { sel: "#performance-view", opt: "selectedPanel", val: () => $("#performance-view-content") },
+      { sel: "#details-pane-container", opt: "selectedPanel", val: () => $("#details-pane") }
     ],
-    loading: [
-      { deck: "#performance-view", pane: "#performance-view-content" },
-      { deck: "#details-pane-container", pane: "#loading-notice" }
+    "loading": [
+      { sel: "#performance-view", opt: "selectedPanel", val: () => $("#performance-view-content") },
+      { sel: "#details-pane-container", opt: "selectedPanel", val: () => $("#loading-notice") }
     ]
   },
 
   /**
    * Sets up the view with event binding and main subviews.
    */
   initialize: Task.async(function* () {
     this._recordButton = $("#main-record-button");
@@ -47,30 +50,36 @@ var PerformanceView = {
     this._clearButton = $("#clear-button");
 
     this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
     this._onImportButtonClick = this._onImportButtonClick.bind(this);
     this._onClearButtonClick = this._onClearButtonClick.bind(this);
     this._onRecordingSelected = this._onRecordingSelected.bind(this);
     this._onProfilerStatusUpdated = this._onProfilerStatusUpdated.bind(this);
     this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
+    this._onNewRecordingFailed = this._onNewRecordingFailed.bind(this);
 
     for (let button of $$(".record-button")) {
       button.addEventListener("click", this._onRecordButtonClick);
     }
     this._importButton.addEventListener("click", this._onImportButtonClick);
     this._clearButton.addEventListener("click", this._onClearButtonClick);
 
     // Bind to controller events to unlock the record button
     PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
     PerformanceController.on(EVENTS.PROFILER_STATUS_UPDATED, this._onProfilerStatusUpdated);
     PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
     PerformanceController.on(EVENTS.NEW_RECORDING, this._onRecordingStateChange);
+    PerformanceController.on(EVENTS.NEW_RECORDING_FAILED, this._onNewRecordingFailed);
 
-    this.setState("empty");
+    if (yield PerformanceController.canCurrentlyRecord()) {
+      this.setState("empty");
+    } else {
+      this.setState("unavailable");
+    }
 
     // Initialize the ToolbarView first, because other views may need access
     // to the OptionsView via the controller, to read prefs.
     yield ToolbarView.initialize();
     yield RecordingsView.initialize();
     yield OverviewView.initialize();
     yield DetailsView.initialize();
   }),
@@ -84,41 +93,45 @@ var PerformanceView = {
     }
     this._importButton.removeEventListener("click", this._onImportButtonClick);
     this._clearButton.removeEventListener("click", this._onClearButtonClick);
 
     PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
     PerformanceController.off(EVENTS.PROFILER_STATUS_UPDATED, this._onProfilerStatusUpdated);
     PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
     PerformanceController.off(EVENTS.NEW_RECORDING, this._onRecordingStateChange);
+    PerformanceController.off(EVENTS.NEW_RECORDING_FAILED, this._onNewRecordingFailed);
 
     yield ToolbarView.destroy();
     yield RecordingsView.destroy();
     yield OverviewView.destroy();
     yield DetailsView.destroy();
   }),
 
   /**
-   * Sets the state of the profiler view. Possible options are "empty",
-   * "recording", "console-recording", "recorded".
+   * Sets the state of the profiler view. Possible options are "unavailable",
+   * "empty", "recording", "console-recording", "recorded".
    */
   setState: function (state) {
     let viewConfig = this.states[state];
     if (!viewConfig) {
       throw new Error(`Invalid state for PerformanceView: ${state}`);
     }
-    for (let { deck, pane } of viewConfig) {
-      $(deck).selectedPanel = $(pane);
+    for (let { sel, opt, val } of viewConfig) {
+      for (let el of $$(sel)) {
+        el[opt] = val();
+      }
     }
 
     this._state = state;
 
     if (state === "console-recording") {
       let recording = PerformanceController.getCurrentRecording();
       let label = recording.getLabel() || "";
+
       // Wrap the label in quotes if it exists for the commands.
       label = label ? `"${label}"` : "";
 
       let startCommand = $(".console-profile-recording-notice .console-profile-command");
       let stopCommand = $(".console-profile-stop-notice .console-profile-command");
 
       startCommand.value = `console.profile(${label})`;
       stopCommand.value = `console.profileEnd(${label})`;
@@ -187,63 +200,71 @@ var PerformanceView = {
   },
 
   /*
    * Toggles the `checked` attribute on the record buttons based
    * on `activate`.
    *
    * @param {boolean} activate
    */
-  _activateRecordButtons: function (activate) {
+  _toggleRecordButtons: function (activate) {
     for (let button of $$(".record-button")) {
       if (activate) {
         button.setAttribute("checked", "true");
       } else {
         button.removeAttribute("checked");
       }
     }
   },
 
   /**
    * When a recording has started.
    */
   _onRecordingStateChange: function () {
     let currentRecording = PerformanceController.getCurrentRecording();
     let recordings = PerformanceController.getRecordings();
 
-    this._activateRecordButtons(recordings.find(r => !r.isConsole() && r.isRecording()));
+    this._toggleRecordButtons(recordings.find(r => !r.isConsole() && r.isRecording()));
     this._lockRecordButtons(recordings.find(r => !r.isConsole() && r.isFinalizing()));
 
     if (currentRecording && currentRecording.isFinalizing()) {
       this.setState("loading");
     }
     if (currentRecording && currentRecording.isCompleted()) {
       this.setState("recorded");
     }
     if (currentRecording && currentRecording.isRecording()) {
       this.updateBufferStatus();
     }
   },
 
   /**
+   * When starting a recording has failed.
+   */
+  _onNewRecordingFailed: function (e) {
+    this._lockRecordButtons(false);
+    this._toggleRecordButtons(false);
+  },
+
+  /**
    * Handler for clicking the clear button.
    */
   _onClearButtonClick: function (e) {
     this.emit(EVENTS.UI_CLEAR_RECORDINGS);
   },
 
   /**
    * Handler for clicking the record button.
    */
   _onRecordButtonClick: function (e) {
     if (this._recordButton.hasAttribute("checked")) {
       this.emit(EVENTS.UI_STOP_RECORDING);
     } else {
       this._lockRecordButtons(true);
-      this._activateRecordButtons(true);
+      this._toggleRecordButtons(true);
       this.emit(EVENTS.UI_START_RECORDING);
     }
   },
 
   /**
    * Handler for clicking the import button.
    */
   _onImportButtonClick: function(e) {
--- a/devtools/client/performance/performance.xul
+++ b/devtools/client/performance/performance.xul
@@ -152,16 +152,40 @@
                          popup="performance-options-menupopup"
                          tooltiptext="&performanceUI.options.gear.tooltiptext;"/>
         </hbox>
       </toolbar>
 
       <!-- Recording contents and general notice messages -->
       <deck id="performance-view" flex="1">
 
+        <!-- A default notice, shown while initially opening the tool.
+             Keep this element the first child of #performance-view. -->
+        <hbox id="tool-loading-notice"
+              class="notice-container"
+              flex="1">
+        </hbox>
+
+        <!-- "Unavailable" notice, shown when the entire tool is disabled,
+             for example, when in private browsing mode. -->
+        <vbox id="unavailable-notice"
+              class="notice-container"
+              align="center"
+              pack="center"
+              flex="1">
+          <hbox class="devtools-toolbarbutton-group"
+                pack="center">
+            <toolbarbutton class="devtools-toolbarbutton record-button"
+                           label="&performanceUI.startRecording;"
+                           standalone="true"/>
+          </hbox>
+          <label class="tool-disabled-message"
+                 value="&performanceUI.unavailableNoticePB;"/>
+        </vbox>
+
         <!-- "Empty" notice, shown when there's no recordings available -->
         <hbox id="empty-notice"
               class="notice-container"
               align="center"
               pack="center"
               flex="1">
           <hbox class="devtools-toolbarbutton-group"
                 pack="center">
--- a/devtools/client/performance/test/browser.ini
+++ b/devtools/client/performance/test/browser.ini
@@ -3,19 +3,16 @@ tags = devtools
 subsuite = devtools
 support-files =
   doc_allocs.html
   doc_innerHTML.html
   doc_markers.html
   doc_simple-test.html
   head.js
 
-# Commented out tests are profiler tests
-# that need to be moved over to performance tool
-
 [browser_aaa-run-first-leaktest.js]
 [browser_perf-categories-js-calltree.js]
 [browser_perf-clear-01.js]
 [browser_perf-clear-02.js]
 [browser_perf-columns-js-calltree.js]
 [browser_perf-columns-memory-calltree.js]
 [browser_perf-console-record-01.js]
 [browser_perf-console-record-02.js]
@@ -76,16 +73,17 @@ skip-if = os == 'linux' # Bug 1172120
 [browser_perf-overview-render-02.js]
 [browser_perf-overview-render-03.js]
 [browser_perf-overview-render-04.js]
 skip-if = os == 'linux' # bug 1186322
 [browser_perf-overview-selection-01.js]
 [browser_perf-overview-selection-02.js]
 [browser_perf-overview-selection-03.js]
 [browser_perf-overview-time-interval.js]
+[browser_perf-private-browsing.js]
 [browser_perf-states.js]
 skip-if = debug # bug 1203888
 [browser_perf-refresh.js]
 [browser_perf-ui-recording.js]
 skip-if = os == 'linux' # bug 1186322
 [browser_perf-recording-notices-01.js]
 skip-if = os == 'linux' # bug 1186322
 [browser_perf-recording-notices-02.js]
--- a/devtools/client/performance/test/browser_perf-console-record-01.js
+++ b/devtools/client/performance/test/browser_perf-console-record-01.js
@@ -17,17 +17,17 @@ function* spawnTest() {
   yield profileStart;
 
   busyWait(WAIT_TIME);
   let profileEnd = once(front, "recording-stopped");
   console.profileEnd("rust");
   yield profileEnd;
 
   yield gDevTools.showToolbox(target, "performance");
-  let panel = toolbox.getCurrentPanel();
+  let panel = yield toolbox.getCurrentPanel().open();
   let { panelWin: { PerformanceController, RecordingsView }} = panel;
 
   let recordings = PerformanceController.getRecordings();
   yield waitUntil(() => PerformanceController.getRecordings().length === 1);
   is(recordings.length, 1, "one recording found in the performance panel.");
   is(recordings[0].isConsole(), true, "recording came from console.profile.");
   is(recordings[0].getLabel(), "rust", "correct label in the recording model.");
 
--- a/devtools/client/performance/test/browser_perf-console-record-02.js
+++ b/devtools/client/performance/test/browser_perf-console-record-02.js
@@ -1,31 +1,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the profiler is populated by in-progress console recordings
  * when it is opened.
  */
 
-var WAIT_TIME = 10;
-
 function* spawnTest() {
   let { target, toolbox, console } = yield initConsole(SIMPLE_URL);
   let front = toolbox.performance;
 
   let profileStart = once(front, "recording-started");
   console.profile("rust");
   yield profileStart;
+
   profileStart = once(front, "recording-started");
   console.profile("rust2");
   yield profileStart;
 
   yield gDevTools.showToolbox(target, "performance");
-  let panel = toolbox.getCurrentPanel();
+  let panel = yield toolbox.getCurrentPanel().open();
   let { panelWin: { PerformanceController, RecordingsView }} = panel;
 
   yield waitUntil(() => PerformanceController.getRecordings().length === 2);
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "two recordings found in the performance panel.");
   is(recordings[0].isConsole(), true, "recording came from console.profile (1).");
   is(recordings[0].getLabel(), "rust", "correct label in the recording model (1).");
   is(recordings[0].isRecording(), true, "recording is still recording (1).");
@@ -34,15 +33,16 @@ function* spawnTest() {
   is(recordings[1].isRecording(), true, "recording is still recording (2).");
 
   is(RecordingsView.selectedItem.attachment, recordings[0],
     "The first console recording should be selected.");
 
   let profileEnd = once(front, "recording-stopped");
   console.profileEnd("rust");
   yield profileEnd;
+
   profileEnd = once(front, "recording-stopped");
   console.profileEnd("rust2");
   yield profileEnd;
 
   yield teardown(panel);
   finish();
 }
--- a/devtools/client/performance/test/browser_perf-console-record-03.js
+++ b/devtools/client/performance/test/browser_perf-console-record-03.js
@@ -1,18 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the profiler is populated by in-progress console recordings, and
  * also console recordings that have finished before it was opened.
  */
 
-var WAIT_TIME = 10;
-
 function* spawnTest() {
   let { target, toolbox, console } = yield initConsole(SIMPLE_URL);
   let front = toolbox.performance;
 
   let profileStart = once(front, "recording-started");
   console.profile("rust");
   yield profileStart;
 
@@ -20,17 +18,17 @@ function* spawnTest() {
   console.profileEnd("rust");
   yield profileEnd;
 
   profileStart = once(front, "recording-started");
   console.profile("rust2");
   yield profileStart;
 
   yield gDevTools.showToolbox(target, "performance");
-  let panel = toolbox.getCurrentPanel();
+  let panel = yield toolbox.getCurrentPanel().open();
   let { panelWin: { PerformanceController, RecordingsView }} = panel;
 
   yield waitUntil(() => PerformanceController.getRecordings().length === 2);
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "two recordings found in the performance panel.");
   is(recordings[0].isConsole(), true, "recording came from console.profile (1).");
   is(recordings[0].getLabel(), "rust", "correct label in the recording model (1).");
   is(recordings[0].isRecording(), false, "recording is still recording (1).");
--- a/devtools/client/performance/test/browser_perf-highlighted.js
+++ b/devtools/client/performance/test/browser_perf-highlighted.js
@@ -23,17 +23,17 @@ function* spawnTest() {
   let profileEnd = once(front, "recording-stopped");
   console.profileEnd("rust");
   yield profileEnd;
 
   ok(!tab.hasAttribute("highlighted"),
     "performance tab is no longer highlighted when console.profile recording finishes");
 
   yield gDevTools.showToolbox(target, "performance");
-  let panel = toolbox.getCurrentPanel();
+  let panel = yield toolbox.getCurrentPanel().open();
   let { panelWin: { PerformanceController, RecordingsView }} = panel;
 
   yield startRecording(panel);
 
   ok(tab.hasAttribute("highlighted"),
     "performance tab is highlighted during recording while in performance tool");
 
   yield stopRecording(panel);
--- a/devtools/client/performance/test/browser_perf-legacy-front-06.js
+++ b/devtools/client/performance/test/browser_perf-legacy-front-06.js
@@ -15,17 +15,17 @@ function* spawnTest() {
   let profileStart = once(front, "recording-started");
   console.profile("rust");
   yield profileStart;
   profileStart = once(front, "recording-started");
   console.profile("rust2");
   yield profileStart;
 
   yield gDevTools.showToolbox(target, "performance");
-  let panel = toolbox.getCurrentPanel();
+  let panel = yield toolbox.getCurrentPanel().open();
   let { panelWin: { PerformanceController, RecordingsView }} = panel;
 
   yield waitUntil(() => PerformanceController.getRecordings().length === 2);
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "two recordings found in the performance panel.");
   is(recordings[0].isConsole(), true, "recording came from console.profile (1).");
   is(recordings[0].getLabel(), "rust", "correct label in the recording model (1).");
   is(recordings[0].isRecording(), true, "recording is still recording (1).");
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-private-browsing.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that disables the frontend when in private browsing mode.
+ */
+
+let gPanelWinTuples = [];
+
+function* spawnTest() {
+  yield testNormalWindow();
+  yield testPrivateWindow();
+  yield testRecordingFailingInWindow(0);
+  yield testRecordingFailingInWindow(1);
+  yield teardownPerfInWindow(1);
+  yield testRecordingSucceedingInWindow(0);
+  yield teardownPerfInWindow(0);
+
+  gPanelWinTuples = null;
+  finish();
+}
+
+function* createPanelInWindow(options) {
+  let win = yield addWindow(options);
+  let tab = yield addTab(SIMPLE_URL, win);
+  let target = TargetFactory.forTab(tab);
+  yield target.makeRemote();
+
+  let toolbox = yield gDevTools.showToolbox(target, "performance");
+  yield toolbox.initPerformance();
+
+  let panel = yield toolbox.getCurrentPanel().open();
+  gPanelWinTuples.push({ panel, win });
+
+  return { panel, win };
+}
+
+function* testNormalWindow() {
+  let { panel } = yield createPanelInWindow({ private: false });
+  let { PerformanceView } = panel.panelWin;
+
+  is(PerformanceView.getState(), "empty",
+    "The initial state of the performance panel view is correct (1).");
+}
+
+function* testPrivateWindow() {
+  let { panel } = yield createPanelInWindow({ private: true });
+  let { PerformanceView } = panel.panelWin;
+
+  is(PerformanceView.getState(), "unavailable",
+    "The initial state of the performance panel view is correct (2).");
+}
+
+function* testRecordingFailingInWindow(index) {
+  let { panel } = gPanelWinTuples[index];
+  let { EVENTS, PerformanceController } = panel.panelWin;
+
+  let onRecordingStarted = () => {
+    ok(false, "Recording should not start while a private window is present.");
+  };
+
+  PerformanceController.on(EVENTS.RECORDING_STARTED, onRecordingStarted);
+
+  let whenFailed = once(PerformanceController, EVENTS.NEW_RECORDING_FAILED);
+  PerformanceController.startRecording();
+  yield whenFailed;
+  ok(true, "Recording has failed.");
+
+  PerformanceController.off(EVENTS.RECORDING_STARTED, onRecordingStarted);
+}
+
+function* testRecordingSucceedingInWindow(index) {
+  let { panel } = gPanelWinTuples[index];
+  let { EVENTS, PerformanceController } = panel.panelWin;
+
+  let onRecordingFailed = () => {
+    ok(false, "Recording should start while now private windows are present.");
+  };
+
+  PerformanceController.on(EVENTS.NEW_RECORDING_FAILED, onRecordingFailed);
+
+  yield startRecording(panel);
+  yield stopRecording(panel);
+  ok(true, "Recording has succeeded.");
+
+  PerformanceController.off(EVENTS.RECORDING_STARTED, onRecordingFailed);
+}
+
+function* teardownPerfInWindow(index) {
+  let { panel, win } = gPanelWinTuples[index];
+  yield teardown(panel, win);
+  win.close();
+}
--- a/devtools/client/performance/test/browser_timeline-waterfall-generic.js
+++ b/devtools/client/performance/test/browser_timeline-waterfall-generic.js
@@ -1,16 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the waterfall is properly built after finishing a recording.
  */
 
 function* spawnTest() {
+  // This test seems to take a long time to cleanup on Ubuntu VMs.
+  requestLongerTimeout(2);
+
   let { target, panel } = yield initPerformance(SIMPLE_URL);
   let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView, DetailsView } = panel.panelWin;
   let { WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS } = require("devtools/client/performance/modules/widgets/marker-view");
 
   yield startRecording(panel);
   ok(true, "Recording has started.");
 
   let updated = 0;
--- a/devtools/client/performance/test/head.js
+++ b/devtools/client/performance/test/head.js
@@ -100,16 +100,39 @@ registerCleanupFunction(() => {
   // Rollback any pref changes
   Object.keys(DEFAULT_PREFS).forEach(pref => {
     Preferences.set(pref, DEFAULT_PREFS[pref]);
   });
 
   Cu.forceGC();
 });
 
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+  Services.obs.addObserver(function observer(aSubject, aTopic) {
+    if (aWindow == aSubject) {
+      Services.obs.removeObserver(observer, aTopic);
+      executeSoon(aCallback);
+    }
+  }, "browser-delayed-startup-finished", false);
+}
+
+function addWindow(windowOptions) {
+  let deferred = Promise.defer();
+  let win = OpenBrowserWindow(windowOptions);
+
+  whenDelayedStartupFinished(win, () => {
+    executeSoon(() => {
+      deferred.resolve(win);
+    });
+  });
+
+  return deferred.promise;
+}
+
 function addTab(aUrl, aWindow) {
   info("Adding tab: " + aUrl);
 
   let deferred = Promise.defer();
   let targetWindow = aWindow || window;
   let targetBrowser = targetWindow.gBrowser;
 
   targetWindow.focus();
@@ -228,16 +251,18 @@ function initPerformance(aUrl, tool="per
     // TEST_PROFILER_FILTER_STATUS = array
     merge(target, targetOps);
 
     let toolbox = yield gDevTools.showToolbox(target, tool);
 
     // Wait for the performance tool to be spun up
     yield toolbox.initPerformance();
 
+    // Panel is already initialized after `showToolbox` and `initPerformance`,
+    // no need to wait for `open` here.
     let panel = toolbox.getCurrentPanel();
     return { target, panel, toolbox };
   });
 }
 
 /**
  * Initializes a webconsole panel. Returns a target, panel and toolbox reference.
  * Also returns a console property that allows calls to `profile` and `profileEnd`.
@@ -271,22 +296,22 @@ function consoleExecute (console, method
         resolve();
         return;
       }
     }
   }
   return promise;
 }
 
-function* teardown(panel) {
+function* teardown(panel, win = window) {
   info("Destroying the performance tool.");
 
   let tab = panel.target.tab;
   yield panel._toolbox.destroy();
-  yield removeTab(tab);
+  yield removeTab(tab, win);
 }
 
 function idleWait(time) {
   return DevToolsUtils.waitForTime(time);
 }
 
 function busyWait(time) {
   let start = Date.now();
--- a/devtools/client/shared/redux/create-store.js
+++ b/devtools/client/shared/redux/create-store.js
@@ -2,30 +2,32 @@
  * 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 { createStore, applyMiddleware } = require("devtools/client/shared/vendor/redux");
 const { thunk } = require("./middleware/thunk");
 const { waitUntilService } = require("./middleware/wait-service");
 const { log } = require("./middleware/log");
+const { promise } = require("./middleware/promise");
 
 /**
  * This creates a dispatcher with all the standard middleware in place
  * that all code requires. It can also be optionally configured in
  * various ways, such as logging and recording.
  *
  * @param {object} opts - boolean configuration flags
  *        - log: log all dispatched actions to console
  *        - middleware: array of middleware to be included in the redux store
  */
 module.exports = (opts={}) => {
   const middleware = [
     thunk,
-    waitUntilService
+    waitUntilService,
+    promise,
   ];
 
   if (opts.log) {
     middleware.push(log);
   }
 
   if (opts.middleware) {
     opts.middleware.forEach(fn => middleware.push(fn));
--- a/devtools/client/shared/redux/middleware/moz.build
+++ b/devtools/client/shared/redux/middleware/moz.build
@@ -1,11 +1,12 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'log.js',
+    'promise.js',
     'thunk.js',
     'wait-service.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/promise.js
@@ -0,0 +1,52 @@
+/* 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 uuidgen = require("sdk/util/uuid").uuid;
+const {
+  entries, toObject, reportException, executeSoon
+} = require("devtools/shared/DevToolsUtils");
+const PROMISE = exports.PROMISE = "@@dispatch/promise";
+
+function promiseMiddleware ({ dispatch, getState }) {
+  return next => action => {
+    if (!(PROMISE in action)) {
+      return next(action);
+    }
+
+    const promise = action[PROMISE];
+    const seqId = uuidgen().toString();
+
+    // Create a new action that doesn't have the promise field and has
+    // the `seqId` field that represents the sequence id
+    action = Object.assign(
+      toObject(entries(action).filter(pair => pair[0] !== PROMISE)), { seqId }
+    );
+
+    dispatch(Object.assign({}, action, { status: "start" }));
+
+    promise.then(value => {
+      executeSoon(() => {
+        dispatch(Object.assign({}, action, {
+          status: "done",
+          value: value
+        }));
+      });
+    }).catch(error => {
+      executeSoon(() => {
+        dispatch(Object.assign({}, action, {
+          status: "error",
+          error
+        }));
+      });
+      reportException(`@@redux/middleware/promise#${action.type}`, error);
+    });
+
+    // Return the promise so action creators can still compose if they
+    // want to.
+    return promise;
+  };
+}
+
+exports.promise = promiseMiddleware;
rename from devtools/client/sourceeditor/codemirror/comment/comment.js
rename to devtools/client/sourceeditor/codemirror/addon/comment/comment.js
rename from devtools/client/sourceeditor/codemirror/comment/continuecomment.js
rename to devtools/client/sourceeditor/codemirror/addon/comment/continuecomment.js
rename from devtools/client/sourceeditor/codemirror/dialog/dialog.css
rename to devtools/client/sourceeditor/codemirror/addon/dialog/dialog.css
rename from devtools/client/sourceeditor/codemirror/dialog/dialog.js
rename to devtools/client/sourceeditor/codemirror/addon/dialog/dialog.js
rename from devtools/client/sourceeditor/codemirror/edit/closebrackets.js
rename to devtools/client/sourceeditor/codemirror/addon/edit/closebrackets.js
rename from devtools/client/sourceeditor/codemirror/edit/closetag.js
rename to devtools/client/sourceeditor/codemirror/addon/edit/closetag.js
rename from devtools/client/sourceeditor/codemirror/edit/continuelist.js
rename to devtools/client/sourceeditor/codemirror/addon/edit/continuelist.js
rename from devtools/client/sourceeditor/codemirror/edit/matchbrackets.js
rename to devtools/client/sourceeditor/codemirror/addon/edit/matchbrackets.js
rename from devtools/client/sourceeditor/codemirror/edit/matchtags.js
rename to devtools/client/sourceeditor/codemirror/addon/edit/matchtags.js
rename from devtools/client/sourceeditor/codemirror/edit/trailingspace.js
rename to devtools/client/sourceeditor/codemirror/addon/edit/trailingspace.js
rename from devtools/client/sourceeditor/codemirror/fold/brace-fold.js
rename to devtools/client/sourceeditor/codemirror/addon/fold/brace-fold.js
rename from devtools/client/sourceeditor/codemirror/fold/comment-fold.js
rename to devtools/client/sourceeditor/codemirror/addon/fold/comment-fold.js
rename from devtools/client/sourceeditor/codemirror/fold/foldcode.js
rename to devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js
rename from devtools/client/sourceeditor/codemirror/fold/foldgutter.css
rename to devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.css
rename from devtools/client/sourceeditor/codemirror/fold/foldgutter.js
rename to devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.js
rename from devtools/client/sourceeditor/codemirror/fold/indent-fold.js
rename to devtools/client/sourceeditor/codemirror/addon/fold/indent-fold.js
rename from devtools/client/sourceeditor/codemirror/fold/markdown-fold.js
rename to devtools/client/sourceeditor/codemirror/addon/fold/markdown-fold.js
rename from devtools/client/sourceeditor/codemirror/fold/xml-fold.js
rename to devtools/client/sourceeditor/codemirror/addon/fold/xml-fold.js
rename from devtools/client/sourceeditor/codemirror/hint/show-hint.js
rename to devtools/client/sourceeditor/codemirror/addon/hint/show-hint.js
rename from devtools/client/sourceeditor/codemirror/search/match-highlighter.js
rename to devtools/client/sourceeditor/codemirror/addon/search/match-highlighter.js
rename from devtools/client/sourceeditor/codemirror/search/search.js
rename to devtools/client/sourceeditor/codemirror/addon/search/search.js
rename from devtools/client/sourceeditor/codemirror/search/searchcursor.js
rename to devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js
rename from devtools/client/sourceeditor/codemirror/selection/active-line.js
rename to devtools/client/sourceeditor/codemirror/addon/selection/active-line.js
rename from devtools/client/sourceeditor/codemirror/selection/mark-selection.js
rename to devtools/client/sourceeditor/codemirror/addon/selection/mark-selection.js
rename from devtools/client/sourceeditor/codemirror/tern/tern.css
rename to devtools/client/sourceeditor/codemirror/addon/tern/tern.css
rename from devtools/client/sourceeditor/codemirror/tern/tern.js
rename to devtools/client/sourceeditor/codemirror/addon/tern/tern.js
rename from devtools/client/sourceeditor/codemirror/codemirror.css
rename to devtools/client/sourceeditor/codemirror/lib/codemirror.css
rename from devtools/client/sourceeditor/codemirror/codemirror.js
rename to devtools/client/sourceeditor/codemirror/lib/codemirror.js
--- a/devtools/client/sourceeditor/test/browser.ini
+++ b/devtools/client/sourceeditor/test/browser.ini
@@ -1,32 +1,33 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
-  cm_comment_test.js
-  cm_doc_test.js
-  cm_driver.js
-  cm_emacs_test.js
-  cm_mode_test.css
-  cm_mode_test.js
-  cm_multi_test.js
-  cm_mode_ruby.js
-  cm_script_injection_test.js
-  cm_search_test.js
-  cm_sublime_test.js
-  cm_test.js
-  cm_vim_test.js
-  codemirror.html
+  codemirror/comment_test.js
+  codemirror/doc_test.js
+  codemirror/driver.js
+  codemirror/emacs_test.js
+  codemirror/mode_test.css
+  codemirror/mode_test.js
+  codemirror/multi_test.js
+  codemirror/search_test.js
+  codemirror/sublime_test.js
+  codemirror/test.js
+  codemirror/vim_test.js
+  codemirror/codemirror.html
+  codemirror/vimemacs.html
+  codemirror/mode/javascript/test.js
   css_statemachine_testcases.css
   css_statemachine_tests.json
   css_autocompletion_tests.json
-  vimemacs.html
   head.js
   helper_codemirror_runner.js
+  cm_mode_ruby.js
+  cm_script_injection_test.js
 
 [browser_editor_autocomplete_basic.js]
 [browser_editor_autocomplete_events.js]
 [browser_editor_autocomplete_js.js]
 [browser_editor_basic.js]
 [browser_editor_cursor.js]
 [browser_editor_find_again.js]
 [browser_editor_goto_line.js]
--- a/devtools/client/sourceeditor/test/browser_codemirror.js
+++ b/devtools/client/sourceeditor/test/browser_codemirror.js
@@ -1,15 +1,15 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const URI = "chrome://mochitests/content/browser/devtools/client/sourceeditor/test/codemirror.html";
+const URI = "chrome://mochitests/content/browser/devtools/client/sourceeditor/test/codemirror/codemirror.html";
 loadHelperScript("helper_codemirror_runner.js");
 
 function test() {
   requestLongerTimeout(3);
   waitForExplicitFinish();
 
   let tab = gBrowser.addTab();
   gBrowser.selectedTab = tab;
--- a/devtools/client/sourceeditor/test/browser_vimemacs.js
+++ b/devtools/client/sourceeditor/test/browser_vimemacs.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const URI = "chrome://mochitests/content/browser/devtools/client/sourceeditor/test/vimemacs.html";
+const URI = "chrome://mochitests/content/browser/devtools/client/sourceeditor/test/codemirror/vimemacs.html";
 loadHelperScript("helper_codemirror_runner.js");
 
 function test() {
   requestLongerTimeout(3);
   waitForExplicitFinish();
 
   let tab = gBrowser.addTab();
   gBrowser.selectedTab = tab;
rename from devtools/client/sourceeditor/test/codemirror.html
rename to devtools/client/sourceeditor/test/codemirror/codemirror.html
--- a/devtools/client/sourceeditor/test/codemirror.html
+++ b/devtools/client/sourceeditor/test/codemirror/codemirror.html
@@ -53,26 +53,26 @@
     <div style="border: 1px solid black; padding: 1px; max-width: 700px;">
       <div style="width: 0px;" id=progress><div style="padding: 3px;">Ran <span id="progress_ran">0</span><span id="progress_total"> of 0</span> tests</div></div>
     </div>
     <p id=status>Please enable JavaScript...</p>
     <div id=output></div>
 
     <div id=testground></div>
 
-    <script src="cm_driver.js"></script>
-    <script src="cm_test.js"></script>
-    <script src="cm_comment_test.js"></script>
-    <script src="cm_doc_test.js"></script>
-    <script src="cm_driver.js"></script>
-    <script src="cm_emacs_test.js"></script>
-    <script src="cm_mode_test.js"></script>
-    <script src="cm_mode_javascript_test.js"></script>
-    <script src="cm_multi_test.js"></script>
-    <script src="cm_search_test.js"></script>
+    <script src="driver.js"></script>
+    <script src="test.js"></script>
+    <script src="comment_test.js"></script>
+    <script src="doc_test.js"></script>
+    <script src="driver.js"></script>
+    <script src="emacs_test.js"></script>
+    <script src="mode_test.js"></script>
+    <script src="mode/javascript/test.js"></script>
+    <script src="multi_test.js"></script>
+    <script src="search_test.js"></script>
 
     <!-- VIM and Emacs mode tests are in vimemacs.html
     <script src="cm_sublime_test.js"></script>
     <script src="cm_vim_test.js"></script>
     <script src="cm_emacs_test.js"></script>
     -->
 
     <!-- These modes/addons are not used by Editor
rename from devtools/client/sourceeditor/test/cm_comment_test.js
rename to devtools/client/sourceeditor/test/codemirror/comment_test.js
rename from devtools/client/sourceeditor/test/cm_doc_test.js
rename to devtools/client/sourceeditor/test/codemirror/doc_test.js
rename from devtools/client/sourceeditor/test/cm_driver.js
rename to devtools/client/sourceeditor/test/codemirror/driver.js
rename from devtools/client/sourceeditor/test/cm_emacs_test.js
rename to devtools/client/sourceeditor/test/codemirror/emacs_test.js
rename from devtools/client/sourceeditor/test/cm_mode_javascript_test.js
rename to devtools/client/sourceeditor/test/codemirror/mode/javascript/test.js
rename from devtools/client/sourceeditor/test/cm_mode_test.css
rename to devtools/client/sourceeditor/test/codemirror/mode_test.css
rename from devtools/client/sourceeditor/test/cm_mode_test.js
rename to devtools/client/sourceeditor/test/codemirror/mode_test.js
rename from devtools/client/sourceeditor/test/cm_multi_test.js
rename to devtools/client/sourceeditor/test/codemirror/multi_test.js
rename from devtools/client/sourceeditor/test/cm_search_test.js
rename to devtools/client/sourceeditor/test/codemirror/search_test.js
rename from devtools/client/sourceeditor/test/cm_sublime_test.js
rename to devtools/client/sourceeditor/test/codemirror/sublime_test.js
rename from devtools/client/sourceeditor/test/cm_test.js
rename to devtools/client/sourceeditor/test/codemirror/test.js
rename from devtools/client/sourceeditor/test/cm_vim_test.js
rename to devtools/client/sourceeditor/test/codemirror/vim_test.js
rename from devtools/client/sourceeditor/test/vimemacs.html
rename to devtools/client/sourceeditor/test/codemirror/vimemacs.html
--- a/devtools/client/sourceeditor/test/vimemacs.html
+++ b/devtools/client/sourceeditor/test/codemirror/vimemacs.html
@@ -53,20 +53,20 @@
     <div style="border: 1px solid black; padding: 1px; max-width: 700px;">
       <div style="width: 0px;" id=progress><div style="padding: 3px;">Ran <span id="progress_ran">0</span><span id="progress_total"> of 0</span> tests</div></div>
     </div>
     <p id=status>Please enable JavaScript...</p>
     <div id=output></div>
 
     <div id=testground></div>
 
-    <script src="cm_driver.js"></script>
-    <script src="cm_sublime_test.js"></script>
-    <script src="cm_vim_test.js"></script>
-    <script src="cm_emacs_test.js"></script>
+    <script src="driver.js"></script>
+    <script src="sublime_test.js"></script>
+    <script src="vim_test.js"></script>
+    <script src="emacs_test.js"></script>
 
     <!-- Basic tests are in codemirror.html
     <script src="cm_driver.js"></script>
     <script src="cm_test.js"></script>
     <script src="cm_comment_test.js"></script>
     <script src="cm_doc_test.js"></script>
     <script src="cm_driver.js"></script>
     <script src="cm_emacs_test.js"></script>
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/heap-snapshot-file.js
@@ -0,0 +1,74 @@
+/* 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 protocol = require("devtools/server/protocol");
+const { method, Arg } = protocol;
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "DevToolsUtils",
+                         "devtools/shared/DevToolsUtils");
+loader.lazyRequireGetter(this, "OS", "resource://gre/modules/osfile.jsm", true);
+loader.lazyRequireGetter(this, "Task", "resource://gre/modules/Task.jsm", true);
+loader.lazyRequireGetter(this, "HeapSnapshotFileUtils",
+                         "devtools/shared/heapsnapshot/HeapSnapshotFileUtils");
+
+/**
+ * The HeapSnapshotFileActor handles transferring heap snapshot files from the
+ * server to the client. This has to be a global actor in the parent process
+ * because child processes are sandboxed and do not have access to the file
+ * system.
+ */
+exports.HeapSnapshotFileActor = protocol.ActorClass({
+  typeName: "heapSnapshotFile",
+
+  initialize: function (conn, parent) {
+    if (Services.appInfo &&
+        (Services.appInfo.processType !==
+         Services.appInfo.PROCESS_TYPE_DEFAULT)) {
+      const err = new Error("Attempt to create a HeapSnapshotFileActor in a " +
+                            "child process! The HeapSnapshotFileActor *MUST* " +
+                            "be in the parent process!");
+      DevToolsUtils.reportException(
+        "HeapSnapshotFileActor.prototype.initialize", err);
+      return;
+    }
+
+    protocol.Actor.prototype.initialize.call(this, conn, parent);
+  },
+
+  /**
+   * @see MemoryFront.prototype.transferHeapSnapshot
+   */
+  transferHeapSnapshot: method(Task.async(function* (snapshotId) {
+    const snapshotFilePath =
+          HeapSnapshotFileUtils.getHeapSnapshotTempFilePath(snapshotId);
+    if (!snapshotFilePath) {
+      throw new Error(`No heap snapshot with id: ${snapshotId}`);
+    }
+
+    const streamPromise = DevToolsUtils.openFileStream(snapshotFilePath);
+
+    const { size } = yield OS.File.stat(snapshotFilePath);
+    const bulkPromise = this.conn.startBulkSend({
+      actor: this.actorID,
+      type: "heap-snapshot",
+      length: size
+    });
+
+    const [bulk, stream] = yield Promise.all([bulkPromise, streamPromise]);
+
+    try {
+      yield bulk.copyFrom(stream);
+    } finally {
+      stream.close();
+    }
+  }), {
+    request: {
+      snapshotId: Arg(0, "string")
+    }
+  }),
+
+});
--- a/devtools/server/actors/memory.js
+++ b/devtools/server/actors/memory.js
@@ -1,28 +1,26 @@
 /* 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 { Cc, Ci, Cu, components } = require("chrome");
-const { openFileStream } = require("devtools/shared/DevToolsUtils");
 const protocol = require("devtools/server/protocol");
 const { method, RetVal, Arg, types } = protocol;
 const { Memory } = require("devtools/shared/shared/memory");
 const { actorBridge } = require("devtools/server/actors/common");
 loader.lazyRequireGetter(this, "events", "sdk/event/core");
 loader.lazyRequireGetter(this, "StackFrameCache",
                          "devtools/server/actors/utils/stack", true);
 loader.lazyRequireGetter(this, "FileUtils",
                          "resource://gre/modules/FileUtils.jsm", true);
 loader.lazyRequireGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm", true);
 loader.lazyRequireGetter(this, "Task", "resource://gre/modules/Task.jsm", true);
-loader.lazyRequireGetter(this, "OS", "resource://gre/modules/osfile.jsm", true);
 loader.lazyRequireGetter(this, "HeapSnapshotFileUtils",
                          "devtools/shared/heapsnapshot/HeapSnapshotFileUtils");
 loader.lazyRequireGetter(this, "ThreadSafeChromeUtils");
 
 types.addDictType("AllocationsRecordingOptions", {
   // The probability we sample any given allocation when recording
   // allocations. Must be between 0.0 and 1.0. Defaults to 1.0, or sampling
   // every allocation.
@@ -110,45 +108,16 @@ var MemoryActor = exports.MemoryActor = 
   saveHeapSnapshot: method(function () {
     return this.bridge.saveHeapSnapshot();
   }, {
     response: {
       snapshotId: RetVal("string")
     }
   }),
 
-  transferHeapSnapshot: method(Task.async(function* (snapshotId) {
-    const snapshotFilePath =
-      HeapSnapshotFileUtils.getHeapSnapshotTempFilePath(snapshotId);
-    if (!snapshotFilePath) {
-      throw new Error(`No heap snapshot with id: ${snapshotId}`);
-    }
-
-    const streamPromise = openFileStream(snapshotFilePath);
-
-    const { size } = yield OS.File.stat(snapshotFilePath);
-    const bulkPromise = this.conn.startBulkSend({
-      actor: this.actorID,
-      type: "heap-snapshot",
-      length: size
-    });
-
-    const [bulk, stream] = yield Promise.all([bulkPromise, streamPromise]);
-
-    try {
-      yield bulk.copyFrom(stream);
-    } finally {
-      stream.close();
-    }
-  }), {
-    request: {
-      snapshotId: Arg(0, "string")
-    }
-  }),
-
   takeCensus: actorBridge("takeCensus", {
     request: {},
     response: RetVal("json")
   }),
 
   startRecordingAllocations: actorBridge("startRecordingAllocations", {
     request: {
       options: Arg(0, "nullable:AllocationsRecordingOptions")
@@ -208,31 +177,33 @@ var MemoryActor = exports.MemoryActor = 
   _onAllocations: function (data) {
     if (this.conn.transport) {
       events.emit(this, "allocations", data);
     }
   },
 });
 
 exports.MemoryFront = protocol.FrontClass(MemoryActor, {
-  initialize: function(client, form) {
+  initialize: function(client, form, rootForm = null) {
     protocol.Front.prototype.initialize.call(this, client, form);
     this._client = client;
     this.actorID = form.memoryActor;
+    this.heapSnapshotFileActorID = rootForm
+      ? rootForm.heapSnapshotFileActor
+      : null;
     this.manage(this);
   },
 
   /**
    * Save a heap snapshot, transfer it from the server to the client if the
    * server and client do not share a file system, and return the local file
    * path to the heap snapshot.
    *
-   * NB: This will not work with sandboxed child processes, as they do not have
-   * access to the filesystem and the hep snapshot APIs do not support that use
-   * case yet.
+   * Note that this is safe to call for actors inside sandoxed child processes,
+   * as we jump through the correct IPDL hoops.
    *
    * @params Boolean options.forceCopy
    *         Always force a bulk data copy of the saved heap snapshot, even when
    *         the server and client share a file system.
    *
    * @returns Promise<String>
    */
   saveHeapSnapshot: protocol.custom(Task.async(function* (options = {}) {
@@ -253,18 +224,22 @@ exports.MemoryFront = protocol.FrontClas
    * heap snapshot file to the client. The path to the client's local file is
    * returned.
    *
    * @param {String} snapshotId
    *
    * @returns Promise<String>
    */
   transferHeapSnapshot: protocol.custom(function (snapshotId) {
+    if (!this.heapSnapshotFileActorID) {
+      throw new Error("MemoryFront initialized without a rootForm");
+    }
+
     const request = this._client.request({
-      to: this.actorID,
+      to: this.heapSnapshotFileActorID,
       type: "transferHeapSnapshot",
       snapshotId
     });
 
     return new Promise((resolve, reject) => {
       const outFilePath =
         HeapSnapshotFileUtils.getNewUniqueHeapSnapshotTempFilePath();
       const outFile = new FileUtils.File(outFilePath);
--- a/devtools/server/actors/moz.build
+++ b/devtools/server/actors/moz.build
@@ -21,16 +21,17 @@ DevToolsModules(
     'common.js',
     'csscoverage.js',
     'device.js',
     'director-manager.js',
     'director-registry.js',
     'eventlooplag.js',
     'framerate.js',
     'gcli.js',
+    'heap-snapshot-file.js',
     'highlighters.css',
     'highlighters.js',
     'inspector.js',
     'layout.js',
     'memory.js',
     'memprof.js',
     'monitor.js',
     'object.js',
--- a/devtools/server/actors/performance.js
+++ b/devtools/server/actors/performance.js
@@ -102,35 +102,48 @@ var PerformanceActor = exports.Performan
   connect: method(function (config) {
     this.bridge.connect({ systemClient: config.systemClient });
     return { traits: this.traits };
   }, {
     request: { options: Arg(0, "nullable:json") },
     response: RetVal("json")
   }),
 
+  canCurrentlyRecord: method(function() {
+    return this.bridge.canCurrentlyRecord();
+  }, {
+    response: { value: RetVal("json") }
+  }),
+
   startRecording: method(Task.async(function *(options={}) {
+    if (!this.bridge.canCurrentlyRecord().success) {
+      return null;
+    }
+
     let normalizedOptions = normalizePerformanceFeatures(options, this.traits.features);
     let recording = yield this.bridge.startRecording(normalizedOptions);
-
     this.manage(recording);
 
     return recording;
   }), {
     request: {
       options: Arg(0, "nullable:json"),
     },
-    response: RetVal("performance-recording"),
+    response: {
+      recording: RetVal("nullable:performance-recording")
+    }
   }),
 
   stopRecording: actorBridge("stopRecording", {
     request: {
       options: Arg(0, "performance-recording"),
     },
-    response: RetVal("performance-recording"),
+    response: {
+      recording: RetVal("performance-recording")
+    }
   }),
 
   isRecording: actorBridge("isRecording", {
     response: { isRecording: RetVal("boolean") }
   }),
 
   getRecordings: actorBridge("getRecordings", {
     response: { recordings: RetVal("array:performance-recording") }
--- a/devtools/server/main.js
+++ b/devtools/server/main.js
@@ -408,16 +408,21 @@ var DebuggerServer = {
       constructor: "DeviceActor",
       type: { global: true }
     });
     this.registerModule("devtools/server/actors/director-registry", {
       prefix: "directorRegistry",
       constructor: "DirectorRegistryActor",
       type: { global: true }
     });
+    this.registerModule("devtools/server/actors/heap-snapshot-file", {
+      prefix: "heapSnapshotFile",
+      constructor: "HeapSnapshotFileActor",
+      type: { global: true }
+    });
   },
 
   /**
    * Install tab actors in documents loaded in content childs
    */
   addChildActors: function () {
     // In case of apps being loaded in parent process, DebuggerServer is already
     // initialized and browser actors are already loaded,
--- a/devtools/server/tests/mochitest/memory-helpers.js
+++ b/devtools/server/tests/mochitest/memory-helpers.js
@@ -31,17 +31,17 @@ function startServerAndGetSelectedTabMem
 
       client.listTabs(response => {
         if (response.error) {
           reject(new Error(response.error + ": " + response.message));
           return;
         }
 
         var form = response.tabs[response.selected];
-        var memory = MemoryFront(client, form);
+        var memory = MemoryFront(client, form, response);
 
         resolve({ memory, client });
       });
     });
   });
 }
 
 function destroyServerAndFinish(client) {
--- a/devtools/server/tests/unit/head_dbg.js
+++ b/devtools/server/tests/unit/head_dbg.js
@@ -47,20 +47,31 @@ var loadSubScript = Cc[
  * @returns `run_test` function
  */
 function makeMemoryActorTest(testGeneratorFunction) {
   const TEST_GLOBAL_NAME = "test_MemoryActor";
 
   return function run_test() {
     do_test_pending();
     startTestDebuggerServer(TEST_GLOBAL_NAME).then(client => {
-      getTestTab(client, TEST_GLOBAL_NAME, function (tabForm) {
+      DebuggerServer.registerModule("devtools/server/actors/heap-snapshot-file", {
+        prefix: "heapSnapshotFile",
+        constructor: "HeapSnapshotFileActor",
+        type: { global: true }
+      });
+
+      getTestTab(client, TEST_GLOBAL_NAME, function (tabForm, rootForm) {
+        if (!tabForm || !rootForm) {
+          ok(false, "Could not attach to test tab: " + TEST_GLOBAL_NAME);
+          return;
+        }
+
         Task.spawn(function* () {
           try {
-            const memoryFront = new MemoryFront(client, tabForm);
+            const memoryFront = new MemoryFront(client, tabForm, rootForm);
             yield memoryFront.attach();
             yield* testGeneratorFunction(client, memoryFront);
             yield memoryFront.detach();
           } catch(err) {
             DevToolsUtils.reportException("makeMemoryActorTest", err);
             ok(false, "Got an error: " + err);
           }
 
@@ -289,17 +300,17 @@ function addTestGlobal(aName, aServer = 
 }
 
 // List the DebuggerClient |aClient|'s tabs, look for one whose title is
 // |aTitle|, and apply |aCallback| to the packet's entry for that tab.
 function getTestTab(aClient, aTitle, aCallback) {
   aClient.listTabs(function (aResponse) {
     for (let tab of aResponse.tabs) {
       if (tab.title === aTitle) {
-        aCallback(tab);
+        aCallback(tab, aResponse);
         return;
       }
     }
     aCallback(null);
   });
 }
 
 // Attach to |aClient|'s tab whose title is |aTitle|; pass |aCallback| the
--- a/devtools/shared/DevToolsUtils.js
+++ b/devtools/shared/DevToolsUtils.js
@@ -7,16 +7,17 @@
 /* General utilities used throughout devtools. */
 
 var { Ci, Cu, Cc, components } = require("chrome");
 var Services = require("Services");
 var promise = require("promise");
 
 loader.lazyRequireGetter(this, "FileUtils",
                          "resource://gre/modules/FileUtils.jsm", true);
+loader.lazyRequireGetter(this, "setTimeout", "Timer", true);
 
 /**
  * Turn the error |aError| into a string, without fail.
  */
 exports.safeErrorString = function safeErrorString(aError) {
   try {
     let errorString = aError.toString();
     if (typeof errorString == "string") {
@@ -128,16 +129,28 @@ exports.zip = function zip(a, b) {
  * @param object obj
  * @returns array
  */
 exports.entries = function entries(obj) {
   return Object.keys(obj).map(k => [k, obj[k]]);
 }
 
 /**
+ * Takes an array of 2-element arrays as key/values pairs and
+ * constructs an object using them.
+ */
+exports.toObject = function(arr) {
+  const obj = {};
+  for(let pair of arr) {
+    obj[pair[0]] = pair[1];
+  }
+  return obj;
+}
+
+/**
  * Composes the given functions into a single function, which will
  * apply the results of each function right-to-left, starting with
  * applying the given arguments to the right-most function.
  * `compose(foo, bar, baz)` === `args => foo(bar(baz(args)`
  *
  * @param ...function funcs
  * @returns function
  */
@@ -181,17 +194,17 @@ exports.waitForTick = function waitForTi
  *
  * @param number aDelay
  *        The amount of time to wait, in milliseconds.
  * @return Promise
  *         A promise that is resolved after the specified amount of time passes.
  */
 exports.waitForTime = function waitForTime(aDelay) {
   let deferred = promise.defer();
-  require("Timer").setTimeout(deferred.resolve, aDelay);
+  setTimeout(deferred.resolve, aDelay);
   return deferred.promise;
 };
 
 /**
  * Like Array.prototype.forEach, but doesn't cause jankiness when iterating over
  * very large arrays by yielding to the browser and continuing execution on the
  * next tick.
  *
--- a/devtools/shared/performance/recorder.js
+++ b/devtools/shared/performance/recorder.js
@@ -278,16 +278,35 @@ const PerformanceRecorder = exports.Perf
     let activeRecordings = this._recordings.filter(r => r.isRecording());
 
     if (activeRecordings.length) {
       events.emit(this, "timeline-data", eventName, eventData, activeRecordings);
     }
   },
 
   /**
+   * Checks whether or not recording is currently supported. At the moment,
+   * this is only influenced by private browsing mode and the profiler.
+   */
+  canCurrentlyRecord: function() {
+    let success = true;
+    let reasons = [];
+
+    if (!Profiler.canProfile()) {
+      success = false,
+      reasons.push("profiler-unavailable");
+    }
+
+    // Check other factors that will affect the possibility of successfully
+    // starting a recording here.
+
+    return { success, reasons };
+  },
+
+  /**
    * Begins a recording session
    *
    * @param boolean options.withMarkers
    * @param boolean options.withJITOptimizations
    * @param boolean options.withTicks
    * @param boolean options.withMemory
    * @param boolean options.withAllocations
    * @param boolean options.allocationsSampleProbability
--- a/devtools/shared/shared/profiler.js
+++ b/devtools/shared/shared/profiler.js
@@ -50,25 +50,34 @@ const ProfilerManager = (function () {
 
     // How many subscribers there
     _profilerStatusSubscribers: 0,
 
     /**
      * The nsIProfiler is target agnostic and interacts with the whole platform.
      * Therefore, special care needs to be given to make sure different profiler
      * consumers (i.e. "toolboxes") don't interfere with each other. Register
-     * the instance here.
+     * the profiler actor instances here.
+     *
+     * @param Profiler instance
+     *        A profiler actor class.
      */
     addInstance: function (instance) {
       consumers.add(instance);
 
       // Lazily register events
       this.registerEventListeners();
     },
 
+    /**
+     * Remove the profiler actor instances here.
+     *
+     * @param Profiler instance
+     *        A profiler actor class.
+     */
     removeInstance: function (instance) {
       consumers.delete(instance);
 
       if (this.length < 0) {
         let msg = "Somehow the number of started profilers is now negative.";
         DevToolsUtils.reportException("Profiler", msg);
       }
 
@@ -96,35 +105,46 @@ const ProfilerManager = (function () {
         features: options.features || DEFAULT_PROFILER_OPTIONS.features,
         threadFilters: options.threadFilters || DEFAULT_PROFILER_OPTIONS.threadFilters,
       };
 
       // The start time should be before any samples we might be
       // interested in.
       let currentTime = nsIProfilerModule.getElapsedTime();
 
-      nsIProfilerModule.StartProfiler(
-        config.entries,
-        config.interval,
-        config.features,
-        config.features.length,
-        config.threadFilters,
-        config.threadFilters.length
-      );
-      let { position, totalSize, generation } = this.getBufferInfo();
+      try {
+        nsIProfilerModule.StartProfiler(
+          config.entries,
+          config.interval,
+          config.features,
+          config.features.length,
+          config.threadFilters,
+          config.threadFilters.length
+        );
+      } catch (e) {
+        // For some reason, the profiler couldn't be started. This could happen,
+        // for example, when in private browsing mode.
+        Cu.reportError(`Could not start the profiler module: ${e.message}`);
+        return { started: false, reason: e, currentTime };
+      }
 
       this._updateProfilerStatusPolling();
+
+      let { position, totalSize, generation } = this.getBufferInfo();
       return { started: true, position, totalSize, generation, currentTime };
     },
 
+    /**
+     * Attempts to stop the nsIProfiler module.
+     */
     stop: function () {
       // Actually stop the profiler only if the last client has stopped profiling.
-      // Since this is used as a root actor, and the profiler module interacts with the
-      // whole platform, we need to avoid a case in which the profiler is stopped
-      // when there might be other clients still profiling.
+      // Since this is used as a root actor, and the profiler module interacts
+      // with the whole platform, we need to avoid a case in which the profiler
+      // is stopped when there might be other clients still profiling.
       if (this.length <= 1) {
         nsIProfilerModule.StopProfiler();
       }
       this._updateProfilerStatusPolling();
       return { started: false };
     },
 
     /**
@@ -301,17 +321,18 @@ const ProfilerManager = (function () {
       }
     },
 
     /**
      * Unregisters handlers for all system events.
      */
     unregisterEventListeners: function () {
       if (this._eventsRegistered) {
-        PROFILER_SYSTEM_EVENTS.forEach(eventName => Services.obs.removeObserver(this, eventName));
+        PROFILER_SYSTEM_EVENTS.forEach(eventName =>
+          Services.obs.removeObserver(this, eventName));
         this._eventsRegistered = false;
       }
     },
 
     /**
      * Takes an event name and additional data and emits them
      * through each profiler instance that is subscribed to the event.
      *
@@ -478,16 +499,24 @@ var Profiler = exports.Profiler = Class(
         response.push(e);
       }
     });
     return { registered: response };
   },
 });
 
 /**
+ * Checks whether or not the profiler module can currently run.
+ * @return boolean
+ */
+Profiler.canProfile = function() {
+  return nsIProfilerModule.CanProfile();
+};
+
+/**
  * JSON.stringify callback used in Profiler.prototype.observe.
  */
 function cycleBreaker(key, value) {
   if (key == "wrappedJSObject") {
     return undefined;
   }
   return value;
 }
--- a/dom/notification/Notification.cpp
+++ b/dom/notification/Notification.cpp
@@ -648,38 +648,65 @@ NotificationPermissionRequest::GetTypes(
 {
   nsTArray<nsString> emptyOptions;
   return nsContentPermissionUtils::CreatePermissionArray(NS_LITERAL_CSTRING("desktop-notification"),
                                                          NS_LITERAL_CSTRING("unused"),
                                                          emptyOptions,
                                                          aTypes);
 }
 
-class NotificationObserver : public nsIObserver
+// Observer that the alert service calls to do common tasks and/or dispatch to the
+// specific observer for the context e.g. main thread, worker, or service worker.
+class NotificationObserver final : public nsIObserver
 {
 public:
-  UniquePtr<NotificationRef> mNotificationRef;
+  nsCOMPtr<nsIObserver> mObserver;
+  nsCOMPtr<nsIPrincipal> mPrincipal;
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
 
-  explicit NotificationObserver(UniquePtr<NotificationRef> aRef)
-    : mNotificationRef(Move(aRef))
+  NotificationObserver(nsIObserver* aObserver, nsIPrincipal* aPrincipal)
+    : mObserver(aObserver), mPrincipal(aPrincipal)
   {
     AssertIsOnMainThread();
+    MOZ_ASSERT(mObserver);
+    MOZ_ASSERT(mPrincipal);
   }
 
 protected:
   virtual ~NotificationObserver()
   {
     AssertIsOnMainThread();
   }
 };
 
 NS_IMPL_ISUPPORTS(NotificationObserver, nsIObserver)
 
+class MainThreadNotificationObserver : public nsIObserver
+{
+public:
+  UniquePtr<NotificationRef> mNotificationRef;
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIOBSERVER
+
+  explicit MainThreadNotificationObserver(UniquePtr<NotificationRef> aRef)
+    : mNotificationRef(Move(aRef))
+  {
+    AssertIsOnMainThread();
+  }
+
+protected:
+  virtual ~MainThreadNotificationObserver()
+  {
+    AssertIsOnMainThread();
+  }
+};
+
+NS_IMPL_ISUPPORTS(MainThreadNotificationObserver, nsIObserver)
+
 NS_IMETHODIMP
 NotificationTask::Run()
 {
   AssertIsOnMainThread();
 
   // Get a pointer to notification before the notification takes ownership of
   // the ref (it owns itself temporarily, with ShowInternal() and
   // CloseInternal() passing on the ownership appropriately.)
@@ -979,24 +1006,24 @@ Notification::GetPrincipal()
     return mWorkerPrivate->GetPrincipal();
   } else {
     nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(GetOwner());
     NS_ENSURE_TRUE(sop, nullptr);
     return sop->GetPrincipal();
   }
 }
 
-class WorkerNotificationObserver final : public NotificationObserver
+class WorkerNotificationObserver final : public MainThreadNotificationObserver
 {
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_NSIOBSERVER
 
   explicit WorkerNotificationObserver(UniquePtr<NotificationRef> aRef)
-    : NotificationObserver(Move(aRef))
+    : MainThreadNotificationObserver(Move(aRef))
   {
     AssertIsOnMainThread();
     MOZ_ASSERT(mNotificationRef->GetNotification()->mWorkerPrivate);
   }
 
   void
   ForgetNotification()
   {
@@ -1012,17 +1039,17 @@ protected:
     MOZ_ASSERT(mNotificationRef);
     Notification* notification = mNotificationRef->GetNotification();
     if (notification) {
       notification->mObserver = nullptr;
     }
   }
 };
 
-NS_IMPL_ISUPPORTS_INHERITED0(WorkerNotificationObserver, NotificationObserver)
+NS_IMPL_ISUPPORTS_INHERITED0(WorkerNotificationObserver, MainThreadNotificationObserver)
 
 class ServiceWorkerNotificationObserver final : public nsIObserver
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
 
   ServiceWorkerNotificationObserver(const nsAString& aScope,
@@ -1120,16 +1147,35 @@ public:
   }
 };
 
 NS_IMETHODIMP
 NotificationObserver::Observe(nsISupports* aSubject, const char* aTopic,
                               const char16_t* aData)
 {
   AssertIsOnMainThread();
+
+  if (!strcmp("alertdisablecallback", aTopic)) {
+    nsCOMPtr<nsIPermissionManager> permissionManager =
+      mozilla::services::GetPermissionManager();
+    if (!permissionManager) {
+      return NS_ERROR_FAILURE;
+    }
+    permissionManager->RemoveFromPrincipal(mPrincipal, "desktop-notification");
+    return NS_OK;
+  }
+
+  return mObserver->Observe(aSubject, aTopic, aData);
+}
+
+NS_IMETHODIMP
+MainThreadNotificationObserver::Observe(nsISupports* aSubject, const char* aTopic,
+                                        const char16_t* aData)
+{
+  AssertIsOnMainThread();
   MOZ_ASSERT(mNotificationRef);
   Notification* notification = mNotificationRef->GetNotification();
   MOZ_ASSERT(notification);
   if (!strcmp("alertclickcallback", aTopic)) {
     nsCOMPtr<nsPIDOMWindow> window = notification->GetOwner();
     if (NS_WARN_IF(!window || !window->IsCurrentInnerWindow())) {
       // Window has been closed, this observer is not valid anymore
       return NS_ERROR_FAILURE;
@@ -1379,26 +1425,29 @@ Notification::ShowInternal()
     if (mWorkerPrivate) {
       // Scope better be set on ServiceWorker initiated requests.
       MOZ_ASSERT(!mWorkerPrivate->IsServiceWorker());
       // Keep a pointer so that the feature can tell the observer not to release
       // the notification.
       mObserver = new WorkerNotificationObserver(Move(ownership));
       observer = mObserver;
     } else {
-      observer = new NotificationObserver(Move(ownership));
+      observer = new MainThreadNotificationObserver(Move(ownership));
     }
   } else {
     // This observer does not care about the Notification. It will be released
     // at the end of this function.
     //
-    // The observer is wholly owned by the alerts service.
+    // The observer is wholly owned by the NotificationObserver passed to the alert service.
     observer = new ServiceWorkerNotificationObserver(mScope, GetPrincipal(), mID);
   }
   MOZ_ASSERT(observer);
+  nsCOMPtr<nsIObserver> alertObserver = new NotificationObserver(observer,
+                                                                 GetPrincipal());
+
 
 #ifdef MOZ_B2G
   nsCOMPtr<nsIAppNotificationService> appNotifier =
     do_GetService("@mozilla.org/system-alerts-service;1");
   if (appNotifier) {
     uint32_t appId = nsIScriptSecurityManager::UNKNOWN_APP_ID;
     if (mWorkerPrivate) {
       appId = mWorkerPrivate->GetPrincipal()->GetAppId();
@@ -1427,17 +1476,17 @@ Notification::ShowInternal()
         ops.mMozbehavior.mSoundFile = soundUrl;
 
         if (!ToJSValue(cx, ops, &val)) {
           NS_WARNING("Converting dict to object failed!");
           return;
         }
 
         appNotifier->ShowAppNotification(iconUrl, mTitle, mBody,
-                                         observer, val);
+                                         alertObserver, val);
         return;
       }
     }
   }
 #endif
 
   // In the case of IPC, the parent process uses the cookie to map to
   // nsIObserver. Thus the cookie must be unique to differentiate observers.
@@ -1458,17 +1507,17 @@ Notification::ShowInternal()
     NS_QueryNotificationCallbacks(nullptr, loadGroup, NS_GET_IID(nsILoadContext),
                                   getter_AddRefs(loadContext));
     inPrivateBrowsing = loadContext && loadContext->UsePrivateBrowsing();
   }
 
   nsAutoString alertName;
   GetAlertName(alertName);
   alertService->ShowAlertNotification(iconUrl, mTitle, mBody, true,
-                                      uniqueCookie, observer, alertName,
+                                      uniqueCookie, alertObserver, alertName,
                                       DirectionToString(mDir), mLang,
                                       mDataAsBase64, GetPrincipal(),
                                       inPrivateBrowsing);
 }
 
 /* static */ bool
 Notification::RequestPermissionEnabledForScope(JSContext* aCx, JSObject* /* unused */)
 {
--- a/dom/notification/Notification.h
+++ b/dom/notification/Notification.h
@@ -97,17 +97,17 @@ public:
  * dispatch a control runnable instead.
  *
  */
 class Notification : public DOMEventTargetHelper
 {
   friend class CloseNotificationRunnable;
   friend class NotificationTask;
   friend class NotificationPermissionRequest;
-  friend class NotificationObserver;
+  friend class MainThreadNotificationObserver;
   friend class NotificationStorageCallback;
   friend class ServiceWorkerNotificationObserver;
   friend class WorkerGetRunnable;
   friend class WorkerNotificationObserver;
 
 public:
   IMPL_EVENT_HANDLER(click)
   IMPL_EVENT_HANDLER(show)
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1,16 +1,17 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.AdjustConstants;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.DynamicToolbar.PinReason;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.TransitionsTracker;
 import org.mozilla.gecko.animation.ViewHelper;
@@ -857,16 +858,23 @@ public class BrowserApp extends GeckoApp
 
         JavaAddonManager.getInstance().init(appContext);
         mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
         mOrderedBroadcastHelper = new OrderedBroadcastHelper(appContext);
         mBrowserHealthReporter = new BrowserHealthReporter();
         mReadingListHelper = new ReadingListHelper(appContext, getProfile(), this);
         mAccountsHelper = new AccountsHelper(appContext, getProfile());
 
+        if (AppConstants.MOZ_INSTALL_TRACKING) {
+            final SharedPreferences prefs = GeckoSharedPrefs.forApp(this);
+            if (prefs.getBoolean(GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true)) {
+                AdjustConstants.getAdjustHelper().onCreate(this, AdjustConstants.MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN);
+            }
+        }
+
         if (AppConstants.MOZ_ANDROID_BEAM) {
             NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
             if (nfc != null) {
                 nfc.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
                     @Override
                     public NdefMessage createNdefMessage(NfcEvent event) {
                         Tab tab = Tabs.getInstance().getSelectedTab();
                         if (tab == null || tab.isPrivate()) {
@@ -1803,16 +1811,21 @@ public class BrowserApp extends GeckoApp
             Telemetry.addToHistogram("FENNEC_FAVICONS_COUNT", db.getCount(cr, "favicons"));
             Telemetry.addToHistogram("FENNEC_THUMBNAILS_COUNT", db.getCount(cr, "thumbnails"));
             Telemetry.addToHistogram("FENNEC_READING_LIST_COUNT", db.getReadingListAccessor().getCount(cr));
             Telemetry.addToHistogram("BROWSER_IS_USER_DEFAULT", (isDefaultBrowser(Intent.ACTION_VIEW) ? 1 : 0));
             Telemetry.addToHistogram("FENNEC_TABQUEUE_ENABLED", (TabQueueHelper.isTabQueueEnabled(BrowserApp.this) ? 1 : 0));
             if (Versions.feature16Plus) {
                 Telemetry.addToHistogram("BROWSER_IS_ASSIST_DEFAULT", (isDefaultBrowser(Intent.ACTION_ASSIST) ? 1 : 0));
             }
+
+            final SharedPreferences sharedPrefs = GeckoSharedPrefs.forApp(BrowserApp.this);
+            if (sharedPrefs.getBoolean(GeckoPreferences.PREFS_OPEN_URLS_IN_PRIVATE, false)) {
+                Telemetry.addToHistogram("FENNEC_OPEN_URLS_IN_PRIVATE", 1);
+            }
         } else if ("Updater:Launch".equals(event)) {
             handleUpdaterLaunch();
         } else {
             super.handleMessage(event, message, callback);
         }
     }
 
     private void getFaviconFromCache(final EventCallback callback, final String url) {
--- a/mobile/android/base/GeckoApplication.java
+++ b/mobile/android/base/GeckoApplication.java
@@ -1,16 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
-import org.mozilla.gecko.AdjustConstants;
-import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.LocalBrowserDB;
 import org.mozilla.gecko.home.HomePanelsManager;
 import org.mozilla.gecko.lwt.LightweightTheme;
 import org.mozilla.gecko.mdns.MulticastDNSManager;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.HardwareUtils;
@@ -150,20 +148,16 @@ public class GeckoApplication extends Ap
                 // Note that we don't use the profile directory -- we
                 // send operations to the ContentProvider, which does
                 // its own thing.
                 return new LocalBrowserDB(profileName);
             }
         });
 
         super.onCreate();
-
-        if (AppConstants.MOZ_INSTALL_TRACKING) {
-            AdjustConstants.getAdjustHelper().onCreate(this, AdjustConstants.MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN);
-        }
     }
 
     public boolean isApplicationInBackground() {
         return mInBackground;
     }
 
     public LightweightTheme getLightweightTheme() {
         return mLightweightTheme;
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -574,16 +574,18 @@ var BrowserApp = {
 
       InitLater(() => Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""));
       InitLater(() => Messaging.sendRequest({ type: "Gecko:DelayedStartup" }));
 
       if (AppConstants.NIGHTLY_BUILD) {
         InitLater(() => ShumwayUtils.init(), window, "ShumwayUtils");
         InitLater(() => Telemetry.addData("TRACKING_PROTECTION_ENABLED",
             Services.prefs.getBoolPref("privacy.trackingprotection.enabled")));
+        InitLater(() => Telemetry.addData("TRACKING_PROTECTION_PBM_DISABLED",
+            !Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled")));
         InitLater(() => WebcompatReporter.init());
       }
 
       InitLater(() => LightWeightThemeWebInstaller.init());
       InitLater(() => SpatialNavigation.init(BrowserApp.deck, null), window, "SpatialNavigation");
       InitLater(() => CastingApps.init(), window, "CastingApps");
       InitLater(() => Services.search.init(), Services, "search");
       InitLater(() => DownloadNotifications.init(), window, "DownloadNotifications");
--- a/mobile/android/confvars.sh
+++ b/mobile/android/confvars.sh
@@ -114,10 +114,12 @@ MOZ_ADDON_SIGNING=1
 
 # Enable the Switchboard A/B framework code.
 # Note: The framework is always included in the app. This flag controls
 # usage of the framework.
 if test "$NIGHTLY_BUILD"; then
   MOZ_SWITCHBOARD=1
 fi
 
-# Use native Firefox Accounts UI regardless of channel.
+# Use native Firefox Accounts UI after Nightly.
+if ! test "$NIGHTLY_BUILD"; then
 MOZ_ANDROID_NATIVE_ACCOUNT_UI=1
+fi
--- a/mobile/android/tests/browser/robocop/StringHelper.java
+++ b/mobile/android/tests/browser/robocop/StringHelper.java
@@ -356,17 +356,17 @@ public class StringHelper {
                 CONTEXT_MENU_EDIT_SITE_SETTINGS,
                 CONTEXT_MENU_ADD_TO_HOME_SCREEN
         };
 
         TITLE_PLACE_HOLDER = res.getString(R.string.url_bar_default_text);
 
         // Import strings
         IMPORT = res.getString(R.string.bookmarkhistory_button_import);
-        BOOKMARKS = res.getString(R.string.bookmark);
+        BOOKMARKS = res.getString(R.string.bookmarks_title);
 
         // Settings menu strings
         // Section labels - ordered as found in the settings menu
         CUSTOMIZE_SECTION_LABEL = res.getString(R.string.pref_category_customize);
         DISPLAY_SECTION_LABEL = res.getString(R.string.pref_category_display);
         PRIVACY_SECTION_LABEL = res.getString(R.string.pref_category_privacy_short);
         MOZILLA_SECTION_LABEL = res.getString(R.string.pref_category_vendor);
         DEVELOPER_TOOLS_SECTION_LABEL = res.getString(R.string.pref_category_devtools);
--- a/mobile/android/tests/browser/robocop/testSettingsMenuItems.java
+++ b/mobile/android/tests/browser/robocop/testSettingsMenuItems.java
@@ -147,17 +147,16 @@ public class testSettingsMenuItems exten
                 "The Settings menu did not load", mStringHelper.SETTINGS_LABEL);
 
         // Dismiss the Settings screen and verify that the view is returned to about:home page
         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),
                 "The Settings menu did not load", mStringHelper.SETTINGS_LABEL);
 
         checkForSync(mDevice);
 
         checkMenuHierarchy(settingsMenuItems);
--- a/services/datareporting/datareporting-prefs.js
+++ b/services/datareporting/datareporting-prefs.js
@@ -1,12 +1,13 @@
 /* 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/. */
 
 pref("datareporting.policy.dataSubmissionEnabled", true);
+pref("datareporting.policy.dataSubmissionEnabled.v2", true);
 pref("datareporting.policy.firstRunTime", "0");
 pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "0");
 pref("datareporting.policy.dataSubmissionPolicyAcceptedVersion", 0);
 pref("datareporting.policy.dataSubmissionPolicyBypassNotification", false);
 pref("datareporting.policy.currentPolicyVersion", 2);
 pref("datareporting.policy.minimumPolicyVersion", 1);
 pref("datareporting.policy.minimumPolicyVersion.channel-beta", 2);
--- a/services/datareporting/policy.jsm
+++ b/services/datareporting/policy.jsm
@@ -373,16 +373,26 @@ this.DataReportingPolicy.prototype = Obj
     // Default is true because we are opt-out.
     return this._prefs.get("dataSubmissionEnabled", true);
   },
 
   set dataSubmissionEnabled(value) {
     this._prefs.set("dataSubmissionEnabled", !!value);
   },
 
+  /**
+   * Whether submission of data is allowed for v2.
+   *
+   * This is used to gently turn off data submission for FHR v2 in Firefox 42+.
+   */
+  get dataSubmissionEnabledV2() {
+    // Default is true because we are opt-out.
+    return this._prefs.get("dataSubmissionEnabled.v2", true);
+  },
+
   get currentPolicyVersion() {
     return this._prefs.get("currentPolicyVersion", DATAREPORTING_POLICY_VERSION);
   },
 
   /**
    * The minimum policy version which for dataSubmissionPolicyAccepted to
    * to be valid.
    */
@@ -643,17 +653,17 @@ this.DataReportingPolicy.prototype = Obj
    *
    * Typically this function is called automatically by the background polling.
    * But, it can safely be called manually as needed.
    */
   checkStateAndTrigger: function checkStateAndTrigger() {
     // If the master data submission kill switch is toggled, we have nothing
     // to do. We don't notify about data policies because this would have
     // no effect.
-    if (!this.dataSubmissionEnabled) {
+    if (!this.dataSubmissionEnabled || !this.dataSubmissionEnabledV2) {
       this._log.debug("Data submission is disabled. Doing nothing.");
       return;
     }
 
     let now = this.now();
     let nowT = now.getTime();
     let nextSubmissionDate = this.nextDataSubmissionDate;
 
--- a/services/sync/tests/unit/test_fxa_node_reassignment.js
+++ b/services/sync/tests/unit/test_fxa_node_reassignment.js
@@ -1,321 +1,321 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-_("Test that node reassignment happens correctly using the FxA identity mgr.");
-// The node-reassignment logic is quite different for FxA than for the legacy
-// provider.  In particular, there's no special request necessary for
-// reassignment - it comes from the token server - so we need to ensure the
-// Fxa cluster manager grabs a new token.
-
-Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://services-common/rest.js");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/service.js");
-Cu.import("resource://services-sync/status.js");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://testing-common/services/sync/rotaryengine.js");
-Cu.import("resource://services-sync/browserid_identity.js");
-Cu.import("resource://testing-common/services/sync/utils.js");
-
-Service.engineManager.clear();
-
-function run_test() {
-  Log.repository.getLogger("Sync.AsyncResource").level = Log.Level.Trace;
-  Log.repository.getLogger("Sync.ErrorHandler").level  = Log.Level.Trace;
-  Log.repository.getLogger("Sync.Resource").level      = Log.Level.Trace;
-  Log.repository.getLogger("Sync.RESTRequest").level   = Log.Level.Trace;
-  Log.repository.getLogger("Sync.Service").level       = Log.Level.Trace;
-  Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
-  initTestLogging();
-
-  Service.engineManager.register(RotaryEngine);
-
-  // Setup the FxA identity manager and cluster manager.
-  Status.__authManager = Service.identity = new BrowserIDManager();
-  Service._clusterManager = Service.identity.createClusterManager(Service);
-
-  // None of the failures in this file should result in a UI error.
-  function onUIError() {
-    do_throw("Errors should not be presented in the UI.");
-  }
-  Svc.Obs.add("weave:ui:login:error", onUIError);
-  Svc.Obs.add("weave:ui:sync:error", onUIError);
-
-  run_next_test();
-}
-
-
-// API-compatible with SyncServer handler. Bind `handler` to something to use
-// as a ServerCollection handler.
-function handleReassign(handler, req, resp) {
-  resp.setStatusLine(req.httpVersion, 401, "Node reassignment");
-  resp.setHeader("Content-Type", "application/json");
-  let reassignBody = JSON.stringify({error: "401inator in place"});
-  resp.bodyOutputStream.write(reassignBody, reassignBody.length);
-}
-
-var numTokenRequests = 0;
-
-function prepareServer(cbAfterTokenFetch) {
-  let config = makeIdentityConfig({username: "johndoe"});
-  let server = new SyncServer();
-  server.registerUser("johndoe");
-  server.start();
-
-  // Set the token endpoint for the initial token request that's done implicitly
-  // via configureIdentity.
-  config.fxaccount.token.endpoint = server.baseURI + "1.1/johndoe";
-  // And future token fetches will do magic around numReassigns.
-  let numReassigns = 0;
-  return configureIdentity(config).then(() => {
-    Service.identity._tokenServerClient = {
-      getTokenFromBrowserIDAssertion: function(uri, assertion, cb) {
-        // Build a new URL with trailing zeros for the SYNC_VERSION part - this
-        // will still be seen as equivalent by the test server, but different
-        // by sync itself.
-        numReassigns += 1;
-        let trailingZeros = new Array(numReassigns + 1).join('0');
-        let token = config.fxaccount.token;
-        token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe";
-        token.uid = config.username;
-        numTokenRequests += 1;
-        cb(null, token);
-        if (cbAfterTokenFetch) {
-          cbAfterTokenFetch();
-        }
-      },
-    };
-    Service.clusterURL = config.fxaccount.token.endpoint;
-    return server;
-  });
-}
-
-function getReassigned() {
-  try {
-    return Services.prefs.getBoolPref("services.sync.lastSyncReassigned");
-  } catch (ex if (ex.result == Cr.NS_ERROR_UNEXPECTED)) {
-    return false;
-  } catch (ex) {
-    do_throw("Got exception retrieving lastSyncReassigned: " +
-             Utils.exceptionStr(ex));
-  }
-}
-
-/**
- * Make a test request to `url`, then watch the result of two syncs
- * to ensure that a node request was made.
- * Runs `between` between the two. This can be used to undo deliberate failure
- * setup, detach observers, etc.
- */
-function syncAndExpectNodeReassignment(server, firstNotification, between,
-                                       secondNotification, url) {
-  _("Starting syncAndExpectNodeReassignment\n");
-  let deferred = Promise.defer();
-  function onwards() {
-    let numTokenRequestsBefore;
-    function onFirstSync() {
-      _("First sync completed.");
-      Svc.Obs.remove(firstNotification, onFirstSync);
-      Svc.Obs.add(secondNotification, onSecondSync);
-
-      do_check_eq(Service.clusterURL, "");
-
-      // Track whether we fetched a new token.
-      numTokenRequestsBefore = numTokenRequests;
-
-      // Allow for tests to clean up error conditions.
-      between();
-    }
-    function onSecondSync() {
-      _("Second sync completed.");
-      Svc.Obs.remove(secondNotification, onSecondSync);
-      Service.scheduler.clearSyncTriggers();
-
-      // Make absolutely sure that any event listeners are done with their work
-      // before we proceed.
-      waitForZeroTimer(function () {
-        _("Second sync nextTick.");
-        do_check_eq(numTokenRequests, numTokenRequestsBefore + 1, "fetched a new token");
-        Service.startOver();
-        server.stop(deferred.resolve);
-      });
-    }
-
-    Svc.Obs.add(firstNotification, onFirstSync);
-    Service.sync();
-  }
-
-  // Make sure that it works!
-  _("Making request to " + url + " which should 401");
-  let request = new RESTRequest(url);
-  request.get(function () {
-    do_check_eq(request.response.status, 401);
-    Utils.nextTick(onwards);
-  });
-  yield deferred.promise;
-}
-
-add_task(function test_momentary_401_engine() {
-  _("Test a failure for engine URLs that's resolved by reassignment.");
-  let server = yield prepareServer();
-  let john   = server.user("johndoe");
-
-  _("Enabling the Rotary engine.");
-  let engine = Service.engineManager.get("rotary");
-  engine.enabled = true;
-
-  // We need the server to be correctly set up prior to experimenting. Do this
-  // through a sync.
-  let global = {syncID: Service.syncID,
-                storageVersion: STORAGE_VERSION,
-                rotary: {version: engine.version,
-                         syncID:  engine.syncID}}
-  john.createCollection("meta").insert("global", global);
-
-  _("First sync to prepare server contents.");
-  Service.sync();
-
-  _("Setting up Rotary collection to 401.");
-  let rotary = john.createCollection("rotary");
-  let oldHandler = rotary.collectionHandler;
-  rotary.collectionHandler = handleReassign.bind(this, undefined);
-
-  // We want to verify that the clusterURL pref has been cleared after a 401
-  // inside a sync. Flag the Rotary engine to need syncing.
-  john.collection("rotary").timestamp += 1000;
-
-  function between() {
-    _("Undoing test changes.");
-    rotary.collectionHandler = oldHandler;
-
-    function onLoginStart() {
-      // lastSyncReassigned shouldn't be cleared until a sync has succeeded.
-      _("Ensuring that lastSyncReassigned is still set at next sync start.");
-      Svc.Obs.remove("weave:service:login:start", onLoginStart);
-      do_check_true(getReassigned());
-    }
-
-    _("Adding observer that lastSyncReassigned is still set on login.");
-    Svc.Obs.add("weave:service:login:start", onLoginStart);
-  }
-
-  yield syncAndExpectNodeReassignment(server,
-                                      "weave:service:sync:finish",
-                                      between,
-                                      "weave:service:sync:finish",
-                                      Service.storageURL + "rotary");
-});
-
-// This test ends up being a failing info fetch *after we're already logged in*.
-add_task(function test_momentary_401_info_collections_loggedin() {
-  _("Test a failure for info/collections after login that's resolved by reassignment.");
-  let server = yield prepareServer();
-
-  _("First sync to prepare server contents.");
-  Service.sync();
-
-  _("Arrange for info/collections to return a 401.");
-  let oldHandler = server.toplevelHandlers.info;
-  server.toplevelHandlers.info = handleReassign;
-
-  function undo() {
-    _("Undoing test changes.");
-    server.toplevelHandlers.info = oldHandler;
-  }
-
-  do_check_true(Service.isLoggedIn, "already logged in");
-
-  yield syncAndExpectNodeReassignment(server,
-                                      "weave:service:sync:error",
-                                      undo,
-                                      "weave:service:sync:finish",
-                                      Service.infoURL);
-});
-
-// This test ends up being a failing info fetch *before we're logged in*.
-// In this case we expect to recover during the login phase - so the first
-// sync succeeds.
-add_task(function test_momentary_401_info_collections_loggedout() {
-  _("Test a failure for info/collections before login that's resolved by reassignment.");
-
-  let oldHandler;
-  let sawTokenFetch = false;
-
-  function afterTokenFetch() {
-    // After a single token fetch, we undo our evil handleReassign hack, so
-    // the next /info request returns the collection instead of a 401
-    server.toplevelHandlers.info = oldHandler;
-    sawTokenFetch = true;
-  }
-
-  let server = yield prepareServer(afterTokenFetch);
-
-  // Return a 401 for the next /info request - it will be reset immediately
-  // after a new token is fetched.
-  oldHandler = server.toplevelHandlers.info
-  server.toplevelHandlers.info = handleReassign;
-
-  do_check_false(Service.isLoggedIn, "not already logged in");
-
-  Service.sync();
-  do_check_eq(Status.sync, SYNC_SUCCEEDED, "sync succeeded");
-  // sync was successful - check we grabbed a new token.
-  do_check_true(sawTokenFetch, "a new token was fetched by this test.")
-  // and we are done.
-  Service.startOver();
-  let deferred = Promise.defer();
-  server.stop(deferred.resolve);
-  yield deferred.promise;
-});
-
-// This test ends up being a failing meta/global fetch *after we're already logged in*.
-add_task(function test_momentary_401_storage_loggedin() {
-  _("Test a failure for any storage URL after login that's resolved by" +
-    "reassignment.");
-  let server = yield prepareServer();
-
-  _("First sync to prepare server contents.");
-  Service.sync();
-
-  _("Arrange for meta/global to return a 401.");
-  let oldHandler = server.toplevelHandlers.storage;
-  server.toplevelHandlers.storage = handleReassign;
-
-  function undo() {
-    _("Undoing test changes.");
-    server.toplevelHandlers.storage = oldHandler;
-  }
-
-  do_check_true(Service.isLoggedIn, "already logged in");
-
-  yield syncAndExpectNodeReassignment(server,
-                                      "weave:service:sync:error",
-                                      undo,
-                                      "weave:service:sync:finish",
-                                      Service.storageURL + "meta/global");
-});
-
-// This test ends up being a failing meta/global fetch *before we've logged in*.
-add_task(function test_momentary_401_storage_loggedout() {
-  _("Test a failure for any storage URL before login, not just engine parts. " +
-    "Resolved by reassignment.");
-  let server = yield prepareServer();
-
-  // Return a 401 for all storage requests.
-  let oldHandler = server.toplevelHandlers.storage;
-  server.toplevelHandlers.storage = handleReassign;
-
-  function undo() {
-    _("Undoing test changes.");
-    server.toplevelHandlers.storage = oldHandler;
-  }
-
-  do_check_false(Service.isLoggedIn, "already logged in");
-
-  yield syncAndExpectNodeReassignment(server,
-                                      "weave:service:login:error",
-                                      undo,
-                                      "weave:service:sync:finish",
-                                      Service.storageURL + "meta/global");
-});
-
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+_("Test that node reassignment happens correctly using the FxA identity mgr.");
+// The node-reassignment logic is quite different for FxA than for the legacy
+// provider.  In particular, there's no special request necessary for
+// reassignment - it comes from the token server - so we need to ensure the
+// Fxa cluster manager grabs a new token.
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-common/rest.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/status.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/rotaryengine.js");
+Cu.import("resource://services-sync/browserid_identity.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+
+Service.engineManager.clear();
+
+function run_test() {
+  Log.repository.getLogger("Sync.AsyncResource").level = Log.Level.Trace;
+  Log.repository.getLogger("Sync.ErrorHandler").level  = Log.Level.Trace;
+  Log.repository.getLogger("Sync.Resource").level      = Log.Level.Trace;
+  Log.repository.getLogger("Sync.RESTRequest").level   = Log.Level.Trace;
+  Log.repository.getLogger("Sync.Service").level       = Log.Level.Trace;
+  Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
+  initTestLogging();
+
+  Service.engineManager.register(RotaryEngine);
+
+  // Setup the FxA identity manager and cluster manager.
+  Status.__authManager = Service.identity = new BrowserIDManager();
+  Service._clusterManager = Service.identity.createClusterManager(Service);
+
+  // None of the failures in this file should result in a UI error.
+  function onUIError() {
+    do_throw("Errors should not be presented in the UI.");
+  }
+  Svc.Obs.add("weave:ui:login:error", onUIError);
+  Svc.Obs.add("weave:ui:sync:error", onUIError);
+
+  run_next_test();
+}
+
+
+// API-compatible with SyncServer handler. Bind `handler` to something to use
+// as a ServerCollection handler.
+function handleReassign(handler, req, resp) {
+  resp.setStatusLine(req.httpVersion, 401, "Node reassignment");
+  resp.setHeader("Content-Type", "application/json");
+  let reassignBody = JSON.stringify({error: "401inator in place"});
+  resp.bodyOutputStream.write(reassignBody, reassignBody.length);
+}
+
+var numTokenRequests = 0;
+
+function prepareServer(cbAfterTokenFetch) {
+  let config = makeIdentityConfig({username: "johndoe"});
+  let server = new SyncServer();
+  server.registerUser("johndoe");
+  server.start();
+
+  // Set the token endpoint for the initial token request that's done implicitly
+  // via configureIdentity.
+  config.fxaccount.token.endpoint = server.baseURI + "1.1/johndoe";
+  // And future token fetches will do magic around numReassigns.
+  let numReassigns = 0;
+  return configureIdentity(config).then(() => {
+    Service.identity._tokenServerClient = {
+      getTokenFromBrowserIDAssertion: function(uri, assertion, cb) {
+        // Build a new URL with trailing zeros for the SYNC_VERSION part - this
+        // will still be seen as equivalent by the test server, but different
+        // by sync itself.
+        numReassigns += 1;
+        let trailingZeros = new Array(numReassigns + 1).join('0');
+        let token = config.fxaccount.token;
+        token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe";
+        token.uid = config.username;
+        numTokenRequests += 1;
+        cb(null, token);
+        if (cbAfterTokenFetch) {
+          cbAfterTokenFetch();
+        }
+      },
+    };
+    Service.clusterURL = config.fxaccount.token.endpoint;
+    return server;
+  });
+}
+
+function getReassigned() {
+  try {
+    return Services.prefs.getBoolPref("services.sync.lastSyncReassigned");
+  } catch (ex if (ex.result == Cr.NS_ERROR_UNEXPECTED)) {
+    return false;
+  } catch (ex) {
+    do_throw("Got exception retrieving lastSyncReassigned: " +
+             Utils.exceptionStr(ex));
+  }
+}
+
+/**
+ * Make a test request to `url`, then watch the result of two syncs
+ * to ensure that a node request was made.
+ * Runs `between` between the two. This can be used to undo deliberate failure
+ * setup, detach observers, etc.
+ */
+function syncAndExpectNodeReassignment(server, firstNotification, between,
+                                       secondNotification, url) {
+  _("Starting syncAndExpectNodeReassignment\n");
+  let deferred = Promise.defer();
+  function onwards() {
+    let numTokenRequestsBefore;
+    function onFirstSync() {
+      _("First sync completed.");
+      Svc.Obs.remove(firstNotification, onFirstSync);
+      Svc.Obs.add(secondNotification, onSecondSync);
+
+      do_check_eq(Service.clusterURL, "");
+
+      // Track whether we fetched a new token.
+      numTokenRequestsBefore = numTokenRequests;
+
+      // Allow for tests to clean up error conditions.
+      between();
+    }
+    function onSecondSync() {
+      _("Second sync completed.");
+      Svc.Obs.remove(secondNotification, onSecondSync);
+      Service.scheduler.clearSyncTriggers();
+
+      // Make absolutely sure that any event listeners are done with their work
+      // before we proceed.
+      waitForZeroTimer(function () {
+        _("Second sync nextTick.");
+        do_check_eq(numTokenRequests, numTokenRequestsBefore + 1, "fetched a new token");
+        Service.startOver();
+        server.stop(deferred.resolve);
+      });
+    }
+
+    Svc.Obs.add(firstNotification, onFirstSync);
+    Service.sync();
+  }
+
+  // Make sure that it works!
+  _("Making request to " + url + " which should 401");
+  let request = new RESTRequest(url);
+  request.get(function () {
+    do_check_eq(request.response.status, 401);
+    Utils.nextTick(onwards);
+  });
+  yield deferred.promise;
+}
+
+add_task(function test_momentary_401_engine() {
+  _("Test a failure for engine URLs that's resolved by reassignment.");
+  let server = yield prepareServer();
+  let john   = server.user("johndoe");
+
+  _("Enabling the Rotary engine.");
+  let engine = Service.engineManager.get("rotary");
+  engine.enabled = true;
+
+  // We need the server to be correctly set up prior to experimenting. Do this
+  // through a sync.
+  let global = {syncID: Service.syncID,
+                storageVersion: STORAGE_VERSION,
+                rotary: {version: engine.version,
+                         syncID:  engine.syncID}}
+  john.createCollection("meta").insert("global", global);
+
+  _("First sync to prepare server contents.");
+  Service.sync();
+
+  _("Setting up Rotary collection to 401.");
+  let rotary = john.createCollection("rotary");
+  let oldHandler = rotary.collectionHandler;
+  rotary.collectionHandler = handleReassign.bind(this, undefined);
+
+  // We want to verify that the clusterURL pref has been cleared after a 401
+  // inside a sync. Flag the Rotary engine to need syncing.
+  john.collection("rotary").timestamp += 1000;
+
+  function between() {
+    _("Undoing test changes.");
+    rotary.collectionHandler = oldHandler;
+
+    function onLoginStart() {
+      // lastSyncReassigned shouldn't be cleared until a sync has succeeded.
+      _("Ensuring that lastSyncReassigned is still set at next sync start.");
+      Svc.Obs.remove("weave:service:login:start", onLoginStart);
+      do_check_true(getReassigned());
+    }
+
+    _("Adding observer that lastSyncReassigned is still set on login.");
+    Svc.Obs.add("weave:service:login:start", onLoginStart);
+  }
+
+  yield syncAndExpectNodeReassignment(server,
+                                      "weave:service:sync:finish",
+                                      between,
+                                      "weave:service:sync:finish",
+                                      Service.storageURL + "rotary");
+});
+
+// This test ends up being a failing info fetch *after we're already logged in*.
+add_task(function test_momentary_401_info_collections_loggedin() {
+  _("Test a failure for info/collections after login that's resolved by reassignment.");
+  let server = yield prepareServer();
+
+  _("First sync to prepare server contents.");
+  Service.sync();
+
+  _("Arrange for info/collections to return a 401.");
+  let oldHandler = server.toplevelHandlers.info;
+  server.toplevelHandlers.info = handleReassign;
+
+  function undo() {
+    _("Undoing test changes.");
+    server.toplevelHandlers.info = oldHandler;
+  }
+
+  do_check_true(Service.isLoggedIn, "already logged in");
+
+  yield syncAndExpectNodeReassignment(server,
+                                      "weave:service:sync:error",
+                                      undo,
+                                      "weave:service:sync:finish",
+                                      Service.infoURL);
+});
+
+// This test ends up being a failing info fetch *before we're logged in*.
+// In this case we expect to recover during the login phase - so the first
+// sync succeeds.
+add_task(function test_momentary_401_info_collections_loggedout() {
+  _("Test a failure for info/collections before login that's resolved by reassignment.");
+
+  let oldHandler;
+  let sawTokenFetch = false;
+
+  function afterTokenFetch() {
+    // After a single token fetch, we undo our evil handleReassign hack, so
+    // the next /info request returns the collection instead of a 401
+    server.toplevelHandlers.info = oldHandler;
+    sawTokenFetch = true;
+  }
+
+  let server = yield prepareServer(afterTokenFetch);
+
+  // Return a 401 for the next /info request - it will be reset immediately
+  // after a new token is fetched.
+  oldHandler = server.toplevelHandlers.info
+  server.toplevelHandlers.info = handleReassign;
+
+  do_check_false(Service.isLoggedIn, "not already logged in");
+
+  Service.sync();
+  do_check_eq(Status.sync, SYNC_SUCCEEDED, "sync succeeded");
+  // sync was successful - check we grabbed a new token.
+  do_check_true(sawTokenFetch, "a new token was fetched by this test.")
+  // and we are done.
+  Service.startOver();
+  let deferred = Promise.defer();
+  server.stop(deferred.resolve);
+  yield deferred.promise;
+});
+
+// This test ends up being a failing meta/global fetch *after we're already logged in*.
+add_task(function test_momentary_401_storage_loggedin() {
+  _("Test a failure for any storage URL after login that's resolved by" +
+    "reassignment.");
+  let server = yield prepareServer();
+
+  _("First sync to prepare server contents.");
+  Service.sync();
+
+  _("Arrange for meta/global to return a 401.");
+  let oldHandler = server.toplevelHandlers.storage;
+  server.toplevelHandlers.storage = handleReassign;
+
+  function undo() {
+    _("Undoing test changes.");
+    server.toplevelHandlers.storage = oldHandler;
+  }
+
+  do_check_true(Service.isLoggedIn, "already logged in");
+
+  yield syncAndExpectNodeReassignment(server,
+                                      "weave:service:sync:error",
+                                      undo,
+                                      "weave:service:sync:finish",
+                                      Service.storageURL + "meta/global");
+});
+
+// This test ends up being a failing meta/global fetch *before we've logged in*.
+add_task(function test_momentary_401_storage_loggedout() {
+  _("Test a failure for any storage URL before login, not just engine parts. " +
+    "Resolved by reassignment.");
+  let server = yield prepareServer();
+
+  // Return a 401 for all storage requests.
+  let oldHandler = server.toplevelHandlers.storage;
+  server.toplevelHandlers.storage = handleReassign;
+
+  function undo() {
+    _("Undoing test changes.");
+    server.toplevelHandlers.storage = oldHandler;
+  }
+
+  do_check_false(Service.isLoggedIn, "already logged in");
+
+  yield syncAndExpectNodeReassignment(server,
+                                      "weave:service:login:error",
+                                      undo,
+                                      "weave:service:sync:finish",
+                                      Service.storageURL + "meta/global");
+});
+
--- a/services/sync/tests/unit/test_fxa_service_cluster.js
+++ b/services/sync/tests/unit/test_fxa_service_cluster.js
@@ -1,68 +1,68 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Cu.import("resource://services-sync/service.js");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://testing-common/services/sync/fxa_utils.js");
-Cu.import("resource://testing-common/services/sync/utils.js");
-
-add_task(function test_findCluster() {
-  _("Test FxA _findCluster()");
-
-  _("_findCluster() throws on 500 errors.");
-  initializeIdentityWithTokenServerResponse({
-    status: 500,
-    headers: [],
-    body: "",
-  });
-
-  yield Service.identity.initializeWithCurrentIdentity();
-  yield Assert.rejects(Service.identity.whenReadyToAuthenticate.promise,
-                       "should reject due to 500");
-
-  Assert.throws(function() {
-    Service._clusterManager._findCluster();
-  });
-
-  _("_findCluster() returns null on authentication errors.");
-  initializeIdentityWithTokenServerResponse({
-    status: 401,
-    headers: {"content-type": "application/json"},
-    body: "{}",
-  });
-
-  yield Service.identity.initializeWithCurrentIdentity();
-  yield Assert.rejects(Service.identity.whenReadyToAuthenticate.promise,
-                       "should reject due to 401");
-
-  cluster = Service._clusterManager._findCluster();
-  Assert.strictEqual(cluster, null);
-
-  _("_findCluster() works with correct tokenserver response.");
-  let endpoint = "http://example.com/something";
-  initializeIdentityWithTokenServerResponse({
-    status: 200,
-    headers: {"content-type": "application/json"},
-    body:
-      JSON.stringify({
-        api_endpoint: endpoint,
-        duration: 300,
-        id: "id",
-        key: "key",
-        uid: "uid",
-      })
-  });
-
-  yield Service.identity.initializeWithCurrentIdentity();
-  yield Service.identity.whenReadyToAuthenticate.promise;
-  cluster = Service._clusterManager._findCluster();
-  // The cluster manager ensures a trailing "/"
-  Assert.strictEqual(cluster, endpoint + "/");
-
-  Svc.Prefs.resetBranch("");
-});
-
-function run_test() {
-  initTestLogging();
-  run_next_test();
-}
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/fxa_utils.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+
+add_task(function test_findCluster() {
+  _("Test FxA _findCluster()");
+
+  _("_findCluster() throws on 500 errors.");
+  initializeIdentityWithTokenServerResponse({
+    status: 500,
+    headers: [],
+    body: "",
+  });
+
+  yield Service.identity.initializeWithCurrentIdentity();
+  yield Assert.rejects(Service.identity.whenReadyToAuthenticate.promise,
+                       "should reject due to 500");
+
+  Assert.throws(function() {
+    Service._clusterManager._findCluster();
+  });
+
+  _("_findCluster() returns null on authentication errors.");
+  initializeIdentityWithTokenServerResponse({
+    status: 401,
+    headers: {"content-type": "application/json"},
+    body: "{}",
+  });
+
+  yield Service.identity.initializeWithCurrentIdentity();
+  yield Assert.rejects(Service.identity.whenReadyToAuthenticate.promise,
+                       "should reject due to 401");
+
+  cluster = Service._clusterManager._findCluster();
+  Assert.strictEqual(cluster, null);
+
+  _("_findCluster() works with correct tokenserver response.");
+  let endpoint = "http://example.com/something";
+  initializeIdentityWithTokenServerResponse({
+    status: 200,
+    headers: {"content-type": "application/json"},
+    body:
+      JSON.stringify({
+        api_endpoint: endpoint,
+        duration: 300,
+        id: "id",
+        key: "key",
+        uid: "uid",
+      })
+  });
+
+  yield Service.identity.initializeWithCurrentIdentity();
+  yield Service.identity.whenReadyToAuthenticate.promise;
+  cluster = Service._clusterManager._findCluster();
+  // The cluster manager ensures a trailing "/"
+  Assert.strictEqual(cluster, endpoint + "/");
+
+  Svc.Prefs.resetBranch("");
+});
+
+function run_test() {
+  initTestLogging();
+  run_next_test();
+}
--- a/services/sync/tests/unit/test_fxa_startOver.js
+++ b/services/sync/tests/unit/test_fxa_startOver.js
@@ -1,63 +1,63 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Cu.import("resource://testing-common/services/sync/utils.js");
-Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/browserid_identity.js");
-Cu.import("resource://services-sync/service.js");
-
-function run_test() {
-  initTestLogging("Trace");
-  run_next_test();
-}
-
-add_task(function* test_startover() {
-  let oldValue = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity", true);
-  Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", false);
-
-  ensureLegacyIdentityManager();
-  yield configureIdentity({username: "johndoe"});
-
-  // The boolean flag on the xpcom service should reflect a legacy provider.
-  let xps = Cc["@mozilla.org/weave/service;1"]
-            .getService(Components.interfaces.nsISupports)
-            .wrappedJSObject;
-  do_check_false(xps.fxAccountsEnabled);
-
-  // we expect the "legacy" provider (but can't instanceof that, as BrowserIDManager
-  // extends it)
-  do_check_false(Service.identity instanceof BrowserIDManager);
-
-  Service.serverURL = "https://localhost/";
-  Service.clusterURL = Service.serverURL;
-
-  Service.login();
-  // We should have a cluster URL
-  do_check_true(Service.clusterURL.length > 0);
-
-  // remember some stuff so we can reset it after.
-  let oldIdentity = Service.identity;
-  let oldClusterManager = Service._clusterManager;
-  let deferred = Promise.defer();
-  Services.obs.addObserver(function observeStartOverFinished() {
-    Services.obs.removeObserver(observeStartOverFinished, "weave:service:start-over:finish");
-    deferred.resolve();
-  }, "weave:service:start-over:finish", false);
-
-  Service.startOver();
-  yield deferred.promise; // wait for the observer to fire.
-
-  // the xpcom service should indicate FxA is enabled.
-  do_check_true(xps.fxAccountsEnabled);
-  // should have swapped identities.
-  do_check_true(Service.identity instanceof BrowserIDManager);
-  // should have clobbered the cluster URL
-  do_check_eq(Service.clusterURL, "");
-
-  // we should have thrown away the old identity provider and cluster manager.
-  do_check_neq(oldIdentity, Service.identity);
-  do_check_neq(oldClusterManager, Service._clusterManager);
-
-  // reset the world.
-  Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", oldValue);
-});
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://testing-common/services/sync/utils.js");
+Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/browserid_identity.js");
+Cu.import("resource://services-sync/service.js");
+
+function run_test() {
+  initTestLogging("Trace");
+  run_next_test();
+}
+
+add_task(function* test_startover() {
+  let oldValue = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity", true);
+  Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", false);
+
+  ensureLegacyIdentityManager();
+  yield configureIdentity({username: "johndoe"});
+
+  // The boolean flag on the xpcom service should reflect a legacy provider.
+  let xps = Cc["@mozilla.org/weave/service;1"]
+            .getService(Components.interfaces.nsISupports)
+            .wrappedJSObject;
+  do_check_false(xps.fxAccountsEnabled);
+
+  // we expect the "legacy" provider (but can't instanceof that, as BrowserIDManager
+  // extends it)
+  do_check_false(Service.identity instanceof BrowserIDManager);
+
+  Service.serverURL = "https://localhost/";
+  Service.clusterURL = Service.serverURL;
+
+  Service.login();
+  // We should have a cluster URL
+  do_check_true(Service.clusterURL.length > 0);
+
+  // remember some stuff so we can reset it after.
+  let oldIdentity = Service.identity;
+  let oldClusterManager = Service._clusterManager;
+  let deferred = Promise.defer();
+  Services.obs.addObserver(function observeStartOverFinished() {
+    Services.obs.removeObserver(observeStartOverFinished, "weave:service:start-over:finish");
+    deferred.resolve();
+  }, "weave:service:start-over:finish", false);
+
+  Service.startOver();
+  yield deferred.promise; // wait for the observer to fire.
+
+  // the xpcom service should indicate FxA is enabled.
+  do_check_true(xps.fxAccountsEnabled);
+  // should have swapped identities.
+  do_check_true(Service.identity instanceof BrowserIDManager);
+  // should have clobbered the cluster URL
+  do_check_eq(Service.clusterURL, "");
+
+  // we should have thrown away the old identity provider and cluster manager.
+  do_check_neq(oldIdentity, Service.identity);
+  do_check_neq(oldClusterManager, Service._clusterManager);
+
+  // reset the world.
+  Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", oldValue);
+});
--- a/testing/mozharness/configs/developer_config.py
+++ b/testing/mozharness/configs/developer_config.py
@@ -32,16 +32,18 @@ config = {
 
     # Pip
     "find_links": ["http://pypi.pub.build.mozilla.org/pub"],
     "pip_index": False,
 
     # Talos related
     "python_webserver": True,
     "virtualenv_path": '%s/build/venv' % os.getcwd(),
+    "preflight_run_cmd_suites": [],
+    "postflight_run_cmd_suites": [],
 
     # Tooltool related
     "download_tooltool": True,
     "tooltool_cache": os.path.join(LOCAL_WORKDIR, "builds/tooltool_cache"),
     "tooltool_cache_path": os.path.join(LOCAL_WORKDIR, "builds/tooltool_cache"),
 
     # VCS tools
     "hgtool.py": 'http://hg.mozilla.org/build/puppet/raw-file/faaf5abd792e/modules/packages/files/hgtool.py',
--- a/toolkit/components/alerts/resources/content/alert.js
+++ b/toolkit/components/alerts/resources/content/alert.js
@@ -1,29 +1,23 @@
-// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
-
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-Components.utils.import("resource://gre/modules/Services.jsm");
-
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-
-var windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]
-                       .getService(Ci.nsIWindowMediator);
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 // Copied from nsILookAndFeel.h, see comments on eMetric_AlertNotificationOrigin
 const NS_ALERT_HORIZONTAL = 1;
 const NS_ALERT_LEFT = 2;
 const NS_ALERT_TOP = 4;
 
 const WINDOW_MARGIN = 10;
 
+Cu.import("resource://gre/modules/Services.jsm");
+
 var gOrigin = 0; // Default value: alert from bottom right.
 var gReplacedWindow = null;
 var gAlertListener = null;
 var gAlertTextClickable = false;
 var gAlertCookie = "";
 var gIsReplaced = false;
 
 function prefillAlertInfo() {
@@ -114,17 +108,17 @@ function onAlertLoad() {
   }
 }
 
 function moveWindowToReplace(aReplacedAlert) {
   let heightDelta = window.outerHeight - aReplacedAlert.outerHeight;
 
   // Move windows that come after the replaced alert if the height is different.
   if (heightDelta != 0) {
-    let windows = windowMediator.getEnumerator('alert:alert');
+    let windows = Services.wm.getEnumerator('alert:alert');
     while (windows.hasMoreElements()) {
       let alertWindow = windows.getNext();
       // boolean to determine if the alert window is after the replaced alert.
       let alertIsAfter = gOrigin & NS_ALERT_TOP ?
                          alertWindow.screenY > aReplacedAlert.screenY :
                          aReplacedAlert.screenY > alertWindow.screenY;
       if (alertIsAfter) {
         // The new Y position of the window.
@@ -144,17 +138,17 @@ function moveWindowToReplace(aReplacedAl
 function moveWindowToEnd() {
   // Determine position
   let x = gOrigin & NS_ALERT_LEFT ? screen.availLeft :
           screen.availLeft + screen.availWidth - window.outerWidth;
   let y = gOrigin & NS_ALERT_TOP ? screen.availTop :
           screen.availTop + screen.availHeight - window.outerHeight;
 
   // Position the window at the end of all alerts.
-  let windows = windowMediator.getEnumerator('alert:alert');
+  let windows = Services.wm.getEnumerator('alert:alert');
   while (windows.hasMoreElements()) {
     let alertWindow = windows.getNext();
     if (alertWindow != window) {
       if (gOrigin & NS_ALERT_TOP) {
         y = Math.max(y, alertWindow.screenY + alertWindow.outerHeight);
       } else {
         y = Math.min(y, alertWindow.screenY - window.outerHeight);
       }
@@ -167,17 +161,17 @@ function moveWindowToEnd() {
 
   window.moveTo(x, y);
 }
 
 function onAlertBeforeUnload() {
   if (!gIsReplaced) {
     // Move other alert windows to fill the gap left by closing alert.
     let heightDelta = window.outerHeight + WINDOW_MARGIN;
-    let windows = windowMediator.getEnumerator('alert:alert');
+    let windows = Services.wm.getEnumerator('alert:alert');
     while (windows.hasMoreElements()) {
       let alertWindow = windows.getNext();
       if (alertWindow != window) {
         if (gOrigin & NS_ALERT_TOP) {
           if (alertWindow.screenY > window.screenY) {
             alertWindow.moveTo(alertWindow.screenX, alertWindow.screenY - heightDelta);
           }
         } else {
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -8166,16 +8166,21 @@
     "n_values": 8,
     "description": "Baseline Requirements section 9.2.2: subject common name field (0: present, in subject alt. names; 1: not present; 2: not present in subject alt. names)"
   },
   "TRACKING_PROTECTION_ENABLED": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Whether or not a session has tracking protection enabled"
   },
+  "TRACKING_PROTECTION_PBM_DISABLED": {
+    "expires_in_version": "60",
+    "kind": "boolean",
+    "description": "Is the tracking protection in private browsing mode disabled?"
+  },
   "TRACKING_PROTECTION_SHIELD": {
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 4,
     "description": "Tracking protection shield (0 = not shown, 1 = loaded, 2 = blocked)"
   },
   "TRACKING_PROTECTION_EVENTS": {
     "expires_in_version": "never",
@@ -8911,16 +8916,22 @@
     "n_values": 3,
     "description": "The number of times the tab queue prompt was seen before the user selected NO."
   },
   "FENNEC_TABQUEUE_ENABLED": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Has the tab queue functionality been enabled."
   },
+  "FENNEC_OPEN_URLS_IN_PRIVATE": {
+    "alert_emails": ["margaret@mozilla.com"],
+    "expires_in_version": "44",
+    "kind": "flag",
+    "description": "Reports the state of the open external links in private tabs preference"
+  },
   "VIDEO_EME_DISABLED": {
     "alert_emails": ["edwin@mozilla.com"],
     "expires_in_version": "45",
     "kind": "boolean",
     "description": "Set if media.eme.enabled is false, in a build that supports the Adobe Primetime Content Decryption Module."
   },
   "GRAPHICS_DRIVER_STARTUP_TEST": {
     "alert_emails": ["danderson@mozilla.com"],
--- a/toolkit/components/telemetry/TelemetryController.jsm
+++ b/toolkit/components/telemetry/TelemetryController.jsm
@@ -18,16 +18,17 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/DeferredTask.jsm", this);
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
+Cu.import("resource://gre/modules/AppConstants.jsm");
 
 const Utils = TelemetryUtils;
 
 const LOGGER_NAME = "Toolkit.Telemetry";
 const LOGGER_PREFIX = "TelemetryController::";
 
 const PREF_BRANCH = "toolkit.telemetry.";
 const PREF_BRANCH_LOG = PREF_BRANCH + "log.";
@@ -669,28 +670,28 @@ var Impl = {
     // Configure base Telemetry recording.
     // Unified Telemetry makes it opt-out unless the unifedOptin pref is set.
     // Additionally, we make Telemetry opt-out for a 5% sample.
     // If extended Telemetry is enabled, base recording is always on as well.
     const enabled = Preferences.get(PREF_ENABLED, false);
     const isOptout = IS_UNIFIED_TELEMETRY && (!Policy.isUnifiedOptin() || this._isInOptoutSample());
     Telemetry.canRecordBase = enabled || isOptout;
 
-#ifdef MOZILLA_OFFICIAL
-    // Enable extended telemetry if:
-    //  * the telemetry preference is set and
-    //  * this is an official build or we are in test-mode
-    // We only do the latter check for official builds so that e.g. developer builds
-    // still enable Telemetry based on prefs.
-    Telemetry.canRecordExtended = enabled && (Telemetry.isOfficialTelemetry || this._testMode);
-#else
-    // Turn off extended telemetry recording if disabled by preferences or if base/telemetry
-    // telemetry recording is off.
-    Telemetry.canRecordExtended = enabled;
-#endif
+    if (AppConstants.MOZILLA_OFFICIAL) {
+      // Enable extended telemetry if:
+      //  * the telemetry preference is set and
+      //  * this is an official build or we are in test-mode
+      // We only do the latter check for official builds so that e.g. developer builds
+      // still enable Telemetry based on prefs.
+      Telemetry.canRecordExtended = enabled && (Telemetry.isOfficialTelemetry || this._testMode);
+    } else {
+      // Turn off extended telemetry recording if disabled by preferences or if base/telemetry
+      // telemetry recording is off.
+      Telemetry.canRecordExtended = enabled;
+    }
 
     this._log.config("enableTelemetryRecording - canRecordBase:" + Telemetry.canRecordBase +
                      ", canRecordExtended: " + Telemetry.canRecordExtended);
 
     return Telemetry.canRecordBase;
   },
 
   /**
--- a/toolkit/components/telemetry/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -15,26 +15,27 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/PromiseUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
 Cu.import("resource://gre/modules/ObjectUtils.jsm");
 Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/AppConstants.jsm");
 
 const Utils = TelemetryUtils;
 
 XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
                                   "resource://gre/modules/ctypes.jsm");
-#ifndef MOZ_WIDGET_GONK
-Cu.import("resource://gre/modules/AddonManager.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
-                                  "resource://gre/modules/LightweightThemeManager.jsm");
-#endif
+if (AppConstants.platform !== "gonk") {
+  Cu.import("resource://gre/modules/AddonManager.jsm");
+  XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
+                                    "resource://gre/modules/LightweightThemeManager.jsm");
+}
 XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
                                   "resource://gre/modules/ProfileAge.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
                                   "resource://gre/modules/UpdateUtils.jsm");
 
 const CHANGE_THROTTLE_INTERVAL_MS = 5 * 60 * 1000;
 
 /**
@@ -269,24 +270,29 @@ function getGfxAdapter(aSuffix = "") {
     subsysID: getGfxField("adapterSubsysID" + aSuffix, null),
     RAM: memoryMB,
     driver: getGfxField("adapterDriver" + aSuffix, null),
     driverVersion: getGfxField("adapterDriverVersion" + aSuffix, null),
     driverDate: getGfxField("adapterDriverDate" + aSuffix, null),
   };
 }
 
-#ifdef XP_WIN
 /**
  * Gets the service pack information on Windows platforms. This was copied from
  * nsUpdateService.js.
  *
  * @return An object containing the service pack major and minor versions.
  */
 function getServicePack() {
+  const UNKNOWN_SERVICE_PACK = {major: null, minor: null};
+
+  if (AppConstants.platform !== "win") {
+    return UNKNOWN_SERVICE_PACK;
+  }
+
   const BYTE = ctypes.uint8_t;
   const WORD = ctypes.uint16_t;
   const DWORD = ctypes.uint32_t;
   const WCHAR = ctypes.char16_t;
   const BOOL = ctypes.int;
 
   // This structure is described at:
   // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx
@@ -319,25 +325,21 @@ function getServicePack() {
       throw("Failure in GetVersionEx (returned 0)");
     }
 
     return {
       major: winVer.wServicePackMajor,
       minor: winVer.wServicePackMinor,
     };
   } catch (e) {
-    return {
-      major: null,
-      minor: null,
-    };
+    return UNKNOWN_SERVICE_PACK;
   } finally {
     kernel32.close();
   }
 }
-#endif
 
 /**
  * Encapsulates the asynchronous magic interfacing with the addon manager. The builder
  * is owned by a parent environment object and is an addon listener.
  */
 function EnvironmentAddonBuilder(environment) {
   this._environment = environment;
 
@@ -443,22 +445,22 @@ EnvironmentAddonBuilder.prototype = {
    *
    * @returns Promise<Object> This returns a Promise resolved with a status object with the following members:
    *   changed - Whether the environment changed.
    *   oldEnvironment - Only set if a change occured, contains the environment data before the change.
    */
   _updateAddons: Task.async(function* () {
     this._environment._log.trace("_updateAddons");
     let personaId = null;
-#ifndef MOZ_WIDGET_GONK
-    let theme = LightweightThemeManager.currentTheme;
-    if (theme) {
-      personaId = theme.id;
+    if (AppConstants.platform !== "gonk") {
+      let theme = LightweightThemeManager.currentTheme;
+      if (theme) {
+        personaId = theme.id;
+      }
     }
-#endif
 
     let addons = {
       activeAddons: yield this._getActiveAddons(),
       theme: yield this._getActiveTheme(),
       activePlugins: this._getActivePlugins(),
       activeGMPlugins: yield this._getActiveGMPlugins(),
       activeExperiment: this._getActiveExperiment(),
       persona: personaId,
@@ -663,30 +665,30 @@ function EnvironmentCache() {
 
   this._updateSettings();
   // Fill in the default search engine, if the search provider is already initialized.
   this._updateSearchEngine();
 
   // Build the remaining asynchronous parts of the environment. Don't register change listeners
   // until the initial environment has been built.
 
-#ifdef MOZ_WIDGET_GONK
-  this._addonBuilder = {
-    watchForChanges: function() {}
+  let p = [];
+  if (AppConstants.platform === "gonk") {
+    this._addonBuilder = {
+      watchForChanges: function() {}
+    };
+  } else {
+    this._addonBuilder = new EnvironmentAddonBuilder(this);
+    p = [ this._addonBuilder.init() ];
   }
-  let p = [];
-#else
-  this._addonBuilder = new EnvironmentAddonBuilder(this);
 
-  let p = [ this._addonBuilder.init() ];
-#endif
-#ifndef MOZ_WIDGET_ANDROID
-  this._currentEnvironment.profile = {};
-  p.push(this._updateProfile());
-#endif
+  if (AppConstants.platform !== "android") {
+    this._currentEnvironment.profile = {};
+    p.push(this._updateProfile());
+  }
 
   let setup = () => {
     this._initTask = null;
     this._startWatchingPrefs();
     this._addonBuilder.watchForChanges();
     this._addObservers();
     this._updateGraphicsFeatures();
     return this.currentEnvironment;
@@ -967,19 +969,19 @@ EnvironmentCache.prototype = {
     return buildData;
   },
 
   /**
    * Determine if we're the default browser.
    * @returns null on error, true if we are the default browser, or false otherwise.
    */
   _isDefaultBrowser: function () {
-#ifdef MOZ_WIDGET_GONK
-    return true;
-#else
+    if (AppConstants.platform === "gonk") {
+      return true;
+    }
     if (!("@mozilla.org/browser/shell-service;1" in Cc)) {
       this._log.info("_isDefaultBrowser - Could not obtain browser shell service");
       return null;
     }
 
     let shellService;
     try {
       shellService = Cc["@mozilla.org/browser/shell-service;1"]
@@ -993,48 +995,51 @@ EnvironmentCache.prototype = {
       // This uses the same set of flags used by the pref pane.
       return shellService.isDefaultBrowser(false, true) ? true : false;
     } catch (ex) {
       this._log.error("_isDefaultBrowser - Could not determine if default browser", ex);
       return null;
     }
 
     return null;
-#endif
   },
 
   /**
    * Update the cached settings data.
    */
   _updateSettings: function () {
     let updateChannel = null;
     try {
       updateChannel = UpdateUtils.getUpdateChannel(false);
     } catch (e) {}
 
     this._currentEnvironment.settings = {
-#ifndef MOZ_WIDGET_GONK
-      addonCompatibilityCheckEnabled: AddonManager.checkCompatibility,
-#endif
       blocklistEnabled: Preferences.get(PREF_BLOCKLIST_ENABLED, true),
-#ifndef MOZ_WIDGET_ANDROID
-      isDefaultBrowser: this._isDefaultBrowser(),
-#endif
       e10sEnabled: Services.appinfo.browserTabsRemoteAutostart,
       telemetryEnabled: Preferences.get(PREF_TELEMETRY_ENABLED, false),
       isInOptoutSample: TelemetryController.isInOptoutSample,
       locale: getBrowserLocale(),
       update: {
         channel: updateChannel,
         enabled: Preferences.get(PREF_UPDATE_ENABLED, true),
         autoDownload: Preferences.get(PREF_UPDATE_AUTODOWNLOAD, true),
       },
       userPrefs: this._getPrefData(),
     };
 
+    if (AppConstants.platform !== "gonk") {
+      this._currentEnvironment.settings.addonCompatibilityCheckEnabled =
+        AddonManager.checkCompatibility;
+    }
+
+    if (AppConstants.platform !== "android") {
+      this._currentEnvironment.settings.isDefaultBrowser =
+        this._isDefaultBrowser();
+    }
+
     this._updateSearchEngine();
   },
 
   /**
    * Update the cached profile data.
    * @returns Promise<> resolved when the I/O is complete.
    */
   _updateProfile: Task.async(function* () {
@@ -1101,53 +1106,55 @@ EnvironmentCache.prototype = {
       }
     }
 
     cpuData.extensions = availableExts;
 
     return cpuData;
   },
 
-#if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_ANDROID)
   /**
    * Get the device information, if we are on a portable device.
-   * @return Object containing the device information data.
+   * @return Object containing the device information data, or null if
+   * not a portable device.
    */
   _getDeviceData: function () {
+    if (["gonk", "android"].indexOf(AppConstants.platform) === -1) {
+      return null;
+    }
+
     return {
       model: getSysinfoProperty("device", null),
       manufacturer: getSysinfoProperty("manufacturer", null),
       hardware: getSysinfoProperty("hardware", null),
       isTablet: getSysinfoProperty("tablet", null),
     };
   },
-#endif
 
   /**
    * Get the OS information.
    * @return Object containing the OS data.
    */
   _getOSData: function () {
-#ifdef XP_WIN
-    // Try to get service pack information.
-    let servicePack = getServicePack();
-#endif
-
-    return {
+    let data = {
       name: getSysinfoProperty("name", null),
       version: getSysinfoProperty("version", null),
-#if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_ANDROID)
-      kernelVersion: getSysinfoProperty("kernel_version", null),
-#elif defined(XP_WIN)
-      servicePackMajor: servicePack.major,
-      servicePackMinor: servicePack.minor,
-      installYear: getSysinfoProperty("installYear", null),
-#endif
       locale: getSystemLocale(),
     };
+
+    if (["gonk", "android"].indexOf(AppConstants.platform) !== -1) {
+      data.kernelVersion = getSysinfoProperty("kernel_version", null);
+    } else if (AppConstants.platform === "win") {
+      let servicePack = getServicePack();
+      data.servicePackMajor = servicePack.major;
+      data.servicePackMinor = servicePack.minor;
+      data.installYear = getSysinfoProperty("installYear", null);
+    }
+
+    return data;
   },
 
   /**
    * Get the HDD information.
    * @return Object containing the HDD data.
    */
   _getHDDData: function () {
     return {
@@ -1177,24 +1184,24 @@ EnvironmentCache.prototype = {
       // The following line is disabled due to main thread jank and will be enabled
       // again as part of bug 1154500.
       //DWriteVersion: getGfxField("DWriteVersion", null),
       adapters: [],
       monitors: [],
       features: {},
     };
 
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) && !defined(MOZ_WIDGET_GTK)
-    let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
-    try {
-      gfxData.monitors = gfxInfo.getMonitors();
-    } catch (e) {
-      this._log.error("nsIGfxInfo.getMonitors() caught error", e);
+    if (["gonk", "android", "linux"].indexOf(AppConstants.platform) === -1) {
+      let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+      try {
+        gfxData.monitors = gfxInfo.getMonitors();
+      } catch (e) {
+        this._log.error("nsIGfxInfo.getMonitors() caught error", e);
+      }
     }
-#endif
 
     try {
       let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
       gfxData.features = gfxInfo.getFeatures();
     } catch (e) {
       this._log.error("nsIGfxInfo.getFeatures() caught error", e);
     }
 
@@ -1231,30 +1238,32 @@ EnvironmentCache.prototype = {
 
     let virtualMB = getSysinfoProperty("virtualmemsize", null);
     if (virtualMB) {
       // Send the total virtual memory size in megabytes. Rounding because
       // sysinfo doesn't always provide RAM in multiples of 1024.
       virtualMB = Math.round(virtualMB / 1024 / 1024);
     }
 
-    return {
+    let data = {
       memoryMB: memoryMB,
       virtualMaxMB: virtualMB,
-#ifdef XP_WIN
-      isWow64: getSysinfoProperty("isWow64", null),
-#endif
       cpu: this._getCpuData(),
-#if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_ANDROID)
-      device: this._getDeviceData(),
-#endif
       os: this._getOSData(),
       hdd: this._getHDDData(),
       gfx: this._getGFXData(),
     };
+
+    if (AppConstants.platform === "win") {
+      data.isWow64 = getSysinfoProperty("isWow64", null);
+    } else if (["gonk", "android"].indexOf(AppConstants.platform) !== -1) {
+      data.device = this._getDeviceData();
+    }
+
+    return data;
   },
 
   _onEnvironmentChange: function (what, oldEnvironment) {
     this._log.trace("_onEnvironmentChange for " + what);
     if (this._shutdown) {
       this._log.trace("_onEnvironmentChange - Already shut down.");
       return;
     }
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -17,16 +17,17 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/DeferredTask.jsm", this);
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
 Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
+Cu.import("resource://gre/modules/AppConstants.jsm");
 
 const Utils = TelemetryUtils;
 
 const myScope = this;
 
 // When modifying the payload in incompatible ways, please bump this version number
 const PAYLOAD_VERSION = 4;
 const PING_TYPE_MAIN = "main";
@@ -1300,22 +1301,23 @@ var Impl = {
     this._previousSubsessionId = this._subsessionId;
     this._subsessionId = Policy.generateSubsessionUUID();
     this._subsessionCounter++;
     this._profileSubsessionCounter++;
   },
 
   getSessionPayload: function getSessionPayload(reason, clearSubsession) {
     this._log.trace("getSessionPayload - reason: " + reason + ", clearSubsession: " + clearSubsession);
-#if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_ANDROID)
-    clearSubsession = false;
-    const isSubsession = false;
-#else
-    const isSubsession = !this._isClassicReason(reason);
-#endif
+
+    const isMobile = ["gonk", "android"].indexOf(AppConstants.platform) !== -1;
+    const isSubsession = isMobile ? false : !this._isClassicReason(reason);
+
+    if (isMobile) {
+      clearSubsession = false;
+    }
 
     let measurements =
       this.getSimpleMeasurements(reason == REASON_SAVED_SESSION, isSubsession, clearSubsession);
     let info = !Utils.isContentProcess ? this.getMetadata(reason) : null;
     let payload = this.assemblePayloadWithMeasurements(measurements, info, reason, clearSubsession);
 
     if (!Utils.isContentProcess && clearSubsession) {
       this.startNewSubsession();
@@ -1410,19 +1412,19 @@ var Impl = {
       Preferences.set(PREF_PREVIOUS_BUILDID, thisBuildID);
     }
 
     TelemetryController.shutdown.addBlocker("TelemetrySession: shutting down",
                                       () => this.shutdownChromeProcess(),
                                       () => this._getState());
 
     Services.obs.addObserver(this, "sessionstore-windows-restored", false);
-#ifdef MOZ_WIDGET_ANDROID
-    Services.obs.addObserver(this, "application-background", false);
-#endif
+    if (AppConstants.platform === "android") {
+      Services.obs.addObserver(this, "application-background", false);
+    }
     Services.obs.addObserver(this, "xul-window-visible", false);
     this._hasWindowRestoredObserver = true;
     this._hasXulWindowVisibleObserver = true;
 
     ppml.addMessageListener(MESSAGE_TELEMETRY_PAYLOAD, this);
 
     // Delay full telemetry initialization to give the browser time to
     // run various late initializers. Otherwise our gathered memory
@@ -1622,19 +1624,19 @@ var Impl = {
     if (this._hasWindowRestoredObserver) {
       Services.obs.removeObserver(this, "sessionstore-windows-restored");
       this._hasWindowRestoredObserver = false;
     }
     if (this._hasXulWindowVisibleObserver) {
       Services.obs.removeObserver(this, "xul-window-visible");
       this._hasXulWindowVisibleObserver = false;
     }
-#ifdef MOZ_WIDGET_ANDROID
-    Services.obs.removeObserver(this, "application-background", false);
-#endif
+    if (AppConstants.platform === "android") {
+      Services.obs.removeObserver(this, "application-background", false);
+    }
   },
 
   getPayload: function getPayload(reason, clearSubsession) {
     this._log.trace("getPayload - clearSubsession: " + clearSubsession);
     reason = reason || REASON_GATHER_PAYLOAD;
     // This function returns the current Telemetry payload to the caller.
     // We only gather startup info once.
     if (Object.keys(this._slowSQLStartup).length == 0) {
@@ -1725,44 +1727,45 @@ var Impl = {
       Services.tm.mainThread.dispatch((function() {
         // Notify that data should be gathered now.
         // TODO: We are keeping this behaviour for now but it will be removed as soon as
         // bug 1127907 lands.
         Services.obs.notifyObservers(null, "gather-telemetry", null);
       }).bind(this), Ci.nsIThread.DISPATCH_NORMAL);
       break;
 
-#ifdef MOZ_WIDGET_ANDROID
-    // On Android, we can get killed without warning once we are in the background,
-    // but we may also submit data and/or come back into the foreground without getting
-    // killed. To deal with this, we save the current session data to file when we are
-    // put into the background. This handles the following post-backgrounding scenarios:
-    // 1) We are killed immediately. In this case the current session data (which we
-    //    save to a file) will be loaded and submitted on a future run.
-    // 2) We submit the data while in the background, and then are killed. In this case
-    //    the file that we saved will be deleted by the usual process in
-    //    finishPingRequest after it is submitted.
-    // 3) We submit the data, and then come back into the foreground. Same as case (2).
-    // 4) We do not submit the data, but come back into the foreground. In this case
-    //    we have the option of either deleting the file that we saved (since we will either
-    //    send the live data while in the foreground, or create the file again on the next
-    //    backgrounding), or not (in which case we will delete it on submit, or overwrite
-    //    it on the next backgrounding). Not deleting it is faster, so that's what we do.
     case "application-background":
+      if (AppConstants.platform !== "android") {
+        break;
+      }
+      // On Android, we can get killed without warning once we are in the background,
+      // but we may also submit data and/or come back into the foreground without getting
+      // killed. To deal with this, we save the current session data to file when we are
+      // put into the background. This handles the following post-backgrounding scenarios:
+      // 1) We are killed immediately. In this case the current session data (which we
+      //    save to a file) will be loaded and submitted on a future run.
+      // 2) We submit the data while in the background, and then are killed. In this case
+      //    the file that we saved will be deleted by the usual process in
+      //    finishPingRequest after it is submitted.
+      // 3) We submit the data, and then come back into the foreground. Same as case (2).
+      // 4) We do not submit the data, but come back into the foreground. In this case
+      //    we have the option of either deleting the file that we saved (since we will either
+      //    send the live data while in the foreground, or create the file again on the next
+      //    backgrounding), or not (in which case we will delete it on submit, or overwrite
+      //    it on the next backgrounding). Not deleting it is faster, so that's what we do.
       if (Telemetry.isOfficialTelemetry) {
         let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
         let options = {
           addClientId: true,
           addEnvironment: true,
           overwrite: true,
         };
         TelemetryController.addPendingPing(getPingType(payload), payload, options);
       }
       break;
-#endif
     }
   },
 
   /**
    * This tells TelemetrySession to uninitialize and save any pending pings.
    * @param testing Optional. If true, always saves the ping whether Telemetry
    *                can send pings or not, which is used for testing.
    */
new file mode 100644
--- /dev/null
+++ b/toolkit/locales/en-US/chrome/alerts/alert.properties
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE(closeButton.title): Used as the close button text for web notifications on OS X.
+# This should ideally match the string that OS X uses for the close button on alert-type
+# notifications. OS X will truncate the value if it's too long.
+closeButton.title = Close
+# LOCALIZATION NOTE(actionButton.label): Used as the button label to provide more actions on OS X notifications. OS X will truncate this if it's too long.
+actionButton.label = …
+webActions.disable.label = Disable notifications from this site
deleted file mode 100644
--- a/toolkit/locales/en-US/chrome/alerts/notificationNames.properties
+++ /dev/null
@@ -1,5 +0,0 @@
-# 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/.
-
-general=General Notification
--- a/toolkit/locales/jar.mn
+++ b/toolkit/locales/jar.mn
@@ -118,17 +118,17 @@
   locale/@AB_CD@/mozapps/update/updates.properties                (%chrome/mozapps/update/updates.properties)
   locale/@AB_CD@/mozapps/update/history.dtd                       (%chrome/mozapps/update/history.dtd)
   locale/@AB_CD@/mozapps/xpinstall/xpinstallConfirm.dtd           (%chrome/mozapps/extensions/xpinstallConfirm.dtd)
   locale/@AB_CD@/mozapps/xpinstall/xpinstallConfirm.properties    (%chrome/mozapps/extensions/xpinstallConfirm.properties)
 % locale pluginproblem @AB_CD@ %locale/@AB_CD@/pluginproblem/
   locale/@AB_CD@/pluginproblem/pluginproblem.dtd                  (%chrome/pluginproblem/pluginproblem.dtd)
 % locale alerts @AB_CD@ %locale/@AB_CD@/alerts/
   locale/@AB_CD@/alerts/alert.dtd                                (%chrome/alerts/alert.dtd)
-  locale/@AB_CD@/alerts/notificationNames.properties             (%chrome/alerts/notificationNames.properties)
+  locale/@AB_CD@/alerts/alert.properties                         (%chrome/alerts/alert.properties)
 % locale cookie @AB_CD@ %locale/@AB_CD@/cookie/
   locale/@AB_CD@/cookie/cookieAcceptDialog.dtd           (%chrome/cookie/cookieAcceptDialog.dtd)
   locale/@AB_CD@/cookie/cookieAcceptDialog.properties    (%chrome/cookie/cookieAcceptDialog.properties)
 % locale formautofill @AB_CD@ %locale/@AB_CD@/formautofill/
   locale/@AB_CD@/formautofill/requestAutocomplete.dtd (%chrome/formautofill/requestAutocomplete.dtd)
 % locale passwordmgr @AB_CD@ %locale/@AB_CD@/passwordmgr/
   locale/@AB_CD@/passwordmgr/passwordmgr.properties (%chrome/passwordmgr/passwordmgr.properties)
   locale/@AB_CD@/passwordmgr/passwordManager.dtd    (%chrome/passwordmgr/passwordManager.dtd)
--- a/toolkit/modules/NewTabUtils.jsm
+++ b/toolkit/modules/NewTabUtils.jsm
@@ -395,29 +395,27 @@ var PinnedLinks = {
 
     return this._links;
   },
 
   /**
    * Pins a link at the given position.
    * @param aLink The link to pin.
    * @param aIndex The grid index to pin the cell at.
+   * @return true if link changes, false otherwise
    */
   pin: function PinnedLinks_pin(aLink, aIndex) {
     // Clear the link's old position, if any.
     this.unpin(aLink);
 
     // change pinned link into a history link
-    // update all pages on link change
-    let updatePages = this._makeHistoryLink(aLink);
+    let changed = this._makeHistoryLink(aLink);
     this.links[aIndex] = aLink;
     this.save();
-    if (updatePages) {
-      AllPages.update();
-    }
+    return changed;
   },
 
   /**
    * Unpins a given link.
    * @param aLink The link to unpin.
    */
   unpin: function PinnedLinks_unpin(aLink) {
     let index = this._indexOfLink(aLink);
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -1675,17 +1675,17 @@ function sortList(aList, aSortBy, aAscen
     aList.appendChild(element);
 }
 
 function getAddonsAndInstalls(aType, aCallback) {
   let addons = null, installs = null;
   let types = (aType != null) ? [aType] : null;
 
   AddonManager.getAddonsByTypes(types, function getAddonsAndInstalls_getAddonsByTypes(aAddonsList) {
-    addons = aAddonsList;
+    addons = aAddonsList.filter(a => !a.hidden);
     if (installs != null)
       aCallback(addons, installs);
   });
 
   AddonManager.getInstallsByTypes(types, function getAddonsAndInstalls_getInstallsByTypes(aInstallsList) {
     // skip over upgrade installs and non-active installs
     installs = aInstallsList.filter(function installsFilter(aInstall) {
       return !(aInstall.existingAddon ||
@@ -2737,16 +2737,19 @@ var gListView = {
     sortList(this._listBox, aSortBy, aAscending);
   },
 
   onExternalInstall: function gListView_onExternalInstall(aAddon, aExistingAddon, aRequiresRestart) {
     // The existing list item will take care of upgrade installs
     if (aExistingAddon)
       return;
 
+    if (aAddon.hidden)
+      return;
+
     this.addItem(aAddon);
   },
 
   onDownloadStarted: function gListView_onDownloadStarted(aInstall) {
     this.addItem(aInstall, true);
   },
 
   onInstallStarted: function gListView_onInstallStarted(aInstall) {
@@ -3529,17 +3532,17 @@ var gUpdatesView = {
     var self = this;
     AddonManager.getAllAddons(function showRecentUpdates_getAllAddons(aAddonsList) {
       if (gViewController && aRequest != gViewController.currentViewRequest)
         return;
 
       var elements = [];
       let threshold = Date.now() - UPDATES_RECENT_TIMESPAN;
       for (let addon of aAddonsList) {
-        if (!addon.updateDate || addon.updateDate.getTime() < threshold)
+        if (addon.hidden || !addon.updateDate || addon.updateDate.getTime() < threshold)
           continue;
 
         elements.push(createItem(addon));
       }
 
       self.showEmptyNotice(elements.length == 0);
       if (elements.length > 0) {
         sortElements(elements, [self._sorters.sortBy], self._sorters.ascending);
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -130,16 +130,17 @@ const FILE_OLD_CACHE                  = 
 const FILE_RDF_MANIFEST               = "install.rdf";
 const FILE_WEB_MANIFEST               = "manifest.json";
 const FILE_XPI_ADDONS_LIST            = "extensions.ini";
 
 const KEY_PROFILEDIR                  = "ProfD";
 const KEY_APPDIR                      = "XCurProcD";
 const KEY_TEMPDIR                     = "TmpD";
 const KEY_APP_DISTRIBUTION            = "XREAppDist";
+const KEY_APP_FEATURES                = "XREAppFeat";
 
 const KEY_APP_PROFILE                 = "app-profile";
 const KEY_APP_SYSTEM_ADDONS           = "app-system-addons";
 const KEY_APP_SYSTEM_DEFAULTS         = "app-system-defaults";
 const KEY_APP_GLOBAL                  = "app-global";
 const KEY_APP_SYSTEM_LOCAL            = "app-system-local";
 const KEY_APP_SYSTEM_SHARE            = "app-system-share";
 const KEY_APP_SYSTEM_USER             = "app-system-user";
@@ -2440,19 +2441,18 @@ this.XPIProvider = {
       addDirectoryInstallLocation(KEY_APP_PROFILE, KEY_PROFILEDIR,
                                   [DIR_EXTENSIONS],
                                   AddonManager.SCOPE_PROFILE, false);
 
       addSystemAddonInstallLocation(KEY_APP_SYSTEM_ADDONS, KEY_PROFILEDIR,
                                     [DIR_SYSTEM_ADDONS],
                                     AddonManager.SCOPE_PROFILE);
 
-      addDirectoryInstallLocation(KEY_APP_SYSTEM_DEFAULTS, KEY_APP_DISTRIBUTION,
-                                  [DIR_SYSTEM_ADDONS],
-                                  AddonManager.SCOPE_PROFILE, true);
+      addDirectoryInstallLocation(KEY_APP_SYSTEM_DEFAULTS, KEY_APP_FEATURES,
+                                  [], AddonManager.SCOPE_PROFILE, true);
 
       if (enabledScopes & AddonManager.SCOPE_USER) {
         addDirectoryInstallLocation(KEY_APP_SYSTEM_USER, "XREUSysExt",
                                     [Services.appinfo.ID],
                                     AddonManager.SCOPE_USER, true);
         if (hasRegistry) {
           addRegistryInstallLocation("winreg-app-user",
                                      Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
@@ -6802,16 +6802,21 @@ function AddonWrapper(aAddon) {
       // Only set softDisabled if not already disabled
       if (!aAddon.userDisabled)
         aAddon.softDisabled = val;
     }
 
     return val;
   });
 
+  this.__defineGetter__("hidden", function AddonWrapper_hidden() {
+    return (aAddon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS ||
+            aAddon._installLocation.name == KEY_APP_SYSTEM_ADDONS);
+  });
+
   this.isCompatibleWith = function AddonWrapper_isCompatiblewith(aAppVersion, aPlatformVersion) {
     return aAddon.isCompatibleWith(aAppVersion, aPlatformVersion);
   };
 
   this.uninstall = function AddonWrapper_uninstall() {
     if (!(aAddon.inDatabase))
       throw new Error("Cannot uninstall an add-on that isn't installed");
     if (aAddon.pendingUninstall)
--- a/toolkit/mozapps/extensions/test/browser/browser_list.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_list.js
@@ -113,16 +113,20 @@ add_task(function*() {
     isActive: false,
     appDisabled: true,
     foreignInstall: true,
   }, {
     id: "addon13@tests.mozilla.org",
     name: "Test add-on 13",
     signedState: AddonManager.SIGNEDSTATE_SIGNED,
     foreignInstall: true,
+  }, {
+    id: "addon15@tests.mozilla.org",
+    name: "Test add-on 15",
+    hidden: true,
   }]);
 
   gManagerWindow = yield open_manager(null);
   gCategoryUtilities = new CategoryUtilities(gManagerWindow);
 });
 
 function get_test_items() {
   var tests = "@tests.mozilla.org";
@@ -681,16 +685,20 @@ add_task(function*() {
 add_task(function*() {
   gProvider.createAddons([{
     id: "addon1@tests.mozilla.org",
     name: "Test add-on replacement",
     version: "2.0",
     description: "A test add-on with a new description",
     updateDate: gDate,
     operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+  }, {
+    id: "addon14@tests.mozilla.org",
+    name: "Test add-on 14",
+    hidden: true,
   }]);
 
   let items = get_test_items();
   is(Object.keys(items).length, EXPECTED_ADDONS, "Should be the right number of add-ons installed");
 
   let addon = items["Test add-on replacement"];
   addon.parentNode.ensureElementIsVisible(addon);
   let { name, version } = yield get_tooltip_info(addon);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
@@ -3,61 +3,55 @@
 const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet";
 
 // Enable signature checks for these tests
 Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
 
 const featureDir = FileUtils.getDir("ProfD", ["features"]);
 
 // Build the test sets
-let dir = FileUtils.getDir("ProfD", ["sysfeatures", "app1", "features"], true);
+let dir = FileUtils.getDir("ProfD", ["sysfeatures", "app1"], true);
 do_get_file("data/system_addons/system1_1.xpi").copyTo(dir, "system1@tests.mozilla.org.xpi");
 do_get_file("data/system_addons/system2_1.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi");
 
-dir = FileUtils.getDir("ProfD", ["sysfeatures", "app2", "features"], true);
+dir = FileUtils.getDir("ProfD", ["sysfeatures", "app2"], true);
 do_get_file("data/system_addons/system1_2.xpi").copyTo(dir, "system1@tests.mozilla.org.xpi");
 do_get_file("data/system_addons/system3_1.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi");
 
-dir = FileUtils.getDir("ProfD", ["sysfeatures", "app3", "features"], true);
+dir = FileUtils.getDir("ProfD", ["sysfeatures", "app3"], true);
 do_get_file("data/system_addons/system1_1_badcert.xpi").copyTo(dir, "system1@tests.mozilla.org.xpi");
 do_get_file("data/system_addons/system3_1.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi");
 
 const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true);
-registerDirectory("XREAppDist", distroDir);
+registerDirectory("XREAppFeat", distroDir);
 
 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "0");
 
 function makeUUID() {
   let uuidGen = AM_Cc["@mozilla.org/uuid-generator;1"].
                 getService(AM_Ci.nsIUUIDGenerator);
   return uuidGen.generateUUID().toString();
 }
 
 function* check_installed(inProfile, ...versions) {
-  let expectedDir;
-  if (inProfile) {
-    expectedDir = featureDir;
-  }
-  else {
-    expectedDir = distroDir.clone();
-    expectedDir.append("features");
-  }
+  let expectedDir = inProfile ? featureDir : distroDir;
 
   for (let i = 0; i < versions.length; i++) {
     let id = "system" + (i + 1) + "@tests.mozilla.org";
     let addon = yield promiseAddonByID(id);
 
     if (versions[i]) {
       // Add-on should be installed
       do_check_neq(addon, null);
       do_check_eq(addon.version, versions[i]);
       do_check_true(addon.isActive);
       do_check_false(addon.foreignInstall);
       do_check_false(hasFlag(addon.permissions, AddonManager.PERM_CAN_UPGRADE));
       do_check_false(hasFlag(addon.permissions, AddonManager.PERM_CAN_UNINSTALL));
+      do_check_true(addon.hidden);
 
       // Verify the add-ons file is in the right place
       let file = expectedDir.clone();
       file.append(id + ".xpi");
       do_check_true(file.exists());
       do_check_true(file.isFile());
 
       let uri = addon.getResourceURI(null);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update.js
@@ -8,16 +8,23 @@ const PREF_APP_UPDATE_ENABLED         = 
 Components.utils.import("resource://testing-common/httpd.js");
 const { computeHash } = Components.utils.import("resource://gre/modules/addons/ProductAddonChecker.jsm");
 
 // Enable signature checks for these tests
 //Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
 
 const featureDir = FileUtils.getDir("ProfD", ["features"]);
 
+function getCurrentFeatureDir() {
+  let dir = featureDir.clone();
+  let set = JSON.parse(Services.prefs.getCharPref(PREF_SYSTEM_ADDON_SET));
+  dir.append(set.directory);
+  return dir;
+}
+
 // Build the test sets
 let dir = FileUtils.getDir("ProfD", ["features", "prefilled"], true);
 do_get_file("data/system_addons/system2_2.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi");
 do_get_file("data/system_addons/system3_2.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi");
 
 // Mark these in the past so the startup file scan notices when files have changed properly
 FileUtils.getFile("ProfD", ["features", "prefilled", "system2@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000;
 FileUtils.getFile("ProfD", ["features", "prefilled", "system3@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000;
@@ -30,26 +37,26 @@ const prefilledSet = {
       version: "2.0"
     },
     "system3@tests.mozilla.org": {
       version: "2.0"
     },
   }
 };
 
-dir = FileUtils.getDir("ProfD", ["sysfeatures", "hidden", "features"], true);
+dir = FileUtils.getDir("ProfD", ["sysfeatures", "hidden"], true);
 do_get_file("data/system_addons/system1_1.xpi").copyTo(dir, "system1@tests.mozilla.org.xpi");
 do_get_file("data/system_addons/system2_1.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi");
 
-dir = FileUtils.getDir("ProfD", ["sysfeatures", "prefilled", "features"], true);
+dir = FileUtils.getDir("ProfD", ["sysfeatures", "prefilled"], true);
 do_get_file("data/system_addons/system2_2.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi");
 do_get_file("data/system_addons/system3_2.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi");
 
 const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"], true);
-registerDirectory("XREAppDist", distroDir);
+registerDirectory("XREAppFeat", distroDir);
 
 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2");
 
 let testserver = new HttpServer();
 testserver.registerDirectory("/data/", do_get_file("data/system_addons"));
 testserver.start();
 let root = testserver.identity.primaryScheme + "://" +
            testserver.identity.primaryHost + ":" +
@@ -123,43 +130,39 @@ function* build_xml(addons) {
     xml += `  </addons>\n`;
   }
   xml += `</updates>\n`;
 
   return xml;
 }
 
 function* check_installed(inProfile, ...versions) {
+  let expectedDir = inProfile ? getCurrentFeatureDir() : distroDir;
+
   for (let i = 0; i < versions.length; i++) {
     let id = "system" + (i + 1) + "@tests.mozilla.org";
     let addon = yield promiseAddonByID(id);
 
     if (versions[i]) {
       // Add-on should be installed
       do_check_neq(addon, null);
       do_check_eq(addon.version, versions[i]);
       do_check_true(addon.isActive);
       do_check_false(addon.foreignInstall);
+      do_check_true(addon.hidden);
 
       // Verify the add-ons file is in the right place
+      let file = expectedDir.clone();
+      file.append(id + ".xpi");
+      do_check_true(file.exists());
+      do_check_true(file.isFile());
+
       let uri = addon.getResourceURI(null);
       do_check_true(uri instanceof AM_Ci.nsIFileURL);
-
-      let file = uri.file.parent;
-      if (inProfile) {
-        file = file.parent;
-        do_check_eq(file.leafName, "features");
-        file = file.parent;
-        do_check_eq(file.path, gProfD.path);
-      }
-      else {
-        do_check_eq(file.leafName, "features");
-        file = file.parent;
-        do_check_eq(file.path, distroDir.path);
-      }
+      do_check_eq(uri.file.path, file.path);
 
       //do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_SYSTEM);
 
       // Verify the add-on actually started
       let installed = Services.prefs.getCharPref("bootstraptest." + id + ".active_version");
       do_check_eq(installed, versions[i]);
     }
     else {
--- a/toolkit/themes/linux/global/alerts/alert.css
+++ b/toolkit/themes/linux/global/alerts/alert.css
@@ -1,65 +1,23 @@
 /* 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/. */
 
 /* ===== alert.css =====================================================
   == Styles specific to the alerts dialog.
   ======================================================================= */
 
-@import url("chrome://global/skin/");
+@import url("chrome://global/skin/alerts/alert-common.css");
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
 .alertBox {
   border: 1px solid threedshadow;
   background-color: -moz-Dialog;
 }
 
-.alertImageBox {
-  padding: 8px 0;
-  width: 64px;
-  background-image: linear-gradient(rgba(255,255,255,0.7), rgba(255,255,255,0.6));
-  -moz-border-end: 1px solid rgba(0,0,0,.1);
-}
-
-.alertTextBox {
-  padding: 8px;
-  -moz-padding-start: 16px;
-  width: 255px;
-}
-
-.alertTextBox,
-.alertCloseBox {
-  background-image: linear-gradient(rgba(255,255,255,0.2), rgba(255,255,255,0.1));
-}
-
-#alertNotification[clickable="true"]:hover .alertTextBox,
-#alertNotification[clickable="true"]:hover .alertCloseBox {
-  background-image: linear-gradient(rgba(255,255,255,0.4), rgba(255,255,255,0.3));
-}
-
-.alertTitle {
-  font-weight: bold;
-  font-size: 110%;
-}
-
-#alertImage {
-  max-width: 48px;
-  max-height: 48px;
-  list-style-image: url(chrome://global/skin/alerts/notification-48.png);
-}
-
-#alertNotification[clickable="true"] {
-  cursor: pointer;
-}
-
-label {
-  cursor: inherit;
-}
-
 .alertCloseButton {
   -moz-appearance: none;
   height: 16px;
   padding: 4px 2px;
   width: 16px;
 }
--- a/toolkit/themes/osx/global/alerts/alert.css
+++ b/toolkit/themes/osx/global/alerts/alert.css
@@ -1,79 +1,37 @@
 /* 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/. */
 
 /* ===== alert.css =====================================================
   == Styles specific to the alerts dialog.
   ======================================================================= */
 
-@import url("chrome://global/skin/");
+@import url("chrome://global/skin/alerts/alert-common.css");
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
 #alertNotification {
   -moz-appearance: none;
   background: transparent;
 }
 
 .alertBox {
   border-radius: 5px;
   overflow: hidden;
   background-color: rgba(240,240,240,0.93);
   box-shadow: inset 0 0 0 1px rgba(255,255,255,0.3);
 }
 
-.alertImageBox {
-  padding: 8px 0;
-  width: 64px;
-  background-image: linear-gradient(rgba(255,255,255,0.7), rgba(255,255,255,0.6));
-  -moz-border-end: 1px solid rgba(0,0,0,.08);
-}
-
 .alertTitle,
 .alertTextBox {
   text-shadow: 0 1px white;
 }
 
-.alertTextBox {
-  padding: 8px;
-  -moz-padding-start: 16px;
-  width: 255px;
-}
-
-.alertTextBox,
-.alertCloseBox {
-  background-image: linear-gradient(rgba(255,255,255,0.2), rgba(255,255,255,0.1));
-}
-
-#alertNotification[clickable="true"]:hover .alertTextBox,
-#alertNotification[clickable="true"]:hover .alertCloseBox {
-  background-image: linear-gradient(rgba(255,255,255,0.4), rgba(255,255,255,0.3));
-}
-
-.alertTitle {
-  font-weight: bold;
-  font-size: 110%;
-}
-
-#alertImage {
-  max-width: 48px;
-  max-height: 48px;
-  list-style-image: url(chrome://global/skin/alerts/notification-48.png);
-}
-
-#alertNotification[clickable="true"] {
-  cursor: pointer;
-}
-
-label {
-  cursor: inherit;
-}
-
 @keyframes alert-animation {
   from {
     opacity: 0;
   }
   6.25% {
     opacity: 1;
   }
   93.75% {
copy from toolkit/themes/linux/global/alerts/alert.css
copy to toolkit/themes/shared/alert-common.css
--- a/toolkit/themes/linux/global/alerts/alert.css
+++ b/toolkit/themes/shared/alert-common.css
@@ -1,25 +1,20 @@
 /* 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/. */
 
 /* ===== alert.css =====================================================
-  == Styles specific to the alerts dialog.
+  == Shared styles specific to the alerts dialog.
   ======================================================================= */
 
 @import url("chrome://global/skin/");
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
-.alertBox {
-  border: 1px solid threedshadow;
-  background-color: -moz-Dialog;
-}
-
 .alertImageBox {
   padding: 8px 0;
   width: 64px;
   background-image: linear-gradient(rgba(255,255,255,0.7), rgba(255,255,255,0.6));
   -moz-border-end: 1px solid rgba(0,0,0,.1);
 }
 
 .alertTextBox {
@@ -51,15 +46,8 @@
 
 #alertNotification[clickable="true"] {
   cursor: pointer;
 }
 
 label {
   cursor: inherit;
 }
-
-.alertCloseButton {
-  -moz-appearance: none;
-  height: 16px;
-  padding: 4px 2px;
-  width: 16px;
-}
--- a/toolkit/themes/shared/jar.inc.mn
+++ b/toolkit/themes/shared/jar.inc.mn
@@ -13,16 +13,17 @@
   skin/classic/global/aboutMemory.css                      (../../shared/aboutMemory.css)
   skin/classic/global/aboutReader.css                      (../../shared/aboutReader.css)
   skin/classic/global/aboutReaderContent.css               (../../shared/aboutReaderContent.css)
 * skin/classic/global/aboutReaderControls.css              (../../shared/aboutReaderControls.css)
   skin/classic/global/aboutSupport.css                     (../../shared/aboutSupport.css)
   skin/classic/global/appPicker.css                        (../../shared/appPicker.css)
   skin/classic/global/config.css                           (../../shared/config.css)
   skin/classic/global/icons/warning.svg                    (../../shared/incontent-icons/warning.svg)
+  skin/classic/global/alerts/alert-common.css              (../../shared/alert-common.css)
   skin/classic/global/menu/shared-menu-check@2x.png        (../../shared/menu-check@2x.png)
   skin/classic/global/menu/shared-menu-check.png           (../../shared/menu-check.png)
   skin/classic/global/menu/shared-menu-check-active.svg    (../../shared/menu-check-active.svg)
   skin/classic/global/menu/shared-menu-check-black.svg     (../../shared/menu-check-black.svg)
   skin/classic/global/menu/shared-menu-check-hover.svg     (../../shared/menu-check-hover.svg)
   skin/classic/global/in-content/check.svg                 (../../shared/in-content/check.svg)
   skin/classic/global/in-content/check-partial.svg         (../../shared/in-content/check-partial.svg)
   skin/classic/global/in-content/dropdown.svg              (../../shared/in-content/dropdown.svg)
--- a/toolkit/themes/windows/global/alerts/alert.css
+++ b/toolkit/themes/windows/global/alerts/alert.css
@@ -1,68 +1,26 @@
 /* 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/. */
 
 /* ===== alert.css =====================================================
   == Styles specific to the alerts dialog.
   ======================================================================= */
 
-@import url("chrome://global/skin/");
+@import url("chrome://global/skin/alerts/alert-common.css");
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
 .alertBox {
   border: 1px solid threedshadow;
   border-radius: 3px;
   background-color: -moz-Dialog;
 }
 
-.alertImageBox {
-  padding: 8px 0;
-  width: 64px;
-  background-image: linear-gradient(rgba(255,255,255,0.7), rgba(255,255,255,0.6));
-  -moz-border-end: 1px solid rgba(0,0,0,.1);
-}
-
-.alertTextBox {
-  padding: 8px;
-  -moz-padding-start: 16px;
-  width: 255px;
-}
-
-.alertTextBox,
-.alertCloseBox {
-  background-image: linear-gradient(rgba(255,255,255,0.2), rgba(255,255,255,0.1));
-}
-
-#alertNotification[clickable="true"]:hover .alertTextBox,
-#alertNotification[clickable="true"]:hover .alertCloseBox {
-  background-image: linear-gradient(rgba(255,255,255,0.4), rgba(255,255,255,0.3));
-}
-
-.alertTitle {
-  font-weight: bold;
-  font-size: 110%;
-}
-
-#alertImage {
-  max-width: 48px;
-  max-height: 48px;
-  list-style-image: url(chrome://global/skin/alerts/notification-48.png);
-}
-
-#alertNotification[clickable="true"] {
-  cursor: pointer;
-}
-
-label {
-  cursor: inherit;
-}
-
 @keyframes alert-animation {
   from {
     opacity: 0;
   }
   6.25% {
     opacity: 1;
   }
   93.75% {
--- a/toolkit/xre/nsXREDirProvider.cpp
+++ b/toolkit/xre/nsXREDirProvider.cpp
@@ -384,16 +384,21 @@ nsXREDirProvider::GetFile(const char* aP
 #endif
   }
   else if (!strcmp(aProperty, XRE_APP_DISTRIBUTION_DIR)) {
     bool persistent = false;
     rv = GetFile(NS_GRE_DIR, &persistent, getter_AddRefs(file));
     if (NS_SUCCEEDED(rv))
       rv = file->AppendNative(NS_LITERAL_CSTRING("distribution"));
   }
+  else if (!strcmp(aProperty, XRE_APP_FEATURES_DIR)) {
+    rv = GetAppDir()->Clone(getter_AddRefs(file));
+    if (NS_SUCCEEDED(rv))
+      rv = file->AppendNative(NS_LITERAL_CSTRING("features"));
+  }
   else if (NS_SUCCEEDED(GetProfileStartupDir(getter_AddRefs(file)))) {
     // We need to allow component, xpt, and chrome registration to
     // occur prior to the profile-after-change notification.
     if (!strcmp(aProperty, NS_APP_USER_CHROME_DIR)) {
       rv = file->AppendNative(NS_LITERAL_CSTRING("chrome"));
     }
   }
 
--- a/tools/profiler/gecko/nsIProfiler.idl
+++ b/tools/profiler/gecko/nsIProfiler.idl
@@ -1,25 +1,26 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- 
+
 #include "nsISupports.idl"
 
 %{C++
 template<class T> class nsTArray;
 class nsCString;
 %}
 
 [ref] native StringArrayRef(const nsTArray<nsCString>);
 
-[scriptable, uuid(921e1223-b1ea-4906-bb26-a846e6b6835b)]
+[scriptable, uuid(ff398a14-df1c-4966-9ab2-772ea6a6da6c)]
 interface nsIProfiler : nsISupports
 {
+  boolean CanProfile();
   void StartProfiler(in uint32_t aEntries, in double aInterval,
                       [array, size_is(aFeatureCount)] in string aFeatures,
                       in uint32_t aFeatureCount,
                       [array, size_is(aFilterCount), optional] in string aThreadNameFilters,
                       [optional] in uint32_t aFilterCount);
   void StopProfiler();
   boolean IsPaused();
   void PauseSampling();
--- a/tools/profiler/gecko/nsProfiler.cpp
+++ b/tools/profiler/gecko/nsProfiler.cpp
@@ -66,16 +66,23 @@ nsProfiler::Observe(nsISupports *aSubjec
   } else if (strcmp(aTopic, "last-pb-context-exited") == 0) {
     mLockedForPrivateBrowsing = false;
     profiler_unlock();
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsProfiler::CanProfile(bool *aCanProfile)
+{
+  *aCanProfile = !mLockedForPrivateBrowsing;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsProfiler::StartProfiler(uint32_t aEntries, double aInterval,
                           const char** aFeatures, uint32_t aFeatureCount,
                           const char** aThreadNameFilters, uint32_t aFilterCount)
 {
   if (mLockedForPrivateBrowsing) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
--- a/widget/cocoa/OSXNotificationCenter.h
+++ b/widget/cocoa/OSXNotificationCenter.h
@@ -1,25 +1,29 @@
-/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef OSXNotificationCenter_h
 #define OSXNotificationCenter_h
 
 #import <Foundation/Foundation.h>
 #include "nsIAlertsService.h"
 #include "imgINotificationObserver.h"
 #include "nsITimer.h"
 #include "nsTArray.h"
 #include "mozilla/RefPtr.h"
 
 @class mozNotificationCenterDelegate;
 
+#if !defined(MAC_OS_X_VERSION_10_8) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8)
+typedef NSInteger NSUserNotificationActivationType;
+#endif
+
 namespace mozilla {
 
 class OSXNotificationInfo;
 
 class OSXNotificationCenter : public nsIAlertsService,
                               public imgINotificationObserver,
                               public nsITimerCallback
 {
@@ -28,17 +32,18 @@ public:
   NS_DECL_NSIALERTSSERVICE
   NS_DECL_IMGINOTIFICATIONOBSERVER
   NS_DECL_NSITIMERCALLBACK
 
   OSXNotificationCenter();
 
   nsresult Init();
   void CloseAlertCocoaString(NSString *aAlertName);
-  void OnClick(NSString *aAlertName);
+  void OnActivate(NSString *aAlertName, NSUserNotificationActivationType aActivationType,
+                  unsigned long long aAdditionalActionIndex);
   void ShowPendingNotification(OSXNotificationInfo *osxni);
 
 protected:
   virtual ~OSXNotificationCenter();
 
 private:
   mozNotificationCenterDelegate *mDelegate;
   nsTArray<nsRefPtr<OSXNotificationInfo> > mActiveAlerts;
--- a/widget/cocoa/OSXNotificationCenter.mm
+++ b/widget/cocoa/OSXNotificationCenter.mm
@@ -1,39 +1,52 @@
-/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "OSXNotificationCenter.h"
 #import <AppKit/AppKit.h>
 #include "imgIRequest.h"
 #include "imgIContainer.h"
+#include "nsIStringBundle.h"
 #include "nsNetUtil.h"
 #include "imgLoader.h"
 #import "nsCocoaUtils.h"
+#include "nsContentUtils.h"
 #include "nsObjCExceptions.h"
 #include "nsString.h"
 #include "nsCOMPtr.h"
 #include "nsIObserver.h"
 #include "nsIContentPolicy.h"
 #include "imgRequestProxy.h"
 
 using namespace mozilla;
 
 #if !defined(MAC_OS_X_VERSION_10_8) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8)
 @protocol NSUserNotificationCenterDelegate
 @end
 static NSString * const NSUserNotificationDefaultSoundName = @"DefaultSoundName";
 enum {
   NSUserNotificationActivationTypeNone = 0,
   NSUserNotificationActivationTypeContentsClicked = 1,
-  NSUserNotificationActivationTypeActionButtonClicked = 2
+  NSUserNotificationActivationTypeActionButtonClicked = 2,
 };
-typedef NSInteger NSUserNotificationActivationType;
+#endif
+
+#if !defined(MAC_OS_X_VERSION_10_9) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9)
+enum {
+  NSUserNotificationActivationTypeReplied = 3,
+};
+#endif
+
+#if !defined(MAC_OS_X_VERSION_10_10) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_10)
+enum {
+  NSUserNotificationActivationTypeAdditionalActionClicked = 4
+};
 #endif
 
 @protocol FakeNSUserNotification <NSObject>
 @property (copy) NSString* title;
 @property (copy) NSString* subtitle;
 @property (copy) NSString* informativeText;
 @property (copy) NSString* actionButtonTitle;
 @property (copy) NSDictionary* userInfo;
@@ -85,17 +98,20 @@ typedef NSInteger NSUserNotificationActi
         didDeliverNotification:(id<FakeNSUserNotification>)notification
 {
 
 }
 
 - (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
        didActivateNotification:(id<FakeNSUserNotification>)notification
 {
-  mOSXNC->OnClick([[notification userInfo] valueForKey:@"name"]);
+  NSNumber *alternateActionIndex = [(NSObject*)notification valueForKey:@"_alternateActionIndex"];
+  mOSXNC->OnActivate([[notification userInfo] valueForKey:@"name"],
+                     notification.activationType,
+                     [alternateActionIndex unsignedLongLongValue]);
 }
 
 - (BOOL)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
      shouldPresentNotification:(id<FakeNSUserNotification>)notification
 {
   return YES;
 }
 
@@ -105,20 +121,32 @@ typedef NSInteger NSUserNotificationActi
   didRemoveDeliveredNotifications:(NSArray *)notifications
 {
   for (id<FakeNSUserNotification> notification in notifications) {
     NSString *name = [[notification userInfo] valueForKey:@"name"];
     mOSXNC->CloseAlertCocoaString(name);
   }
 }
 
+// This is an undocumented method that we need to be notified if a user clicks the close button.
+- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+  didDismissAlert:(id<FakeNSUserNotification>)notification
+{
+  NSString *name = [[notification userInfo] valueForKey:@"name"];
+  mOSXNC->CloseAlertCocoaString(name);
+}
+
 @end
 
 namespace mozilla {
 
+enum {
+  OSXNotificationActionDisable = 0
+};
+
 class OSXNotificationInfo {
 private:
   ~OSXNotificationInfo();
 
 public:
   NS_INLINE_DECL_REFCOUNTING(OSXNotificationInfo)
   OSXNotificationInfo(NSString *name, nsIObserver *observer,
                       const nsAString & alertCookie);
@@ -212,16 +240,43 @@ OSXNotificationCenter::ShowAlertNotifica
   Class unClass = NSClassFromString(@"NSUserNotification");
   id<FakeNSUserNotification> notification = [[unClass alloc] init];
   notification.title = [NSString stringWithCharacters:(const unichar *)aAlertTitle.BeginReading()
                                                length:aAlertTitle.Length()];
   notification.informativeText = [NSString stringWithCharacters:(const unichar *)aAlertText.BeginReading()
                                                          length:aAlertText.Length()];
   notification.soundName = NSUserNotificationDefaultSoundName;
   notification.hasActionButton = NO;
+
+  // If this is not an application/extension alert, show additional actions dealing with permissions.
+  if (aPrincipal && !nsContentUtils::IsSystemOrExpandedPrincipal(aPrincipal)
+      && !aPrincipal->GetIsNullPrincipal()) {
+    nsCOMPtr<nsIStringBundleService> sbs = do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+    nsCOMPtr<nsIStringBundle> bundle;
+    nsresult rv = sbs->CreateBundle("chrome://alerts/locale/alert.properties", getter_AddRefs(bundle));
+    if (NS_SUCCEEDED(rv)) {
+      nsXPIDLString closeButtonTitle, actionButtonTitle, disableButtonTitle;
+      bundle->GetStringFromName(NS_LITERAL_STRING("closeButton.title").get(),
+                                getter_Copies(closeButtonTitle));
+      bundle->GetStringFromName(NS_LITERAL_STRING("actionButton.label").get(),
+                                getter_Copies(actionButtonTitle));
+      bundle->GetStringFromName(NS_LITERAL_STRING("webActions.disable.label").get(),
+                                getter_Copies(disableButtonTitle));
+
+      notification.hasActionButton = YES;
+      notification.otherButtonTitle = nsCocoaUtils::ToNSString(closeButtonTitle);
+      notification.actionButtonTitle = nsCocoaUtils::ToNSString(actionButtonTitle);
+      [(NSObject*)notification setValue:@(YES) forKey:@"_showsButtons"];
+      [(NSObject*)notification setValue:@(YES) forKey:@"_alwaysShowAlternateActionMenu"];
+      [(NSObject*)notification setValue:@[
+                                          nsCocoaUtils::ToNSString(disableButtonTitle)
+                                          ]
+                               forKey:@"_alternateActionButtonTitles"];
+    }
+  }
   NSString *alertName = [NSString stringWithCharacters:(const unichar *)aAlertName.BeginReading() length:aAlertName.Length()];
   if (!alertName) {
     return NS_ERROR_FAILURE;
   }
   notification.userInfo = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:alertName, nil]
                                                       forKeys:[NSArray arrayWithObjects:@"name", nil]];
 
   OSXNotificationInfo *osxni = new OSXNotificationInfo(alertName, aAlertListener, aAlertCookie);
@@ -313,29 +368,46 @@ OSXNotificationCenter::CloseAlertCocoaSt
       break;
     }
   }
 
   NS_OBJC_END_TRY_ABORT_BLOCK;
 }
 
 void
-OSXNotificationCenter::OnClick(NSString *aAlertName)
+OSXNotificationCenter::OnActivate(NSString *aAlertName,
+                                  NSUserNotificationActivationType aActivationType,
+                                  unsigned long long aAdditionalActionIndex)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
   if (!aAlertName) {
     return; // Can't do anything without a name
   }
 
   for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) {
     OSXNotificationInfo *osxni = mActiveAlerts[i];
     if ([aAlertName isEqualToString:osxni->mName]) {
       if (osxni->mObserver) {
-        osxni->mObserver->Observe(nullptr, "alertclickcallback", osxni->mCookie.get());
+        switch (aActivationType) {
+          case NSUserNotificationActivationTypeAdditionalActionClicked:
+          case NSUserNotificationActivationTypeActionButtonClicked:
+            switch (aAdditionalActionIndex) {
+              case OSXNotificationActionDisable:
+                osxni->mObserver->Observe(nullptr, "alertdisablecallback", osxni->mCookie.get());
+                break;
+              default:
+                NS_WARNING("Unknown NSUserNotification additional action clicked");
+                break;
+            }
+            break;
+          default:
+            osxni->mObserver->Observe(nullptr, "alertclickcallback", osxni->mCookie.get());
+            break;
+        }
       }
       return;
     }
   }
 
   NS_OBJC_END_TRY_ABORT_BLOCK;
 }
 
--- a/xpcom/build/nsXULAppAPI.h
+++ b/xpcom/build/nsXULAppAPI.h
@@ -114,16 +114,21 @@
 
 /**
  * A directory service key which specifies the distribution specific files for
  * the application.
  */
 #define XRE_APP_DISTRIBUTION_DIR "XREAppDist"
 
 /**
+ * A directory service key which specifies the location for system add-ons.
+ */
+#define XRE_APP_FEATURES_DIR "XREAppFeat"
+
+/**
  * A directory service key which provides the update directory.
  * At present this is supported only on Windows.
  * Windows: Documents and Settings\<User>\Local Settings\Application Data\
  *          <Vendor>\<Application>\<relative path to app dir from Program Files>
  * If appDir is not under the Program Files, directory service will fail.
  * Callers should fallback to appDir.
  */
 #define XRE_UPDATE_ROOT_DIR "UpdRootD"