Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Fri, 12 Dec 2014 17:18:42 -0800
changeset 245329 f14dcd1c8c0bb4868442f4db40d67cb90430036d
parent 245305 f798a2ef5212482f830df5f09cf5cd3039900091 (current diff)
parent 245328 a1663501b36bdde4d98bceb02ba67a94d21fb2d9 (diff)
child 245335 120465dd70d289d21cf24dddee3f3b3b2de199f9
child 245372 0717d2d7406317fd68cd8d5f8df3f40bef54345d
child 245441 6866978381b6d8c67d381ab582cea607c21e3fd4
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone37.0a1
first release with
nightly linux32
f14dcd1c8c0b / 37.0a1 / 20141213030202 / files
nightly linux64
f14dcd1c8c0b / 37.0a1 / 20141213030202 / files
nightly mac
f14dcd1c8c0b / 37.0a1 / 20141213030202 / files
nightly win32
f14dcd1c8c0b / 37.0a1 / 20141213030202 / files
nightly win64
f14dcd1c8c0b / 37.0a1 / 20141213030202 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c a=merge
--- a/b2g/simulator/lib/main.js
+++ b/b2g/simulator/lib/main.js
@@ -1,30 +1,67 @@
 /* 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/.
  */
 
 const { Cc, Ci, Cu } = require("chrome");
 
-const { SimulatorProcess } = require("./simulator-process");
+const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
 const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 const { Simulator } = Cu.import("resource://gre/modules/devtools/Simulator.jsm");
-const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+const { SimulatorProcess } = require("./simulator-process");
+const Runtime = require("sdk/system/runtime");
+const URL = require("sdk/url");
+
+const ROOT_URI = require("addon").uri;
+const PROFILE_URL = ROOT_URI + "profile/";
+const BIN_URL = ROOT_URI + "b2g/";
 
 let process;
 
-function launch({ port }) {
-  // Close already opened simulation
+function launch(options) {
+  // Close already opened simulation.
   if (process) {
-    return close().then(launch.bind(null, { port: port }));
+    return close().then(launch.bind(null, options));
   }
 
-  process = SimulatorProcess();
-  process.remoteDebuggerPort = port;
+  // Compute B2G runtime path.
+  let path;
+  try {
+    let pref = "extensions." + require("addon").id + ".customRuntime";
+    path = Services.prefs.getComplexValue(pref, Ci.nsIFile);
+  } catch(e) {}
+
+  if (!path) {
+    let executables = {
+      WINNT: "b2g-bin.exe",
+      Darwin: "B2G.app/Contents/MacOS/b2g-bin",
+      Linux: "b2g-bin",
+    };
+    path = URL.toFilename(BIN_URL);
+    path += Runtime.OS == "WINNT" ? "\\" : "/";
+    path += executables[Runtime.OS];
+  }
+  options.runtimePath = path;
+  console.log("simulator path:", options.runtimePath);
+
+  // Compute Gaia profile path.
+  if (!options.profilePath) {
+    let gaiaProfile;
+    try {
+      let pref = "extensions." + require("addon").id + ".gaiaProfile";
+      gaiaProfile = Services.prefs.getComplexValue(pref, Ci.nsIFile).path;
+    } catch(e) {}
+
+    options.profilePath = gaiaProfile || URL.toFilename(PROFILE_URL);
+  }
+
+  process = new SimulatorProcess(options);
   process.run();
 
   return promise.resolve();
 }
 
 function close() {
   if (!process) {
     return promise.resolve();
--- a/b2g/simulator/lib/simulator-process.js
+++ b/b2g/simulator/lib/simulator-process.js
@@ -4,48 +4,43 @@
  */
 
 'use strict';
 
 const { Cc, Ci, Cu, ChromeWorker } = require("chrome");
 
 Cu.import("resource://gre/modules/Services.jsm");
 
-const { EventTarget } = require("sdk/event/target");
-const { emit, off } = require("sdk/event/core");
-const { Class } = require("sdk/core/heritage");
 const Environment = require("sdk/system/environment").env;
 const Runtime = require("sdk/system/runtime");
-const URL = require("sdk/url");
 const Subprocess = require("sdk/system/child_process/subprocess");
 const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
+const { EventEmitter } = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
 
-const ROOT_URI = require("addon").uri;
-const PROFILE_URL = ROOT_URI + "profile/";
-const BIN_URL = ROOT_URI + "b2g/";
 
 // Log subprocess error and debug messages to the console.  This logs messages
 // for all consumers of the API.  We trim the messages because they sometimes
 // have trailing newlines.  And note that registerLogHandler actually registers
 // an error handler, despite its name.
 Subprocess.registerLogHandler(
   function(s) console.error("subprocess: " + s.trim())
 );
 Subprocess.registerDebugHandler(
   function(s) console.debug("subprocess: " + s.trim())
 );
 
-exports.SimulatorProcess = Class({
-  extends: EventTarget,
-  initialize: function initialize(options) {
-    EventTarget.prototype.initialize.call(this, options);
+function SimulatorProcess(options) {
+  this.options = options;
 
-    this.on("stdout", function onStdout(data) console.log(data.trim()));
-    this.on("stderr", function onStderr(data) console.error(data.trim()));
-  },
+  EventEmitter.decorate(this);
+  this.on("stdout", data => { console.log(data.trim()) });
+  this.on("stderr", data => { console.error(data.trim()) });
+}
+
+SimulatorProcess.prototype = {
 
   // check if b2g is running
   get isRunning() !!this.process,
 
   /**
    * Start the process and connect the debugger client.
    */
   run: function() {
@@ -72,131 +67,97 @@ exports.SimulatorProcess = Class({
           command: "/usr/bin/osascript",
           arguments: ["-e", 'tell application "' + path + '" to activate'],
         });
       }
     });
 
     let environment;
     if (Runtime.OS == "Linux") {
-      environment = ["TMPDIR=" + Services.dirsvc.get("TmpD",Ci.nsIFile).path];
+      environment = ["TMPDIR=" + Services.dirsvc.get("TmpD", Ci.nsIFile).path];
       if ("DISPLAY" in Environment) {
         environment.push("DISPLAY=" + Environment.DISPLAY);
       }
     }
 
     // spawn a b2g instance
     this.process = Subprocess.call({
       command: b2gExecutable,
       arguments: this.b2gArguments,
       environment: environment,
 
       // emit stdout event
-      stdout: (function(data) {
-        emit(this, "stdout", data);
-      }).bind(this),
+      stdout: data => {
+        this.emit("stdout", data);
+      },
 
       // emit stderr event
-      stderr: (function(data) {
-        emit(this, "stderr", data);
-      }).bind(this),
+      stderr: data => {
+        this.emit("stderr", data);
+      },
 
-      // on b2g instance exit, reset tracked process, remoteDebuggerPort and
+      // on b2g instance exit, reset tracked process, remote debugger port and
       // shuttingDown flag, then finally emit an exit event
       done: (function(result) {
-        console.log(this.b2gFilename + " terminated with " + result.exitCode);
+        console.log("B2G terminated with " + result.exitCode);
         this.process = null;
-        emit(this, "exit", result.exitCode);
+        this.emit("exit", result.exitCode);
       }).bind(this)
     });
   },
 
   // request a b2g instance kill
   kill: function() {
     let deferred = promise.defer();
     if (this.process) {
       this.once("exit", (exitCode) => {
         this.shuttingDown = false;
         deferred.resolve(exitCode);
       });
       if (!this.shuttingDown) {
         this.shuttingDown = true;
-        emit(this, "kill", null);
+        this.emit("kill", null);
         this.process.kill();
       }
       return deferred.promise;
     } else {
       return promise.resolve(undefined);
     }
   },
 
-  // compute current b2g filename
-  get b2gFilename() {
-    return this._executable ? this._executableFilename : "B2G";
-  },
-
   // compute current b2g file handle
   get b2gExecutable() {
     if (this._executable) {
       return this._executable;
     }
-    let customRuntime;
-    try {
-      let pref = "extensions." + require("addon").id + ".customRuntime";
-      customRuntime = Services.prefs.getComplexValue(pref, Ci.nsIFile);
-    } catch(e) {}
-
-    if (customRuntime) {
-      this._executable = customRuntime;
-      this._executableFilename = "Custom runtime";
-      return this._executable;
-    }
-
-    let bin = URL.toFilename(BIN_URL);
-    let executables = {
-      WINNT: "b2g-bin.exe",
-      Darwin: "B2G.app/Contents/MacOS/b2g-bin",
-      Linux: "b2g-bin",
-    };
-
-    let path = bin;
-    path += Runtime.OS == "WINNT" ? "\\" : "/";
-    path += executables[Runtime.OS];
-    console.log("simulator path: " + path);
 
     let executable = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-    executable.initWithPath(path);
+    executable.initWithPath(this.options.runtimePath);
 
     if (!executable.exists()) {
       // B2G binaries not found
       throw Error("b2g-desktop Executable not found.");
     }
 
     this._executable = executable;
-    this._executableFilename = "b2g-bin";
 
     return executable;
   },
 
   // compute b2g CLI arguments
   get b2gArguments() {
     let args = [];
 
-    let gaiaProfile;
-    try {
-      let pref = "extensions." + require("addon").id + ".gaiaProfile";
-      gaiaProfile = Services.prefs.getComplexValue(pref, Ci.nsIFile).path;
-    } catch(e) {}
-
-    let profile = gaiaProfile || URL.toFilename(PROFILE_URL);
+    let profile = this.options.profilePath;
     args.push("-profile", profile);
     console.log("profile", profile);
 
     // NOTE: push dbgport option on the b2g-desktop commandline
-    args.push("-start-debugger-server", "" + this.remoteDebuggerPort);
+    args.push("-start-debugger-server", "" + this.options.port);
 
     // Ignore eventual zombie instances of b2g that are left over
     args.push("-no-remote");
 
     return args;
   },
-});
+};
 
+exports.SimulatorProcess = SimulatorProcess;
--- a/browser/base/content/browser-safebrowsing.js
+++ b/browser/base/content/browser-safebrowsing.js
@@ -2,17 +2,17 @@
 # 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/.
 
 #ifdef MOZ_SAFE_BROWSING
 var gSafeBrowsing = {
 
   setReportPhishingMenu: function() {
     // A phishing page will have a specific about:blocked content documentURI
-    var uri = getBrowser().currentURI;
+    var uri = gBrowser.currentURI;
     var isPhishingPage = uri && uri.spec.startsWith("about:blocked?e=phishingBlocked");
 
     // Show/hide the appropriate menu item.
     document.getElementById("menu_HelpPopup_reportPhishingtoolmenu")
             .hidden = isPhishingPage;
     document.getElementById("menu_HelpPopup_reportPhishingErrortoolmenu")
             .hidden = !isPhishingPage;
 
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1053,17 +1053,17 @@
         let addEngineList =
           document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
         while (addEngineList.firstChild)
           addEngineList.firstChild.remove();
 
         const kXULNS =
           "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
-        let addEngines = getBrowser().mCurrentBrowser.engines;
+        let addEngines = gBrowser.mCurrentBrowser.engines;
         if (addEngines && addEngines.length > 0) {
           const kBundleURI = "chrome://browser/locale/search.properties";
           let bundle = Services.strings.createBundle(kBundleURI);
           for (let engine of addEngines) {
             let button = document.createElementNS(kXULNS, "button");
             let label = bundle.formatStringFromName("cmd_addFoundEngine",
                                                     [engine.title], 1);
             button.setAttribute("class", "addengine-item");
--- a/browser/components/feeds/WebContentConverter.js
+++ b/browser/components/feeds/WebContentConverter.js
@@ -447,17 +447,17 @@ WebContentConverterRegistrar.prototype =
           handlerInfo.alwaysAskBeforeHandling = true;
 
           var hs = Cc["@mozilla.org/uriloader/handler-service;1"].
                    getService(Ci.nsIHandlerService);
           hs.store(handlerInfo);
         }
     };
     var browserElement = this._getBrowserForContentWindow(browserWindow, aContentWindow);
-    var notificationBox = browserWindow.getBrowser().getNotificationBox(browserElement);
+    var notificationBox = browserWindow.gBrowser.getNotificationBox(browserElement);
     notificationBox.appendNotification(message,
                                        notificationValue,
                                        notificationIcon,
                                        notificationBox.PRIORITY_INFO_LOW,
                                        [addButton]);
   },
 
   /**
@@ -476,17 +476,17 @@ WebContentConverterRegistrar.prototype =
     if (contentType != TYPE_MAYBE_FEED)
       return;
 
     if (aContentWindow) {
       var uri = this._checkAndGetURI(aURIString, aContentWindow);
   
       var browserWindow = this._getBrowserWindowForContentWindow(aContentWindow);
       var browserElement = this._getBrowserForContentWindow(browserWindow, aContentWindow);
-      var notificationBox = browserWindow.getBrowser().getNotificationBox(browserElement);
+      var notificationBox = browserWindow.gBrowser.getNotificationBox(browserElement);
       this._appendFeedReaderNotification(uri, aTitle, notificationBox);
     }
     else
       this._registerContentHandler(contentType, aURIString, aTitle);
   },
 
   /**
    * Returns the browser chrome window in which the content window is in
@@ -511,17 +511,17 @@ WebContentConverterRegistrar.prototype =
    * @param aContentWindow
    *        The content window. It's possible to pass a child content window
    *        (i.e. the content window of a frame/iframe).
    */
   _getBrowserForContentWindow:
   function WCCR__getBrowserForContentWindow(aBrowserWindow, aContentWindow) {
     // This depends on pseudo APIs of browser.js and tabbrowser.xml
     aContentWindow = aContentWindow.top;
-    var browsers = aBrowserWindow.getBrowser().browsers;
+    var browsers = aBrowserWindow.gBrowser.browsers;
     for (var i = 0; i < browsers.length; ++i) {
       if (browsers[i].contentWindow == aContentWindow)
         return browsers[i];
     }
   },
 
   /**
    * Appends a notifcation for the given feed reader details.
--- a/browser/components/loop/LoopRooms.jsm
+++ b/browser/components/loop/LoopRooms.jsm
@@ -130,17 +130,17 @@ let LoopRoomsInternal = {
 
   /**
    * @var {Number} participantsCount The total amount of participants currently
    *                                 inside all rooms.
    */
   get participantsCount() {
     let count = 0;
     for (let room of this.rooms.values()) {
-      if (!("participants" in room)) {
+      if (room.deleted || !("participants" in room)) {
         continue;
       }
       count += room.participants.length;
     }
     return count;
   },
 
   /**
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -1068,21 +1068,23 @@ this.MozLoopService = {
       gFxAEnabled = Services.prefs.getBoolPref("loop.fxa.enabled");
       if (!gFxAEnabled) {
         yield this.logOutFromFxA();
       }
     }
 
     // The Loop toolbar button should change icon when the room participant count
     // changes from 0 to something.
-    const onRoomsChange = () => {
-      MozLoopServiceInternal.notifyStatusChanged();
+    const onRoomsChange = (e) => {
+      // Pass the event name as notification reason for better logging.
+      MozLoopServiceInternal.notifyStatusChanged("room-" + e);
     };
     LoopRooms.on("add", onRoomsChange);
     LoopRooms.on("update", onRoomsChange);
+    LoopRooms.on("delete", onRoomsChange);
     LoopRooms.on("joined", (e, room, participant) => {
       // Don't alert if we're in the doNotDisturb mode, or the participant
       // is the owner - the content code deals with the rest of the sounds.
       if (MozLoopServiceInternal.doNotDisturb || participant.owner) {
         return;
       }
 
       let window = gWM.getMostRecentWindow("navigator:browser");
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -314,17 +314,18 @@ loop.panel = (function(_, mozL10n) {
             SettingsDropdownEntry({label: mozL10n.get("settings_menu_item_settings"), 
                                    onClick: this.handleClickSettingsEntry, 
                                    displayed: false, 
                                    icon: "settings"}), 
             SettingsDropdownEntry({label: mozL10n.get("settings_menu_item_account"), 
                                    onClick: this.handleClickAccountEntry, 
                                    icon: "account", 
                                    displayed: this._isSignedIn()}), 
-            SettingsDropdownEntry({label: mozL10n.get("tour_label"), 
+            SettingsDropdownEntry({icon: "tour", 
+                                   label: mozL10n.get("tour_label"), 
                                    onClick: this.openGettingStartedTour}), 
             SettingsDropdownEntry({label: this._isSignedIn() ?
                                           mozL10n.get("settings_menu_item_signout") :
                                           mozL10n.get("settings_menu_item_signin"), 
                                    onClick: this.handleClickAuthEntry, 
                                    displayed: navigator.mozLoop.fxAEnabled, 
                                    icon: this._isSignedIn() ? "signout" : "signin"}), 
             SettingsDropdownEntry({label: mozL10n.get("help_label"), 
@@ -690,17 +691,17 @@ loop.panel = (function(_, mozL10n) {
       );
     }
   });
 
   /**
    * Room list.
    */
   var RoomList = React.createClass({displayName: 'RoomList',
-    mixins: [Backbone.Events],
+    mixins: [Backbone.Events, sharedMixins.WindowCloseMixin],
 
     propTypes: {
       store: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       userDisplayName: React.PropTypes.string.isRequired  // for room creation
     },
 
     getInitialState: function() {
@@ -732,16 +733,18 @@ loop.panel = (function(_, mozL10n) {
       return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
     },
 
     _hasPendingOperation: function() {
       return this.state.pendingCreation || this.state.pendingInitialRetrieval;
     },
 
     handleCreateButtonClick: function() {
+      this.closeWindow();
+
       this.props.dispatcher.dispatch(new sharedActions.CreateRoom({
         nameTemplate: mozL10n.get("rooms_default_room_name_template"),
         roomOwner: this.props.userDisplayName
       }));
     },
 
     render: function() {
       if (this.state.error) {
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -314,17 +314,18 @@ loop.panel = (function(_, mozL10n) {
             <SettingsDropdownEntry label={mozL10n.get("settings_menu_item_settings")}
                                    onClick={this.handleClickSettingsEntry}
                                    displayed={false}
                                    icon="settings" />
             <SettingsDropdownEntry label={mozL10n.get("settings_menu_item_account")}
                                    onClick={this.handleClickAccountEntry}
                                    icon="account"
                                    displayed={this._isSignedIn()} />
-            <SettingsDropdownEntry label={mozL10n.get("tour_label")}
+            <SettingsDropdownEntry icon="tour"
+                                   label={mozL10n.get("tour_label")}
                                    onClick={this.openGettingStartedTour} />
             <SettingsDropdownEntry label={this._isSignedIn() ?
                                           mozL10n.get("settings_menu_item_signout") :
                                           mozL10n.get("settings_menu_item_signin")}
                                    onClick={this.handleClickAuthEntry}
                                    displayed={navigator.mozLoop.fxAEnabled}
                                    icon={this._isSignedIn() ? "signout" : "signin"} />
             <SettingsDropdownEntry label={mozL10n.get("help_label")}
@@ -690,17 +691,17 @@ loop.panel = (function(_, mozL10n) {
       );
     }
   });
 
   /**
    * Room list.
    */
   var RoomList = React.createClass({
-    mixins: [Backbone.Events],
+    mixins: [Backbone.Events, sharedMixins.WindowCloseMixin],
 
     propTypes: {
       store: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       userDisplayName: React.PropTypes.string.isRequired  // for room creation
     },
 
     getInitialState: function() {
@@ -732,16 +733,18 @@ loop.panel = (function(_, mozL10n) {
       return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
     },
 
     _hasPendingOperation: function() {
       return this.state.pendingCreation || this.state.pendingInitialRetrieval;
     },
 
     handleCreateButtonClick: function() {
+      this.closeWindow();
+
       this.props.dispatcher.dispatch(new sharedActions.CreateRoom({
         nameTemplate: mozL10n.get("rooms_default_room_name_template"),
         roomOwner: this.props.userDisplayName
       }));
     },
 
     render: function() {
       if (this.state.error) {
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -507,16 +507,46 @@
 .local-stream-audio .OT_publisher .OT_video-poster {
   background-image: url("../img/audio-call-avatar.svg");
   background-repeat: no-repeat;
   background-color: #4BA6E7;
   background-size: contain;
   background-position: center;
 }
 
+/*
+ * Ensure that the publisher (i.e. local) video is never cropped, so that it's
+ * not possible for someone to be presented with a picture that displays
+ * (for example) a person from the neck up, even though the camera is capturing
+ * and transmitting a picture of that person from the waist up.
+ *
+ * The !importants are necessary to override the SDK attempts to avoid
+ * letterboxing entirely.
+ *
+ * If we could easily use test video streams with the SDK (eg if initPublisher
+ * supported something like a "testMediaToStreamURI" parameter that it would
+ * use to source the stream rather than the output of gUM, it wouldn't be too
+ * hard to generate a video with a 1 pixel border at the edges that one could
+ * at least visually see wasn't being cropped.
+ *
+ * Another less ugly possibility would be to work with Ted Mielczarek to use
+ * the fake camera drivers he has for Linux.
+ */
+.room-conversation .OT_publisher .OT_video-container {
+  height: 100% !important;
+  width: 100% !important;
+  top: 0 !important;
+  left: 0 !important;
+  background-color: transparent; /* avoid visually obvious letterboxing */
+}
+
+.room-conversation .OT_publisher .OT_video-container video {
+  background-color: transparent; /* avoid visually obvious letterboxing */
+}
+
 .fx-embedded .media.nested {
   min-height: 200px;
 }
 
 .fx-embedded-call-identifier {
   display: inline;
   width: 100%;
   padding: 1.2em;
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -675,16 +675,20 @@ body[dir=rtl] .generate-url-spinner {
   height: 12px;
   margin-right: 1em;
 }
 
 .settings-menu .icon-settings {
   background: transparent url(../img/svg/glyph-settings-16x16.svg) no-repeat center center;
 }
 
+.settings-menu .icon-tour {
+  background: transparent url("../img/icons-16x16.svg#tour") no-repeat center center;
+}
+
 .settings-menu .icon-account {
   background: transparent url(../img/svg/glyph-account-16x16.svg) no-repeat center center;
 }
 
 .settings-menu .icon-signin {
   background: transparent url(../img/svg/glyph-signin-16x16.svg) no-repeat center center;
 }
 
--- a/browser/components/loop/content/shared/img/icons-16x16.svg
+++ b/browser/components/loop/content/shared/img/icons-16x16.svg
@@ -117,16 +117,20 @@ use[id$="-red"] {
     <rect x="7.75" y="7.542" fill="#FFFFFF" width="0.5" height="4"/>
     <polyline fill="#FFFFFF" points="9.25,7.542 8.75,7.542 8.75,11.542 9.25,11.542  "/>
     <rect x="6.75" y="7.542" fill="#FFFFFF" width="0.5" height="4"/>
   </g>
   <g id="leave-shape">
     <polygon fill="#FFFFFF" points="2.08,11.52 2.08,4 8,4 8,2.24 0.32,2.24 0.32,13.28 8,13.28 8,11.52"/>
     <polygon fill="#FFFFFF" points="15.66816,7.77344 9.6,2.27456 9.6,5.6 3.68,5.6 3.68,9.92 9.6,9.92 9.6,13.27232"/>
   </g>
+  <path id="tour-shape" fill="#5A5A5A" d="M8,0C4.831,0,2.262,2.674,2.262,5.972c0,1.393,1.023,3.398,2.206,5.249l0.571,0.866C6.504,14.245,8,16,8,16
+	s1.496-1.755,2.961-3.912l0.571-0.866c1.182-1.852,2.206-3.856,2.206-5.249C13.738,2.674,11.169,0,8,0z M8,7.645
+	c-0.603,0-1.146-0.262-1.534-0.681C6.098,6.566,5.87,6.025,5.87,5.428c0-1.224,0.954-2.217,2.13-2.217s2.13,0.992,2.13,2.217
+	C10.13,6.653,9.177,7.645,8,7.645z"/>
 </defs>
 <use id="audio"               xlink:href="#audio-shape"/>
 <use id="audio-hover"         xlink:href="#audio-shape"/>
 <use id="audio-active"        xlink:href="#audio-shape"/>
 <use id="block"               xlink:href="#block-shape"/>
 <use id="block-red"           xlink:href="#block-shape"/>
 <use id="block-hover"         xlink:href="#block-shape"/>
 <use id="block-active"        xlink:href="#block-shape"/>
@@ -153,9 +157,10 @@ use[id$="-red"] {
 <use id="tag-active"          xlink:href="#tag-shape"/>
 <use id="trash"               xlink:href="#trash-shape"/>
 <use id="unblock"             xlink:href="#unblock-shape"/>
 <use id="unblock-hover"       xlink:href="#unblock-shape"/>
 <use id="unblock-active"      xlink:href="#unblock-shape"/>
 <use id="video"               xlink:href="#video-shape"/>
 <use id="video-hover"         xlink:href="#video-shape"/>
 <use id="video-active"        xlink:href="#video-shape"/>
+<use id="tour"                xlink:href="#tour-shape"/>
 </svg>
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -892,78 +892,83 @@ describe("loop.panel", function() {
         expect(
           roomEntry.getDOMNode().querySelector(".edit-in-place").textContent)
         .eql("New room name");
       });
     });
   });
 
   describe("loop.panel.RoomList", function() {
-    var roomStore, dispatcher, fakeEmail;
+    var roomStore, dispatcher, fakeEmail, dispatch;
 
     beforeEach(function() {
       fakeEmail = "fakeEmail@example.com";
       dispatcher = new loop.Dispatcher();
       roomStore = new loop.store.RoomStore(dispatcher, {
         mozLoop: navigator.mozLoop
       });
       roomStore.setStoreState({
         pendingCreation: false,
         pendingInitialRetrieval: false,
         rooms: [],
         error: undefined
       });
+      dispatch = sandbox.stub(dispatcher, "dispatch");
     });
 
     function createTestComponent() {
       return TestUtils.renderIntoDocument(loop.panel.RoomList({
         store: roomStore,
         dispatcher: dispatcher,
         userDisplayName: fakeEmail
       }));
     }
 
     it("should dispatch a GetAllRooms action on mount", function() {
-      var dispatch = sandbox.stub(dispatcher, "dispatch");
-
       createTestComponent();
 
       sinon.assert.calledOnce(dispatch);
       sinon.assert.calledWithExactly(dispatch, new sharedActions.GetAllRooms());
     });
 
     it("should dispatch a CreateRoom action when clicking on the Start a " +
        "conversation button",
       function() {
         navigator.mozLoop.userProfile = {email: fakeEmail};
-        var dispatch = sandbox.stub(dispatcher, "dispatch");
         var view = createTestComponent();
 
         TestUtils.Simulate.click(view.getDOMNode().querySelector("button"));
 
         sinon.assert.calledWith(dispatch, new sharedActions.CreateRoom({
           nameTemplate: "fakeText",
           roomOwner: fakeEmail
         }));
       });
 
+    it("should close the panel when 'Start a Conversation' is clicked",
+      function() {
+        var view = createTestComponent();
+
+        TestUtils.Simulate.click(view.getDOMNode().querySelector("button"));
+
+        sinon.assert.calledOnce(fakeWindow.close);
+      });
+
     it("should disable the create button when a creation operation is ongoing",
       function() {
-        var dispatch = sandbox.stub(dispatcher, "dispatch");
         roomStore.setStoreState({pendingCreation: true});
 
         var view = createTestComponent();
 
         var buttonNode = view.getDOMNode().querySelector("button[disabled]");
         expect(buttonNode).to.not.equal(null);
       });
 
     it("should disable the create button when a list retrieval operation is pending",
       function() {
-        var dispatch = sandbox.stub(dispatcher, "dispatch");
         roomStore.setStoreState({pendingInitialRetrieval: true});
 
         var view = createTestComponent();
 
         var buttonNode = view.getDOMNode().querySelector("button[disabled]");
         expect(buttonNode).to.not.equal(null);
       });
   });
--- a/browser/components/preferences/in-content/advanced.js
+++ b/browser/components/preferences/in-content/advanced.js
@@ -74,16 +74,25 @@ var gAdvancedPane = {
                      gAdvancedPane.updateWritePrefs);
     setEventListener("showUpdateHistory", "command",
                      gAdvancedPane.showUpdates);
 #endif
     setEventListener("viewCertificatesButton", "command",
                      gAdvancedPane.showCertificates);
     setEventListener("viewSecurityDevicesButton", "command",
                      gAdvancedPane.showSecurityDevices);
+
+#ifdef MOZ_WIDGET_GTK
+    // GTK tabbox' allow the scroll wheel to change the selected tab,
+    // but we don't want this behavior for the in-content preferences.
+    let tabsElement = document.getElementById("tabsElement");
+    tabsElement.addEventListener("DOMMouseScroll", event => {
+      event.stopPropagation();
+    }, true);
+#endif
   },
 
   /**
    * Stores the identity of the current tab in preferences so that the selected
    * tab can be persisted between openings of the preferences window.
    */
   tabSelectionChanged: function ()
   {
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -344,17 +344,17 @@
           // indexes as items are removed.
           var items = popup.childNodes;
           for (var i = items.length - 1; i >= 0; i--) {
             if (items[i].classList.contains("addengine-item") ||
                 items[i].classList.contains("addengine-separator"))
               popup.removeChild(items[i]);
           }
 
-          var addengines = getBrowser().mCurrentBrowser.engines;
+          var addengines = gBrowser.mCurrentBrowser.engines;
           if (addengines && addengines.length > 0) {
             const kXULNS =
                "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
             // Find the (first) separator in the remaining menu, or the first item
             // if no separators are present.
             var insertLocation = popup.firstChild;
             while (insertLocation.nextSibling &&
--- a/browser/devtools/inspector/test/browser_inspector_delete-selected-node-02.js
+++ b/browser/devtools/inspector/test/browser_inspector_delete-selected-node-02.js
@@ -37,17 +37,17 @@ add_task(function* () {
 
     info("Clicking 'Delete Node' in the context menu.");
     inspector.panelDoc.getElementById("node-menu-delete").click();
 
     info("Waiting for inspector to update.");
     yield inspector.once("inspector-updated");
 
     info("Inspector updated, performing checks.");
-    yield assertNodeSelectedAndPanelsUpdated("#deleteChildren", "ul#deleteChildren");
+    yield assertNodeSelectedAndPanelsUpdated("#selectedAfterDelete", "li#selectedAfterDelete");
   }
 
   function* testAutomaticallyDeleteSelectedNode() {
     info("Selecting a node, deleting it via javascript and checking that " +
          "its parent node is selected and breadcrumbs are updated.");
 
     let div = yield getNodeFront("#deleteAutomatically", inspector);
     yield selectNode(div, inspector);
--- a/browser/devtools/inspector/test/doc_inspector_delete-selected-node-02.html
+++ b/browser/devtools/inspector/test/doc_inspector_delete-selected-node-02.html
@@ -2,13 +2,14 @@
 <html lang="en">
 <head>
   <meta charset="utf-8">
   <title>node delete - reset selection - test</title>
 </head>
 <body>
   <ul id="deleteChildren">
     <li id="deleteManually">Delete me via the inspector</li>
+    <li id="selectedAfterDelete">This node is selected after manual delete</li>
     <li id="deleteAutomatically">Delete me via javascript</li>
   </ul>
   <iframe id="deleteIframe" src="data:text/html,%3C!DOCTYPE%20html%3E%3Chtml%20lang%3D%22en%22%3E%3Cbody%3E%3Cp%20id%3D%22deleteInIframe%22%3EDelete my container iframe%3C%2Fp%3E%3C%2Fbody%3E%3C%2Fhtml%3E"></iframe>
 </body>
 </html>
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -421,18 +421,20 @@ MarkupView.prototype = {
           if (node.hidden) {
             this.walker.unhideNode(node).then(() => this.nodeChanged(node));
           } else {
             this.walker.hideNode(node).then(() => this.nodeChanged(node));
           }
         }
         break;
       case Ci.nsIDOMKeyEvent.DOM_VK_DELETE:
+        this.deleteNode(this._selectedContainer.node);
+        break;
       case Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE:
-        this.deleteNode(this._selectedContainer.node);
+        this.deleteNode(this._selectedContainer.node, true);
         break;
       case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
         let rootContainer = this.getContainer(this._rootNode);
         this.navigate(rootContainer.children.firstChild.container);
         break;
       case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
         if (this._selectedContainer.expanded) {
           this.collapseNode(this._selectedContainer.node);
@@ -503,34 +505,45 @@ MarkupView.prototype = {
       aEvent.stopPropagation();
       aEvent.preventDefault();
     }
   },
 
   /**
    * Delete a node from the DOM.
    * This is an undoable action.
+   *
+   * @param {NodeFront} aNode The node to remove.
+   * @param {boolean} moveBackward If set to true, focus the previous sibling,
+   *  otherwise the next one.
    */
-  deleteNode: function(aNode) {
+  deleteNode: function(aNode, moveBackward) {
     if (aNode.isDocumentElement ||
         aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE ||
         aNode.isAnonymous) {
       return;
     }
 
     let container = this.getContainer(aNode);
 
     // Retain the node so we can undo this...
     this.walker.retainNode(aNode).then(() => {
       let parent = aNode.parentNode();
       let nextSibling = null;
       this.undo.do(() => {
         this.walker.removeNode(aNode).then(siblings => {
-          let focusNode = siblings.previousSibling || parent;
           nextSibling = siblings.nextSibling;
+          let focusNode = moveBackward ? siblings.previousSibling : nextSibling;
+
+          // If we can't move as the user wants, we move to the other direction.
+          // If there is no sibling elements anymore, move to the parent node.
+          if (!focusNode) {
+            focusNode = nextSibling || siblings.previousSibling || parent;
+          }
+
           if (container.selected) {
             this.navigate(this.getContainer(focusNode));
           }
         });
       }, () => {
         this.walker.insertBefore(aNode, parent, nextSibling);
       });
     }).then(null, console.error);
--- a/browser/devtools/markupview/test/browser_markupview_tag_edit_04.js
+++ b/browser/devtools/markupview/test/browser_markupview_tag_edit_04.js
@@ -5,23 +5,23 @@
 "use strict";
 
 // Tests that a node can be deleted from the markup-view with the delete key.
 // Also checks that after deletion the correct element is highlighted.
 // The next sibling is preferred, but the parent is a fallback.
 
 const TEST_URL = "data:text/html,<div id='parent'><div id='first'></div><div id='second'></div><div id='third'></div></div>";
 
-function* checkDeleteAndSelection(inspector, nodeSelector, focusedNodeSelector) {
+function* checkDeleteAndSelection(inspector, key, nodeSelector, focusedNodeSelector) {
   yield selectNode(nodeSelector, inspector);
   yield clickContainer(nodeSelector, inspector);
 
-  info("Deleting the element \"" + nodeSelector + "\" with the keyboard");
+  info(`Deleting the element "${nodeSelector}" using the ${key} key`);
   let mutated = inspector.once("markupmutation");
-  EventUtils.sendKey("delete", inspector.panelWin);
+  EventUtils.sendKey(key, inspector.panelWin);
 
   yield Promise.all([mutated, inspector.once("inspector-updated")]);
 
   let nodeFront = yield getNodeFront(focusedNodeSelector, inspector);
   is(inspector.selection.nodeFront, nodeFront,
     focusedNodeSelector + " should be selected after " + nodeSelector + " node gets deleted.");
 
   info("Checking that it's gone, baby gone!");
@@ -31,14 +31,29 @@ function* checkDeleteAndSelection(inspec
   ok(content.document.querySelector(nodeSelector), "The test node is back!");
 }
 
 let test = asyncTest(function*() {
   let {inspector} = yield addTab(TEST_URL).then(openInspector);
 
   info("Selecting the test node by clicking on it to make sure it receives focus");
 
-  yield checkDeleteAndSelection(inspector, "#first", "#parent");
-  yield checkDeleteAndSelection(inspector, "#second", "#first");
-  yield checkDeleteAndSelection(inspector, "#third", "#second");
+  yield checkDeleteAndSelection(inspector, "delete", "#first", "#second");
+  yield checkDeleteAndSelection(inspector, "delete", "#second", "#third");
+  yield checkDeleteAndSelection(inspector, "delete", "#third", "#second");
+
+  yield checkDeleteAndSelection(inspector, "back_space", "#first", "#second");
+  yield checkDeleteAndSelection(inspector, "back_space", "#second", "#first");
+  yield checkDeleteAndSelection(inspector, "back_space", "#third", "#second");
+
+  // Removing the siblings of #first.
+  let mutated = inspector.once("markupmutation");
+  for (let node of content.document.querySelectorAll("#second, #third")) {
+    node.remove();
+  }
+  yield mutated;
+  // Testing with an only child.
+  info("testing with an only child");
+  yield checkDeleteAndSelection(inspector, "delete", "#first", "#parent");
+  yield checkDeleteAndSelection(inspector, "back_space", "#first", "#parent");
 
   yield inspector.once("inspector-updated");
 });
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -50,16 +50,20 @@ const EVENTS = {
   OVERVIEW_RANGE_CLEARED: "Performance:UI:OverviewRangeCleared",
 
   // Emitted by the DetailsView when a subview is selected
   DETAILS_VIEW_SELECTED: "Performance:UI:DetailsViewSelected",
 
   // Emitted by the CallTreeView when a call tree has been rendered
   CALL_TREE_RENDERED: "Performance:UI:CallTreeRendered",
 
+  // When a source is shown in the JavaScript Debugger at a specific location.
+  SOURCE_SHOWN_IN_JS_DEBUGGER: "Performance:UI:SourceShownInJsDebugger",
+  SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger",
+
   // Emitted by the WaterfallView when it has been rendered
   WATERFALL_RENDERED: "Performance:UI:WaterfallRendered"
 };
 
 /**
  * The current target and the profiler connection, set by this tool's host.
  */
 let gToolbox, gTarget, gFront;
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -30,11 +30,13 @@ support-files =
 [browser_perf-data-samples.js]
 [browser_perf-data-massaging-01.js]
 [browser_perf-ui-recording.js]
 [browser_perf-overview-render-01.js]
 [browser_perf-overview-render-02.js]
 [browser_perf-overview-selection.js]
 
 [browser_perf-details.js]
+[browser_perf-jump-to-debugger-01.js]
+[browser_perf-jump-to-debugger-02.js]
 [browser_perf-details-calltree-render-01.js]
 [browser_perf-details-calltree-render-02.js]
 [browser_perf-details-waterfall-render-01.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-jump-to-debugger-01.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the performance tool can jump to the debugger.
+ */
+
+function spawnTest () {
+  let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+  let { viewSourceInDebugger } = panel.panelWin;
+
+  yield viewSourceInDebugger(SIMPLE_URL, 14);
+
+  let debuggerPanel = toolbox.getPanel("jsdebugger");
+  ok(debuggerPanel, "The debugger panel was opened.");
+
+  let { DebuggerView } = debuggerPanel.panelWin;
+  let Sources = DebuggerView.Sources;
+
+  is(Sources.selectedValue, getSourceActor(Sources, SIMPLE_URL),
+    "The correct source is shown in the debugger.");
+  is(DebuggerView.editor.getCursor().line + 1, 14,
+    "The correct line is highlighted in the debugger's source editor.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-jump-to-debugger-02.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the performance tool can jump to the debugger, when the source was already
+ * loaded in that tool.
+ */
+
+function spawnTest() {
+  let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL, "jsdebugger");
+  let debuggerWin = panel.panelWin;
+  let debuggerEvents = debuggerWin.EVENTS;
+  let { DebuggerView } = debuggerWin;
+  let Sources = DebuggerView.Sources;
+
+  yield debuggerWin.once(debuggerEvents.SOURCE_SHOWN);
+  ok("A source was shown in the debugger.");
+
+  is(Sources.selectedValue, getSourceActor(Sources, SIMPLE_URL),
+    "The correct source is initially shown in the debugger.");
+  is(DebuggerView.editor.getCursor().line, 0,
+    "The correct line is initially highlighted in the debugger's source editor.");
+
+  yield toolbox.selectTool("performance");
+  let perfPanel = toolbox.getCurrentPanel();
+  let perfWin = perfPanel.panelWin;
+  let { viewSourceInDebugger } = perfWin;
+
+  yield viewSourceInDebugger(SIMPLE_URL, 14);
+
+  panel = toolbox.getPanel("jsdebugger");
+  ok(panel, "The debugger panel was reselected.");
+
+  is(DebuggerView.Sources.selectedValue, getSourceActor(Sources, SIMPLE_URL),
+    "The correct source is still shown in the debugger.");
+  is(DebuggerView.editor.getCursor().line + 1, 14,
+    "The correct line is now highlighted in the debugger's source editor.");
+
+  yield teardown(perfPanel);
+  finish();
+}
--- a/browser/devtools/performance/test/head.js
+++ b/browser/devtools/performance/test/head.js
@@ -153,27 +153,27 @@ function initBackend(aUrl) {
 
     let connection = getPerformanceActorsConnection(target);
     yield connection.open();
     let front = new PerformanceFront(connection);
     return { target, front };
   });
 }
 
-function initPerformance(aUrl) {
+function initPerformance(aUrl, selectedTool="performance") {
   info("Initializing a performance pane.");
 
   return Task.spawn(function*() {
     let tab = yield addTab(aUrl);
     let target = TargetFactory.forTab(tab);
 
     yield target.makeRemote();
 
     Services.prefs.setBoolPref("devtools.performance_dev.enabled", true);
-    let toolbox = yield gDevTools.showToolbox(target, "performance");
+    let toolbox = yield gDevTools.showToolbox(target, selectedTool);
     let panel = toolbox.getCurrentPanel();
     return { target, panel, toolbox };
   });
 }
 
 function* teardown(panel) {
   info("Destroying the performance tool.");
 
@@ -312,8 +312,13 @@ function dragStop(graph, x, y = 1) {
   graph._onMouseMove({ clientX: x, clientY: y });
   graph._onMouseUp({ clientX: x, clientY: y });
 }
 
 function dropSelection(graph) {
   graph.dropSelection();
   graph.emit("mouseup");
 }
+
+function getSourceActor(aSources, aURL) {
+  let item = aSources.getItemForAttachment(a => a.source.url === aURL);
+  return item && item.value;
+}
--- a/browser/devtools/performance/views/details-call-tree.js
+++ b/browser/devtools/performance/views/details-call-tree.js
@@ -9,16 +9,17 @@
 let CallTreeView = {
   /**
    * Sets up the view with event binding.
    */
   initialize: function () {
     this.el = $(".call-tree");
     this._graphEl = $(".call-tree-cells-container");
     this._onRangeChange = this._onRangeChange.bind(this);
+    this._onLink = this._onLink.bind(this);
     this._stop = this._stop.bind(this);
 
     OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
     OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._stop);
   },
 
   /**
@@ -53,16 +54,23 @@ let CallTreeView = {
    */
   _onRangeChange: function (_, params) {
     // When a range is cleared, we'll have no beginAt/endAt data,
     // so the rebuild will just render all the data again.
     let { beginAt, endAt } = params || {};
     this.render(this._profilerData, beginAt, endAt);
   },
 
+  _onLink: function (_, treeItem) {
+    let { url, line } = treeItem.frame.getInfo();
+    viewSourceInDebugger(url, line).then(
+      () => this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER),
+      () => this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER));
+  },
+
   /**
    * Called when the recording is stopped and prepares data to
    * populate the call tree.
    */
   _prepareCallTree: function (profilerData, beginAt, endAt, options) {
     let threadSamples = profilerData.profile.threads[0].samples;
     let contentOnly = !Prefs.showPlatformData;
     // TODO handle inverted tree bug 1102347
@@ -80,21 +88,51 @@ let CallTreeView = {
   _populateCallTree: function (frameNode, options={}) {
     let root = new CallView({
       autoExpandDepth: options.inverted ? 0 : undefined,
       frame: frameNode,
       hidden: options.inverted,
       inverted: options.inverted
     });
 
+    // Bind events
+    root.on("link", this._onLink);
+
     // Clear out other graphs
     this._graphEl.innerHTML = "";
     root.attachTo(this._graphEl);
 
     let contentOnly = !Prefs.showPlatformData;
     root.toggleCategories(!contentOnly);
   }
 };
 
 /**
  * Convenient way of emitting events from the view.
  */
 EventEmitter.decorate(CallTreeView);
+
+/**
+ * Opens/selects the debugger in this toolbox and jumps to the specified
+ * file name and line number.
+ * @param string url
+ * @param number line
+ */
+let viewSourceInDebugger = Task.async(function *(url, line) {
+  // If the Debugger was already open, switch to it and try to show the
+  // source immediately. Otherwise, initialize it and wait for the sources
+  // to be added first.
+  let debuggerAlreadyOpen = gToolbox.getPanel("jsdebugger");
+
+  let { panelWin: dbg } = yield gToolbox.selectTool("jsdebugger");
+
+  if (!debuggerAlreadyOpen) {
+    yield new Promise((resolve) => dbg.once(dbg.EVENTS.SOURCES_ADDED, () => resolve(dbg)));
+  }
+
+  let { DebuggerView } = dbg;
+  let item = DebuggerView.Sources.getItemForAttachment(a => a.source.url === url);
+  
+  if (item) {
+    return DebuggerView.setEditorLocation(item.attachment.source.actor, line, { noDebug: true });
+  }
+  return Promise.reject();
+});
--- a/browser/devtools/shared/DeveloperToolbar.jsm
+++ b/browser/devtools/shared/DeveloperToolbar.jsm
@@ -212,17 +212,17 @@ let CommandUtils = {
         return this.target.tab.ownerDocument.defaultView;
       },
 
       get chromeDocument() {
         return this.chromeWindow.document;
       },
 
       get window() {
-        return this.chromeWindow.getBrowser().selectedTab.linkedBrowser.contentWindow;
+        return this.chromeWindow.gBrowser.selectedTab.linkedBrowser.contentWindow;
       },
 
       get document() {
         return this.window.document;
       }
     };
   },
 };
@@ -291,17 +291,17 @@ const NOTIFICATIONS = {
  */
 DeveloperToolbar.prototype.NOTIFICATIONS = NOTIFICATIONS;
 
 /**
  * target is dynamic because the selectedTab changes
  */
 Object.defineProperty(DeveloperToolbar.prototype, "target", {
   get: function() {
-    return TargetFactory.forTab(this._chromeWindow.getBrowser().selectedTab);
+    return TargetFactory.forTab(this._chromeWindow.gBrowser.selectedTab);
   },
   enumerable: true
 });
 
 /**
  * Is the toolbar open?
  */
 Object.defineProperty(DeveloperToolbar.prototype, 'visible', {
@@ -405,17 +405,17 @@ DeveloperToolbar.prototype.show = functi
     ];
     return promise.all(panelPromises).then(panels => {
       [ this.tooltipPanel, this.outputPanel ] = panels;
 
       this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "true");
 
       return gcli.load().then(() => {
         this.display = gcli.createDisplay({
-          contentDocument: this._chromeWindow.getBrowser().contentDocument,
+          contentDocument: this._chromeWindow.gBrowser.contentDocument,
           chromeDocument: this._doc,
           chromeWindow: this._chromeWindow,
           hintElement: this.tooltipPanel.hintElement,
           inputElement: this._input,
           completeElement: this._doc.querySelector(".gclitoolbar-complete-node"),
           backgroundElement: this._doc.querySelector(".gclitoolbar-stack-node"),
           outputDocument: this.outputPanel.document,
           environment: CommandUtils.createEnvironment(this, "target"),
@@ -428,17 +428,17 @@ DeveloperToolbar.prototype.show = functi
         this.display.focusManager.addMonitoredElement(this._element);
 
         this.display.onVisibilityChange.add(this.outputPanel._visibilityChanged,
                                             this.outputPanel);
         this.display.onVisibilityChange.add(this.tooltipPanel._visibilityChanged,
                                             this.tooltipPanel);
         this.display.onOutput.add(this.outputPanel._outputChanged, this.outputPanel);
 
-        let tabbrowser = this._chromeWindow.getBrowser();
+        let tabbrowser = this._chromeWindow.gBrowser;
         tabbrowser.tabContainer.addEventListener("TabSelect", this, false);
         tabbrowser.tabContainer.addEventListener("TabClose", this, false);
         tabbrowser.addEventListener("load", this, true);
         tabbrowser.addEventListener("beforeunload", this, true);
 
         this._initErrorsCount(tabbrowser.selectedTab);
         this._devtoolsUnloaded = this._devtoolsUnloaded.bind(this);
         this._devtoolsLoaded = this._devtoolsLoaded.bind(this);
@@ -495,26 +495,26 @@ DeveloperToolbar.prototype.hide = functi
   return this._hidePromise;
 };
 
 /**
  * The devtools-unloaded event handler.
  * @private
  */
 DeveloperToolbar.prototype._devtoolsUnloaded = function() {
-  let tabbrowser = this._chromeWindow.getBrowser();
+  let tabbrowser = this._chromeWindow.gBrowser;
   Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
 };
 
 /**
  * The devtools-loaded event handler.
  * @private
  */
 DeveloperToolbar.prototype._devtoolsLoaded = function() {
-  let tabbrowser = this._chromeWindow.getBrowser();
+  let tabbrowser = this._chromeWindow.gBrowser;
   this._initErrorsCount(tabbrowser.selectedTab);
 };
 
 /**
  * Initialize the listeners needed for tracking the number of errors for a given
  * tab.
  *
  * @private
@@ -570,17 +570,17 @@ DeveloperToolbar.prototype._stopErrorsCo
 /**
  * Hide the developer toolbar
  */
 DeveloperToolbar.prototype.destroy = function() {
   if (this._input == null) {
     return; // Already destroyed
   }
 
-  let tabbrowser = this._chromeWindow.getBrowser();
+  let tabbrowser = this._chromeWindow.gBrowser;
   tabbrowser.tabContainer.removeEventListener("TabSelect", this, false);
   tabbrowser.tabContainer.removeEventListener("TabClose", this, false);
   tabbrowser.removeEventListener("load", this, true);
   tabbrowser.removeEventListener("beforeunload", this, true);
 
   Services.obs.removeObserver(this._devtoolsUnloaded, "devtools-unloaded");
   Services.obs.removeObserver(this._devtoolsLoaded, "devtools-loaded");
   Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
@@ -619,17 +619,17 @@ DeveloperToolbar.prototype._notify = fun
 
 /**
  * Update various parts of the UI when the current tab changes
  */
 DeveloperToolbar.prototype.handleEvent = function(ev) {
   if (ev.type == "TabSelect" || ev.type == "load") {
     if (this.visible) {
       this.display.reattach({
-        contentDocument: this._chromeWindow.getBrowser().contentDocument
+        contentDocument: this._chromeWindow.gBrowser.contentDocument
       });
 
       if (ev.type == "TabSelect") {
         this._initErrorsCount(ev.target);
       }
     }
   }
   else if (ev.type == "TabClose") {
@@ -671,17 +671,17 @@ DeveloperToolbar.prototype._onPageError 
  * @param nsIDOMEvent ev the beforeunload DOM event.
  */
 DeveloperToolbar.prototype._onPageBeforeUnload = function(ev) {
   let window = ev.target.defaultView;
   if (window.top !== window) {
     return;
   }
 
-  let tabs = this._chromeWindow.getBrowser().tabs;
+  let tabs = this._chromeWindow.gBrowser.tabs;
   Array.prototype.some.call(tabs, function(tab) {
     if (tab.linkedBrowser.contentWindow === window) {
       let tabId = tab.linkedPanel;
       if (tabId in this._errorsCount || tabId in this._warningsCount) {
         this._errorsCount[tabId] = 0;
         this._warningsCount[tabId] = 0;
         this._updateErrorsCount(tabId);
       }
@@ -696,17 +696,17 @@ DeveloperToolbar.prototype._onPageBefore
  * currently selected tab.
  *
  * @private
  * @param string [changedTabId] Optional. The tab ID that had its page errors
  * count changed. If this is provided and it doesn't match the currently
  * selected tab, then the button is not updated.
  */
 DeveloperToolbar.prototype._updateErrorsCount = function(changedTabId) {
-  let tabId = this._chromeWindow.getBrowser().selectedTab.linkedPanel;
+  let tabId = this._chromeWindow.gBrowser.selectedTab.linkedPanel;
   if (changedTabId && tabId != changedTabId) {
     return;
   }
 
   let errors = this._errorsCount[tabId];
   let warnings = this._warningsCount[tabId];
   let btn = this._errorCounterButton;
   if (errors) {
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -410,17 +410,17 @@ Tooltip.prototype = {
 
   _onBaseNodeMouseMove: function(event) {
     if (event.target !== this._lastHovered) {
       this.hide();
       this._lastHovered = event.target;
       setNamedTimeout(this.uid, this._showDelay, () => {
         this.isValidHoverTarget(event.target).then(target => {
           this.show(target);
-        }).catch((reason) => {
+        }, reason => {
           if (reason === false) {
             // isValidHoverTarget rejects with false if the tooltip should
             // not be shown. This can be safely ignored.
             return;
           }
           // Report everything else. Reason might be error that should not be
           // hidden.
           console.error("isValidHoverTarget rejected with an unexpected reason:");
--- a/browser/extensions/pdfjs/test/browser_pdfjs_navigation.js
+++ b/browser/extensions/pdfjs/test/browser_pdfjs_navigation.js
@@ -55,16 +55,70 @@ const TESTS = [
       selector: "#thumbnailView a:nth-child(2)",
       event: "click"
     },
     expectedPage: 2,
     message: "navigated to 2nd page using thumbnail view"
   },
   {
     action: {
+      selector: "#viewer",
+      event: "keydown",
+      keyCode: 36
+    },
+    expectedPage: 1,
+    message: "navigated to 1st page using 'home' key"
+  },
+  {
+    action: {
+      selector: "#viewer",
+      event: "keydown",
+      keyCode: 34
+    },
+    expectedPage: 2,
+    message: "navigated to 2nd page using 'Page Down' key"
+  },
+  {
+    action: {
+      selector: "#viewer",
+      event: "keydown",
+      keyCode: 33
+    },
+    expectedPage: 1,
+    message: "navigated to 1st page using 'Page Up' key"
+  },
+  {
+    action: {
+      selector: "#viewer",
+      event: "keydown",
+      keyCode: 39
+    },
+    expectedPage: 2,
+    message: "navigated to 2nd page using 'right' key"
+  },
+  {
+    action: {
+      selector: "#viewer",
+      event: "keydown",
+      keyCode: 37
+    },
+    expectedPage: 1,
+    message: "navigated to 1st page using 'left' key"
+  },
+  {
+    action: {
+      selector: "#viewer",
+      event: "keydown",
+      keyCode: 35
+    },
+    expectedPage: 5,
+    message: "navigated to last page using 'home' key"
+  },
+  {
+    action: {
       selector: ".outlineItem:nth-child(1) a",
       event: "click"
     },
     expectedPage: 1,
     message: "navigated to 1st page using outline view"
   },
   {
     action: {
@@ -125,17 +179,24 @@ function test() {
 
 function runTests(document, window, finish) {
   // Check if PDF is opened with internal viewer
   ok(document.querySelector('div#viewer'), "document content has viewer UI");
   ok('PDFJS' in window.wrappedJSObject, "window content has PDFJS object");
 
   // Wait for outline items, the start the navigation actions
   waitForOutlineItems(document).then(function () {
-    runNextTest(document, window, finish);
+    // The key navigation has to happen in page-fit, otherwise it won't scroll
+    // trough a complete page
+    setZoomToPageFit(document).then(function () {
+      runNextTest(document, window, finish);
+    }, function () {
+      ok(false, "Current scale has been ste to 'page-fit'");
+      finish();
+    });
   }, function () {
     ok(false, "Outline items have ben found");
     finish();
   });
 }
 
 /**
  * As the page changes asynchronously, we have to wait for the event after
@@ -165,17 +226,27 @@ function runNextTest(document, window, e
   var el = document.querySelector(test.action.selector);
   ok(el, "Element '" + test.action.selector + "' has been found");
 
   // The value option is for input case
   if (test.action.value)
     el.value = test.action.value;
 
   // Dispatch the event for changing the page
-  el.dispatchEvent(new Event(test.action.event));
+  if (test.action.event == "keydown") {
+    var ev = document.createEvent("KeyboardEvent");
+        ev.initKeyEvent("keydown", true, true, null, false, false, false, false,
+                        test.action.keyCode, 0);
+    el.dispatchEvent(ev);
+  }
+  else {
+    var ev = new Event(test.action.event);
+  }
+  el.dispatchEvent(ev);
+
 
   // When the promise gets resolved we call the next test if there are any left
   // or else we call the final callback which will end the test
   deferred.promise.then(function (pgNumber) {
     is(pgNumber, test.expectedPage, test.message);
 
     if (TESTS.length)
       runNextTest(document, window, endCallback);
@@ -202,8 +273,32 @@ function waitForOutlineItems(document) {
       clearInterval(interval);
       clearTimeout(timeout);
       deferred.resolve();
     }
   }, 500);
 
   return deferred.promise;
 }
+
+/**
+ * The key navigation has to happen in page-fit, otherwise it won't scroll
+ * trough a complete page
+ *
+ * @param document
+ * @returns {deferred.promise|*}
+ */
+function setZoomToPageFit(document) {
+  var deferred = Promise.defer();
+  document.addEventListener("pagerendered", function onZoom(e) {
+    document.removeEventListener("pagerendered", onZoom), false;
+    document.querySelector("#viewer").click();
+    deferred.resolve();
+
+  }, false);
+
+  var select = document.querySelector("select#scaleSelect");
+  select.selectedIndex = 2;
+  select.dispatchEvent(new Event("change"));
+
+  return deferred.promise;
+}
+
--- a/browser/fuel/fuelApplication.js
+++ b/browser/fuel/fuelApplication.js
@@ -89,17 +89,17 @@ function Window(aWindow) {
 }
 
 Window.prototype = {
   get events() {
     return this._events;
   },
 
   get _tabbrowser() {
-    return this._window.getBrowser();
+    return this._window.gBrowser;
   },
 
   /*
    * Helper used to setup event handlers on the XBL element. Note that the events
    * are actually dispatched to tabs, so we capture them.
    */
   _watch: function win_watch(aType) {
     this._tabbrowser.tabContainer.addEventListener(aType, this,
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
@@ -148,17 +148,17 @@
 <!ENTITY options.stylesheetAutocompletion.label      "Autocomplete CSS">
 <!ENTITY options.stylesheetAutocompletion.tooltip    "Autocomplete CSS properties, values and selectors in Style Editor as you type">
 
 <!-- LOCALIZATION NOTE (options.profiler.label): This is the label for the
   -  heading of the group of JavaScript Profiler preferences in the options
   -  panel. -->
 <!ENTITY options.profiler.label            "JavaScript Profiler">
 
-<!-- LOCALICATION NOTE (options.commonprefs): This is the label for the heading
+<!-- LOCALIZATION NOTE (options.commonprefs): This is the label for the heading
       of all preferences that affect both the Web Console and the Network
       Monitor -->
 <!ENTITY options.commonPrefs.label           "Common Preferences">
 
 <!-- LOCALIZATION NOTE (options.enablePersistentLogs.label): This is the
   -  label for the checkbox that toggles persistent logs in the Web Console and
   -  network monitor,  i.e. devtools.webconsole.persistlog a boolean preference in
   -  about:config, in the options panel. -->
--- a/browser/locales/en-US/chrome/browser/preferences/search.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/search.dtd
@@ -9,9 +9,18 @@
 <!ENTITY provideSearchSuggestions.label        "Provide search suggestions">
 <!ENTITY provideSearchSuggestions.accesskey    "s">
 
 
 <!ENTITY oneClickSearchEngines.label           "One-click search engines">
 
 <!ENTITY chooseWhichOneToDisplay.label         "The search bar lets you search alternate engines directly. Choose which ones to display.">
 
+<!ENTITY engineNameColumn.label                "Search Engine">
+<!ENTITY engineKeywordColumn.label             "Keyword">
+
+<!ENTITY restoreDefaultSearchEngines.label     "Restore Default Search Engines">
+<!ENTITY restoreDefaultSearchEngines.accesskey "d">
+
+<!ENTITY removeEngine.label                    "Remove">
+<!ENTITY removeEngine.accesskey                "r">
+
 <!ENTITY addMoreSearchEngines.label            "Add more search engines…">
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -136,111 +136,32 @@ treecol {
 #applicationsContent {
   padding: 15px 0;
 }
 
 #filter {
   -moz-margin-start: 0;
 }
 
-#handlersView {
-  -moz-appearance: none;
-  -moz-margin-start: 0;
-  font-size: 1.25rem;
-  line-height: 22px;
-  color: #333333;
-  border: 1px solid #C1C1C1;
-  border-radius: 2px;
-  background-color: #FBFBFB;
-  overflow-y: auto;
-  height: 500px;
-}
-
-#handlersView > listheader {
-  -moz-appearance: none;
-  border: 0;
-  padding: 0;
-}
-
-#typeColumn,
-#actionColumn {
-  -moz-appearance: none;
-  line-height: 20px;
-  color: #333333;
-  height: 36px;
-  padding: 0 10px;
-  background-color: #FBFBFB;
-  border: 1px solid #C1C1C1;
-  -moz-border-top-colors: none;
-  -moz-border-right-colors: none;
-  -moz-border-bottom-colors: none;
-  -moz-border-left-colors: none;
-}
-
-#typeColumn:-moz-locale-dir(ltr),
-#actionColumn:-moz-locale-dir(rtl) {
-  border-top-left-radius: 2px;
-}
-
-#typeColumn:-moz-locale-dir(rtl),
-#actionColumn:-moz-locale-dir(ltr) {
-  border-top-right-radius: 2px;
-}
-
-#typeColumn:hover,
-#actionColumn:hover {
-  border-color: #0095DD;
-}
-
-#typeColumn:hover:active,
-#actionColumn:hover:active {
-  padding: 0 10px;
-}
-
-#typeColumn > .treecol-sortdirection[sortDirection=ascending],
-#actionColumn > .treecol-sortdirection[sortDirection=ascending],
-#typeColumn > .treecol-sortdirection[sortDirection=descending],
-#actionColumn > .treecol-sortdirection[sortDirection=descending] {
-  -moz-appearance: none;
-  list-style-image: url("chrome://global/skin/in-content/sorter.png");
-}
-
-#typeColumn > .treecol-sortdirection[sortDirection=descending],
-#actionColumn > .treecol-sortdirection[sortDirection=descending] {
-  transform: scaleY(-1);
-}
-
-@media (min-resolution: 2dppx) {
-  #typeColumn > .treecol-sortdirection[sortDirection=ascending],
-  #actionColumn > .treecol-sortdirection[sortDirection=ascending],
-  #typeColumn > .treecol-sortdirection[sortDirection=descending],
-  #actionColumn > .treecol-sortdirection[sortDirection=descending] {
-    width: 12px;
-    height: 8px;
-    list-style-image: url("chrome://global/skin/in-content/sorter@2x.png");
-  }
-}
-
 #handlersView > richlistitem {
-  min-height: 40px !important;
+  min-height: 36px !important;
 }
 
 .typeIcon {
   -moz-margin-start: 10px !important;
   -moz-margin-end: 9px !important;
 }
 
 .actionIcon {
   -moz-margin-start: 11px !important;
   -moz-margin-end: 8px !important;
 }
 
 .actionsMenu {
-  height: 40px;
-  max-height: 40px;
+  min-height: 36px;
 }
 
 .actionsMenu > menupopup > menuitem {
   -moz-padding-start: 10px !important;
 }
 
 .actionsMenu > menupopup > menuitem > .menu-iconic-left {
   -moz-margin-end: 8px !important;
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -2893,16 +2893,21 @@ nsDocShell::PopProfileTimelineMarkers(JS
   // this array.
   nsTArray<TimelineMarker*> keptMarkers;
 
   for (uint32_t i = 0; i < mProfileTimelineMarkers.Length(); ++i) {
     TimelineMarker* startPayload = mProfileTimelineMarkers[i];
     const char* startMarkerName = startPayload->GetName();
 
     bool hasSeenPaintedLayer = false;
+    bool isPaint = strcmp(startMarkerName, "Paint") == 0;
+
+    // If we are processing a Paint marker, we append information from
+    // all the embedded Layer markers to this array.
+    mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect> layerRectangles;
 
     if (startPayload->GetMetaData() == TRACING_INTERVAL_START) {
       bool hasSeenEnd = false;
 
       // DOM events can be nested, so we must take care when searching
       // for the matching end.  It doesn't hurt to apply this logic to
       // all event types.
       uint32_t markerDepth = 0;
@@ -2910,40 +2915,44 @@ nsDocShell::PopProfileTimelineMarkers(JS
       // The assumption is that the devtools timeline flushes markers frequently
       // enough for the amount of markers to always be small enough that the
       // nested for loop isn't going to be a performance problem.
       for (uint32_t j = i + 1; j < mProfileTimelineMarkers.Length(); ++j) {
         TimelineMarker* endPayload = mProfileTimelineMarkers[j];
         const char* endMarkerName = endPayload->GetName();
 
         // Look for Layer markers to stream out paint markers.
-        if (strcmp(endMarkerName, "Layer") == 0) {
+        if (isPaint && strcmp(endMarkerName, "Layer") == 0) {
           hasSeenPaintedLayer = true;
+          endPayload->AddLayerRectangles(layerRectangles);
         }
 
         if (!startPayload->Equals(endPayload)) {
           continue;
         }
-        bool isPaint = strcmp(startMarkerName, "Paint") == 0;
 
         // Pair start and end markers.
         if (endPayload->GetMetaData() == TRACING_INTERVAL_START) {
           ++markerDepth;
         } else if (endPayload->GetMetaData() == TRACING_INTERVAL_END) {
           if (markerDepth > 0) {
             --markerDepth;
           } else {
             // But ignore paint start/end if no layer has been painted.
             if (!isPaint || (isPaint && hasSeenPaintedLayer)) {
               mozilla::dom::ProfileTimelineMarker marker;
 
               marker.mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
               marker.mStart = startPayload->GetTime();
               marker.mEnd = endPayload->GetTime();
-              startPayload->AddDetails(marker);
+              if (isPaint) {
+                marker.mRectangles.Construct(layerRectangles);
+              } else {
+                startPayload->AddDetails(marker);
+              }
               profileTimelineMarkers.AppendElement(marker);
             }
 
             // We want the start to be dropped either way.
             hasSeenEnd = true;
 
             break;
           }
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -300,16 +300,21 @@ public:
         }
 
         // Add details specific to this marker type to aMarker.  The
         // standard elements have already been set.
         virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker)
         {
         }
 
+        virtual void AddLayerRectangles(mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect>&)
+        {
+            MOZ_ASSERT_UNREACHABLE("can only be called on layer markers");
+        }
+
         const char* GetName() const
         {
             return mName;
         }
 
         TracingMetadata GetMetaData() const
         {
             return mMetaData;
--- a/docshell/test/browser/browser_timelineMarkers-02.js
+++ b/docshell/test/browser/browser_timelineMarkers-02.js
@@ -2,43 +2,57 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test that the docShell profile timeline API returns the right markers when
 // restyles, reflows and paints occur
 
 let URL = '<!DOCTYPE html><style>' +
+          'body {margin:0; padding: 0;} ' +
           'div {width:100px;height:100px;background:red;} ' +
           '.resize-change-color {width:50px;height:50px;background:blue;} ' +
           '.change-color {width:50px;height:50px;background:yellow;} ' +
           '.add-class {}' +
           '</style><div></div>';
 
 let TESTS = [{
   desc: "Changing the width of the test element",
   setup: function(div) {
     div.setAttribute("class", "resize-change-color");
   },
   check: function(markers) {
     ok(markers.length > 0, "markers were returned");
     console.log(markers);
+    info(JSON.stringify(markers.filter(m => m.name == "Paint")));
     ok(markers.some(m => m.name == "Reflow"), "markers includes Reflow");
     ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
+    for (let marker of markers.filter(m => m.name == "Paint")) {
+      // This change should generate at least one rectangle.
+      ok(marker.rectangles.length >= 1, "marker has one rectangle");
+      // One of the rectangles should contain the div.
+      ok(marker.rectangles.some(r => rectangleContains(r, 0, 0, 100, 100)));
+    }
     ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
   }
 }, {
   desc: "Changing the test element's background color",
   setup: function(div) {
     div.setAttribute("class", "change-color");
   },
   check: function(markers) {
     ok(markers.length > 0, "markers were returned");
     ok(!markers.some(m => m.name == "Reflow"), "markers doesn't include Reflow");
     ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
+    for (let marker of markers.filter(m => m.name == "Paint")) {
+      // This change should generate at least one rectangle.
+      ok(marker.rectangles.length >= 1, "marker has one rectangle");
+      // One of the rectangles should contain the div.
+      ok(marker.rectangles.some(r => rectangleContains(r, 0, 0, 50, 50)));
+    }
     ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
   }
 }, {
   desc: "Changing the test element's classname",
   setup: function(div) {
     div.setAttribute("class", "change-color add-class");
   },
   check: function(markers) {
@@ -140,8 +154,13 @@ function waitForMarkers(docshell) {
       if (waitIterationCount > maxWaitIterationCount) {
         clearInterval(interval);
         resolve([]);
       }
       waitIterationCount++;
     }, 200);
   });
 }
+
+function rectangleContains(rect, x, y, width, height) {
+  return rect.x <= x && rect.y <= y && rect.width >= width &&
+    rect.height >= height;
+}
--- a/dom/webidl/ProfileTimelineMarker.webidl
+++ b/dom/webidl/ProfileTimelineMarker.webidl
@@ -1,16 +1,25 @@
 /* -*- 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/.
  */
 
+dictionary ProfileTimelineLayerRect {
+  long x = 0;
+  long y = 0;
+  long width = 0;
+  long height = 0;
+};
+
 dictionary ProfileTimelineMarker {
   DOMString name = "";
   DOMHighResTimeStamp start = 0;
   DOMHighResTimeStamp end = 0;
   /* For ConsoleTime markers.  */
   DOMString causeName;
   /* For DOMEvent markers.  */
   DOMString type;
   unsigned short eventPhase;
+  /* For Paint markers.  */
+  sequence<ProfileTimelineLayerRect> rectangles;
 };
--- a/layout/base/FrameLayerBuilder.cpp
+++ b/layout/base/FrameLayerBuilder.cpp
@@ -21,16 +21,17 @@
 #include "LayerTreeInvalidation.h"
 #include "nsSVGIntegrationUtils.h"
 #include "ImageContainer.h"
 #include "ActiveLayerTracker.h"
 #include "gfx2DGlue.h"
 #include "mozilla/LookAndFeel.h"
 #include "nsDocShell.h"
 #include "nsImageFrame.h"
+#include "mozilla/dom/ProfileTimelineMarkerBinding.h"
 
 #include "GeckoProfiler.h"
 #include "mozilla/gfx/Tools.h"
 #include "mozilla/gfx/2D.h"
 #include "gfxPrefs.h"
 
 #include <algorithm>
 
@@ -4414,16 +4415,46 @@ static void DrawForcedBackgroundColor(Dr
 {
   if (NS_GET_A(aBackgroundColor) > 0) {
     nsIntRect r = aLayer->GetVisibleRegion().GetBounds();
     ColorPattern color(ToDeviceColor(aBackgroundColor));
     aDrawTarget.FillRect(Rect(r.x, r.y, r.width, r.height), color);
   }
 }
 
+class LayerTimelineMarker : public nsDocShell::TimelineMarker
+{
+public:
+  LayerTimelineMarker(nsDocShell* aDocShell, const nsIntRegion& aRegion)
+    : nsDocShell::TimelineMarker(aDocShell, "Layer", TRACING_EVENT)
+    , mRegion(aRegion)
+  {
+  }
+
+  ~LayerTimelineMarker()
+  {
+  }
+
+  virtual void AddLayerRectangles(mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect>& aRectangles)
+  {
+    nsIntRegionRectIterator it(mRegion);
+    while (const nsIntRect* iterRect = it.Next()) {
+      mozilla::dom::ProfileTimelineLayerRect rect;
+      rect.mX = iterRect->X();
+      rect.mY = iterRect->Y();
+      rect.mWidth = iterRect->Width();
+      rect.mHeight = iterRect->Height();
+      aRectangles.AppendElement(rect);
+    }
+  }
+
+private:
+  nsIntRegion mRegion;
+};
+
 /*
  * A note on residual transforms:
  *
  * In a transformed subtree we sometimes apply the PaintedLayer's
  * "residual transform" when drawing content into the PaintedLayer.
  * This is a translation by components in the range [-0.5,0.5) provided
  * by the layer system; applying the residual transform followed by the
  * transforms used by layer compositing ensures that the subpixel alignment
@@ -4563,17 +4594,23 @@ FrameLayerBuilder::DrawPaintedLayer(Pain
         gfxUtils::ClipToRegion(aContext, aRegionToDraw);
       }
     }
     FlashPaint(aContext);
   }
 
   if (presContext && presContext->GetDocShell() && isActiveLayerManager) {
     nsDocShell* docShell = static_cast<nsDocShell*>(presContext->GetDocShell());
-    docShell->AddProfileTimelineMarker("Layer", TRACING_EVENT);
+    bool isRecording;
+    docShell->GetRecordProfileTimelineMarkers(&isRecording);
+    if (isRecording) {
+      mozilla::UniquePtr<nsDocShell::TimelineMarker> marker =
+        MakeUnique<LayerTimelineMarker>(docShell, aRegionToDraw);
+      docShell->AddProfileTimelineMarker(marker);
+    }
   }
 
   if (!aRegionToInvalidate.IsEmpty()) {
     aLayer->AddInvalidRect(aRegionToInvalidate.GetBounds());
   }
 }
 
 bool
--- a/mobile/android/base/AppConstants.java.in
+++ b/mobile/android/base/AppConstants.java.in
@@ -230,16 +230,23 @@ public class AppConstants {
     // https://wiki.mozilla.org/Platform/Channel-specific_build_defines
     public static final boolean RELEASE_BUILD =
 //#ifdef RELEASE_BUILD
     true;
 //#else
     false;
 //#endif
 
+    public static final boolean NIGHTLY_BUILD =
+//#ifdef NIGHTLY_BUILD
+    true;
+//#else
+    false;
+//#endif
+
     public static final boolean DEBUG_BUILD =
 //#ifdef MOZ_DEBUG
     true;
 //#else
     false;
 //#endif
 
     public static final boolean MOZ_MEDIA_PLAYER =
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -167,16 +167,18 @@
 <!ENTITY pref_header_vendor "&vendorShortName;">
 <!ENTITY pref_header_devtools "Developer tools">
 
 <!ENTITY pref_cookies_menu "Cookies">
 <!ENTITY pref_cookies_accept_all "Enabled">
 <!ENTITY pref_cookies_not_accept_foreign "Enabled, excluding 3rd party">
 <!ENTITY pref_cookies_disabled "Disabled">
 
+<!ENTITY pref_tracking_protection_title "Tracking protection">
+<!ENTITY pref_tracking_protection_summary "&brandShortName; will prevent sites from tracking you">
 <!ENTITY pref_donottrack_title "Do not track">
 <!ENTITY pref_donottrack_summary "&brandShortName; will tell sites that you do not want to be tracked">
 
 <!ENTITY pref_char_encoding "Character encoding">
 <!ENTITY pref_char_encoding_on "Show menu">
 <!ENTITY pref_char_encoding_off "Don\'t show menu">
 <!ENTITY pref_clear_private_data2 "Clear now">
 <!ENTITY pref_clear_private_data_category "Clear private data">
--- a/mobile/android/base/preferences/GeckoPreferences.java
+++ b/mobile/android/base/preferences/GeckoPreferences.java
@@ -118,16 +118,18 @@ OnSharedPreferenceChangeListener
     private static final String PREFS_UPDATER_AUTODOWNLOAD = "app.update.autodownload";
     private static final String PREFS_GEO_REPORTING = NON_PREF_PREFIX + "app.geo.reportdata";
     private static final String PREFS_GEO_LEARN_MORE = NON_PREF_PREFIX + "geo.learn_more";
     private static final String PREFS_HEALTHREPORT_LINK = NON_PREF_PREFIX + "healthreport.link";
     private static final String PREFS_DEVTOOLS_REMOTE_ENABLED = "devtools.debugger.remote-enabled";
     private static final String PREFS_DISPLAY_REFLOW_ON_ZOOM = "browser.zoom.reflowOnZoom";
     private static final String PREFS_DISPLAY_TITLEBAR_MODE = "browser.chrome.titlebarMode";
     private static final String PREFS_SYNC = NON_PREF_PREFIX + "sync";
+    private static final String PREFS_TRACKING_PROTECTION = "privacy.trackingprotection.enabled";
+    private static final String PREFS_TRACKING_PROTECTION_LEARN_MORE = NON_PREF_PREFIX + "trackingprotection.learn_more";
 
     private static final String ACTION_STUMBLER_UPLOAD_PREF = AppConstants.ANDROID_PACKAGE_NAME + ".STUMBLER_PREF";
 
     // This isn't a Gecko pref, even if it looks like one.
     private static final String PREFS_BROWSER_LOCALE = "locale";
 
     public static final String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession3";
     public static final String PREFS_SUGGESTED_SITES = NON_PREF_PREFIX + "home_suggested_sites";
@@ -677,16 +679,23 @@ OnSharedPreferenceChangeListener
                     i--;
                     continue;
                 } else if ((AppConstants.RELEASE_BUILD || !HardwareUtils.isTablet()) &&
                            PREFS_NEW_TABLET_UI.equals(key)) {
                     // Remove toggle for new tablet UI on release builds and phones.
                     preferences.removePreference(pref);
                     i--;
                     continue;
+                } else if (!AppConstants.NIGHTLY_BUILD &&
+                           (PREFS_TRACKING_PROTECTION.equals(key) ||
+                            PREFS_TRACKING_PROTECTION_LEARN_MORE.equals(key))) {
+                    // Remove UI for tracking protection preference on non-Nightly builds.
+                    preferences.removePreference(pref);
+                    i--;
+                    continue;
                 } else if (!AppConstants.MOZ_TELEMETRY_REPORTING &&
                            PREFS_TELEMETRY_ENABLED.equals(key)) {
                     preferences.removePreference(pref);
                     i--;
                     continue;
                 } else if (!AppConstants.MOZ_SERVICES_HEALTHREPORT &&
                            (PREFS_HEALTHREPORT_UPLOAD_ENABLED.equals(key) ||
                             PREFS_HEALTHREPORT_LINK.equals(key))) {
--- a/mobile/android/base/resources/xml/preferences_privacy.xml
+++ b/mobile/android/base/resources/xml/preferences_privacy.xml
@@ -3,36 +3,50 @@
    - 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/. -->
 
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
                   xmlns:gecko="http://schemas.android.com/apk/res-auto"
                   android:title="@string/pref_category_privacy_short"
                   android:enabled="false">
 
+    <CheckBoxPreference android:key="privacy.trackingprotection.enabled"
+                        android:title="@string/pref_tracking_protection_title"
+                        android:summary="@string/pref_tracking_protection_summary"
+                        android:persistent="false" />
+
+    <org.mozilla.gecko.preferences.AlignRightLinkPreference
+            android:key="android.not_a_preference.trackingprotection.learn_more"
+            android:title="@string/pref_learn_more"
+            android:persistent="false"
+            url="https://support.mozilla.org/kb/firefox-android-tracking-protection" />
+
     <CheckBoxPreference android:key="privacy.donottrackheader.enabled"
                         android:title="@string/pref_donottrack_title"
                         android:summary="@string/pref_donottrack_summary"
-                        android:defaultValue="false"
                         android:persistent="false" />
 
+    <org.mozilla.gecko.preferences.AlignRightLinkPreference
+            android:key="android.not_a_preference.donottrackheader.learn_more"
+            android:title="@string/pref_learn_more"
+            android:persistent="false"
+            url="https://www.mozilla.org/firefox/dnt/" />
+
     <ListPreference android:key="network.cookie.cookieBehavior"
                     android:title="@string/pref_cookies_menu"
                     android:entries="@array/pref_cookies_entries"
                     android:entryValues="@array/pref_cookies_values"
                     android:persistent="false" />
 
     <CheckBoxPreference android:key="signon.rememberSignons"
                         android:title="@string/pref_remember_signons"
-                        android:defaultValue="true"
                         android:persistent="false" />
 
     <CheckBoxPreference android:key="privacy.masterpassword.enabled"
                         android:title="@string/pref_use_master_password"
-                        android:defaultValue="false"
                         android:persistent="false" />
 
     <!-- keys prefixed with "android.not_a_preference." are not synced with Gecko -->
     <PreferenceCategory android:title="@string/pref_clear_private_data_category">
 
         <org.mozilla.gecko.preferences.PrivateDataPreference
                             android:key="android.not_a_preference.privacy.clear"
                             android:title="@string/pref_clear_private_data"
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -177,16 +177,18 @@
 
   <string name="pref_remember_signons">&pref_remember_signons;</string>
 
   <string name="pref_cookies_menu">&pref_cookies_menu;</string>
   <string name="pref_cookies_accept_all">&pref_cookies_accept_all;</string>
   <string name="pref_cookies_not_accept_foreign">&pref_cookies_not_accept_foreign;</string>
   <string name="pref_cookies_disabled">&pref_cookies_disabled;</string>
 
+  <string name="pref_tracking_protection_title">&pref_tracking_protection_title;</string>
+  <string name="pref_tracking_protection_summary">&pref_tracking_protection_summary;</string>
   <string name="pref_donottrack_title">&pref_donottrack_title;</string>
   <string name="pref_donottrack_summary">&pref_donottrack_summary;</string>
 
   <string name="pref_char_encoding">&pref_char_encoding;</string>
   <string name="pref_char_encoding_on">&pref_char_encoding_on;</string>
   <string name="pref_char_encoding_off">&pref_char_encoding_off;</string>
   <string name="pref_clear_private_data">&pref_clear_private_data2;</string>
   <string name="pref_clear_private_data_category">&pref_clear_private_data_category;</string>
--- a/mobile/android/base/tests/BaseRobocopTest.java
+++ b/mobile/android/base/tests/BaseRobocopTest.java
@@ -50,16 +50,19 @@ public abstract class BaseRobocopTest ex
             cl = Activity.class;
         }
         BROWSER_INTENT_CLASS = cl;
     }
 
     protected Assert mAsserter;
     protected String mLogFile;
 
+    protected String mBaseHostnameUrl;
+    protected String mBaseIpUrl;
+
     protected Map<String, String> mConfig;
     protected String mRootPath;
 
     /**
      * The browser is started at the beginning of this test. A single test is a
      * class inheriting from <code>BaseRobocopTest</code> that contains test
      * methods.
      * <p>
@@ -107,16 +110,19 @@ public abstract class BaseRobocopTest ex
         // Initialize the asserter.
         if (getTestType() == Type.TALOS) {
             mAsserter = new FennecTalosAssert();
         } else {
             mAsserter = new FennecMochitestAssert();
         }
         mAsserter.setLogFile(mLogFile);
         mAsserter.setTestName(getClass().getName());
+
+        mBaseHostnameUrl = mConfig.get("host").replaceAll("(/$)", "");
+        mBaseIpUrl = mConfig.get("rawhost").replaceAll("(/$)", "");
     }
 
     /**
      * Function to early abort if we can't reach the given HTTP server. Provides local testers
      * with diagnostic information. Not currently available for TALOS tests, which are rarely run
      * locally in any case.
      */
     public void throwIfHttpGetFails() {
--- a/mobile/android/base/tests/BaseTest.java
+++ b/mobile/android/base/tests/BaseTest.java
@@ -72,18 +72,16 @@ abstract class BaseTest extends BaseRobo
 
     private static final String URL_HTTP_PREFIX = "http://";
 
     private Activity mActivity;
     private int mPreferenceRequestID = 0;
     protected Solo mSolo;
     protected Driver mDriver;
     protected Actions mActions;
-    protected String mBaseUrl;
-    protected String mRawBaseUrl;
     protected String mProfile;
     public Device mDevice;
     protected DatabaseHelper mDatabaseHelper;
     protected int mScreenMidWidth;
     protected int mScreenMidHeight;
     private final HashSet<Integer> mKnownTabIDs = new HashSet<Integer>();
 
     protected void blockForDelayedStartup() {
@@ -108,18 +106,16 @@ abstract class BaseTest extends BaseRobo
         }
     }
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
 
         // Create the intent to be used with all the important arguments.
-        mBaseUrl = mConfig.get("host").replaceAll("(/$)", "");
-        mRawBaseUrl = mConfig.get("rawhost").replaceAll("(/$)", "");
         Intent i = new Intent(Intent.ACTION_MAIN);
         mProfile = mConfig.get("profile");
         i.putExtra("args", "-no-remote -profile " + mProfile);
         String envString = mConfig.get("envvars");
         if (envString != "") {
             String[] envStrings = envString.split(",");
             for (int iter = 0; iter < envStrings.length; iter++) {
                 i.putExtra("env" + iter, envStrings[iter]);
@@ -306,21 +302,21 @@ abstract class BaseTest extends BaseRobo
         @Override
         public boolean isSatisfied() {
             String textValue = mTextView.getText().toString();
             return mExpected.equals(textValue);
         }
     }
 
     protected final String getAbsoluteUrl(String url) {
-        return mBaseUrl + "/" + url.replaceAll("(^/)", "");
+        return mBaseHostnameUrl + "/" + url.replaceAll("(^/)", "");
     }
 
     protected final String getAbsoluteRawUrl(String url) {
-        return mRawBaseUrl + "/" + url.replaceAll("(^/)", "");
+        return mBaseIpUrl + "/" + url.replaceAll("(^/)", "");
     }
 
     /*
      * Wrapper method for mSolo.waitForCondition with additional logging.
      */
     protected final boolean waitForCondition(Condition condition, int timeout) {
         boolean result = mSolo.waitForCondition(condition, timeout);
         if (!result) {
--- a/mobile/android/base/tests/StringHelper.java
+++ b/mobile/android/base/tests/StringHelper.java
@@ -172,17 +172,18 @@ public class StringHelper {
     public static final String CHARACTER_ENCODING_LABEL = "Character encoding";
     public static final String PLUGINS_LABEL = "Plugins";
 
     // Title bar
     public static final String SHOW_PAGE_TITLE_LABEL = "Show page title";
     public static final String SHOW_PAGE_ADDRESS_LABEL = "Show page address";
 
     // Privacy
-    public static final String TRACKING_LABEL = "Do not track";
+    public static final String TRACKING_PROTECTION_LABEL = "Tracking protection";
+    public static final String DNT_LABEL = "Do not track";
     public static final String COOKIES_LABEL = "Cookies";
     public static final String REMEMBER_PASSWORDS_LABEL = "Remember passwords";
     public static final String MASTER_PASSWORD_LABEL = "Use master password";
     public static final String CLEAR_PRIVATE_DATA_LABEL = "Clear now";
 
     // Mozilla
     public static final String BRAND_NAME = "(Fennec|Nightly|Aurora|Firefox Beta|Firefox)";
     public static final String ABOUT_LABEL = "About " + BRAND_NAME;
--- a/mobile/android/base/tests/UITest.java
+++ b/mobile/android/base/tests/UITest.java
@@ -40,21 +40,16 @@ abstract class UITest extends BaseRoboco
 
     private static final String JUNIT_FAILURE_MSG = "A JUnit method was called. Make sure " +
         "you are using AssertionHelper to make assertions. Try `fAssert*(...);`";
 
     private Solo mSolo;
     private Driver mDriver;
     private Actions mActions;
 
-    // Base to build hostname URLs
-    private String mBaseHostnameUrl;
-    // Base to build IP URLs
-    private String mBaseIpUrl;
-
     protected AboutHomeComponent mAboutHome;
     protected AppMenuComponent mAppMenu;
     protected GeckoViewComponent mGeckoView;
     protected ToolbarComponent mToolbar;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
@@ -63,19 +58,16 @@ abstract class UITest extends BaseRoboco
         final Intent intent = createActivityIntent(mConfig);
         setActivityIntent(intent);
         final Activity activity = getActivity();
 
         mSolo = new Solo(getInstrumentation(), activity);
         mDriver = new FennecNativeDriver(activity, mSolo, mRootPath);
         mActions = new FennecNativeActions(activity, mSolo, getInstrumentation(), mAsserter);
 
-        mBaseHostnameUrl = mConfig.get("host").replaceAll("(/$)", "");
-        mBaseIpUrl = mConfig.get("rawhost").replaceAll("(/$)", "");
-
         // Helpers depend on components so initialize them first.
         initComponents();
         initHelpers();
 
         // Ensure Robocop tests have access to network, and are run with Display powered on.
         throwIfHttpGetFails();
         throwIfScreenNotOn();
     }
--- a/mobile/android/base/tests/testSettingsMenuItems.java
+++ b/mobile/android/base/tests/testSettingsMenuItems.java
@@ -55,17 +55,18 @@ public class testSettingsMenuItems exten
         { "Advanced" },
         { StringHelper.CHARACTER_ENCODING_LABEL, "Don't show menu", "Show menu", "Don't show menu" },
         { StringHelper.PLUGINS_LABEL, "Tap to play", "Enabled", "Tap to play", "Disabled" },
     };
 
     // Privacy menu items.
     String[] PATH_PRIVACY = { StringHelper.PRIVACY_SECTION_LABEL };
     String[][] OPTIONS_PRIVACY = {
-        { StringHelper.TRACKING_LABEL },
+        { StringHelper.TRACKING_PROTECTION_LABEL },
+        { StringHelper.DNT_LABEL },
         { StringHelper.COOKIES_LABEL, "Enabled", "Enabled, excluding 3rd party", "Disabled" },
         { StringHelper.REMEMBER_PASSWORDS_LABEL },
         { StringHelper.MASTER_PASSWORD_LABEL },
         { StringHelper.CLEAR_PRIVATE_DATA_LABEL, "", "Browsing history", "Downloads", "Form & search history", "Cookies & active logins", "Saved passwords", "Cache", "Offline website data", "Site settings", "Clear data" },
     };
 
     // Mozilla/vendor menu items.
     String[] PATH_MOZILLA = { StringHelper.MOZILLA_SECTION_LABEL };
--- a/mobile/android/base/toolbar/SiteIdentityPopup.java
+++ b/mobile/android/base/toolbar/SiteIdentityPopup.java
@@ -34,17 +34,17 @@ import android.widget.TextView;
  */
 public class SiteIdentityPopup extends ArrowPopup {
     private static final String LOGTAG = "GeckoSiteIdentityPopup";
 
     private static final String MIXED_CONTENT_SUPPORT_URL =
         "https://support.mozilla.org/kb/how-does-insecure-content-affect-safety-android";
 
     private static final String TRACKING_CONTENT_SUPPORT_URL =
-        "https://support.mozilla.org/kb/how-does-insecure-content-affect-safety-android";
+        "https://support.mozilla.org/kb/firefox-android-tracking-protection";
 
     private SiteIdentity mSiteIdentity;
 
     private LinearLayout mIdentity;
 
     private LinearLayout mIdentityKnownContainer;
     private LinearLayout mIdentityUnknownContainer;
 
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -451,93 +451,25 @@ let Bookmarks = Object.freeze({
    *
    * Note that roots are preserved, only their children will be removed.
    *
    * @return {Promise} resolved when the removal is complete.
    * @resolves once the removal is complete.
    */
   eraseEverything: Task.async(function* () {
     let db = yield DBConnPromised;
-
     yield db.executeTransaction(function* () {
-      let rows = yield db.executeCached(
-        `WITH RECURSIVE
-         descendants(did) AS (
-           SELECT b.id FROM moz_bookmarks b
-           JOIN moz_bookmarks p ON b.parent = p.id
-           WHERE p.guid IN ( :toolbarGuid, :menuGuid, :unfiledGuid )
-           UNION ALL
-           SELECT id FROM moz_bookmarks
-           JOIN descendants ON parent = did
-         )
-         SELECT b.id AS _id, b.parent AS _parentId, b.position AS 'index',
-                b.type, url, b.guid, p.guid AS parentGuid, b.dateAdded,
-                b.lastModified, b.title, p.parent AS _grandParentId,
-                NULL AS _childCount, NULL AS keyword
-         FROM moz_bookmarks b
-         JOIN moz_bookmarks p ON p.id = b.parent
-         LEFT JOIN moz_places h ON b.fk = h.id
-         WHERE b.id IN descendants
-        `, { menuGuid: this.menuGuid, toolbarGuid: this.toolbarGuid,
-             unfiledGuid: this.unfiledGuid });
-      let items = rowsToItemsArray(rows);
-
-      yield db.executeCached(
-        `WITH RECURSIVE
-         descendants(did) AS (
-           SELECT b.id FROM moz_bookmarks b
-           JOIN moz_bookmarks p ON b.parent = p.id
-           WHERE p.guid IN ( :toolbarGuid, :menuGuid, :unfiledGuid )
-           UNION ALL
-           SELECT id FROM moz_bookmarks
-           JOIN descendants ON parent = did
-         )
-         DELETE FROM moz_bookmarks WHERE id IN descendants
-        `, { menuGuid: this.menuGuid, toolbarGuid: this.toolbarGuid,
-             unfiledGuid: this.unfiledGuid });
-
-      // Clenup orphans.
-      yield removeOrphanAnnotations(db);
-      yield removeOrphanKeywords(db);
-
-      // TODO (Bug 1087576): this may leave orphan tags behind.
-
-      // Update roots' lastModified.
-      yield db.executeCached(
-        `UPDATE moz_bookmarks SET lastModified = :time
-         WHERE id IN (SELECT id FROM moz_bookmarks
-                      WHERE guid IN ( :rootGuid, :toolbarGuid, :menuGuid, :unfiledGuid ))
-        `, { time: toPRTime(new Date()), rootGuid: this.rootGuid,
-             menuGuid: this.menuGuid, toolbarGuid: this.toolbarGuid,
-             unfiledGuid: this.unfiledGuid });
-
-      let urls = [for (item of items) if (item.url) item.url];
-      updateFrecency(db, urls).then(null, Cu.reportError);
-
-      // Send onItemRemoved notifications to listeners.
-      // TODO (Bug 1087580): this should send a single clear bookmarks
-      // notification rather than notifying for each bookmark.
-
-      // Notify listeners in reverse order to serve children before parents.
-      let observers = PlacesUtils.bookmarks.getObservers();
-      for (let item of items.reverse()) {
-        let uri = item.hasOwnProperty("url") ? toURI(item.url) : null;
-        notify(observers, "onItemRemoved", [ item._id, item._parentId,
-                                             item.index, item.type, uri,
-                                             item.guid, item.parentGuid ]);
-
-        let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
-        if (isUntagging) {
-          for (let entry of (yield fetchBookmarksByURL(item))) {
-            notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
-                                                 toPRTime(entry.lastModified),
-                                                 entry.type, entry._parentId,
-                                                 entry.guid, entry.parentGuid ]);
-          }
-        }
+      const folderGuids = [this.toolbarGuid, this.menuGuid, this.unfiledGuid];
+      yield removeFoldersContents(db, folderGuids);
+      const time = toPRTime(new Date());
+      for (let folderGuid of folderGuids) {
+        yield db.executeCached(
+          `UPDATE moz_bookmarks SET lastModified = :time
+           WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid )
+          `, { folderGuid, time });
       }
     }.bind(this));
   }),
 
   /**
    * Fetches information about a bookmark-item.
    *
    * REMARK: any successful call to this method resolves to a single
@@ -1021,16 +953,20 @@ function* fetchBookmarksByKeyword(info) 
 // Remove implementation.
 
 function* removeBookmark(item) {
   let db = yield DBConnPromised;
 
   let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
 
   yield db.executeTransaction(function* transaction() {
+    // If it's a folder, remove its contents first.
+    if (item.type == Bookmarks.TYPE_FOLDER)
+      yield removeFoldersContents(db, [item.guid]);
+
     // Remove annotations first.  If it's a tag, we can avoid paying that cost.
     if (!isUntagging) {
       // We don't go through the annotations service for this cause otherwise
       // we'd get a pointless onItemChanged notification and it would also
       // set lastModified to an unexpected value.
       yield removeAnnotationsForItem(db, item._id);
     }
 
@@ -1409,8 +1345,88 @@ let setAncestorsLastModified = Task.asyn
        JOIN ancestors ON id = aid
        WHERE type = :type
      )
      UPDATE moz_bookmarks SET lastModified = :time
      WHERE id IN ancestors
     `, { guid: folderGuid, type: Bookmarks.TYPE_FOLDER,
          time: toPRTime(time) });
 });
+
+/**
+ * Remove all descendants of one or more bookmark folders.
+ *
+ * @param db
+ *        the Sqlite.jsm connection handle.
+ * @param folderGuids
+ *        array of folder guids.
+ */
+let removeFoldersContents =
+Task.async(function* (db, folderGuids) {
+  let itemsRemoved = [];
+  for (let folderGuid of folderGuids) {
+    let rows = yield db.executeCached(
+      `WITH RECURSIVE
+       descendants(did) AS (
+         SELECT b.id FROM moz_bookmarks b
+         JOIN moz_bookmarks p ON b.parent = p.id
+         WHERE p.guid = :folderGuid
+         UNION ALL
+         SELECT id FROM moz_bookmarks
+         JOIN descendants ON parent = did
+       )
+       SELECT b.id AS _id, b.parent AS _parentId, b.position AS 'index',
+              b.type, url, b.guid, p.guid AS parentGuid, b.dateAdded,
+              b.lastModified, b.title, p.parent AS _grandParentId,
+              NULL AS _childCount, NULL AS keyword
+       FROM moz_bookmarks b
+       JOIN moz_bookmarks p ON p.id = b.parent
+       LEFT JOIN moz_places h ON b.fk = h.id
+       WHERE b.id IN descendants`, { folderGuid });
+
+    itemsRemoved = itemsRemoved.concat(rowsToItemsArray(rows));
+
+    yield db.executeCached(
+      `WITH RECURSIVE
+       descendants(did) AS (
+         SELECT b.id FROM moz_bookmarks b
+         JOIN moz_bookmarks p ON b.parent = p.id
+         WHERE p.guid = :folderGuid
+         UNION ALL
+         SELECT id FROM moz_bookmarks
+         JOIN descendants ON parent = did
+       )
+       DELETE FROM moz_bookmarks WHERE id IN descendants`, { folderGuid });
+  }
+
+  // Cleanup orphans.
+  yield removeOrphanAnnotations(db);
+  yield removeOrphanKeywords(db);
+
+  // TODO (Bug 1087576): this may leave orphan tags behind.
+
+  let urls = [for (item of itemsRemoved) if (item.url) item.url];
+  updateFrecency(db, urls).then(null, Cu.reportError);
+
+  // Send onItemRemoved notifications to listeners.
+  // TODO (Bug 1087580): for the case of eraseEverything, this should send a
+  // single clear bookmarks notification rather than notifying for each
+  // bookmark.
+
+  // Notify listeners in reverse order to serve children before parents.
+  let observers = PlacesUtils.bookmarks.getObservers();
+  for (let item of itemsRemoved.reverse()) {
+    let uri = item.hasOwnProperty("url") ? toURI(item.url) : null;
+    notify(observers, "onItemRemoved", [ item._id, item._parentId,
+                                         item.index, item.type, uri,
+                                         item.guid, item.parentGuid ]);
+
+    let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
+    if (isUntagging) {
+      for (let entry of (yield fetchBookmarksByURL(item))) {
+        notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
+                                             toPRTime(entry.lastModified),
+                                             entry.type, entry._parentId,
+                                             entry.guid, entry.parentGuid ]);
+      }
+    }
+  }
+});
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
@@ -310,16 +310,56 @@ add_task(function* remove_bookmark_tag_n
                                   tag.url, tag.guid, tag.parentGuid ] },
                    { name: "onItemChanged",
                      arguments: [ itemId, "tags", false, "",
                                   bm.lastModified, bm.type, parentId,
                                   bm.guid, bm.parentGuid ] }
                  ]);
 });
 
+add_task(function* remove_folder_notification() {
+  let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+                                                     parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+  let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid);
+  let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid);
+
+  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+                                                parentGuid: folder1.guid,
+                                                url: new URL("http://example.com/") });
+  let bmItemId = yield PlacesUtils.promiseItemId(bm.guid);
+
+  let folder2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+                                                     parentGuid: folder1.guid });
+  let folder2Id = yield PlacesUtils.promiseItemId(folder2.guid);
+
+  let bm2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+                                                 parentGuid: folder2.guid,
+                                                 url: new URL("http://example.com/") });
+  let bm2ItemId = yield PlacesUtils.promiseItemId(bm2.guid);
+
+  let observer = expectNotifications();
+  yield PlacesUtils.bookmarks.remove(folder1.guid);
+
+  observer.check([ { name: "onItemRemoved",
+                     arguments: [ bm2ItemId, folder2Id, bm2.index, bm2.type,
+                                  bm2.url, bm2.guid, bm2.parentGuid ] },
+                   { name: "onItemRemoved",
+                     arguments: [ folder2Id, folder1Id, folder2.index,
+                                  folder2.type, null, folder2.guid,
+                                  folder2.parentGuid ] },
+                   { name: "onItemRemoved",
+                     arguments: [ bmItemId, folder1Id, bm.index, bm.type,
+                                  bm.url, bm.guid, bm.parentGuid ] },
+                   { name: "onItemRemoved",
+                     arguments: [ folder1Id, folder1ParentId, folder1.index,
+                                  folder1.type, null, folder1.guid,
+                                  folder1.parentGuid ] }
+                 ]);
+});
+
 add_task(function* eraseEverything_notification() {
   // Let's start from a clean situation.
   yield PlacesUtils.bookmarks.eraseEverything();
 
   let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                      parentGuid: PlacesUtils.bookmarks.unfiledGuid });
   let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid);
   let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid);
@@ -343,39 +383,39 @@ add_task(function* eraseEverything_notif
 
   let menuBm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                     parentGuid: PlacesUtils.bookmarks.menuGuid,
                                                     url: new URL("http://example.com/") });
   let menuBmId = yield PlacesUtils.promiseItemId(menuBm.guid);
   let menuBmParentId = yield PlacesUtils.promiseItemId(menuBm.parentGuid);
 
   let observer = expectNotifications();
-  let removed = yield PlacesUtils.bookmarks.eraseEverything();
+  yield PlacesUtils.bookmarks.eraseEverything();
 
   observer.check([ { name: "onItemRemoved",
-                     arguments: [ menuBmId, menuBmParentId,
-                                  menuBm.index, menuBm.type,
-                                  menuBm.url, menuBm.guid,
-                                  menuBm.parentGuid ] },
-                   { name: "onItemRemoved",
-                     arguments: [ toolbarBmId, toolbarBmParentId,
-                                  toolbarBm.index, toolbarBm.type,
-                                  toolbarBm.url, toolbarBm.guid,
-                                  toolbarBm.parentGuid ] },
-                   { name: "onItemRemoved",
                      arguments: [ folder2Id, folder2ParentId, folder2.index,
                                   folder2.type, null, folder2.guid,
                                   folder2.parentGuid ] },
                    { name: "onItemRemoved",
                      arguments: [ itemId, parentId, bm.index, bm.type,
                                   bm.url, bm.guid, bm.parentGuid ] },
                    { name: "onItemRemoved",
                      arguments: [ folder1Id, folder1ParentId, folder1.index,
                                   folder1.type, null, folder1.guid,
-                                  folder1.parentGuid ] }
+                                  folder1.parentGuid ] },
+                   { name: "onItemRemoved",
+                     arguments: [ menuBmId, menuBmParentId,
+                                  menuBm.index, menuBm.type,
+                                  menuBm.url, menuBm.guid,
+                                  menuBm.parentGuid ] },
+                    { name: "onItemRemoved",
+                     arguments: [ toolbarBmId, toolbarBmParentId,
+                                  toolbarBm.index, toolbarBm.type,
+                                  toolbarBm.url, toolbarBm.guid,
+                                  toolbarBm.parentGuid ] }
                  ]);
 });
 
 function expectNotifications() {
   let notifications = [];
   let observer = new Proxy(NavBookmarkObserver, {
     get(target, name) {
       if (name == "check") {
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
@@ -142,16 +142,30 @@ add_task(function* remove_folder() {
   Assert.equal(bm2.index, 0);
   Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
   Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_FOLDER);
   Assert.equal(bm2.title, "a folder");
   Assert.ok(!("url" in bm2));
   Assert.ok(!("keyword" in bm2));
 });
 
+add_task(function* test_nested_contents_removed() {
+  let folder1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+                                                     type: PlacesUtils.bookmarks.TYPE_FOLDER,
+                                                     title: "a folder" });
+  let folder2 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder1.guid,
+                                                     type: PlacesUtils.bookmarks.TYPE_FOLDER,
+                                                     title: "a folder" });
+  let sep = yield PlacesUtils.bookmarks.insert({ parentGuid: folder2.guid,
+                                                 type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
+  yield PlacesUtils.bookmarks.remove(folder1);
+  Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(folder1.guid)), null);
+  Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(folder2.guid)), null);
+  Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(sep.guid)), null);
+});
 add_task(function* remove_folder_empty_title() {
   let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                  title: "" });
   checkBookmarkObject(bm1);
 
   let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid);
   checkBookmarkObject(bm2);
--- a/toolkit/themes/shared/in-content/common.inc.css
+++ b/toolkit/themes/shared/in-content/common.inc.css
@@ -161,16 +161,32 @@ xul|menulist[open="true"]:not([disabled=
 html|button:disabled,
 xul|button[disabled="true"],
 xul|colorpicker[type="button"][disabled="true"],
 xul|menulist[disabled="true"] {
   cursor: not-allowed;
   opacity: 0.5;
 }
 
+*|button.primary {
+  background-color: #0095dd;
+  border-color: transparent;
+  color: #fff;
+}
+
+html|button.primary:enabled:hover,
+xul|button.primary:not([disabled="true"]):hover {
+  background-color: #008acb;
+}
+
+html|button.primary:enabled:hover:active,
+xul|button.primary:not([disabled="true"]):hover:active {
+  background-color: #006b9d;
+}
+
 xul|colorpicker[type="button"] {
   padding: 6px;
   width: 50px;
 }
 
 xul|button > xul|*.button-box,
 xul|menulist > xul|*.menulist-label-box {
   padding-right: 10px !important;
@@ -556,8 +572,83 @@ xul|filefield + xul|button:-moz-locale-d
   border-top-right-radius: 0;
   border-bottom-right-radius: 0;
 }
 
 xul|textbox + xul|button,
 xul|filefield + xul|button {
   -moz-border-start: none;
 }
+
+/* List boxes */
+
+xul|richlistbox,
+xul|listbox {
+  -moz-appearance: none;
+  border: 1px solid #c1c1c1;
+}
+
+xul|treechildren::-moz-tree-row,
+xul|listbox xul|listitem {
+  padding: 5px;
+  margin: 0;
+  border: none;
+  background-image: none;
+}
+
+xul|treechildren::-moz-tree-row(selected),
+xul|listbox xul|listitem[selected="true"] {
+  background-color: #f1f1f1;
+}
+
+/* Trees */
+
+xul|tree {
+  -moz-appearance: none;
+  font-size: 1em;
+  border: 1px solid #c1c1c1;
+}
+
+xul|listheader,
+xul|treecols {
+  -moz-appearance: none;
+  border: none;
+  border-bottom: 1px solid #c1c1c1;
+  padding: 0;
+}
+
+xul|treecol:not([hideheader="true"]),
+xul|treecolpicker {
+  -moz-appearance: none;
+  border: none;
+  background-color: #ebebeb;
+  color: #808080;
+  padding: 5px 10px;
+}
+
+xul|treecol:not([hideheader="true"]):hover,
+xul|treecolpicker:hover {
+  background-color: #dadada;
+  color: #333;
+}
+
+xul|treecol:not([hideheader="true"]):not(:first-child),
+xul|treecolpicker {
+  -moz-border-start-width: 1px;
+  -moz-border-start-style: solid;
+  border-image: linear-gradient(transparent 0%, transparent 20%, #c1c1c1 20%, #c1c1c1 80%, transparent 80%, transparent 100%) 1 1;
+}
+
+xul|treecol:not([hideheader="true"]) > xul|*.treecol-sortdirection[sortDirection] {
+  list-style-image: url("chrome://global/skin/in-content/sorter.png");
+  width: 12px;
+  height: 8px;
+}
+
+xul|treecol:not([hideheader="true"]) > xul|*.treecol-sortdirection[sortDirection="descending"] {
+  transform: scaleY(-1);
+}
+
+@media (min-resolution: 2dppx) {
+  xul|treecol:not([hideheader="true"]) > xul|*.treecol-sortdirection[sortDirection] {
+    list-style-image: url("chrome://global/skin/in-content/sorter@2x.png");
+  }
+}
\ No newline at end of file