Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 13 Nov 2014 15:40:39 -0500
changeset 239921 7f0d92595432f9c0a8e83c21f7a7ed4bd43d2b9d
parent 239892 c33ccd6c872595bbf94b30ac430f4e8db4625fe7 (current diff)
parent 239920 8b35d3ba140d75c881286468d733110402f0a454 (diff)
child 239924 a859d43e7e1c22ccd704815f9b53f0d83162abf3
child 239968 a05b5362429f96bd69cd8bd34d50d1eaebbdc97f
child 240038 d4fd43b50ba4363dff9562602ac497698c5570b2
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone36.0a1
first release with
nightly linux32
7f0d92595432 / 36.0a1 / 20141114030206 / files
nightly linux64
7f0d92595432 / 36.0a1 / 20141114030206 / files
nightly mac
7f0d92595432 / 36.0a1 / 20141114030206 / files
nightly win32
7f0d92595432 / 36.0a1 / 20141114030206 / files
nightly win64
7f0d92595432 / 36.0a1 / 20141114030206 / 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
browser/components/places/tests/browser/browser_library_left_pane_commands.js
browser/devtools/shared/test/browser_graphs-09.js
browser/devtools/timeline/widgets/overview.js
browser/themes/shared/devtools/highlighter.css
dom/base/nsGlobalWindow.cpp
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -250,16 +250,17 @@ pref("extensions.{972ce4c6-7e08-4474-a28
 pref("extensions.{972ce4c6-7e08-4474-a285-3208198ce6fd}.description", "chrome://browser/locale/browser.properties");
 
 pref("lightweightThemes.update.enabled", true);
 pref("lightweightThemes.getMoreURL", "https://addons.mozilla.org/%LOCALE%/firefox/themes");
 pref("lightweightThemes.recommendedThemes", "[{\"id\":\"recommended-1\",\"homepageURL\":\"https://addons.mozilla.org/firefox/addon/a-web-browser-renaissance/\",\"headerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/1.header.jpg\",\"footerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/1.footer.jpg\",\"textcolor\":\"#000000\",\"accentcolor\":\"#f2d9b1\",\"iconURL\":\"resource:///chrome/browser/content/browser/defaultthemes/1.icon.jpg\",\"previewURL\":\"resource:///chrome/browser/content/browser/defaultthemes/1.preview.jpg\",\"author\":\"Sean.Martell\",\"version\":\"0\"},{\"id\":\"recommended-2\",\"homepageURL\":\"https://addons.mozilla.org/firefox/addon/space-fantasy/\",\"headerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/2.header.jpg\",\"footerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/2.footer.jpg\",\"textcolor\":\"#ffffff\",\"accentcolor\":\"#d9d9d9\",\"iconURL\":\"resource:///chrome/browser/content/browser/defaultthemes/2.icon.jpg\",\"previewURL\":\"resource:///chrome/browser/content/browser/defaultthemes/2.preview.jpg\",\"author\":\"fx5800p\",\"version\":\"1.0\"},{\"id\":\"recommended-3\",\"homepageURL\":\"https://addons.mozilla.org/firefox/addon/linen-light/\",\"headerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/3.header.png\",\"footerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/3.footer.png\",\"textcolor\":\"#None\",\"accentcolor\":\"#ada8a8\",\"iconURL\":\"resource:///chrome/browser/content/browser/defaultthemes/3.icon.png\",\"previewURL\":\"resource:///chrome/browser/content/browser/defaultthemes/3.preview.png\",\"author\":\"DVemer\",\"version\":\"1.0\"},{\"id\":\"recommended-4\",\"homepageURL\":\"https://addons.mozilla.org/firefox/addon/pastel-gradient/\",\"headerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/4.header.png\",\"footerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/4.footer.png\",\"textcolor\":\"#000000\",\"accentcolor\":\"#000000\",\"iconURL\":\"resource:///chrome/browser/content/browser/defaultthemes/4.icon.png\",\"previewURL\":\"resource:///chrome/browser/content/browser/defaultthemes/4.preview.png\",\"author\":\"darrinhenein\",\"version\":\"1.0\"},{\"id\":\"recommended-5\",\"homepageURL\":\"https://addons.mozilla.org/firefox/addon/carbon-light/\",\"headerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/5.header.png\",\"footerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/5.footer.png\",\"textcolor\":\"#3b3b3b\",\"accentcolor\":\"#2e2e2e\",\"iconURL\":\"resource:///chrome/browser/content/browser/defaultthemes/5.icon.jpg\",\"previewURL\":\"resource:///chrome/browser/content/browser/defaultthemes/5.preview.jpg\",\"author\":\"Jaxivo\",\"version\":\"1.0\"}]");
 
 // UI tour experience.
 pref("browser.uitour.enabled", true);
+pref("browser.uitour.loglevel", "Error");
 pref("browser.uitour.requireSecure", true);
 pref("browser.uitour.themeOrigin", "https://addons.mozilla.org/%LOCALE%/firefox/themes/");
 pref("browser.uitour.pinnedTabUrl", "https://support.mozilla.org/%LOCALE%/kb/pinned-tabs-keep-favorite-websites-open");
 pref("browser.uitour.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/tour/");
 
 pref("browser.customizemode.tip0.shown", false);
 pref("browser.customizemode.tip0.learnMoreUrl", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/customize");
 
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -69,16 +69,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
      * @param {string} [aReason] Some states are only shown if
      *                           a related reason is provided.
      *
      *                 aReason="login": Used after a login is completed
      *                   successfully. This is used so the state can be
      *                   temporarily shown until the next state change.
      */
     updateToolbarState: function(aReason = null) {
+      if (!this.toolbarButton.node) {
+        return;
+      }
       let state = "";
       if (MozLoopService.errors.size) {
         state = "error";
       } else if (aReason == "login" && MozLoopService.userProfile) {
         state = "active";
       } else if (MozLoopService.doNotDisturb) {
         state = "disabled";
       }
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -469,17 +469,18 @@ var PlacesCommandHook = {
    * Opens the Places Organizer. 
    * @param   aLeftPaneRoot
    *          The query to select in the organizer window - options
    *          are: History, AllBookmarks, BookmarksMenu, BookmarksToolbar,
    *          UnfiledBookmarks, Tags and Downloads.
    */
   showPlacesOrganizer: function PCH_showPlacesOrganizer(aLeftPaneRoot) {
     var organizer = Services.wm.getMostRecentWindow("Places:Organizer");
-    if (!organizer) {
+    // Due to bug 528706, getMostRecentWindow can return closed windows.
+    if (!organizer || organizer.closed) {
       // No currently open places window, so open one with the specified mode.
       openDialog("chrome://browser/content/places/places.xul", 
                  "", "chrome,toolbar=yes,dialog=no,resizable", aLeftPaneRoot);
     }
     else {
       organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(aLeftPaneRoot);
       organizer.focus();
     }
--- a/browser/base/content/test/general/browser_restore_isAppTab.js
+++ b/browser/base/content/test/general/browser_restore_isAppTab.js
@@ -65,18 +65,22 @@ function loadFrameScript(browser) {
 }
 
 function isBrowserAppTab(browser) {
   return new Promise(resolve => {
     function listener({ data }) {
       browser.messageManager.removeMessageListener("Test:IsAppTab", listener);
       resolve(data.isAppTab);
     }
-    browser.messageManager.addMessageListener("Test:IsAppTab", listener);
-    browser.messageManager.sendAsyncMessage("Test:GetIsAppTab");
+    // It looks like same-process messages may be reordered by the message
+    // manager, so we need to wait one tick before sending the message.
+    executeSoon(function () {
+      browser.messageManager.addMessageListener("Test:IsAppTab", listener);
+      browser.messageManager.sendAsyncMessage("Test:GetIsAppTab");
+    });
   });
 }
 
 // Restarts the child process by crashing it then reloading the tab
 let restart = Task.async(function*(browser) {
   // If the tab isn't remote this would crash the main process so skip it
   if (!browser.isRemoteBrowser)
     return browser;
--- a/browser/base/content/test/general/browser_search_favicon.js
+++ b/browser/base/content/test/general/browser_search_favicon.js
@@ -1,14 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let gOriginalEngine;
 let gEngine;
 let gUnifiedCompletePref = "browser.urlbar.unifiedcomplete";
+let gRestyleSearchesPref = "browser.urlbar.restyleSearches";
 
 /**
  * Asynchronously adds visits to a page.
  *
  * @param aPlaceInfo
  *        Can be an nsIURI, in such a case a single LINK visit will be added.
  *        Otherwise can be an object describing the visit to add, or an array
  *        of these objects:
@@ -72,23 +73,25 @@ function* promiseAutocompleteResultPopup
   EventUtils.synthesizeKey(inputText.slice(-1) , {});
   yield promiseSearchComplete();
 
   return gURLBar.popup.richlistbox.children;
 }
 
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref(gUnifiedCompletePref);
+  Services.prefs.clearUserPref(gRestyleSearchesPref);
   Services.search.currentEngine = gOriginalEngine;
   Services.search.removeEngine(gEngine);
   return promiseClearHistory();
 });
 
 add_task(function*() {
   Services.prefs.setBoolPref(gUnifiedCompletePref, true);
+  Services.prefs.setBoolPref(gRestyleSearchesPref, true);
 });
 
 add_task(function*() {
 
   Services.search.addEngineWithDetails("SearchEngine", "", "", "",
                                        "GET", "http://s.example.com/search");
   gEngine = Services.search.getEngineByName("SearchEngine");
   gEngine.addParam("q", "{searchTerms}", null);
--- a/browser/base/content/test/general/browser_urlbarTrimURLs.js
+++ b/browser/base/content/test/general/browser_urlbarTrimURLs.js
@@ -36,31 +36,42 @@ function test() {
   testVal("http://ftp42.mozilla.org/", "http://ftp42.mozilla.org");
   testVal("http://ftpx.mozilla.org/", "ftpx.mozilla.org");
   testVal("ftp://ftp.mozilla.org/", "ftp://ftp.mozilla.org");
   testVal("ftp://ftp1.mozilla.org/", "ftp://ftp1.mozilla.org");
   testVal("ftp://ftp42.mozilla.org/", "ftp://ftp42.mozilla.org");
   testVal("ftp://ftpx.mozilla.org/", "ftp://ftpx.mozilla.org");
 
   testVal("https://user:pass@mozilla.org/", "https://user:pass@mozilla.org");
+  testVal("https://user@mozilla.org/", "https://user@mozilla.org");
   testVal("http://user:pass@mozilla.org/", "http://user:pass@mozilla.org");
+  testVal("http://user@mozilla.org/", "user@mozilla.org");
   testVal("http://sub.mozilla.org:666/", "sub.mozilla.org:666");
 
   testVal("https://[fe80::222:19ff:fe11:8c76]/file.ext");
   testVal("http://[fe80::222:19ff:fe11:8c76]/", "[fe80::222:19ff:fe11:8c76]");
   testVal("https://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext");
   testVal("http://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext");
 
   testVal("mailto:admin@mozilla.org");
   testVal("gopher://mozilla.org/");
   testVal("about:config");
   testVal("jar:http://mozilla.org/example.jar!/");
   testVal("view-source:http://mozilla.org/");
 
+  // Behaviour for hosts with no dots depends on the whitelist:
+  let fixupWhitelistPref = "browser.fixup.domainwhitelist.localhost";
+  Services.prefs.setBoolPref(fixupWhitelistPref, false);
   testVal("http://localhost");
+  Services.prefs.setBoolPref(fixupWhitelistPref, true);
+  testVal("http://localhost", "localhost");
+  Services.prefs.clearUserPref(fixupWhitelistPref);
+
+  testVal("http:// invalid url");
+
   testVal("http://someotherhostwithnodots");
   testVal("http://localhost/ foo bar baz");
   testVal("http://localhost.localdomain/ foo bar baz", "localhost.localdomain/ foo bar baz");
 
   Services.prefs.setBoolPref(prefname, false);
 
   testVal("http://mozilla.org/");
 
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -709,20 +709,30 @@ function openPrefsHelp() {
   openHelpLink(helpTopic, !instantApply);
 }
 
 function trimURL(aURL) {
   // This function must not modify the given URL such that calling
   // nsIURIFixup::createFixupURI with the result will produce a different URI.
 
   // remove single trailing slash for http/https/ftp URLs
-  let rv = aURL.replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1");
+  let url = aURL.replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1");
+
+  // remove http://
+  if (!url.startsWith("http://")) {
+    return url;
+  }
+  let urlWithoutProtocol = url.substring(7);
 
-  // Strip the leading http:// only if the host has at least one '.' or
-  // looks like an ipv6 ip:
-  let hostMatch = rv.match(/^http:\/\/([^\/]*)/);
-  let ipv6Regex = /\[[\da-f:]*\]/;
-  if (hostMatch && (hostMatch[1].contains(".") || ipv6Regex.test(hostMatch[1]))) {
-    /* remove http:// unless the host starts with "ftp\d*\." or contains "@" */
-    rv = rv.replace(/^http:\/\/((?!ftp\d*\.)[^\/@]+(?:\/|$))/, "$1");
+  let flags = Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP |
+              Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS;
+  let fixedUpURL = Services.uriFixup.createFixupURI(urlWithoutProtocol, flags);
+  let expectedURLSpec;
+  try {
+    expectedURLSpec = makeURI(aURL).spec;
+  } catch (ex) {
+    return url;
   }
-  return rv;
+  if (fixedUpURL.spec == expectedURLSpec) {
+    return urlWithoutProtocol;
+  }
+  return url;
 }
--- a/browser/components/loop/LoopRooms.jsm
+++ b/browser/components/loop/LoopRooms.jsm
@@ -126,17 +126,19 @@ let LoopRoomsInternal = {
    */
   getAll: function(version = null, callback) {
     if (!callback) {
       callback = version;
       version = null;
     }
 
     Task.spawn(function* () {
-      yield MozLoopService.promiseRegisteredWithServers();
+      let deferredInitialization = Promise.defer();
+      MozLoopService.delayedInitialize(deferredInitialization);
+      yield deferredInitialization.promise;
 
       if (!gDirty) {
         callback(null, [...this.rooms.values()]);
         return;
       }
 
       // Fetch the rooms from the server.
       let url = "/rooms" + (version ? "?version=" + encodeURIComponent(version) : "");
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -313,22 +313,27 @@ let MozLoopServiceInternal = {
   },
 
   get errors() {
     return gErrors;
   },
 
   /**
    * Get endpoints with the push server and register for notifications.
-   * For now we register as both a Guest and FxA user and all must succeed.
+   * This should only be called from promiseRegisteredWithServers to prevent reentrancy.
    *
+   * @param {LOOP_SESSION_TYPE} sessionType
    * @return {Promise} resolves with all push endpoints
    *                   rejects if any of the push registrations failed
    */
-  promiseRegisteredWithPushServer: function() {
+  promiseRegisteredWithPushServer: function(sessionType) {
+    if (!this.deferredRegistrations.has(sessionType)) {
+      return Promise.reject("promiseRegisteredWithPushServer must be called while there is a " +
+                            "deferred in deferredRegistrations in order to prevent reentrancy");
+    }
     // Wrap push notification registration call-back in a Promise.
     function registerForNotification(channelID, onNotification) {
       log.debug("registerForNotification", channelID);
       return new Promise((resolve, reject) => {
         function onRegistered(error, pushUrl) {
           log.debug("registerForNotification onRegistered:", error, pushUrl);
           if (error) {
             reject(Error(error));
@@ -347,29 +352,33 @@ let MozLoopServiceInternal = {
 
         MozLoopServiceInternal.pushHandler.register(channelID, onRegistered, onNotification);
       });
     }
 
     let options = this.mocks.webSocket ? { mockWebSocket: this.mocks.webSocket } : {};
     this.pushHandler.initialize(options);
 
-    let callsRegGuest = registerForNotification(MozLoopService.channelIDs.callsGuest,
+    if (sessionType == LOOP_SESSION_TYPE.GUEST) {
+      let callsRegGuest = registerForNotification(MozLoopService.channelIDs.callsGuest,
+                                                  LoopCalls.onNotification);
+
+      let roomsRegGuest = registerForNotification(MozLoopService.channelIDs.roomsGuest,
+                                                  roomsPushNotification);
+      return Promise.all([callsRegGuest, roomsRegGuest]);
+    } else if (sessionType == LOOP_SESSION_TYPE.FXA) {
+      let callsRegFxA = registerForNotification(MozLoopService.channelIDs.callsFxA,
                                                 LoopCalls.onNotification);
 
-    let roomsRegGuest = registerForNotification(MozLoopService.channelIDs.roomsGuest,
+      let roomsRegFxA = registerForNotification(MozLoopService.channelIDs.roomsFxA,
                                                 roomsPushNotification);
+      return Promise.all([callsRegFxA, roomsRegFxA]);
+    }
 
-    let callsRegFxA = registerForNotification(MozLoopService.channelIDs.callsFxA,
-                                              LoopCalls.onNotification);
-
-    let roomsRegFxA = registerForNotification(MozLoopService.channelIDs.roomsFxA,
-                                              roomsPushNotification);
-
-    return Promise.all([callsRegGuest, roomsRegGuest, callsRegFxA, roomsRegFxA]);
+    return Promise.reject("promiseRegisteredWithPushServer: Invalid sessionType");
   },
 
   /**
    * Starts registration of Loop with the push server, and then will register
    * with the Loop server. It will return early if already registered.
    *
    * @param {LOOP_SESSION_TYPE} sessionType
    * @returns {Promise} a promise that is resolved with no params on completion, or
@@ -384,17 +393,17 @@ let MozLoopServiceInternal = {
     let result = null;
     let deferred = Promise.defer();
     log.debug("assigning to deferredRegistrations for sessionType:", sessionType);
     this.deferredRegistrations.set(sessionType, deferred);
 
     // We grab the promise early in case one of the callers below delete it from the map.
     result = deferred.promise;
 
-    this.promiseRegisteredWithPushServer().then(() => {
+    this.promiseRegisteredWithPushServer(sessionType).then(() => {
       return this.registerWithLoopServer(sessionType);
     }).then(() => {
       deferred.resolve("registered to status:" + sessionType);
       // No need to clear the promise here, everything was good, so we don't need
       // to re-register.
     }, error => {
       log.error("Failed to register with Loop server with sessionType " + sessionType, error);
       deferred.reject(error);
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -227,17 +227,20 @@ loop.roomViews = (function(mozL10n) {
       var localStreamClasses = React.addons.classSet({
         local: true,
         "local-stream": true,
         "local-stream-audio": !this.state.videoMuted,
         "room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS
       });
 
       switch(this.state.roomState) {
-        case ROOM_STATES.FAILED: {
+        case ROOM_STATES.FAILED:
+        case ROOM_STATES.FULL: {
+          // Note: While rooms are set to hold a maximum of 2 participants, the
+          //       FULL case should never happen on desktop.
           return loop.conversation.GenericFailureView({
             cancelCall: this.closeWindow}
           );
         }
         default: {
           return (
             React.DOM.div({className: "room-conversation-wrapper"}, 
               this._renderInvitationOverlay(), 
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -227,17 +227,20 @@ loop.roomViews = (function(mozL10n) {
       var localStreamClasses = React.addons.classSet({
         local: true,
         "local-stream": true,
         "local-stream-audio": !this.state.videoMuted,
         "room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS
       });
 
       switch(this.state.roomState) {
-        case ROOM_STATES.FAILED: {
+        case ROOM_STATES.FAILED:
+        case ROOM_STATES.FULL: {
+          // Note: While rooms are set to hold a maximum of 2 participants, the
+          //       FULL case should never happen on desktop.
           return <loop.conversation.GenericFailureView
             cancelCall={this.closeWindow}
           />;
         }
         default: {
           return (
             <div className="room-conversation-wrapper">
               {this._renderInvitationOverlay()}
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -748,25 +748,32 @@ html, .fx-embedded, #main,
   top: 35%;
   left: 0;
   right: 25%;
   z-index: 1000;
   margin: 0 auto;
   width: 50%;
   color: #fff;
   font-weight: bold;
+  font-size: 1.1em;
 }
 
 .standalone .room-inner-info-area button {
   border-radius: 3px;
   font-size: 1.2em;
   padding: .2em 1.2em;
   cursor: pointer;
 }
 
+.standalone .room-inner-info-area a.btn {
+  padding: .5em 3em .3em 3em;
+  border-radius: 3px;
+  font-weight: normal;
+}
+
 .standalone .room-conversation h2.room-name {
   position: absolute;
   display: inline-block;
   top: 0;
   right: 0;
   color: #fff;
   z-index: 2000000;
   font-size: 1.2em;
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -20,17 +20,19 @@ loop.store.ActiveRoomStore = (function()
     READY: "room-ready",
     // The room is known to be joined on the loop-server
     JOINED: "room-joined",
     // The room is connected to the sdk server.
     SESSION_CONNECTED: "room-session-connected",
     // There are participants in the room.
     HAS_PARTICIPANTS: "room-has-participants",
     // There was an issue with the room
-    FAILED: "room-failed"
+    FAILED: "room-failed",
+    // The room is full
+    FULL: "room-full"
   };
 
   /**
    * Store for things that are local to this instance (in this profile, on
    * this machine) of this roomRoom store, in addition to a mirror of some
    * remote-state.
    *
    * @extends {Backbone.Events}
@@ -100,28 +102,28 @@ loop.store.ActiveRoomStore = (function()
     setStoreState: function(newState) {
       for (var key in newState) {
         this._storeState[key] = newState[key];
       }
       this.trigger("change");
     },
 
     /**
-     * Handles a room failure. Currently this prints the error to the console
-     * and sets the roomState to failed.
+     * Handles a room failure.
      *
      * @param {sharedActions.RoomFailure} actionData
      */
     roomFailure: function(actionData) {
       console.error("Error in state `" + this._storeState.roomState + "`:",
         actionData.error);
 
       this.setStoreState({
         error: actionData.error,
-        roomState: ROOM_STATES.FAILED
+        roomState: actionData.error.errno === 202 ? ROOM_STATES.FULL
+                                                  : ROOM_STATES.FAILED
       });
     },
 
     /**
      * Registers the actions with the dispatcher that this store is interested
      * in.
      */
     _registerActions: function() {
@@ -357,16 +359,20 @@ loop.store.ActiveRoomStore = (function()
     /**
      * Handles leaving a room. Clears any membership timeouts, then
      * signals to the server the leave of the room.
      *
      * @param {ROOM_STATES} nextState Optional; the next state to switch to.
      *                                Switches to READY if undefined.
      */
     _leaveRoom: function(nextState) {
+      if (loop.standaloneMedia) {
+        loop.standaloneMedia.multiplexGum.reset();
+      }
+
       this._sdkDriver.disconnectSession();
 
       if (this._timeout) {
         clearTimeout(this._timeout);
         delete this._timeout;
       }
 
       if (this._storeState.roomState === ROOM_STATES.JOINED ||
--- a/browser/components/loop/content/shared/js/models.js
+++ b/browser/components/loop/content/shared/js/models.js
@@ -172,18 +172,16 @@ loop.shared.models = (function(l10n) {
       });
 
       this.session = this.sdk.initSession(this.get("sessionId"));
       this.listenTo(this.session, "streamCreated", this._streamCreated);
       this.listenTo(this.session, "connectionDestroyed",
                                   this._connectionDestroyed);
       this.listenTo(this.session, "sessionDisconnected",
                                   this._sessionDisconnected);
-      this.listenTo(this.session, "networkDisconnected",
-                                  this._networkDisconnected);
       this.session.connect(this.get("apiKey"), this.get("sessionToken"),
                            this._onConnectCompletion.bind(this));
     },
 
     /**
      * Ends current session.
      */
     endSession: function() {
@@ -318,46 +316,41 @@ loop.shared.models = (function(l10n) {
 
     /**
      * Local user hung up.
      * http://tokbox.com/opentok/libraries/client/js/reference/SessionDisconnectEvent.html
      *
      * @param  {SessionDisconnectEvent} event
      */
     _sessionDisconnected: function(event) {
+      if(event.reason === "networkDisconnected") {
+        this._signalEnd("session:network-disconnected", event);
+      } else {
+        this._signalEnd("session:ended", event);
+      }
+    },
+
+    _signalEnd: function(eventName, event) {
       this.set("connected", false)
           .set("ongoing", false)
-          .trigger("session:ended");
+          .trigger(eventName, event);
     },
 
     /**
      * Peer hung up. Disconnects local session.
      * http://tokbox.com/opentok/libraries/client/js/reference/ConnectionEvent.html
      *
      * @param  {ConnectionEvent} event
      */
     _connectionDestroyed: function(event) {
-      this.set("connected", false)
-          .set("ongoing", false)
-          .trigger("session:peer-hungup", {
-            connectionId: event.connection.connectionId
-          });
-      this.endSession();
-    },
-
-    /**
-     * Network was disconnected.
-     * http://tokbox.com/opentok/libraries/client/js/reference/ConnectionEvent.html
-     *
-     * @param {ConnectionEvent} event
-     */
-    _networkDisconnected: function(event) {
-      this.set("connected", false)
-          .set("ongoing", false)
-          .trigger("session:network-disconnected");
+      if (event.reason === "networkDisconnected") {
+        this._signalEnd("session:network-disconnected", event);
+      } else {
+        this._signalEnd("session:peer-hungup", event);
+      }
       this.endSession();
     },
   });
 
   /**
    * Notification model.
    */
   var NotificationModel = Backbone.Model.extend({
--- a/browser/components/loop/standalone/Makefile
+++ b/browser/components/loop/standalone/Makefile
@@ -13,16 +13,17 @@
 # to the Gruntfile and getting rid of this Makefile entirely.
 
 LOOP_SERVER_URL := $(shell echo $${LOOP_SERVER_URL-http://localhost:5000})
 LOOP_FEEDBACK_API_URL := $(shell echo $${LOOP_FEEDBACK_API_URL-"https://input.allizom.org/api/v1/feedback"})
 LOOP_FEEDBACK_PRODUCT_NAME := $(shell echo $${LOOP_FEEDBACK_PRODUCT_NAME-Loop})
 LOOP_BRAND_WEBSITE_URL := $(shell echo $${LOOP_BRAND_WEBSITE_URL-"https://www.mozilla.org/firefox/"})
 LOOP_PRIVACY_WEBSITE_URL := $(shell echo $${LOOP_PRIVACY_WEBSITE_URL-"https://www.mozilla.org/privacy"})
 LOOP_LEGAL_WEBSITE_URL := $(shell echo $${LOOP_LEGAL_WEBSITE_URL-"/legal/terms"})
+LOOP_PRODUCT_HOMEPAGE_URL := $(shell echo $${LOOP_PRODUCT_HOMEPAGE_URL-"https://www.firefox.com/hello/"})
 
 NODE_LOCAL_BIN=./node_modules/.bin
 
 install: npm_install tos
 
 npm_install:
 	@npm install
 
@@ -74,11 +75,12 @@ config:
 	@echo "var loop = loop || {};" > content/config.js
 	@echo "loop.config = loop.config || {};" >> content/config.js
 	@echo "loop.config.serverUrl = '`echo $(LOOP_SERVER_URL)`';" >> content/config.js
 	@echo "loop.config.feedbackApiUrl = '`echo $(LOOP_FEEDBACK_API_URL)`';" >> content/config.js
 	@echo "loop.config.feedbackProductName = '`echo $(LOOP_FEEDBACK_PRODUCT_NAME)`';" >> content/config.js
 	@echo "loop.config.brandWebsiteUrl = '`echo $(LOOP_BRAND_WEBSITE_URL)`';" >> content/config.js
 	@echo "loop.config.privacyWebsiteUrl = '`echo $(LOOP_PRIVACY_WEBSITE_URL)`';" >> content/config.js
 	@echo "loop.config.legalWebsiteUrl = '`echo $(LOOP_LEGAL_WEBSITE_URL)`';" >> content/config.js
+	@echo "loop.config.learnMoreUrl = '`echo $(LOOP_PRODUCT_HOMEPAGE_URL)`';" >> content/config.js
 	@echo "loop.config.fxosApp = loop.config.fxosApp || {};" >> content/config.js
 	@echo "loop.config.fxosApp.name = 'Loop';" >> content/config.js
 	@echo "loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';" >> content/config.js
--- a/browser/components/loop/standalone/content/js/standaloneMozLoop.js
+++ b/browser/components/loop/standalone/content/js/standaloneMozLoop.js
@@ -42,17 +42,17 @@ loop.StandaloneMozLoop = (function(mozL1
    * @param textStatus See jQuery docs
    * @param errorThrown See jQuery docs
    */
   function failureHandler(callback, jqXHR, textStatus, errorThrown) {
     var jsonErr = jqXHR && jqXHR.responseJSON || {};
     var message = "HTTP " + jqXHR.status + " " + errorThrown;
 
     // Create an error with server error `errno` code attached as a property
-    var err = new Error(message);
+    var err = new Error(message + (jsonErr.error ? "; " + jsonErr.error : ""));
     err.errno = jsonErr.errno;
 
     callback(err);
   }
 
   /**
    * StandaloneMozLoopRooms is used as part of StandaloneMozLoop to define
    * the rooms sub-object. We do it this way so that we can share the options
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -1,31 +1,99 @@
 /** @jsx React.DOM */
 
 /* 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/. */
 
 /* global loop:true, React */
+/* jshint newcap:false, maxlen:false */
 
 var loop = loop || {};
 loop.standaloneRoomViews = (function(mozL10n) {
   "use strict";
 
   var ROOM_STATES = loop.store.ROOM_STATES;
   var sharedActions = loop.shared.actions;
   var sharedViews = loop.shared.views;
 
+  var StandaloneRoomInfoArea = React.createClass({displayName: 'StandaloneRoomInfoArea',
+    propTypes: {
+      helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
+    },
+
+    _renderCallToActionLink: function() {
+      if (this.props.helper.isFirefox(navigator.userAgent)) {
+        return (
+          React.DOM.a({href: loop.config.learnMoreUrl, className: "btn btn-info"}, 
+            mozL10n.get("rooms_room_full_call_to_action_label", {
+              clientShortname: mozL10n.get("clientShortname2")
+            })
+          )
+        );
+      }
+      return (
+        React.DOM.a({href: loop.config.brandWebsiteUrl, className: "btn btn-info"}, 
+          mozL10n.get("rooms_room_full_call_to_action_nonFx_label", {
+            brandShortname: mozL10n.get("brandShortname")
+          })
+        )
+      );
+    },
+
+    _renderContent: function() {
+      switch(this.props.roomState) {
+        case ROOM_STATES.INIT:
+        case ROOM_STATES.READY: {
+          return (
+            React.DOM.button({className: "btn btn-join btn-info", 
+                    onClick: this.props.joinRoom}, 
+              mozL10n.get("rooms_room_join_label")
+            )
+          );
+        }
+        case ROOM_STATES.JOINED:
+        case ROOM_STATES.SESSION_CONNECTED: {
+          return (
+            React.DOM.p({className: "empty-room-message"}, 
+              mozL10n.get("rooms_only_occupant_label")
+            )
+          );
+        }
+        case ROOM_STATES.FULL:
+          return (
+            React.DOM.div(null, 
+              React.DOM.p({className: "full-room-message"}, 
+                mozL10n.get("rooms_room_full_label")
+              ), 
+              React.DOM.p(null, this._renderCallToActionLink())
+            )
+          );
+        default:
+          return null;
+      }
+    },
+
+    render: function() {
+      return (
+        React.DOM.div({className: "room-inner-info-area"}, 
+          this._renderContent()
+        )
+      );
+    }
+  });
+
   var StandaloneRoomView = React.createClass({displayName: 'StandaloneRoomView',
     mixins: [Backbone.Events],
 
     propTypes: {
       activeRoomStore:
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
     },
 
     getInitialState: function() {
       var storeState = this.props.activeRoomStore.getStoreState();
       return _.extend({}, storeState, {
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState
       });
@@ -124,56 +192,29 @@ loop.standaloneRoomViews = (function(moz
      * @return {Boolean}
      */
     _roomIsActive: function() {
       return this.state.roomState === ROOM_STATES.JOINED            ||
              this.state.roomState === ROOM_STATES.SESSION_CONNECTED ||
              this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
     },
 
-    _renderContextualRoomInfo: function() {
-      switch(this.state.roomState) {
-        case ROOM_STATES.INIT:
-        case ROOM_STATES.READY: {
-          // Join button
-          return (
-            React.DOM.div({className: "room-inner-info-area"}, 
-              React.DOM.button({className: "btn btn-join btn-info", onClick: this.joinRoom}, 
-                mozL10n.get("rooms_room_join_label")
-              )
-            )
-          );
-        }
-        case ROOM_STATES.JOINED:
-        case ROOM_STATES.SESSION_CONNECTED: {
-          // Empty room message
-          return (
-            React.DOM.div({className: "room-inner-info-area"}, 
-              React.DOM.p({className: "empty-room-message"}, 
-                mozL10n.get("rooms_only_occupant_label")
-              )
-            )
-          );
-        }
-      }
-      // XXX Render "Start your own" button when room is over capacity (see
-      //     bug 1074709)
-    },
-
     render: function() {
       var localStreamClasses = React.addons.classSet({
         hide: !this._roomIsActive(),
         local: true,
         "local-stream": true,
         "local-stream-audio": false
       });
 
       return (
         React.DOM.div({className: "room-conversation-wrapper"}, 
-          this._renderContextualRoomInfo(), 
+          StandaloneRoomInfoArea({roomState: this.state.roomState, 
+                                  joinRoom: this.joinRoom, 
+                                  helper: this.props.helper}), 
           React.DOM.div({className: "video-layout-wrapper"}, 
             React.DOM.div({className: "conversation room-conversation"}, 
               React.DOM.h2({className: "room-name"}, this.state.roomName), 
               React.DOM.div({className: "media nested"}, 
                 React.DOM.div({className: "video_wrapper remote_wrapper"}, 
                   React.DOM.div({className: "video_inner remote"})
                 ), 
                 React.DOM.div({className: localStreamClasses})
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -1,31 +1,99 @@
 /** @jsx React.DOM */
 
 /* 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/. */
 
 /* global loop:true, React */
+/* jshint newcap:false, maxlen:false */
 
 var loop = loop || {};
 loop.standaloneRoomViews = (function(mozL10n) {
   "use strict";
 
   var ROOM_STATES = loop.store.ROOM_STATES;
   var sharedActions = loop.shared.actions;
   var sharedViews = loop.shared.views;
 
+  var StandaloneRoomInfoArea = React.createClass({
+    propTypes: {
+      helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
+    },
+
+    _renderCallToActionLink: function() {
+      if (this.props.helper.isFirefox(navigator.userAgent)) {
+        return (
+          <a href={loop.config.learnMoreUrl} className="btn btn-info">
+            {mozL10n.get("rooms_room_full_call_to_action_label", {
+              clientShortname: mozL10n.get("clientShortname2")
+            })}
+          </a>
+        );
+      }
+      return (
+        <a href={loop.config.brandWebsiteUrl} className="btn btn-info">
+          {mozL10n.get("rooms_room_full_call_to_action_nonFx_label", {
+            brandShortname: mozL10n.get("brandShortname")
+          })}
+        </a>
+      );
+    },
+
+    _renderContent: function() {
+      switch(this.props.roomState) {
+        case ROOM_STATES.INIT:
+        case ROOM_STATES.READY: {
+          return (
+            <button className="btn btn-join btn-info"
+                    onClick={this.props.joinRoom}>
+              {mozL10n.get("rooms_room_join_label")}
+            </button>
+          );
+        }
+        case ROOM_STATES.JOINED:
+        case ROOM_STATES.SESSION_CONNECTED: {
+          return (
+            <p className="empty-room-message">
+              {mozL10n.get("rooms_only_occupant_label")}
+            </p>
+          );
+        }
+        case ROOM_STATES.FULL:
+          return (
+            <div>
+              <p className="full-room-message">
+                {mozL10n.get("rooms_room_full_label")}
+              </p>
+              <p>{this._renderCallToActionLink()}</p>
+            </div>
+          );
+        default:
+          return null;
+      }
+    },
+
+    render: function() {
+      return (
+        <div className="room-inner-info-area">
+          {this._renderContent()}
+        </div>
+      );
+    }
+  });
+
   var StandaloneRoomView = React.createClass({
     mixins: [Backbone.Events],
 
     propTypes: {
       activeRoomStore:
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
     },
 
     getInitialState: function() {
       var storeState = this.props.activeRoomStore.getStoreState();
       return _.extend({}, storeState, {
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState
       });
@@ -124,56 +192,29 @@ loop.standaloneRoomViews = (function(moz
      * @return {Boolean}
      */
     _roomIsActive: function() {
       return this.state.roomState === ROOM_STATES.JOINED            ||
              this.state.roomState === ROOM_STATES.SESSION_CONNECTED ||
              this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
     },
 
-    _renderContextualRoomInfo: function() {
-      switch(this.state.roomState) {
-        case ROOM_STATES.INIT:
-        case ROOM_STATES.READY: {
-          // Join button
-          return (
-            <div className="room-inner-info-area">
-              <button className="btn btn-join btn-info" onClick={this.joinRoom}>
-                {mozL10n.get("rooms_room_join_label")}
-              </button>
-            </div>
-          );
-        }
-        case ROOM_STATES.JOINED:
-        case ROOM_STATES.SESSION_CONNECTED: {
-          // Empty room message
-          return (
-            <div className="room-inner-info-area">
-              <p className="empty-room-message">
-                {mozL10n.get("rooms_only_occupant_label")}
-              </p>
-            </div>
-          );
-        }
-      }
-      // XXX Render "Start your own" button when room is over capacity (see
-      //     bug 1074709)
-    },
-
     render: function() {
       var localStreamClasses = React.addons.classSet({
         hide: !this._roomIsActive(),
         local: true,
         "local-stream": true,
         "local-stream-audio": false
       });
 
       return (
         <div className="room-conversation-wrapper">
-          {this._renderContextualRoomInfo()}
+          <StandaloneRoomInfoArea roomState={this.state.roomState}
+                                  joinRoom={this.joinRoom}
+                                  helper={this.props.helper} />
           <div className="video-layout-wrapper">
             <div className="conversation room-conversation">
               <h2 className="room-name">{this.state.roomName}</h2>
               <div className="media nested">
                 <div className="video_wrapper remote_wrapper">
                   <div className="video_inner remote"></div>
                 </div>
                 <div className={localStreamClasses}></div>
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -934,17 +934,18 @@ loop.webapp = (function($, _, OT, mozL10
                feedbackApiClient: this.props.feedbackApiClient}
             )
           );
         }
         case "room": {
           return (
             loop.standaloneRoomViews.StandaloneRoomView({
               activeRoomStore: this.props.activeRoomStore, 
-              dispatcher: this.props.dispatcher}
+              dispatcher: this.props.dispatcher, 
+              helper: this.props.helper}
             )
           );
         }
         case "home": {
           return HomeView(null);
         }
         default: {
           // The state hasn't been initialised yet, so don't display
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -935,16 +935,17 @@ loop.webapp = (function($, _, OT, mozL10
             />
           );
         }
         case "room": {
           return (
             <loop.standaloneRoomViews.StandaloneRoomView
               activeRoomStore={this.props.activeRoomStore}
               dispatcher={this.props.dispatcher}
+              helper={this.props.helper}
             />
           );
         }
         case "home": {
           return <HomeView />;
         }
         default: {
           // The state hasn't been initialised yet, so don't display
--- a/browser/components/loop/standalone/server.js
+++ b/browser/components/loop/standalone/server.js
@@ -21,16 +21,17 @@ function getConfigFile(req, res) {
     "loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';",
     "loop.config.feedbackApiUrl = '" + feedbackApiUrl + "';",
     "loop.config.feedbackProductName = '" + feedbackProductName + "';",
     // XXX Update with the real marketplace url once the FxOS Loop app is
     //     uploaded to the marketplace bug 1053424
     "loop.config.marketplaceUrl = 'http://fake-market.herokuapp.com/iframe-install.html'",
     "loop.config.brandWebsiteUrl = 'https://www.mozilla.org/firefox/';",
     "loop.config.privacyWebsiteUrl = 'https://www.mozilla.org/privacy';",
+    "loop.config.learnMoreUrl = 'https://www.mozilla.org/hello/';",
     "loop.config.legalWebsiteUrl = '/legal/terms';",
     "loop.config.fxosApp = loop.config.fxosApp || {};",
     "loop.config.fxosApp.name = 'Loop';",
     "loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';"
   ].join("\n"));
 }
 
 app.get('/content/config.js', getConfigFile);
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -241,16 +241,26 @@ describe("loop.roomViews", function () {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED});
 
           view = mountTestComponent();
 
           TestUtils.findRenderedComponentWithType(view,
             loop.conversation.GenericFailureView);
         });
 
+      it("should render the GenericFailureView if the roomState is `FULL`",
+        function() {
+          activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
+
+          view = mountTestComponent();
+
+          TestUtils.findRenderedComponentWithType(view,
+            loop.conversation.GenericFailureView);
+        });
+
       it("should render the DesktopRoomInvitationView if roomState is `JOINED`",
         function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
 
           view = mountTestComponent();
 
           TestUtils.findRenderedComponentWithType(view,
             loop.roomViews.DesktopRoomInvitationView);
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -1,18 +1,19 @@
-/* global chai */
+/* global chai, loop */
 
 var expect = chai.expect;
 var sharedActions = loop.shared.actions;
 
 describe("loop.store.ActiveRoomStore", function () {
   "use strict";
 
   var ROOM_STATES = loop.store.ROOM_STATES;
   var sandbox, dispatcher, store, fakeMozLoop, fakeSdkDriver;
+  var fakeMultiplexGum;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     sandbox.useFakeTimers();
 
     dispatcher = new loop.Dispatcher();
     sandbox.stub(dispatcher, "dispatch");
 
@@ -25,16 +26,24 @@ describe("loop.store.ActiveRoomStore", f
       }
     };
 
     fakeSdkDriver = {
       connectSession: sandbox.stub(),
       disconnectSession: sandbox.stub()
     };
 
+    fakeMultiplexGum = {
+        reset: sandbox.spy()
+    };
+
+    loop.standaloneMedia = {
+      multiplexGum: fakeMultiplexGum
+    };
+
     store = new loop.store.ActiveRoomStore({
       dispatcher: dispatcher,
       mozLoop: fakeMozLoop,
       sdkDriver: fakeSdkDriver
     });
   });
 
   afterEach(function() {
@@ -77,17 +86,25 @@ describe("loop.store.ActiveRoomStore", f
     it("should log the error", function() {
       store.roomFailure({error: fakeError});
 
       sinon.assert.calledOnce(console.error);
       sinon.assert.calledWith(console.error,
         sinon.match(ROOM_STATES.READY), fakeError);
     });
 
-    it("should set the state to `FAILED`", function() {
+    it("should set the state to `FULL` on server errno 202", function() {
+      fakeError.errno = 202;
+
+      store.roomFailure({error: fakeError});
+
+      expect(store._storeState.roomState).eql(ROOM_STATES.FULL);
+    });
+
+    it("should set the state to `FAILED` on generic error", function() {
       store.roomFailure({error: fakeError});
 
       expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
     });
   });
 
   describe("#setupWindowData", function() {
     var fakeToken, fakeRoomData;
@@ -366,16 +383,22 @@ describe("loop.store.ActiveRoomStore", f
     beforeEach(function() {
       store.setStoreState({
         roomState: ROOM_STATES.JOINED,
         roomToken: "fakeToken",
         sessionToken: "1627384950"
       });
     });
 
+    it("should reset the multiplexGum", function() {
+      store.leaveRoom();
+
+      sinon.assert.calledOnce(fakeMultiplexGum.reset);
+    });
+
     it("should disconnect from the servers via the sdk", function() {
       store.connectionFailure();
 
       sinon.assert.calledOnce(fakeSdkDriver.disconnectSession);
     });
 
     it("should clear any existing timeout", function() {
       sandbox.stub(window, "clearTimeout");
@@ -445,16 +468,22 @@ describe("loop.store.ActiveRoomStore", f
     beforeEach(function() {
       store.setStoreState({
         roomState: ROOM_STATES.JOINED,
         roomToken: "fakeToken",
         sessionToken: "1627384950"
       });
     });
 
+    it("should reset the multiplexGum", function() {
+      store.leaveRoom();
+
+      sinon.assert.calledOnce(fakeMultiplexGum.reset);
+    });
+
     it("should disconnect from the servers via the sdk", function() {
       store.windowUnload();
 
       sinon.assert.calledOnce(fakeSdkDriver.disconnectSession);
     });
 
     it("should clear any existing timeout", function() {
       sandbox.stub(window, "clearTimeout");
@@ -484,16 +513,22 @@ describe("loop.store.ActiveRoomStore", f
     beforeEach(function() {
       store.setStoreState({
         roomState: ROOM_STATES.JOINED,
         roomToken: "fakeToken",
         sessionToken: "1627384950"
       });
     });
 
+    it("should reset the multiplexGum", function() {
+      store.leaveRoom();
+
+      sinon.assert.calledOnce(fakeMultiplexGum.reset);
+    });
+
     it("should disconnect from the servers via the sdk", function() {
       store.leaveRoom();
 
       sinon.assert.calledOnce(fakeSdkDriver.disconnectSession);
     });
 
     it("should clear any existing timeout", function() {
       sandbox.stub(window, "clearTimeout");
--- a/browser/components/loop/test/shared/models_test.js
+++ b/browser/components/loop/test/shared/models_test.js
@@ -248,16 +248,30 @@ describe("loop.shared.models", function(
 
           it("should trigger a session:ended event on sessionDisconnected",
             function(done) {
               model.once("session:ended", function(){ done(); });
 
               fakeSession.trigger("sessionDisconnected", {reason: "ko"});
             });
 
+          it("should trigger network-disconnected on networkDisconnect reason",
+             function(done) {
+               model.once("session:network-disconnected", function() {
+                 done();
+               });
+
+               var fakeEvent = {
+                 connectionId: 42,
+                 reason: "networkDisconnected"
+               };
+
+               fakeSession.trigger("sessionDisconnected", fakeEvent);
+            });
+
           it("should set the connected attribute to false on sessionDisconnected",
             function() {
               fakeSession.trigger("sessionDisconnected", {reason: "ko"});
 
               expect(model.get("connected")).eql(false);
             });
 
           it("should set the ongoing attribute to false on sessionDisconnected",
@@ -268,50 +282,31 @@ describe("loop.shared.models", function(
             });
 
           describe("connectionDestroyed event received", function() {
             var fakeEvent = {reason: "ko", connection: {connectionId: 42}};
 
             it("should trigger a session:peer-hungup model event",
               function(done) {
                 model.once("session:peer-hungup", function(event) {
-                  expect(event.connectionId).eql(42);
+                  expect(event.connection.connectionId).eql(42);
                   done();
                 });
 
                 fakeSession.trigger("connectionDestroyed", fakeEvent);
               });
 
             it("should terminate the session", function() {
               sandbox.stub(model, "endSession");
 
               fakeSession.trigger("connectionDestroyed", fakeEvent);
 
               sinon.assert.calledOnce(model.endSession);
             });
           });
-
-          describe("networkDisconnected event received", function() {
-            it("should trigger a session:network-disconnected event",
-              function(done) {
-                model.once("session:network-disconnected", function() {
-                  done();
-                });
-
-                fakeSession.trigger("networkDisconnected");
-              });
-
-            it("should terminate the session", function() {
-              sandbox.stub(model, "endSession");
-
-              fakeSession.trigger("networkDisconnected", {reason: "ko"});
-
-              sinon.assert.calledOnce(model.endSession);
-            });
-          });
         });
       });
 
       describe("#endSession", function() {
         var model;
 
         beforeEach(function() {
           model = new sharedModels.ConversationModel(fakeSessionData, {
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -29,17 +29,18 @@ describe("loop.standaloneRoomViews", fun
     sandbox.restore();
   });
 
   describe("standaloneRoomView", function() {
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         loop.standaloneRoomViews.StandaloneRoomView({
           dispatcher: dispatcher,
-          activeRoomStore: activeRoomStore
+          activeRoomStore: activeRoomStore,
+          helper: new loop.shared.utils.Helper()
         }));
     }
 
     describe("#componentWillUpdate", function() {
       it("dispatch an `SetupStreamElements` action on room joined", function() {
         activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
         var view = mountTestComponent();
 
@@ -123,16 +124,26 @@ describe("loop.standaloneRoomViews", fun
           function() {
             activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
 
             expect(view.getDOMNode().querySelector(".empty-room-message"))
               .eql(null);
           });
       });
 
+      describe("Full room message", function() {
+        it("should display a full room message on FULL",
+          function() {
+            activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
+
+            expect(view.getDOMNode().querySelector(".full-room-message"))
+              .not.eql(null);
+          });
+      });
+
       describe("Join button", function() {
         function getJoinButton(view) {
           return view.getDOMNode().querySelector(".btn-join");
         }
 
         it("should render the Join button when room isn't active", function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
 
@@ -170,16 +181,23 @@ describe("loop.standaloneRoomViews", fun
 
         it("should disable the Leave button when the room state is FAILED",
           function() {
             activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED});
 
             expect(getLeaveButton(view).disabled).eql(true);
           });
 
+        it("should disable the Leave button when the room state is FULL",
+          function() {
+            activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
+
+            expect(getLeaveButton(view).disabled).eql(true);
+          });
+
         it("should enable the Leave button when the room state is SESSION_CONNECTED",
           function() {
             activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});
 
             expect(getLeaveButton(view).disabled).eql(false);
           });
 
         it("should enable the Leave button when the room state is JOINED",
--- a/browser/components/loop/test/xpcshell/test_loopservice_busy.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_busy.js
@@ -2,131 +2,126 @@
  * 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 { LoopCallsInternal } = Cu.import("resource:///modules/loop/LoopCalls.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "Chat",
                                   "resource:///modules/Chat.jsm");
 
+let actionReceived = false;
 let openChatOrig = Chat.open;
 
 const firstCallId = 4444333221;
 const secondCallId = 1001100101;
 
 let msgHandler = function(msg) {
   if (msg.messageType &&
       msg.messageType === "action" &&
       msg.event === "terminate" &&
       msg.reason === "busy") {
     actionReceived = true;
   }
 };
 
-add_test(function test_busy_2guest_calls() {
+add_task(function* test_busy_2guest_calls() {
   actionReceived = false;
 
   mockPushHandler.registrationPushURL = kEndPointUrl;
 
-  MozLoopService.promiseRegisteredWithServers().then(() => {
-    let opened = 0;
-    let windowId;
-    Chat.open = function(contentWindow, origin, title, url) {
-      opened++;
-      windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
-    };
+  yield MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.GUEST);
 
-    mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
+  let opened = 0;
+  let windowId;
+  Chat.open = function(contentWindow, origin, title, url) {
+    opened++;
+    windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
+  };
 
-    waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
-      do_check_true(opened === 1, "should open only one chat window");
-      do_check_true(actionReceived, "should respond with busy/reject to second call");
-      LoopCalls.clearCallInProgress(windowId);
-      run_next_test();
-    }, () => {
-      do_throw("should have opened a chat window for first call and rejected second call");
-    });
+  mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
 
+  yield waitForCondition(() => { return actionReceived && opened > 0; }).then(() => {
+    do_check_true(opened === 1, "should open only one chat window");
+    do_check_true(actionReceived, "should respond with busy/reject to second call");
+    LoopCalls.clearCallInProgress(windowId);
+  }, () => {
+    do_throw("should have opened a chat window for first call and rejected second call");
   });
 });
 
-add_test(function test_busy_1fxa_1guest_calls() {
+add_task(function* test_busy_1fxa_1guest_calls() {
   actionReceived = false;
 
-  MozLoopService.promiseRegisteredWithServers().then(() => {
-    let opened = 0;
-    let windowId;
-    Chat.open = function(contentWindow, origin, title, url) {
-      opened++;
-      windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
-    };
+  yield MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.GUEST);
+  yield MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.FXA);
+
+  let opened = 0;
+  let windowId;
+  Chat.open = function(contentWindow, origin, title, url) {
+    opened++;
+    windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
+  };
 
-    mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
-    mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
+  mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
+  mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
 
-    waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
-      do_check_true(opened === 1, "should open only one chat window");
-      do_check_true(actionReceived, "should respond with busy/reject to second call");
-      LoopCalls.clearCallInProgress(windowId);
-      run_next_test();
-    }, () => {
-      do_throw("should have opened a chat window for first call and rejected second call");
-    });
-
+  yield waitForCondition(() => { return actionReceived && opened > 0; }).then(() => {
+    do_check_true(opened === 1, "should open only one chat window");
+    do_check_true(actionReceived, "should respond with busy/reject to second call");
+    LoopCalls.clearCallInProgress(windowId);
+  }, () => {
+    do_throw("should have opened a chat window for first call and rejected second call");
   });
 });
 
-add_test(function test_busy_2fxa_calls() {
+add_task(function* test_busy_2fxa_calls() {
   actionReceived = false;
 
-  MozLoopService.promiseRegisteredWithServers().then(() => {
-    let opened = 0;
-    let windowId;
-    Chat.open = function(contentWindow, origin, title, url) {
-      opened++;
-      windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
-    };
+  yield MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.FXA);
 
-    mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
+  let opened = 0;
+  let windowId;
+  Chat.open = function(contentWindow, origin, title, url) {
+    opened++;
+    windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
+  };
 
-    waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
-      do_check_true(opened === 1, "should open only one chat window");
-      do_check_true(actionReceived, "should respond with busy/reject to second call");
-      LoopCalls.clearCallInProgress(windowId);
-      run_next_test();
-    }, () => {
-      do_throw("should have opened a chat window for first call and rejected second call");
-    });
+  mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
 
+  yield waitForCondition(() => { return actionReceived && opened > 0; }).then(() => {
+    do_check_true(opened === 1, "should open only one chat window");
+    do_check_true(actionReceived, "should respond with busy/reject to second call");
+    LoopCalls.clearCallInProgress(windowId);
+  }, () => {
+    do_throw("should have opened a chat window for first call and rejected second call");
   });
 });
 
-add_test(function test_busy_1guest_1fxa_calls() {
+add_task(function* test_busy_1guest_1fxa_calls() {
   actionReceived = false;
 
-  MozLoopService.promiseRegisteredWithServers().then(() => {
-    let opened = 0;
-    let windowId;
-    Chat.open = function(contentWindow, origin, title, url) {
-      opened++;
-      windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
-    };
+  yield MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.GUEST);
+  yield MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.FXA);
+
+  let opened = 0;
+  let windowId;
+  Chat.open = function(contentWindow, origin, title, url) {
+    opened++;
+    windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
+  };
 
-    mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
-    mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
+  mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
+  mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
 
-    waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
-      do_check_true(opened === 1, "should open only one chat window");
-      do_check_true(actionReceived, "should respond with busy/reject to second call");
-      LoopCalls.clearCallInProgress(windowId);
-      run_next_test();
-    }, () => {
-      do_throw("should have opened a chat window for first call and rejected second call");
-    });
-
+  yield waitForCondition(() => { return actionReceived && opened > 0; }).then(() => {
+    do_check_true(opened === 1, "should open only one chat window");
+    do_check_true(actionReceived, "should respond with busy/reject to second call");
+    LoopCalls.clearCallInProgress(windowId);
+  }, () => {
+    do_throw("should have opened a chat window for first call and rejected second call");
   });
 });
 
 function run_test() {
   setupFakeLoopServer();
 
   // Setup fake login state so we get FxA requests.
   const MozLoopServiceInternal = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).MozLoopServiceInternal;
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -560,35 +560,58 @@
           ), 
 
           Section({name: "StandaloneRoomView"}, 
             Example({summary: "Standalone room conversation (ready)"}, 
               React.DOM.div({className: "standalone"}, 
                 StandaloneRoomView({
                   dispatcher: dispatcher, 
                   activeRoomStore: activeRoomStore, 
-                  roomState: ROOM_STATES.READY})
+                  roomState: ROOM_STATES.READY, 
+                  helper: {isFirefox: returnTrue}})
               )
             ), 
 
             Example({summary: "Standalone room conversation (joined)"}, 
               React.DOM.div({className: "standalone"}, 
                 StandaloneRoomView({
                   dispatcher: dispatcher, 
                   activeRoomStore: activeRoomStore, 
-                  roomState: ROOM_STATES.JOINED})
+                  roomState: ROOM_STATES.JOINED, 
+                  helper: {isFirefox: returnTrue}})
               )
             ), 
 
             Example({summary: "Standalone room conversation (has-participants)"}, 
               React.DOM.div({className: "standalone"}, 
                 StandaloneRoomView({
                   dispatcher: dispatcher, 
                   activeRoomStore: activeRoomStore, 
-                  roomState: ROOM_STATES.HAS_PARTICIPANTS})
+                  roomState: ROOM_STATES.HAS_PARTICIPANTS, 
+                  helper: {isFirefox: returnTrue}})
+              )
+            ), 
+
+            Example({summary: "Standalone room conversation (full - FFx user)"}, 
+              React.DOM.div({className: "standalone"}, 
+                StandaloneRoomView({
+                  dispatcher: dispatcher, 
+                  activeRoomStore: activeRoomStore, 
+                  roomState: ROOM_STATES.FULL, 
+                  helper: {isFirefox: returnTrue}})
+              )
+            ), 
+
+            Example({summary: "Standalone room conversation (full - non FFx user)"}, 
+              React.DOM.div({className: "standalone"}, 
+                StandaloneRoomView({
+                  dispatcher: dispatcher, 
+                  activeRoomStore: activeRoomStore, 
+                  roomState: ROOM_STATES.FULL, 
+                  helper: {isFirefox: returnFalse}})
               )
             )
           ), 
 
           Section({name: "SVG icons preview"}, 
             Example({summary: "16x16"}, 
               SVGIcons(null)
             )
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -560,35 +560,58 @@
           </Section>
 
           <Section name="StandaloneRoomView">
             <Example summary="Standalone room conversation (ready)">
               <div className="standalone">
                 <StandaloneRoomView
                   dispatcher={dispatcher}
                   activeRoomStore={activeRoomStore}
-                  roomState={ROOM_STATES.READY} />
+                  roomState={ROOM_STATES.READY}
+                  helper={{isFirefox: returnTrue}} />
               </div>
             </Example>
 
             <Example summary="Standalone room conversation (joined)">
               <div className="standalone">
                 <StandaloneRoomView
                   dispatcher={dispatcher}
                   activeRoomStore={activeRoomStore}
-                  roomState={ROOM_STATES.JOINED} />
+                  roomState={ROOM_STATES.JOINED}
+                  helper={{isFirefox: returnTrue}} />
               </div>
             </Example>
 
             <Example summary="Standalone room conversation (has-participants)">
               <div className="standalone">
                 <StandaloneRoomView
                   dispatcher={dispatcher}
                   activeRoomStore={activeRoomStore}
-                  roomState={ROOM_STATES.HAS_PARTICIPANTS} />
+                  roomState={ROOM_STATES.HAS_PARTICIPANTS}
+                  helper={{isFirefox: returnTrue}} />
+              </div>
+            </Example>
+
+            <Example summary="Standalone room conversation (full - FFx user)">
+              <div className="standalone">
+                <StandaloneRoomView
+                  dispatcher={dispatcher}
+                  activeRoomStore={activeRoomStore}
+                  roomState={ROOM_STATES.FULL}
+                  helper={{isFirefox: returnTrue}} />
+              </div>
+            </Example>
+
+            <Example summary="Standalone room conversation (full - non FFx user)">
+              <div className="standalone">
+                <StandaloneRoomView
+                  dispatcher={dispatcher}
+                  activeRoomStore={activeRoomStore}
+                  roomState={ROOM_STATES.FULL}
+                  helper={{isFirefox: returnFalse}} />
               </div>
             </Example>
           </Section>
 
           <Section name="SVG icons preview">
             <Example summary="16x16">
               <SVGIcons />
             </Example>
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -17,16 +17,18 @@ Cu.import("resource://gre/modules/Places
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
+                                  "resource:///modules/RecentWindow.jsm");
 
 // PlacesUtils exposes multiple symbols, so we can't use defineLazyModuleGetter.
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions",
                                   "resource://gre/modules/PlacesTransactions.jsm");
 
 #ifdef MOZ_SERVICES_CLOUDSYNC
@@ -468,17 +470,17 @@ this.PlacesUIUtils = {
     let features =
       "centerscreen,chrome,modal,resizable=" + (hasFolderPicker ? "yes" : "no");
 
     aParentWindow.openDialog(dialogURL, "",  features, aInfo);
     return ("performed" in aInfo && aInfo.performed);
   },
 
   _getTopBrowserWin: function PUIU__getTopBrowserWin() {
-    return Services.wm.getMostRecentWindow("navigator:browser");
+    return RecentWindow.getMostRecentBrowserWindow();
   },
 
   /**
    * Returns the closet ancestor places view for the given DOM node
    * @param aNode
    *        a DOM node
    * @return the closet ancestor places view if exists, null otherwsie.
    */
@@ -614,16 +616,20 @@ this.PlacesUIUtils = {
     // livemark.
     if (aNode.itemId == -1) {
       // Rather than executing a db query, checking the existence of the feedURI
       // annotation, detect livemark children by the fact that they are the only
       // direct non-bookmark children of bookmark folders.
       return !PlacesUtils.nodeIsFolder(parentNode);
     }
 
+    // Generally it's always possible to remove children of a query.
+    if (PlacesUtils.nodeIsQuery(parentNode))
+      return true;
+
     // Otherwise it has to be a child of an editable folder.
     return !this.isContentsReadOnly(parentNode);
   },
 
   /**
    * DO NOT USE THIS API IN ADDONS. IT IS VERY LIKELY TO CHANGE WHEN THE SWITCH
    * TO GUIDS IS COMPLETE (BUG 1071511).
    *
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -144,29 +144,30 @@ PlacesController.prototype = {
       return PlacesTransactions.topUndoEntry != null;
     case "cmd_redo":
       if (!PlacesUIUtils.useAsyncTransactions)
         return PlacesUtils.transactionManager.numberOfRedoItems > 0;
 
       return PlacesTransactions.topRedoEntry != null;
     case "cmd_cut":
     case "placesCmd_cut":
-      var nodes = this._view.selectedNodes;
-      // If selection includes history nodes there's no reason to allow cut.
-      for (var i = 0; i < nodes.length; i++) {
-        if (nodes[i].itemId == -1)
+    case "placesCmd_moveBookmarks":
+      for (let node of this._view.selectedNodes) {
+        // If selection includes history nodes or tags-as-bookmark, disallow
+        // cutting.
+        if (node.itemId == -1 ||
+            (node.parent && PlacesUtils.nodeIsTagQuery(node.parent))) {
           return false;
+        }
       }
-      // Otherwise fallback to cmd_delete check.
+      // Otherwise fall through the cmd_delete check.
     case "cmd_delete":
     case "placesCmd_delete":
     case "placesCmd_deleteDataHost":
-      return this._hasRemovableSelection(false);
-    case "placesCmd_moveBookmarks":
-      return this._hasRemovableSelection(true);
+      return this._hasRemovableSelection();
     case "cmd_copy":
     case "placesCmd_copy":
       return this._view.hasSelection;
     case "cmd_paste":
     case "placesCmd_paste":
       return this._canInsert(true) && this._isClipboardDataPasteable();
     case "cmd_selectAll":
       if (this._view.selType != "single") {
@@ -305,23 +306,21 @@ PlacesController.prototype = {
 
 
   /**
    * Determine whether or not the selection can be removed, either by the
    * delete or cut operations based on whether or not any of its contents
    * are non-removable. We don't need to worry about recursion here since it
    * is a policy decision that a removable item not be placed inside a non-
    * removable item.
-   * @param aIsMoveCommand
-   *        True if the command for which this method is called only moves the
-   *        selected items to another container, false otherwise.
+   *
    * @return true if all nodes in the selection can be removed,
    *         false otherwise.
    */
-  _hasRemovableSelection: function PC__hasRemovableSelection(aIsMoveCommand) {
+  _hasRemovableSelection() {
     var ranges = this._view.removableSelectionRanges;
     if (!ranges.length)
       return false;
 
     var root = this._view.result.root;
 
     for (var j = 0; j < ranges.length; j++) {
       var nodes = ranges[j];
@@ -1019,17 +1018,17 @@ PlacesController.prototype = {
 
   /**
    * Removes the selection
    * @param   aTxnName
    *          A name for the transaction if this is being performed
    *          as part of another operation.
    */
   remove: Task.async(function* (aTxnName) {
-    if (!this._hasRemovableSelection(false))
+    if (!this._hasRemovableSelection())
       return;
 
     NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name");
 
     var root = this._view.result.root;
 
     if (PlacesUtils.nodeIsFolder(root)) {
       if (PlacesUIUtils.useAsyncTransactions)
@@ -1540,17 +1539,17 @@ let PlacesControllerDragHelper = {
    *
    * @param   aUnwrappedNode
    *          A node unwrapped by PlacesUtils.unwrapNodes().
    * @return True if the node can be moved, false otherwise.
    */
   canMoveUnwrappedNode: function (aUnwrappedNode) {
     return aUnwrappedNode.id > 0 &&
            !PlacesUtils.isRootItem(aUnwrappedNode.id) &&
-           !PlacesUIUtils.isContentsReadOnly(aUnwrappedNode.parent) ||
+           (!aUnwrappedNode.parent || !PlacesUIUtils.isContentsReadOnly(aUnwrappedNode.parent)) &&
            aUnwrappedNode.parent != PlacesUtils.tagsFolderId &&
            aUnwrappedNode.grandParentId != PlacesUtils.tagsFolderId;
   },
 
   /**
    * Determines if a node can be moved.
    *
    * @param   aNode
--- a/browser/components/places/tests/browser/browser.ini
+++ b/browser/components/places/tests/browser/browser.ini
@@ -26,17 +26,17 @@ skip-if = e10s # Bug ?????? - clipboard 
 [browser_history_sidebar_search.js]
 [browser_bookmarksProperties.js]
 skip-if = e10s
 
 [browser_forgetthissite_single.js]
 # disabled for very frequent oranges - bug 551540
 skip-if = true
 
-[browser_library_left_pane_commands.js]
+[browser_library_commands.js]
 [browser_drag_bookmarks_on_toolbar.js]
 skip-if = e10s # Bug ?????? - test fails - "Number of dragged items should be the same. - Got 0, expected 1"
 [browser_library_middleclick.js]
 [browser_library_views_liveupdate.js]
 [browser_views_liveupdate.js]
 
 [browser_sidebarpanels_click.js]
 # temporarily disabled for breaking the treeview - bug 658744
--- a/browser/components/places/tests/browser/browser_423515.js
+++ b/browser/components/places/tests/browser/browser_423515.js
@@ -137,16 +137,17 @@ function test() {
   });
 
   // test that a tag container cannot be moved
   tests.push({
     populate: function() {
       // tag a uri
       this.uri = makeURI("http://foo.com");
       PlacesUtils.tagging.tagURI(this.uri, ["bar"]);
+      registerCleanupFunction(() => PlacesUtils.tagging.untagURI(this.uri, ["bar"]));
     },
     validate: function() {
       // get tag root
       var query = PlacesUtils.history.getNewQuery();
       var options = PlacesUtils.history.getNewQueryOptions();
       options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY;
       var tagsNode = PlacesUtils.history.executeQuery(query, options).root;
 
--- a/browser/components/places/tests/browser/browser_bookmarksProperties.js
+++ b/browser/components/places/tests/browser/browser_bookmarksProperties.js
@@ -29,19 +29,18 @@ const ACTION_ADD = 1;
 const TYPE_FOLDER = 0;
 const TYPE_BOOKMARK = 1;
 
 const TEST_URL = "http://www.example.com/";
 
 const DIALOG_URL = "chrome://browser/content/places/bookmarkProperties.xul";
 const DIALOG_URL_MINIMAL_UI = "chrome://browser/content/places/bookmarkProperties2.xul";
 
-var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
-         getService(Ci.nsIWindowMediator);
-var win = wm.getMostRecentWindow("navigator:browser");
+Cu.import("resource:///modules/RecentWindow.jsm");
+let win = RecentWindow.getMostRecentBrowserWindow();
 var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
          getService(Ci.nsIWindowWatcher);
 
 function add_bookmark(aURI) {
   var bId = PlacesUtils.bookmarks
                        .insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                        aURI,
                                        PlacesUtils.bookmarks.DEFAULT_INDEX,
rename from browser/components/places/tests/browser/browser_library_left_pane_commands.js
rename to browser/components/places/tests/browser/browser_library_commands.js
--- a/browser/components/places/tests/browser/browser_library_left_pane_commands.js
+++ b/browser/components/places/tests/browser/browser_library_commands.js
@@ -3,154 +3,233 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /**
  *  Test enabled commands in the left pane folder of the Library.
  */
 
-const TEST_URI = "http://www.mozilla.org/";
+const TEST_URI = NetUtil.newURI("http://www.mozilla.org/");
 
-var gTests = [];
-var gLibrary;
+registerCleanupFunction(function* () {
+  yield PlacesUtils.bookmarks.eraseEverything();
+  yield promiseClearHistory();
+});
 
-//------------------------------------------------------------------------------
+add_task(function* test_date_container() {
+  let library = yield promiseLibrary();
+  info("Ensure date containers under History cannot be cut but can be deleted");
+
+  yield promiseAddVisits(TEST_URI);
 
-gTests.push({
-  desc: "Bug 489351 - Date containers under History in Library cannot be deleted/cut",
-  run: function() {
-    function addVisitsCallback() {
-      // Select and open the left pane "History" query.
-      var PO = gLibrary.PlacesOrganizer;
-      PO.selectLeftPaneQuery('History');
-      isnot(PO._places.selectedNode, null, "We correctly selected History");
+  // Select and open the left pane "History" query.
+  let PO = library.PlacesOrganizer;
+
+  PO.selectLeftPaneQuery('History');
+  isnot(PO._places.selectedNode, null, "We correctly selected History");
 
-      // Check that both delete and cut commands are disabled.
-      ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
-         "Cut command is disabled");
-      ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
-         "Delete command is disabled");
-      var historyNode = PO._places.selectedNode
-                          .QueryInterface(Ci.nsINavHistoryContainerResultNode);
-      historyNode.containerOpen = true;
+  // Check that both delete and cut commands are disabled, cause this is
+  // a child of the left pane folder.
+  ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+     "Copy command is enabled");
+  ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+     "Cut command is disabled");
+  ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
+     "Delete command is disabled");
+  let historyNode = PlacesUtils.asContainer(PO._places.selectedNode);
+  historyNode.containerOpen = true;
 
-      // Check that we have a child container. It is "Today" container.
-      is(historyNode.childCount, 1, "History node has one child");
-      var todayNode = historyNode.getChild(0);
-      var todayNodeExpectedTitle = PlacesUtils.getString("finduri-AgeInDays-is-0");
-      is(todayNode.title, todayNodeExpectedTitle,
-         "History child is the expected container");
-
-      // Select "Today" container.
-      PO._places.selectNode(todayNode);
-      is(PO._places.selectedNode, todayNode,
-         "We correctly selected Today container");
-      // Check that delete command is enabled but cut command is disabled.
-      ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
-         "Cut command is disabled");
-      ok(PO._places.controller.isCommandEnabled("cmd_delete"),
-         "Delete command is enabled");
+  // Check that we have a child container. It is "Today" container.
+  is(historyNode.childCount, 1, "History node has one child");
+  let todayNode = historyNode.getChild(0);
+  let todayNodeExpectedTitle = PlacesUtils.getString("finduri-AgeInDays-is-0");
+  is(todayNode.title, todayNodeExpectedTitle,
+     "History child is the expected container");
 
-      // Execute the delete command and check visit has been removed.
-      PO._places.controller.doCommand("cmd_delete");
-
-      // Test live update of "History" query.
-      is(historyNode.childCount, 0, "History node has no more children");
-
-      historyNode.containerOpen = false;
+  // Select "Today" container.
+  PO._places.selectNode(todayNode);
+  is(PO._places.selectedNode, todayNode,
+     "We correctly selected Today container");
+  // Check that delete command is enabled but cut command is disabled, cause
+  // this is an history item.
+  ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+     "Copy command is enabled");
+  ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+     "Cut command is disabled");
+  ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+     "Delete command is enabled");
 
-      let testURI = NetUtil.newURI(TEST_URI);
-      PlacesUtils.asyncHistory.isURIVisited(testURI, function(aURI, aIsVisited) {
-        ok(!aIsVisited, "Visit has been removed");
-        nextTest();
-      });
-    }
-    addVisits(
-      {uri: NetUtil.newURI(TEST_URI), visitDate: Date.now() * 1000,
-        transition: PlacesUtils.history.TRANSITION_TYPED},
-      window,
-      addVisitsCallback);
-  }
+  // Execute the delete command and check visit has been removed.
+  let promiseURIRemoved = promiseHistoryNotification("onDeleteURI",
+                                                     () => TEST_URI.equals(arguments[0]));
+  PO._places.controller.doCommand("cmd_delete");
+  yield promiseURIRemoved;
+
+  // Test live update of "History" query.
+  is(historyNode.childCount, 0, "History node has no more children");
+
+  historyNode.containerOpen = false;
+
+  ok(!(yield promiseIsURIVisited(TEST_URI)), "Visit has been removed");
+
+  library.close();
 });
 
-//------------------------------------------------------------------------------
+add_task(function* test_query_on_toolbar() {
+  let library = yield promiseLibrary();
+  info("Ensure queries can be cut or deleted");
+
+  // Select and open the left pane "Bookmarks Toolbar" folder.
+  let PO = library.PlacesOrganizer;
 
-gTests.push({
-  desc: "Bug 490156 - Can't delete smart bookmark containers",
-  run: function() {
-    // Select and open the left pane "Bookmarks Toolbar" folder.
-    var PO = gLibrary.PlacesOrganizer;
-    PO.selectLeftPaneQuery('BookmarksToolbar');
-    isnot(PO._places.selectedNode, null, "We have a valid selection");
-    is(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
-       PlacesUtils.toolbarFolderId,
-       "We have correctly selected bookmarks toolbar node.");
+  PO.selectLeftPaneQuery('BookmarksToolbar');
+  isnot(PO._places.selectedNode, null, "We have a valid selection");
+  is(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
+     PlacesUtils.toolbarFolderId,
+     "We have correctly selected bookmarks toolbar node.");
 
-    // Check that both cut and delete commands are disabled.
-    ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
-       "Cut command is disabled");
-    ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
-       "Delete command is disabled");
+  // Check that both cut and delete commands are disabled, cause this is a child
+  // of AllBookmarksFolderId.
+  ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+     "Copy command is enabled");
+  ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+     "Cut command is disabled");
+  ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
+     "Delete command is disabled");
 
-    var toolbarNode = PO._places.selectedNode
-                        .QueryInterface(Ci.nsINavHistoryContainerResultNode);
-    toolbarNode.containerOpen = true;
+  let toolbarNode = PlacesUtils.asContainer(PO._places.selectedNode);
+  toolbarNode.containerOpen = true;
 
-    // Add an History query to the toolbar.
-    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId,
-                                         NetUtil.newURI("place:sort=4"),
-                                         0, // Insert at start.
-                                         "special_query");
-    // Get first child and check it is the "Most Visited" smart bookmark.
-    ok(toolbarNode.childCount > 0, "Toolbar node has children");
-    var queryNode = toolbarNode.getChild(0);
-    is(queryNode.title, "special_query", "Query node is correctly selected");
+  // Add an History query to the toolbar.
+  let query = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+                                                   url: "place:sort=4",
+                                                   title: "special_query",
+                                                   parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+                                                   index: 0 });
 
-    // Select query node.
-    PO._places.selectNode(queryNode);
-    is(PO._places.selectedNode, queryNode, "We correctly selected query node");
+  // Get first child and check it is the just inserted query.
+  ok(toolbarNode.childCount > 0, "Toolbar node has children");
+  let queryNode = toolbarNode.getChild(0);
+  is(queryNode.title, "special_query", "Query node is correctly selected");
+
+  // Select query node.
+  PO._places.selectNode(queryNode);
+  is(PO._places.selectedNode, queryNode, "We correctly selected query node");
 
-    // Check that both cut and delete commands are enabled.
-    ok(PO._places.controller.isCommandEnabled("cmd_cut"),
-       "Cut command is enabled");
-    ok(PO._places.controller.isCommandEnabled("cmd_delete"),
-       "Delete command is enabled");
+  // Check that both cut and delete commands are enabled.
+  ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+     "Copy command is enabled");
+  ok(PO._places.controller.isCommandEnabled("cmd_cut"),
+     "Cut command is enabled");
+  ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+     "Delete command is enabled");
 
-    // Execute the delete command and check bookmark has been removed.
-    PO._places.controller.doCommand("cmd_delete");
-    try {
-      PlacesUtils.bookmarks.getFolderIdForItem(queryNode.itemId);  
-      ok(false, "Unable to remove query node bookmark");
-    } catch(ex) {
-      ok(true, "Query node bookmark has been correctly removed");
-    }
+  // Execute the delete command and check bookmark has been removed.
+  let promiseItemRemoved = promiseBookmarksNotification("onItemRemoved",
+                                                        () => query.guid == arguments[5]);
+  PO._places.controller.doCommand("cmd_delete");
+  yield promiseItemRemoved;
 
-    toolbarNode.containerOpen = false;
-    nextTest();
-  }
+  is((yield PlacesUtils.bookmarks.fetch(query.guid)), null,
+     "Query node bookmark has been correctly removed");
+
+  toolbarNode.containerOpen = false;
+
+  library.close();
 });
 
-//------------------------------------------------------------------------------
+add_task(function* test_search_contents() {
+  let item = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+                                                  url: "http://example.com/",
+                                                  title: "example page",
+                                                  parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+                                                  index: 0 });
+
+  let library = yield promiseLibrary();
+  info("Ensure query contents can be cut or deleted");
+
+  // Select and open the left pane "Bookmarks Toolbar" folder.
+  let PO = library.PlacesOrganizer;
+
+  PO.selectLeftPaneQuery('BookmarksToolbar');
+  isnot(PO._places.selectedNode, null, "We have a valid selection");
+  is(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
+     PlacesUtils.toolbarFolderId,
+     "We have correctly selected bookmarks toolbar node.");
+
+  let searchBox = library.document.getElementById("searchFilter");
+  searchBox.value = "example";
+  library.PlacesSearchBox.search(searchBox.value);
+
+  let bookmarkNode = library.ContentTree.view.selectedNode;
+  is(bookmarkNode.uri, "http://example.com/", "Found the expected bookmark");
+
+  // Check that both cut and delete commands are enabled.
+  ok(library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
+     "Copy command is enabled");
+  ok(library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
+     "Cut command is enabled");
+  ok(library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
+     "Delete command is enabled");
+
+  library.close();
+});
+
+add_task(function* test_tags() {
+  let item = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+                                                  url: "http://example.com/",
+                                                  title: "example page",
+                                                  parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+                                                  index: 0 });
+  PlacesUtils.tagging.tagURI(NetUtil.newURI("http://example.com/"), ["test"]);
+
+  let library = yield promiseLibrary();
+  info("Ensure query contents can be cut or deleted");
 
-function nextTest() {
-  if (gTests.length) {
-    var test = gTests.shift();
-    info("Start of test: " + test.desc);
-    test.run();
-  }
-  else {
-    // Close Library window.
-    gLibrary.close();
-    // No need to cleanup anything, we have a correct left pane now.
-    finish();
-  }
-}
+  // Select and open the left pane "Bookmarks Toolbar" folder.
+  let PO = library.PlacesOrganizer;
+
+  PO.selectLeftPaneQuery('Tags');
+  let tagsNode = PO._places.selectedNode;
+  isnot(tagsNode, null, "We have a valid selection");
+  let tagsTitle = PlacesUtils.getString("TagsFolderTitle");
+  is(tagsNode.title, tagsTitle,
+     "Tags has been properly selected");
+
+  // Check that both cut and delete commands are disabled.
+  ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+     "Copy command is enabled");
+  ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+     "Cut command is disabled");
+  ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
+     "Delete command is disabled");
 
-function test() {
-  waitForExplicitFinish();
-  // Sanity checks.
-  ok(PlacesUtils, "PlacesUtils is running in chrome context");
-  ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+  // Now select the tag.
+  PlacesUtils.asContainer(tagsNode).containerOpen = true;
+  let tag = tagsNode.getChild(0);
+  PO._places.selectNode(tag);
+  is(PO._places.selectedNode.title, "test",
+     "The created tag has been properly selected");
+
+  // Check that cut is disabled but delete is enabled.
+  ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+     "Copy command is enabled");
+  ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+     "Cut command is disabled");
+  ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+     "Delete command is enabled");
 
-  // Open Library.
-  gLibrary = openLibrary(nextTest);
-}
+  let bookmarkNode = library.ContentTree.view.selectedNode;
+  is(bookmarkNode.uri, "http://example.com/", "Found the expected bookmark");
+
+  // Check that both cut and delete commands are enabled.
+  ok(library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
+     "Copy command is enabled");
+  ok(!library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
+     "Cut command is disabled");
+  ok(library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
+     "Delete command is enabled");
+
+  tagsNode.containerOpen = false;
+
+  library.close();
+});
--- a/browser/components/places/tests/browser/browser_library_infoBox.js
+++ b/browser/components/places/tests/browser/browser_library_infoBox.js
@@ -66,26 +66,34 @@ gTests.push({
       isnot(PO._places.selectedNode, null,
             "Correctly selected bookmarks menu node.");
       checkInfoBoxSelected(PO);
       ok(infoBoxExpanderWrapper.hidden,
          "Expander button is hidden for bookmarks menu node.");
       checkAddInfoFieldsCollapsed(PO);
 
       // open recently bookmarked node
+      PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+                                           NetUtil.newURI("place:folder=BOOKMARKS_MENU" +
+                                                          "&folder=UNFILED_BOOKMARKS" +
+                                                          "&folder=TOOLBAR" +
+                                                          "&queryType=" + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS +
+                                                          "&sort=" + Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING +
+                                                          "&maxResults=10" +
+                                                          "&excludeQueries=1"),
+                                           0, "Recent Bookmarks");
+      PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+                                           NetUtil.newURI("http://mozilla.org/"),
+                                           1, "Mozilla");
       var menuNode = PO._places.selectedNode.
                      QueryInterface(Ci.nsINavHistoryContainerResultNode);
       menuNode.containerOpen = true;
       childNode = menuNode.getChild(0);
       isnot(childNode, null, "Bookmarks menu child node exists.");
-      var recentlyBookmarkedTitle = PlacesUIUtils.
-                                    getString("recentlyBookmarkedTitle");
-      isnot(recentlyBookmarkedTitle, null,
-            "Correctly got the recently bookmarked title locale string.");
-      is(childNode.title, recentlyBookmarkedTitle,
+      is(childNode.title, "Recent Bookmarks",
          "Correctly selected recently bookmarked node.");
       PO._places.selectNode(childNode);
       checkInfoBoxSelected(PO);
       ok(!infoBoxExpanderWrapper.hidden,
          "Expander button is not hidden for recently bookmarked node.");
       checkAddInfoFieldsNotCollapsed(PO);
 
       // open first bookmark
@@ -93,25 +101,16 @@ gTests.push({
       ok(view.rowCount > 0, "Bookmark item exists.");
       view.selection.select(0);
       checkInfoBoxSelected(PO);
       ok(!infoBoxExpanderWrapper.hidden,
          "Expander button is not hidden for bookmark item.");
       checkAddInfoFieldsNotCollapsed(PO);
       checkAddInfoFields(PO, "bookmark item");
 
-      // make sure additional fields are still hidden in second bookmark item
-      ok(view.rowCount > 1, "Second bookmark item exists.");
-      view.selection.select(1);
-      checkInfoBoxSelected(PO);
-      ok(!infoBoxExpanderWrapper.hidden,
-         "Expander button is not hidden for second bookmark item.");
-      checkAddInfoFieldsNotCollapsed(PO);
-      checkAddInfoFields(PO, "second bookmark item");
-
       menuNode.containerOpen = false;
 
       waitForClearHistory(nextTest);
     }
     // add a visit to browser history
     addVisits(
       { uri: PlacesUtils._uri(TEST_URI), visitDate: Date.now() * 1000,
         transition: PlacesUtils.history.TRANSITION_TYPED },
--- a/browser/components/places/tests/browser/head.js
+++ b/browser/components/places/tests/browser/head.js
@@ -38,17 +38,17 @@ function openLibrary(callback, aLeftPane
  * If one is opens returns itm otherwise it opens a new one.
  *
  * @param aLeftPaneRoot
  *        Hierarchy to open and select in the left pane.
  */
 function promiseLibrary(aLeftPaneRoot) {
   let deferred = Promise.defer();
   let library = Services.wm.getMostRecentWindow("Places:Organizer");
-  if (library) {
+  if (library && !library.closed) {
     if (aLeftPaneRoot)
       library.PlacesOrganizer.selectLeftPaneContainerByHierarchy(aLeftPaneRoot);
     deferred.resolve(library);
   }
   else {
     openLibrary(aLibrary => deferred.resolve(aLibrary), aLeftPaneRoot);
   }
   return deferred.promise;
@@ -159,17 +159,17 @@ function addVisits(aPlaceInfo, aWindow, 
 
   // Create mozIVisitInfo for each entry.
   let now = Date.now();
   for (let i = 0; i < places.length; i++) {
     if (!places[i].title) {
       places[i].title = "test visit for " + places[i].uri.spec;
     }
     places[i].visits = [{
-      transitionType: places[i].transition === undefined ? Ci.nsINavHistoryService.TRANSITION_LINK
+      transitionType: places[i].transition === undefined ? PlacesUtils.history.TRANSITION_LINK
                                                          : places[i].transition,
       visitDate: places[i].visitDate || (now++) * 1000,
       referrerURI: places[i].referrer
     }];
   }
 
   aWindow.PlacesUtils.asyncHistory.updatePlaces(
     places,
@@ -198,8 +198,211 @@ function synthesizeClickOnSelectedTreeCe
   // Calculate the click coordinates.
   var rect = tbo.getCoordsForCellItem(rowID, aTree.columns[0], "text");
   var x = rect.x + rect.width / 2;
   var y = rect.y + rect.height / 2;
   // Simulate the click.
   EventUtils.synthesizeMouse(aTree.body, x, y, aOptions || {},
                              aTree.ownerDocument.defaultView);
 }
+
+/**
+ * Asynchronously adds visits to a page.
+ *
+ * @param aPlaceInfo
+ *        Can be an nsIURI, in such a case a single LINK visit will be added.
+ *        Otherwise can be an object describing the visit to add, or an array
+ *        of these objects:
+ *          { uri: nsIURI of the page,
+ *            transition: one of the TRANSITION_* from nsINavHistoryService,
+ *            [optional] title: title of the page,
+ *            [optional] visitDate: visit date in microseconds from the epoch
+ *            [optional] referrer: nsIURI of the referrer for this visit
+ *          }
+ *
+ * @return {Promise}
+ * @resolves When all visits have been added successfully.
+ * @rejects JavaScript exception.
+ */
+function promiseAddVisits(aPlaceInfo)
+{
+  let deferred = Promise.defer();
+  let places = [];
+  if (aPlaceInfo instanceof Ci.nsIURI) {
+    places.push({ uri: aPlaceInfo });
+  }
+  else if (Array.isArray(aPlaceInfo)) {
+    places = places.concat(aPlaceInfo);
+  } else {
+    places.push(aPlaceInfo)
+  }
+
+  // Create mozIVisitInfo for each entry.
+  let now = Date.now();
+  for (let i = 0; i < places.length; i++) {
+    if (!places[i].title) {
+      places[i].title = "test visit for " + places[i].uri.spec;
+    }
+    places[i].visits = [{
+      transitionType: places[i].transition === undefined ? PlacesUtils.history.TRANSITION_LINK
+                                                         : places[i].transition,
+      visitDate: places[i].visitDate || (now++) * 1000,
+      referrerURI: places[i].referrer
+    }];
+  }
+
+  PlacesUtils.asyncHistory.updatePlaces(
+    places,
+    {
+      handleError: function AAV_handleError(aResultCode, aPlaceInfo) {
+        let ex = new Components.Exception("Unexpected error in adding visits.",
+                                          aResultCode);
+        deferred.reject(ex);
+      },
+      handleResult: function () {},
+      handleCompletion: function UP_handleCompletion() {
+        deferred.resolve();
+      }
+    }
+  );
+
+  return deferred.promise;
+}
+
+/**
+ * Asynchronously check a url is visited.
+ *
+ * @param aURI The URI.
+ * @return {Promise}
+ * @resolves When the check has been added successfully.
+ * @rejects JavaScript exception.
+ */
+function promiseIsURIVisited(aURI) {
+  let deferred = Promise.defer();
+
+  PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) {
+    deferred.resolve(aIsVisited);
+  });
+
+  return deferred.promise;
+}
+
+/**
+ * Waits for all pending async statements on the default connection.
+ *
+ * @return {Promise}
+ * @resolves When all pending async statements finished.
+ * @rejects Never.
+ *
+ * @note The result is achieved by asynchronously executing a query requiring
+ *       a write lock.  Since all statements on the same connection are
+ *       serialized, the end of this write operation means that all writes are
+ *       complete.  Note that WAL makes so that writers don't block readers, but
+ *       this is a problem only across different connections.
+ */
+function promiseAsyncUpdates()
+{
+  let deferred = Promise.defer();
+
+  let db = DBConn();
+  let begin = db.createAsyncStatement("BEGIN EXCLUSIVE");
+  begin.executeAsync();
+  begin.finalize();
+
+  let commit = db.createAsyncStatement("COMMIT");
+  commit.executeAsync({
+    handleResult: function () {},
+    handleError: function () {},
+    handleCompletion: function(aReason)
+    {
+      deferred.resolve();
+    }
+  });
+  commit.finalize();
+
+  return deferred.promise;
+}
+
+function promiseBookmarksNotification(notification, conditionFn) {
+  info(`Waiting for ${notification}`);
+  return new Promise((resolve, reject) => {
+    let proxifiedObserver = new Proxy({}, {
+      get: (target, name) => {
+        if (name == "QueryInterface")
+          return XPCOMUtils.generateQI([ Ci.nsINavBookmarkObserver ]);
+        if (name == notification)
+          return () => {
+            if (conditionFn.apply(this, arguments)) {
+              clearTimeout(timeout);
+              PlacesUtils.bookmarks.removeObserver(proxifiedObserver, false);
+              executeSoon(resolve);
+            }
+          }
+        return () => {};
+      }
+    });
+    PlacesUtils.bookmarks.addObserver(proxifiedObserver, false);
+    let timeout = setTimeout(() => {
+      PlacesUtils.bookmarks.removeObserver(proxifiedObserver, false);
+      reject(new Error("Timed out while waiting for bookmarks notification"));
+    }, 2000);
+  });
+}
+
+function promiseHistoryNotification(notification, conditionFn) {
+  info(`Waiting for ${notification}`);
+  return new Promise((resolve, reject) => {
+    let proxifiedObserver = new Proxy({}, {
+      get: (target, name) => {
+        if (name == "QueryInterface")
+          return XPCOMUtils.generateQI([ Ci.nsINavHistoryObserver ]);
+        if (name == notification)
+          return () => {
+            if (conditionFn.apply(this, arguments)) {
+              clearTimeout(timeout);
+              PlacesUtils.history.removeObserver(proxifiedObserver, false);
+              executeSoon(resolve);
+            }
+          }
+        return () => {};
+      }
+    });
+    PlacesUtils.history.addObserver(proxifiedObserver, false);
+    let timeout = setTimeout(() => {
+      PlacesUtils.history.removeObserver(proxifiedObserver, false);
+      reject(new Error("Timed out while waiting for history notification"));
+    }, 2000);
+  });
+}
+
+/**
+ * Clears history asynchronously.
+ *
+ * @return {Promise}
+ * @resolves When history has been cleared.
+ * @rejects Never.
+ */
+function promiseClearHistory() {
+  let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+  PlacesUtils.bhistory.removeAllPages();
+  return promise;
+}
+
+/**
+ * Allows waiting for an observer notification once.
+ *
+ * @param topic
+ *        Notification topic to observe.
+ *
+ * @return {Promise}
+ * @resolves The array [subject, data] from the observed notification.
+ * @rejects Never.
+ */
+function promiseTopicObserved(topic)
+{
+  let deferred = Promise.defer();
+  info("Waiting for observer topic " + topic);
+  Services.obs.addObserver(function PTO_observe(subject, topic, data) {
+    Services.obs.removeObserver(PTO_observe, topic);
+    deferred.resolve([subject, data]);
+  }, topic, false);
+  return deferred.promise;
+}
--- a/browser/devtools/debugger/test/code_script-switching-02.js
+++ b/browser/devtools/debugger/test/code_script-switching-02.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function secondCall() {
   // This comment is useful: ☺
   eval("debugger;");
   function foo() {}
-  if (true) {
+  if (x) {
     foo();
   }
 }
+
+var x = true;
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -19,17 +19,19 @@ support-files =
 [browser_graphs-02.js]
 [browser_graphs-03.js]
 [browser_graphs-04.js]
 [browser_graphs-05.js]
 [browser_graphs-06.js]
 [browser_graphs-07a.js]
 [browser_graphs-07b.js]
 [browser_graphs-08.js]
-[browser_graphs-09.js]
+[browser_graphs-09a.js]
+[browser_graphs-09b.js]
+[browser_graphs-09c.js]
 [browser_graphs-10a.js]
 [browser_graphs-10b.js]
 [browser_graphs-11a.js]
 [browser_graphs-11b.js]
 [browser_graphs-12.js]
 [browser_graphs-13.js]
 [browser_graphs-14.js]
 [browser_inplace-editor.js]
rename from browser/devtools/shared/test/browser_graphs-09.js
rename to browser/devtools/shared/test/browser_graphs-09a.js
--- a/browser/devtools/shared/test/browser_graphs-09.js
+++ b/browser/devtools/shared/test/browser_graphs-09a.js
@@ -22,31 +22,47 @@ function* performTest() {
 
   yield testGraph(graph);
 
   graph.destroy();
   host.destroy();
 }
 
 function* testGraph(graph) {
-  info("Should be able to set the grpah data before waiting for the ready event.");
+  info("Should be able to set the graph data before waiting for the ready event.");
 
   yield graph.setDataWhenReady(TEST_DATA);
   ok(graph.hasData(), "Data was set successfully.");
 
+  is(graph._gutter.hidden, false,
+    "The gutter should not be hidden because the tooltips have arrows.");
+  is(graph._maxTooltip.hidden, false,
+    "The max tooltip should not be hidden.");
+  is(graph._avgTooltip.hidden, false,
+    "The avg tooltip should not be hidden.");
+  is(graph._minTooltip.hidden, false,
+    "The min tooltip should not be hidden.");
+
+  is(graph._maxTooltip.getAttribute("with-arrows"), "true",
+    "The maximum tooltip has the correct 'with-arrows' attribute.");
+  is(graph._avgTooltip.getAttribute("with-arrows"), "true",
+    "The average tooltip has the correct 'with-arrows' attribute.");
+  is(graph._minTooltip.getAttribute("with-arrows"), "true",
+    "The minimum tooltip has the correct 'with-arrows' attribute.");
+
   is(graph._maxTooltip.querySelector("[text=info]").textContent, "max",
     "The maximum tooltip displays the correct info.");
   is(graph._avgTooltip.querySelector("[text=info]").textContent, "avg",
     "The average tooltip displays the correct info.");
   is(graph._minTooltip.querySelector("[text=info]").textContent, "min",
     "The minimum tooltip displays the correct info.");
 
   is(graph._maxTooltip.querySelector("[text=value]").textContent, "60",
     "The maximum tooltip displays the correct value.");
-  is(graph._avgTooltip.querySelector("[text=value]").textContent, "41",
+  is(graph._avgTooltip.querySelector("[text=value]").textContent, "41.71",
     "The average tooltip displays the correct value.");
   is(graph._minTooltip.querySelector("[text=value]").textContent, "10",
     "The minimum tooltip displays the correct value.");
 
   is(graph._maxTooltip.querySelector("[text=metric]").textContent, "fps",
     "The maximum tooltip displays the correct metric.");
   is(graph._avgTooltip.querySelector("[text=metric]").textContent, "fps",
     "The average tooltip displays the correct metric.");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_graphs-09b.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that line graphs properly use the tooltips configuration properties.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
+
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* performTest() {
+  let [host, win, doc] = yield createHost();
+  let graph = new LineGraphWidget(doc.body, "fps");
+  graph.withTooltipArrows = false;
+  graph.withFixedTooltipPositions = true;
+
+  yield testGraph(graph);
+
+  graph.destroy();
+  host.destroy();
+}
+
+function* testGraph(graph) {
+  yield graph.setDataWhenReady(TEST_DATA);
+
+  is(graph._gutter.hidden, true,
+    "The gutter should be hidden because the tooltips don't have arrows.");
+  is(graph._maxTooltip.hidden, false,
+    "The max tooltip should not be hidden.");
+  is(graph._avgTooltip.hidden, false,
+    "The avg tooltip should not be hidden.");
+  is(graph._minTooltip.hidden, false,
+    "The min tooltip should not be hidden.");
+
+  is(graph._maxTooltip.getAttribute("with-arrows"), "false",
+    "The maximum tooltip has the correct 'with-arrows' attribute.");
+  is(graph._avgTooltip.getAttribute("with-arrows"), "false",
+    "The average tooltip has the correct 'with-arrows' attribute.");
+  is(graph._minTooltip.getAttribute("with-arrows"), "false",
+    "The minimum tooltip has the correct 'with-arrows' attribute.");
+
+  is(parseInt(graph._maxTooltip.style.top), 8,
+    "The maximum tooltip is positioned correctly.");
+  is(parseInt(graph._avgTooltip.style.top), 8,
+    "The average tooltip is positioned correctly.");
+  is(parseInt(graph._minTooltip.style.top), 142,
+    "The minimum tooltip is positioned correctly.");
+
+  is(parseInt(graph._maxGutterLine.style.top), 22,
+    "The maximum gutter line is positioned correctly.");
+  is(parseInt(graph._avgGutterLine.style.top), 61,
+    "The average gutter line is positioned correctly.");
+  is(parseInt(graph._minGutterLine.style.top), 128,
+    "The minimum gutter line is positioned correctly.");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_graphs-09c.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that line graphs hide the tooltips when there's no data available.
+
+const TEST_DATA = [];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
+
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* performTest() {
+  let [host, win, doc] = yield createHost();
+  let graph = new LineGraphWidget(doc.body, "fps");
+
+  yield testGraph(graph);
+
+  graph.destroy();
+  host.destroy();
+}
+
+function* testGraph(graph) {
+  yield graph.setDataWhenReady(TEST_DATA);
+
+  is(graph._gutter.hidden, false,
+    "The gutter should not be hidden.");
+  is(graph._maxTooltip.hidden, true,
+    "The max tooltip should be hidden.");
+  is(graph._avgTooltip.hidden, true,
+    "The avg tooltip should be hidden.");
+  is(graph._minTooltip.hidden, true,
+    "The min tooltip should be hidden.");
+}
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -14,16 +14,17 @@ this.EXPORTED_SYMBOLS = [
   "AbstractCanvasGraph",
   "LineGraphWidget",
   "BarGraphWidget",
   "CanvasGraphUtils"
 ];
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
+const L10N = new ViewHelpers.L10N();
 
 // Generic constants.
 
 const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
 const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00075;
 const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.1;
 const GRAPH_WHEEL_MIN_SELECTION_WIDTH = 10; // px
 
@@ -39,18 +40,19 @@ const GRAPH_STRIPE_PATTERN_WIDTH = 16; /
 const GRAPH_STRIPE_PATTERN_HEIGHT = 16; // px
 const GRAPH_STRIPE_PATTERN_LINE_WIDTH = 2; // px
 const GRAPH_STRIPE_PATTERN_LINE_SPACING = 4; // px
 
 // Line graph constants.
 
 const LINE_GRAPH_DAMPEN_VALUES = 0.85;
 const LINE_GRAPH_MIN_SQUARED_DISTANCE_BETWEEN_POINTS = 400; // 20 px
-const LINE_GRAPH_TOOLTIP_SAFE_BOUNDS = 10; // px
+const LINE_GRAPH_TOOLTIP_SAFE_BOUNDS = 8; // px
 
+const LINE_GRAPH_BACKGROUND_COLOR = "#0088cc";
 const LINE_GRAPH_STROKE_WIDTH = 1; // px
 const LINE_GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
 const LINE_GRAPH_HELPER_LINES_DASH = [5]; // px
 const LINE_GRAPH_HELPER_LINES_WIDTH = 1; // px
 const LINE_GRAPH_MAXIMUM_LINE_COLOR = "rgba(255,255,255,0.4)";
 const LINE_GRAPH_AVERAGE_LINE_COLOR = "rgba(255,255,255,0.7)";
 const LINE_GRAPH_MINIMUM_LINE_COLOR = "rgba(255,255,255,0.9)";
 const LINE_GRAPH_BACKGROUND_GRADIENT_START = "rgba(255,255,255,0.25)";
@@ -482,26 +484,28 @@ AbstractCanvasGraph.prototype = {
     this.emit("deselecting");
   },
 
   /**
    * Gets whether or not this graph has a selection.
    * @return boolean
    */
   hasSelection: function() {
-    return this._selection.start != null && this._selection.end != null;
+    return this._selection &&
+      this._selection.start != null && this._selection.end != null;
   },
 
   /**
    * Gets whether or not a selection is currently being made, for example
    * via a click+drag operation.
    * @return boolean
    */
   hasSelectionInProgress: function() {
-    return this._selection.start != null && this._selection.end == null;
+    return this._selection &&
+      this._selection.start != null && this._selection.end == null;
   },
 
   /**
    * Specifies whether or not mouse selection is allowed.
    * @type boolean
    */
   selectionEnabled: true,
 
@@ -547,17 +551,17 @@ AbstractCanvasGraph.prototype = {
     this._shouldRedraw = true;
   },
 
   /**
    * Gets whether or not this graph has a visible cursor.
    * @return boolean
    */
   hasCursor: function() {
-    return this._cursor.x != null;
+    return this._cursor && this._cursor.x != null;
   },
 
   /**
    * Specifies if this graph's selection is different from another one.
    *
    * @param object other
    *        The other graph's selection, as { start, end } values.
    */
@@ -1171,76 +1175,104 @@ this.LineGraphWidget = function(parent, 
     this._minGutterLine = this._createGutterLine("minimum");
     this._maxTooltip = this._createTooltip("maximum", "start", "max", metric);
     this._avgTooltip = this._createTooltip("average", "end", "avg", metric);
     this._minTooltip = this._createTooltip("minimum", "start", "min", metric);
   });
 }
 
 LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+  backgroundColor: LINE_GRAPH_BACKGROUND_COLOR,
+  backgroundGradientStart: LINE_GRAPH_BACKGROUND_GRADIENT_START,
+  backgroundGradientEnd: LINE_GRAPH_BACKGROUND_GRADIENT_END,
+  strokeColor: LINE_GRAPH_STROKE_COLOR,
+  strokeWidth: LINE_GRAPH_STROKE_WIDTH,
+  maximumLineColor: LINE_GRAPH_MAXIMUM_LINE_COLOR,
+  averageLineColor: LINE_GRAPH_AVERAGE_LINE_COLOR,
+  minimumLineColor: LINE_GRAPH_MINIMUM_LINE_COLOR,
   clipheadLineColor: LINE_GRAPH_CLIPHEAD_LINE_COLOR,
   selectionLineColor: LINE_GRAPH_SELECTION_LINE_COLOR,
   selectionBackgroundColor: LINE_GRAPH_SELECTION_BACKGROUND_COLOR,
   selectionStripesColor: LINE_GRAPH_SELECTION_STRIPES_COLOR,
   regionBackgroundColor: LINE_GRAPH_REGION_BACKGROUND_COLOR,
   regionStripesColor: LINE_GRAPH_REGION_STRIPES_COLOR,
 
   /**
    * Optionally offsets the `delta` in the data source by this scalar.
    */
   dataOffsetX: 0,
 
   /**
+   * The scalar used to multiply the graph values to leave some headroom
+   * on the top.
+   */
+  dampenValuesFactor: LINE_GRAPH_DAMPEN_VALUES,
+
+  /**
    * Points that are too close too each other in the graph will not be rendered.
    * This scalar specifies the required minimum squared distance between points.
    */
   minDistanceBetweenPoints: LINE_GRAPH_MIN_SQUARED_DISTANCE_BETWEEN_POINTS,
 
   /**
+   * Specifies if min/max/avg tooltips have arrow handlers on their sides.
+   */
+  withTooltipArrows: true,
+
+  /**
+   * Specifies if min/max/avg tooltips are positioned based on the actual
+   * values, or just placed next to the graph corners.
+   */
+  withFixedTooltipPositions: false,
+
+  /**
    * Renders the graph's data source.
    * @see AbstractCanvasGraph.prototype.buildGraphImage
    */
   buildGraphImage: function() {
     let { canvas, ctx } = this._getNamedCanvas("line-graph-data");
     let width = this._width;
     let height = this._height;
 
     let totalTicks = this._data.length;
-    let firstTick = this._data[0].delta;
-    let lastTick = this._data[totalTicks - 1].delta;
+    let firstTick = totalTicks ? this._data[0].delta : 0;
+    let lastTick = totalTicks ? this._data[totalTicks - 1].delta : 0;
     let maxValue = Number.MIN_SAFE_INTEGER;
     let minValue = Number.MAX_SAFE_INTEGER;
     let sumValues = 0;
 
     for (let { delta, value } of this._data) {
       maxValue = Math.max(value, maxValue);
       minValue = Math.min(value, minValue);
       sumValues += value;
     }
 
     let dataScaleX = this.dataScaleX = width / (lastTick - this.dataOffsetX);
-    let dataScaleY = this.dataScaleY = height / maxValue * LINE_GRAPH_DAMPEN_VALUES;
+    let dataScaleY = this.dataScaleY = height / maxValue * this.dampenValuesFactor;
 
     /**
      * Calculates the squared distance between two 2D points.
      */
     function distSquared(x0, y0, x1, y1) {
       let xs = x1 - x0;
       let ys = y1 - y0;
       return xs * xs + ys * ys;
     }
 
     // Draw the graph.
 
+    ctx.fillStyle = this.backgroundColor;
+    ctx.fillRect(0, 0, width, height);
+
     let gradient = ctx.createLinearGradient(0, height / 2, 0, height);
-    gradient.addColorStop(0, LINE_GRAPH_BACKGROUND_GRADIENT_START);
-    gradient.addColorStop(1, LINE_GRAPH_BACKGROUND_GRADIENT_END);
+    gradient.addColorStop(0, this.backgroundGradientStart);
+    gradient.addColorStop(1, this.backgroundGradientEnd);
     ctx.fillStyle = gradient;
-    ctx.strokeStyle = LINE_GRAPH_STROKE_COLOR;
-    ctx.lineWidth = LINE_GRAPH_STROKE_WIDTH * this._pixelRatio;
+    ctx.strokeStyle = this.strokeColor;
+    ctx.lineWidth = this.strokeWidth * this._pixelRatio;
     ctx.beginPath();
 
     let prevX = 0;
     let prevY = 0;
 
     for (let { delta, value } of this._data) {
       let currX = (delta - this.dataOffsetX) * dataScaleX;
       let currY = height - value * dataScaleY;
@@ -1263,77 +1295,93 @@ LineGraphWidget.prototype = Heritage.ext
       }
     }
 
     ctx.fill();
     ctx.stroke();
 
     // Draw the maximum value horizontal line.
 
-    ctx.strokeStyle = LINE_GRAPH_MAXIMUM_LINE_COLOR;
+    ctx.strokeStyle = this.maximumLineColor;
     ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
     ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
     ctx.beginPath();
-    let maximumY = height - maxValue * dataScaleY - ctx.lineWidth;
+    let maximumY = height - maxValue * dataScaleY;
     ctx.moveTo(0, maximumY);
     ctx.lineTo(width, maximumY);
     ctx.stroke();
 
     // Draw the average value horizontal line.
 
-    ctx.strokeStyle = LINE_GRAPH_AVERAGE_LINE_COLOR;
+    ctx.strokeStyle = this.averageLineColor;
     ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
     ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
     ctx.beginPath();
-    let avgValue = sumValues / totalTicks;
-    let averageY = height - avgValue * dataScaleY - ctx.lineWidth;
+    let avgValue = totalTicks ? sumValues / totalTicks : 0;
+    let averageY = height - avgValue * dataScaleY;
     ctx.moveTo(0, averageY);
     ctx.lineTo(width, averageY);
     ctx.stroke();
 
     // Draw the minimum value horizontal line.
 
-    ctx.strokeStyle = LINE_GRAPH_MINIMUM_LINE_COLOR;
+    ctx.strokeStyle = this.minimumLineColor;
     ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
     ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
     ctx.beginPath();
-    let minimumY = height - minValue * dataScaleY - ctx.lineWidth;
+    let minimumY = height - minValue * dataScaleY;
     ctx.moveTo(0, minimumY);
     ctx.lineTo(width, minimumY);
     ctx.stroke();
 
     // Update the tooltips text and gutter lines.
 
-    this._maxTooltip.querySelector("[text=value]").textContent = maxValue|0;
-    this._avgTooltip.querySelector("[text=value]").textContent = avgValue|0;
-    this._minTooltip.querySelector("[text=value]").textContent = minValue|0;
+    this._maxTooltip.querySelector("[text=value]").textContent =
+      L10N.numberWithDecimals(maxValue, 2);
+    this._avgTooltip.querySelector("[text=value]").textContent =
+      L10N.numberWithDecimals(avgValue, 2);
+    this._minTooltip.querySelector("[text=value]").textContent =
+      L10N.numberWithDecimals(minValue, 2);
 
     /**
      * Constrains a value to a range.
      */
     function clamp(value, min, max) {
       if (value < min) return min;
       if (value > max) return max;
       return value;
     }
 
     let bottom = height / this._pixelRatio;
-    let maxPosY = map(maxValue * LINE_GRAPH_DAMPEN_VALUES, 0, maxValue, bottom, 0);
-    let avgPosY = map(avgValue * LINE_GRAPH_DAMPEN_VALUES, 0, maxValue, bottom, 0);
-    let minPosY = map(minValue * LINE_GRAPH_DAMPEN_VALUES, 0, maxValue, bottom, 0);
+    let maxPosY = map(maxValue * this.dampenValuesFactor, 0, maxValue, bottom, 0);
+    let avgPosY = map(avgValue * this.dampenValuesFactor, 0, maxValue, bottom, 0);
+    let minPosY = map(minValue * this.dampenValuesFactor, 0, maxValue, bottom, 0);
 
     let safeTop = LINE_GRAPH_TOOLTIP_SAFE_BOUNDS;
     let safeBottom = bottom - LINE_GRAPH_TOOLTIP_SAFE_BOUNDS;
 
-    this._maxTooltip.style.top = clamp(maxPosY, safeTop, safeBottom) + "px";
-    this._avgTooltip.style.top = clamp(avgPosY, safeTop, safeBottom) + "px";
-    this._minTooltip.style.top = clamp(minPosY, safeTop, safeBottom) + "px";
-    this._maxGutterLine.style.top = clamp(maxPosY, safeTop, safeBottom) + "px";
-    this._avgGutterLine.style.top = clamp(avgPosY, safeTop, safeBottom) + "px";
-    this._minGutterLine.style.top = clamp(minPosY, safeTop, safeBottom) + "px";
+    this._maxTooltip.style.top = (this.withFixedTooltipPositions
+      ? safeTop : clamp(maxPosY, safeTop, safeBottom)) + "px";
+    this._avgTooltip.style.top = (this.withFixedTooltipPositions
+      ? safeTop : clamp(avgPosY, safeTop, safeBottom)) + "px";
+    this._minTooltip.style.top = (this.withFixedTooltipPositions
+      ? safeBottom : clamp(minPosY, safeTop, safeBottom)) + "px";
+
+    this._maxGutterLine.style.top = maxPosY + "px";
+    this._avgGutterLine.style.top = avgPosY + "px";
+    this._minGutterLine.style.top = minPosY + "px";
+
+    this._maxTooltip.setAttribute("with-arrows", this.withTooltipArrows);
+    this._avgTooltip.setAttribute("with-arrows", this.withTooltipArrows);
+    this._minTooltip.setAttribute("with-arrows", this.withTooltipArrows);
+
+    this._gutter.hidden = !this.withTooltipArrows;
+    this._maxTooltip.hidden = !totalTicks;
+    this._avgTooltip.hidden = !totalTicks;
+    this._minTooltip.hidden = !totalTicks;
 
     return canvas;
   },
 
   /**
    * Creates the gutter node when constructing this graph.
    * @return nsIDOMNode
    */
@@ -1462,16 +1510,22 @@ BarGraphWidget.prototype = Heritage.exte
   format: null,
 
   /**
    * Optionally offsets the `delta` in the data source by this scalar.
    */
   dataOffsetX: 0,
 
   /**
+   * The scalar used to multiply the graph values to leave some headroom
+   * on the top.
+   */
+  dampenValuesFactor: BAR_GRAPH_DAMPEN_VALUES,
+
+  /**
    * Bars that are too close too each other in the graph will be combined.
    * This scalar specifies the required minimum width of each bar.
    */
   minBarsWidth: BAR_GRAPH_MIN_BARS_WIDTH,
 
   /**
    * Blocks in a bar that are too thin inside the bar will not be rendered.
    * This scalar specifies the required minimum height of each block.
@@ -1515,17 +1569,17 @@ BarGraphWidget.prototype = Heritage.exte
     let minBarsWidth = this.minBarsWidth * this._pixelRatio;
     let minBlocksHeight = this.minBlocksHeight * this._pixelRatio;
 
     let dataScaleX = this.dataScaleX = width / (lastTick - this.dataOffsetX);
     let dataScaleY = this.dataScaleY = height / this._calcMaxHeight({
       data: this._data,
       dataScaleX: dataScaleX,
       minBarsWidth: minBarsWidth
-    }) * BAR_GRAPH_DAMPEN_VALUES;
+    }) * this.dampenValuesFactor;
 
     // Draw the graph.
 
     // Iterate over the blocks, then the bars, to draw all rectangles of
     // the same color in a single pass. See the @constructor for more
     // information about the data source, and how a "bar" contains "blocks".
 
     this._blocksBoundingRects = [];
@@ -1918,16 +1972,23 @@ this.CanvasGraphUtils = {
 
   /**
    * Makes sure selections in one graph are reflected in another.
    */
   linkSelection: function(graph1, graph2) {
     if (!graph1 || !graph2) {
       return;
     }
+
+    if (graph1.hasSelection()) {
+      graph2.setSelection(graph1.getSelection());
+    } else {
+      graph2.dropSelection();
+    }
+
     graph1.on("selecting", () => {
       graph2.setSelection(graph1.getSelection());
     });
     graph2.on("selecting", () => {
       graph1.setSelection(graph2.getSelection());
     });
     graph1.on("deselecting", () => {
       graph2.dropSelection();
--- a/browser/devtools/timeline/moz.build
+++ b/browser/devtools/timeline/moz.build
@@ -1,13 +1,14 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 EXTRA_JS_MODULES.devtools.timeline += [
     'panel.js',
     'widgets/global.js',
-    'widgets/overview.js',
+    'widgets/markers-overview.js',
+    'widgets/memory-overview.js',
     'widgets/waterfall.js'
 ]
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/browser/devtools/timeline/test/browser.ini
+++ b/browser/devtools/timeline/test/browser.ini
@@ -5,12 +5,13 @@ support-files =
   head.js
 
 [browser_timeline_aaa_run_first_leaktest.js]
 [browser_timeline_blueprint.js]
 [browser_timeline_overview-initial-selection-01.js]
 [browser_timeline_overview-initial-selection-02.js]
 [browser_timeline_overview-update.js]
 [browser_timeline_panels.js]
+[browser_timeline_recording-without-memory.js]
 [browser_timeline_recording.js]
 [browser_timeline_waterfall-background.js]
 [browser_timeline_waterfall-generic.js]
 [browser_timeline_waterfall-styles.js]
--- a/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js
+++ b/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js
@@ -3,39 +3,45 @@
 
 /**
  * Tests if the overview has an initial selection when recording has finished
  * and there is data available.
  */
 
 let test = Task.async(function*() {
   let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
+  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
   let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin;
 
+  $("#memory-checkbox").checked = true;
+  yield TimelineController.updateMemoryRecording();
+
   yield TimelineController.toggleRecording();
   ok(true, "Recording has started.");
 
   let updated = 0;
   panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
 
   ok((yield waitUntil(() => updated > 10)),
     "The overview graph was updated a bunch of times.");
   ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
     "There are some markers available.");
+  ok((yield waitUntil(() => TimelineController.getMemory().length > 0)),
+    "There are some memory measurements available now.");
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has ended.");
 
+  let interval = TimelineController.getInterval();
   let markers = TimelineController.getMarkers();
-  let selection = TimelineView.overview.getSelection();
+  let selection = TimelineView.markersOverview.getSelection();
 
   is((selection.start) | 0,
-     ((markers[0].start - markers.startTime) * TimelineView.overview.dataScaleX) | 0,
+     ((markers[0].start - interval.startTime) * TimelineView.markersOverview.dataScaleX) | 0,
     "The initial selection start is correct.");
 
   is((selection.end - selection.start) | 0,
-     (selectionRatio * TimelineView.overview.width) | 0,
+     (selectionRatio * TimelineView.markersOverview.width) | 0,
     "The initial selection end is correct.");
 
   yield teardown(panel);
   finish();
 });
--- a/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js
+++ b/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js
@@ -3,30 +3,36 @@
 
 /**
  * Tests if the overview has no initial selection when recording has finished
  * and there is no data available.
  */
 
 let test = Task.async(function*() {
   let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
+  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
   let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin;
 
+  $("#memory-checkbox").checked = true;
+  yield TimelineController.updateMemoryRecording();
+
   yield TimelineController.toggleRecording();
   ok(true, "Recording has started.");
 
   yield TimelineController._stopRecordingAndDiscardData();
   ok(true, "Recording has ended.");
 
   let markers = TimelineController.getMarkers();
-  let selection = TimelineView.overview.getSelection();
+  let memory = TimelineController.getMemory();
+  let selection = TimelineView.markersOverview.getSelection();
 
   is(markers.length, 0,
     "There are no markers available.");
+  is(memory.length, 0,
+    "There are no memory measurements available.");
   is(selection.start, null,
     "The initial selection start is correct.");
   is(selection.end, null,
     "The initial selection end is correct.");
 
   yield teardown(panel);
   finish();
 });
--- a/browser/devtools/timeline/test/browser_timeline_overview-update.js
+++ b/browser/devtools/timeline/test/browser_timeline_overview-update.js
@@ -1,48 +1,74 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Tests if the overview graph is continuously updated.
+ * Tests if the markers and memory overviews are continuously updated.
  */
 
 let test = Task.async(function*() {
   let { target, panel } = yield initTimelinePanel("about:blank");
-  let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
+  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
 
-  yield DevToolsUtils.waitForTime(1000);
+  $("#memory-checkbox").checked = true;
+  yield TimelineController.updateMemoryRecording();
+
   yield TimelineController.toggleRecording();
   ok(true, "Recording has started.");
 
-  ok("selectionEnabled" in TimelineView.overview,
-    "The selection should not be enabled for the overview graph (1).");
-  is(TimelineView.overview.selectionEnabled, false,
-    "The selection should not be enabled for the overview graph (2).");
-  is(TimelineView.overview.hasSelection(), false,
-    "The overview graph shouldn't have a selection before recording.");
+  ok("selectionEnabled" in TimelineView.markersOverview,
+    "The selection should not be enabled for the markers overview (1).");
+  is(TimelineView.markersOverview.selectionEnabled, false,
+    "The selection should not be enabled for the markers overview (2).");
+  is(TimelineView.markersOverview.hasSelection(), false,
+    "The markers overview shouldn't have a selection before recording.");
+
+  ok("selectionEnabled" in TimelineView.memoryOverview,
+    "The selection should not be enabled for the memory overview (1).");
+  is(TimelineView.memoryOverview.selectionEnabled, false,
+    "The selection should not be enabled for the memory overview (2).");
+  is(TimelineView.memoryOverview.hasSelection(), false,
+    "The memory overview shouldn't have a selection before recording.");
 
   let updated = 0;
   panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
 
   ok((yield waitUntil(() => updated > 10)),
-    "The overview graph was updated a bunch of times.");
+    "The overviews were updated a bunch of times.");
+  ok((yield waitUntil(() => TimelineController.getMemory().length > 10)),
+    "There are some memory measurements available now.");
 
-  ok("selectionEnabled" in TimelineView.overview,
-    "The selection should still not be enabled for the overview graph (3).");
-  is(TimelineView.overview.selectionEnabled, false,
-    "The selection should still not be enabled for the overview graph (4).");
-  is(TimelineView.overview.hasSelection(), false,
-    "The overview graph should not have a selection while recording.");
+  ok("selectionEnabled" in TimelineView.markersOverview,
+    "The selection should still not be enabled for the markers overview (3).");
+  is(TimelineView.markersOverview.selectionEnabled, false,
+    "The selection should still not be enabled for the markers overview (4).");
+  is(TimelineView.markersOverview.hasSelection(), false,
+    "The markers overview should not have a selection while recording.");
+
+  ok("selectionEnabled" in TimelineView.memoryOverview,
+    "The selection should still not be enabled for the memory overview (3).");
+  is(TimelineView.memoryOverview.selectionEnabled, false,
+    "The selection should still not be enabled for the memory overview (4).");
+  is(TimelineView.memoryOverview.hasSelection(), false,
+    "The memory overview should not have a selection while recording.");
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has ended.");
 
   is(TimelineController.getMarkers().length, 0,
     "There are no markers available.");
-  is(TimelineView.overview.selectionEnabled, true,
-    "The selection should now be enabled for the overview graph.");
-  is(TimelineView.overview.hasSelection(), false,
-    "The overview graph should not have a selection after recording.");
+  isnot(TimelineController.getMemory().length, 0,
+    "There are some memory measurements available.");
+
+  is(TimelineView.markersOverview.selectionEnabled, true,
+    "The selection should now be enabled for the markers overview.");
+  is(TimelineView.markersOverview.hasSelection(), false,
+    "The markers overview should not have a selection after recording.");
+
+  is(TimelineView.memoryOverview.selectionEnabled, true,
+    "The selection should now be enabled for the memory overview.");
+  is(TimelineView.memoryOverview.hasSelection(), false,
+    "The memory overview should not have a selection after recording.");
 
   yield teardown(panel);
   finish();
 });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_recording-without-memory.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the timeline actor isn't unnecessarily asked to record memory.
+ */
+
+let test = Task.async(function*() {
+  let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
+  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has started.");
+
+  let updated = 0;
+  panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+
+  ok((yield waitUntil(() => updated > 10)),
+    "The overview graph was updated a bunch of times.");
+  ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+    "There are some markers available.");
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has ended.");
+
+  let markers = TimelineController.getMarkers();
+  let memory = TimelineController.getMemory();
+
+  isnot(markers.length, 0,
+    "There are some markers available.");
+  is(memory.length, 0,
+    "There are no memory measurements available.");
+
+  yield teardown(panel);
+  finish();
+});
--- a/browser/devtools/timeline/test/browser_timeline_recording.js
+++ b/browser/devtools/timeline/test/browser_timeline_recording.js
@@ -2,33 +2,39 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the timeline can properly start and stop a recording.
  */
 
 let test = Task.async(function*() {
   let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { gFront, TimelineController } = panel.panelWin;
+  let { $, gFront, TimelineController } = panel.panelWin;
+
+  $("#memory-checkbox").checked = true;
+  yield TimelineController.updateMemoryRecording();
 
   is((yield gFront.isRecording()), false,
     "The timeline actor should not be recording when the tool starts.");
   is(TimelineController.getMarkers().length, 0,
     "There should be no markers available when the tool starts.");
 
   yield TimelineController.toggleRecording();
 
   is((yield gFront.isRecording()), true,
     "The timeline actor should be recording now.");
   ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
     "There are some markers available now.");
+  ok((yield waitUntil(() => TimelineController.getMemory().length > 0)),
+    "There are some memory measurements available now.");
 
-  ok("startTime" in TimelineController.getMarkers(),
-    "A `startTime` field was set on the markers array.");
-  ok("endTime" in TimelineController.getMarkers(),
-    "An `endTime` field was set on the markers array.");
-  ok(TimelineController.getMarkers().endTime >
-     TimelineController.getMarkers().startTime,
+  ok("startTime" in TimelineController.getInterval(),
+    "A `startTime` field was set on the recording data.");
+  ok("endTime" in TimelineController.getInterval(),
+    "An `endTime` field was set on the recording data.");
+
+  ok(TimelineController.getInterval().endTime >
+     TimelineController.getInterval().startTime,
     "Some time has passed since the recording started.");
 
   yield teardown(panel);
   finish();
 });
--- a/browser/devtools/timeline/test/browser_timeline_waterfall-background.js
+++ b/browser/devtools/timeline/test/browser_timeline_waterfall-background.js
@@ -12,17 +12,17 @@ let test = Task.async(function*() {
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has started.");
 
   let updated = 0;
   panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
 
   ok((yield waitUntil(() => updated > 0)),
-    "The overview graph was updated a bunch of times.");
+    "The overview graphs were updated a bunch of times.");
   ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
     "There are some markers available.");
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has ended.");
 
   // Test the waterfall background.
 
--- a/browser/devtools/timeline/test/browser_timeline_waterfall-generic.js
+++ b/browser/devtools/timeline/test/browser_timeline_waterfall-generic.js
@@ -11,58 +11,58 @@ let test = Task.async(function*() {
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has started.");
 
   let updated = 0;
   panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
 
   ok((yield waitUntil(() => updated > 0)),
-    "The overview graph was updated a bunch of times.");
+    "The overview graphs were updated a bunch of times.");
   ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
     "There are some markers available.");
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has ended.");
 
   // Test the header container.
 
-  ok($(".timeline-header-container"),
+  ok($(".waterfall-header-container"),
     "A header container should have been created.");
 
   // Test the header sidebar (left).
 
-  ok($(".timeline-header-sidebar"),
+  ok($(".waterfall-header-container > .waterfall-sidebar"),
     "A header sidebar node should have been created.");
-  ok($(".timeline-header-sidebar > .timeline-header-name"),
+  ok($(".waterfall-header-container > .waterfall-sidebar > .waterfall-header-name"),
     "A header name label should have been created inside the sidebar.");
 
   // Test the header ticks (right).
 
-  ok($(".timeline-header-ticks"),
+  ok($(".waterfall-header-ticks"),
     "A header ticks node should have been created.");
-  ok($$(".timeline-header-ticks > .timeline-header-tick").length > 0,
+  ok($$(".waterfall-header-ticks > .waterfall-header-tick").length > 0,
     "Some header tick labels should have been created inside the tick node.");
 
   // Test the markers container.
 
-  ok($(".timeline-marker-container"),
+  ok($(".waterfall-marker-container"),
     "A marker container should have been created.");
 
   // Test the markers sidebar (left).
 
-  ok($$(".timeline-marker-sidebar").length,
+  ok($$(".waterfall-marker-container > .waterfall-sidebar").length,
     "Some marker sidebar nodes should have been created.");
-  ok($$(".timeline-marker-sidebar:not(spacer) > .timeline-marker-bullet").length,
+  ok($$(".waterfall-marker-container > .waterfall-sidebar:not(spacer) > .waterfall-marker-bullet").length,
     "Some marker color bullets should have been created inside the sidebar.");
-  ok($$(".timeline-marker-sidebar:not(spacer) > .timeline-marker-name").length,
+  ok($$(".waterfall-marker-container > .waterfall-sidebar:not(spacer) > .waterfall-marker-name").length,
     "Some marker name labels should have been created inside the sidebar.");
 
   // Test the markers waterfall (right).
 
-  ok($$(".timeline-marker-waterfall").length,
+  ok($$(".waterfall-marker-item").length,
     "Some marker waterfall nodes should have been created.");
-  ok($$(".timeline-marker-waterfall:not(spacer) > .timeline-marker-bar").length,
+  ok($$(".waterfall-marker-item:not(spacer) > .waterfall-marker-bar").length,
     "Some marker color bars should have been created inside the waterfall.");
 
   yield teardown(panel);
   finish();
 });
--- a/browser/devtools/timeline/test/browser_timeline_waterfall-styles.js
+++ b/browser/devtools/timeline/test/browser_timeline_waterfall-styles.js
@@ -22,17 +22,17 @@ let test = Task.async(function*() {
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has started.");
 
   let updated = 0;
   panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
 
   ok((yield waitUntil(() => updated > 0)),
-    "The overview graph was updated a bunch of times.");
+    "The overview graphs were updated a bunch of times.");
   ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
     "There are some markers available.");
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has ended.");
 
   // Test the table sidebars.
 
--- a/browser/devtools/timeline/timeline.js
+++ b/browser/devtools/timeline/timeline.js
@@ -7,34 +7,39 @@ const { classes: Cc, interfaces: Ci, uti
 
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 
 devtools.lazyRequireGetter(this, "promise");
 devtools.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 
-devtools.lazyRequireGetter(this, "Overview",
-  "devtools/timeline/overview", true);
+devtools.lazyRequireGetter(this, "MarkersOverview",
+  "devtools/timeline/markers-overview", true);
+devtools.lazyRequireGetter(this, "MemoryOverview",
+  "devtools/timeline/memory-overview", true);
 devtools.lazyRequireGetter(this, "Waterfall",
   "devtools/timeline/waterfall", true);
 
+devtools.lazyImporter(this, "CanvasGraphUtils",
+  "resource:///modules/devtools/Graphs.jsm");
+
 devtools.lazyImporter(this, "PluralForm",
   "resource://gre/modules/PluralForm.jsm");
 
 const OVERVIEW_UPDATE_INTERVAL = 200;
 const OVERVIEW_INITIAL_SELECTION_RATIO = 0.15;
 
 // The panel's window global is an EventEmitter firing the following events:
 const EVENTS = {
   // When a recording is started or stopped, via the `stopwatch` button.
   RECORDING_STARTED: "Timeline:RecordingStarted",
   RECORDING_ENDED: "Timeline:RecordingEnded",
 
-  // When the overview graph is populated with new markers.
+  // When the overview graphs are populated with new markers.
   OVERVIEW_UPDATED: "Timeline:OverviewUpdated",
 
   // When the waterfall view is populated with new markers.
   WATERFALL_UPDATED: "Timeline:WaterfallUpdated"
 };
 
 /**
  * The current target and the timeline front, set by this tool's host.
@@ -58,225 +63,330 @@ let shutdownTimeline = Task.async(functi
   yield gFront.stop();
 });
 
 /**
  * Functions handling the timeline frontend controller.
  */
 let TimelineController = {
   /**
-   * Permanent storage for the markers streamed by the backend.
+   * Permanent storage for the markers and the memory measurements streamed by
+   * the backend, along with the start and end timestamps.
    */
+  _starTime: 0,
+  _endTime: 0,
   _markers: [],
+  _memory: [],
 
   /**
    * Initialization function, called when the tool is started.
    */
   initialize: function() {
     this._onRecordingTick = this._onRecordingTick.bind(this);
     this._onMarkers = this._onMarkers.bind(this);
+    this._onMemory = this._onMemory.bind(this);
     gFront.on("markers", this._onMarkers);
+    gFront.on("memory", this._onMemory);
   },
 
   /**
    * Destruction function, called when the tool is closed.
    */
   destroy: function() {
     gFront.off("markers", this._onMarkers);
+    gFront.off("memory", this._onMemory);
+  },
+
+  /**
+   * Gets the { stat, end } time interval for this recording.
+   * @return object
+   */
+  getInterval: function() {
+    return { startTime: this._startTime, endTime: this._endTime };
   },
 
   /**
    * Gets the accumulated markers in this recording.
-   * @return array.
+   * @return array
    */
   getMarkers: function() {
     return this._markers;
   },
 
   /**
+   * Gets the accumulated memory measurements in this recording.
+   * @return array
+   */
+  getMemory: function() {
+    return this._memory;
+  },
+
+  /**
+   * Updates the views to show or hide the memory recording data.
+   */
+  updateMemoryRecording: Task.async(function*() {
+    if ($("#memory-checkbox").checked) {
+      yield TimelineView.showMemoryOverview();
+    } else {
+      yield TimelineView.hideMemoryOverview();
+    }
+  }),
+
+  /**
    * Starts/stops the timeline recording and streaming.
    */
   toggleRecording: Task.async(function*() {
     let isRecording = yield gFront.isRecording();
     if (isRecording == false) {
       yield this._startRecording();
     } else {
       yield this._stopRecording();
     }
   }),
 
   /**
    * Starts the recording, updating the UI as needed.
    */
   _startRecording: function*() {
     TimelineView.handleRecordingStarted();
-    let startTime = yield gFront.start();
+
+    let withMemory = $("#memory-checkbox").checked;
+    let startTime = yield gFront.start({ withMemory });
+
     // Times must come from the actor in order to be self-consistent.
     // However, we also want to update the view with the elapsed time
     // even when the actor is not generating data.  To do this we get
     // the local time and use it to compute a reasonable elapsed time.
     // See _onRecordingTick.
     this._localStartTime = performance.now();
-
+    this._startTime = startTime;
+    this._endTime = startTime;
     this._markers = [];
-    this._markers.startTime = startTime;
-    this._markers.endTime = startTime;
+    this._memory = [];
     this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
   },
 
   /**
    * Stops the recording, updating the UI as needed.
    */
   _stopRecording: function*() {
     clearInterval(this._updateId);
 
     // Sorting markers is only important when displayed in the waterfall.
     this._markers = this._markers.sort((a,b) => (a.start > b.start));
 
-    TimelineView.handleMarkersUpdate(this._markers);
+    TimelineView.handleRecordingUpdate();
     TimelineView.handleRecordingEnded();
     yield gFront.stop();
   },
 
   /**
    * Used in tests. Stops the recording, discarding the accumulated markers and
    * updating the UI as needed.
    */
   _stopRecordingAndDiscardData: function*() {
     yield this._stopRecording();
     this._markers.length = 0;
+    this._memory.length = 0;
   },
 
   /**
    * Callback handling the "markers" event on the timeline front.
    *
    * @param array markers
    *        A list of new markers collected since the last time this
    *        function was invoked.
    * @param number endTime
    *        A time after the last marker in markers was collected.
    */
   _onMarkers: function(markers, endTime) {
     Array.prototype.push.apply(this._markers, markers);
-    this._markers.endTime = endTime;
+    this._endTime = endTime;
+  },
+
+  /**
+   * Callback handling the "memory" event on the timeline front.
+   *
+   * @param number delta
+   *        The number of milliseconds elapsed since epoch.
+   * @param object measurement
+   *        A detailed breakdown of the current memory usage.
+   */
+  _onMemory: function(delta, measurement) {
+    this._memory.push({ delta, value: measurement.total / 1024 / 1024 });
   },
 
   /**
    * Callback invoked at a fixed interval while recording.
-   * Updates the markers store with the current time and the timeline overview.
+   * Updates the current time and the timeline overview.
    */
   _onRecordingTick: function() {
     // Compute an approximate ending time for the view.  This is
     // needed to ensure that the view updates even when new data is
     // not being generated.
-    let fakeTime = this._markers.startTime + (performance.now() - this._localStartTime);
-    if (fakeTime > this._markers.endTime) {
-      this._markers.endTime = fakeTime;
+    let fakeTime = this._startTime + (performance.now() - this._localStartTime);
+    if (fakeTime > this._endTime) {
+      this._endTime = fakeTime;
     }
-    TimelineView.handleMarkersUpdate(this._markers);
+    TimelineView.handleRecordingUpdate();
   }
 };
 
 /**
  * Functions handling the timeline frontend view.
  */
 let TimelineView = {
   /**
    * Initialization function, called when the tool is started.
    */
   initialize: Task.async(function*() {
-    this.overview = new Overview($("#timeline-overview"));
+    this.markersOverview = new MarkersOverview($("#markers-overview"));
     this.waterfall = new Waterfall($("#timeline-waterfall"));
 
     this._onSelecting = this._onSelecting.bind(this);
     this._onRefresh = this._onRefresh.bind(this);
-    this.overview.on("selecting", this._onSelecting);
-    this.overview.on("refresh", this._onRefresh);
+    this.markersOverview.on("selecting", this._onSelecting);
+    this.markersOverview.on("refresh", this._onRefresh);
 
-    yield this.overview.ready();
+    yield this.markersOverview.ready();
     yield this.waterfall.recalculateBounds();
   }),
 
   /**
    * Destruction function, called when the tool is closed.
    */
   destroy: function() {
-    this.overview.off("selecting", this._onSelecting);
-    this.overview.off("refresh", this._onRefresh);
-    this.overview.destroy();
+    this.markersOverview.off("selecting", this._onSelecting);
+    this.markersOverview.off("refresh", this._onRefresh);
+    this.markersOverview.destroy();
+
+    // The memory overview graph is not always available.
+    if (this.memoryOverview) {
+      this.memoryOverview.destroy();
+    }
+  },
+
+  /**
+   * Shows the memory overview graph.
+   */
+  showMemoryOverview: Task.async(function*() {
+    this.memoryOverview = new MemoryOverview($("#memory-overview"));
+    yield this.memoryOverview.ready();
+
+    let interval = TimelineController.getInterval();
+    let memory = TimelineController.getMemory();
+    this.memoryOverview.setData({ interval, memory });
+
+    CanvasGraphUtils.linkAnimation(this.markersOverview, this.memoryOverview);
+    CanvasGraphUtils.linkSelection(this.markersOverview, this.memoryOverview);
+  }),
+
+  /**
+   * Hides the memory overview graph.
+   */
+  hideMemoryOverview: function() {
+    if (!this.memoryOverview) {
+      return;
+    }
+    this.memoryOverview.destroy();
+    this.memoryOverview = null;
   },
 
   /**
    * Signals that a recording session has started and triggers the appropriate
    * changes in the UI.
    */
   handleRecordingStarted: function() {
     $("#record-button").setAttribute("checked", "true");
+    $("#memory-checkbox").setAttribute("disabled", "true");
     $("#timeline-pane").selectedPanel = $("#recording-notice");
 
-    this.overview.selectionEnabled = false;
-    this.overview.dropSelection();
-    this.overview.setData([]);
+    this.markersOverview.clearView();
+
+    // The memory overview graph is not always available.
+    if (this.memoryOverview) {
+      this.memoryOverview.clearView();
+    }
+
     this.waterfall.clearView();
 
     window.emit(EVENTS.RECORDING_STARTED);
   },
 
   /**
    * Signals that a recording session has ended and triggers the appropriate
    * changes in the UI.
    */
   handleRecordingEnded: function() {
     $("#record-button").removeAttribute("checked");
+    $("#memory-checkbox").removeAttribute("disabled");
     $("#timeline-pane").selectedPanel = $("#timeline-waterfall");
 
-    this.overview.selectionEnabled = true;
+    this.markersOverview.selectionEnabled = true;
 
+    // The memory overview graph is not always available.
+    if (this.memoryOverview) {
+      this.memoryOverview.selectionEnabled = true;
+    }
+
+    let interval = TimelineController.getInterval();
     let markers = TimelineController.getMarkers();
+    let memory = TimelineController.getMemory();
+
     if (markers.length) {
-      let start = (markers[0].start - markers.startTime) * this.overview.dataScaleX;
-      let end = start + this.overview.width * OVERVIEW_INITIAL_SELECTION_RATIO;
-      this.overview.setSelection({ start, end });
+      let start = (markers[0].start - interval.startTime) * this.markersOverview.dataScaleX;
+      let end = start + this.markersOverview.width * OVERVIEW_INITIAL_SELECTION_RATIO;
+      this.markersOverview.setSelection({ start, end });
     } else {
-      let duration = markers.endTime - markers.startTime;
-      this.waterfall.setData(markers, markers.startTime, markers.endTime);
+      let timeStart = interval.startTime;
+      let timeEnd = interval.endTime;
+      this.waterfall.setData(markers, timeStart, timeStart, timeEnd);
     }
 
     window.emit(EVENTS.RECORDING_ENDED);
   },
 
   /**
    * Signals that a new set of markers was made available by the controller,
    * or that the overview graph needs to be updated.
-   *
-   * @param array markers
-   *        A list of new markers collected since the recording has started.
    */
-  handleMarkersUpdate: function(markers) {
-    this.overview.setData(markers);
+  handleRecordingUpdate: function() {
+    let interval = TimelineController.getInterval();
+    let markers = TimelineController.getMarkers();
+    let memory = TimelineController.getMemory();
+
+    this.markersOverview.setData({ interval, markers });
+
+    // The memory overview graph is not always available.
+    if (this.memoryOverview) {
+      this.memoryOverview.setData({ interval, memory });
+    }
+
     window.emit(EVENTS.OVERVIEW_UPDATED);
   },
 
   /**
    * Callback handling the "selecting" event on the timeline overview.
    */
   _onSelecting: function() {
-    if (!this.overview.hasSelection() &&
-        !this.overview.hasSelectionInProgress()) {
+    if (!this.markersOverview.hasSelection() &&
+        !this.markersOverview.hasSelectionInProgress()) {
       this.waterfall.clearView();
       return;
     }
-    let selection = this.overview.getSelection();
-    let start = selection.start / this.overview.dataScaleX;
-    let end = selection.end / this.overview.dataScaleX;
+    let selection = this.markersOverview.getSelection();
+    let start = selection.start / this.markersOverview.dataScaleX;
+    let end = selection.end / this.markersOverview.dataScaleX;
 
     let markers = TimelineController.getMarkers();
-    let timeStart = markers.startTime + Math.min(start, end);
-    let timeEnd = markers.startTime + Math.max(start, end);
-    this.waterfall.setData(markers, timeStart, timeEnd);
+    let interval = TimelineController.getInterval();
+
+    let timeStart = interval.startTime + Math.min(start, end);
+    let timeEnd = interval.startTime + Math.max(start, end);
+    this.waterfall.setData(markers, interval.startTime, timeStart, timeEnd);
   },
 
   /**
    * Callback handling the "refresh" event on the timeline overview.
    */
   _onRefresh: function() {
     this.waterfall.recalculateBounds();
     this._onSelecting();
--- a/browser/devtools/timeline/timeline.xul
+++ b/browser/devtools/timeline/timeline.xul
@@ -20,23 +20,27 @@
              class="devtools-toolbar">
       <hbox id="recordings-controls"
             class="devtools-toolbarbutton-group"
             align="center">
         <toolbarbutton id="record-button"
                        class="devtools-toolbarbutton"
                        oncommand="TimelineController.toggleRecording()"
                        tooltiptext="&timelineUI.recordButton.tooltip;"/>
-        <spacer flex="1"/>
+        <checkbox id="memory-checkbox"
+                  label="&timelineUI.memoryCheckbox.label;"
+                  oncommand="TimelineController.updateMemoryRecording()"
+                  tooltiptext="&timelineUI.memoryCheckbox.tooltip;"/>
         <label id="record-label"
                value="&timelineUI.recordLabel;"/>
       </hbox>
     </toolbar>
 
-    <vbox id="timeline-overview"/>
+    <vbox id="markers-overview"/>
+    <vbox id="memory-overview"/>
 
     <deck id="timeline-pane"
           flex="1">
       <hbox id="empty-notice"
             class="notice-container"
             align="center"
             pack="center"
             flex="1">
rename from browser/devtools/timeline/widgets/overview.js
rename to browser/devtools/timeline/widgets/markers-overview.js
--- a/browser/devtools/timeline/widgets/overview.js
+++ b/browser/devtools/timeline/widgets/markers-overview.js
@@ -1,73 +1,73 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 /**
- * This file contains the "overview" graph, which is a minimap of all the
- * timeline data. Regions inside it may be selected, determining which markers
- * are visible in the "waterfall".
+ * This file contains the "markers overview" graph, which is a minimap of all
+ * the timeline data. Regions inside it may be selected, determining which
+ * markers are visible in the "waterfall".
  */
 
 const {Cc, Ci, Cu, Cr} = require("chrome");
 
 Cu.import("resource:///modules/devtools/Graphs.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 loader.lazyRequireGetter(this, "L10N",
   "devtools/timeline/global", true);
 loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
   "devtools/timeline/global", true);
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
-const OVERVIEW_HEADER_HEIGHT = 20; // px
-const OVERVIEW_BODY_HEIGHT = 50; // px
+const OVERVIEW_HEADER_HEIGHT = 14; // px
+const OVERVIEW_BODY_HEIGHT = 55; // 11px * 5 groups
 
 const OVERVIEW_BACKGROUND_COLOR = "#fff";
 const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
 const OVERVIEW_SELECTION_LINE_COLOR = "#555";
 const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)";
 const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
 
 const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms
 const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px
 const OVERVIEW_HEADER_SAFE_BOUNDS = 50; // px
-const OVERVIEW_HEADER_BACKGROUND = "#ebeced";
+const OVERVIEW_HEADER_BACKGROUND = "#fff";
 const OVERVIEW_HEADER_TEXT_COLOR = "#18191a";
 const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
 const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
-const OVERVIEW_HEADER_TEXT_PADDING = 6; // px
-const OVERVIEW_TIMELINE_STROKES = "#aaa";
+const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6; // px
+const OVERVIEW_HEADER_TEXT_PADDING_TOP = 1; // px
+const OVERVIEW_TIMELINE_STROKES = "#ccc";
 const OVERVIEW_MARKERS_COLOR_STOPS = [0, 0.1, 0.75, 1];
 const OVERVIEW_MARKER_DURATION_MIN = 4; // ms
-const OVERVIEW_GROUP_VERTICAL_PADDING = 6; // px
+const OVERVIEW_GROUP_VERTICAL_PADDING = 5; // px
 const OVERVIEW_GROUP_ALTERNATING_BACKGROUND = "rgba(0,0,0,0.05)";
 
 /**
- * An overview for the timeline data.
+ * An overview for the markers data.
  *
  * @param nsIDOMNode parent
  *        The parent node holding the overview.
  */
-function Overview(parent, ...args) {
-  AbstractCanvasGraph.apply(this, [parent, "timeline-overview", ...args]);
+function MarkersOverview(parent, ...args) {
+  AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
   this.once("ready", () => {
+    // Set the list of names, properties and colors used to paint this overview.
     this.setBlueprint(TIMELINE_BLUEPRINT);
 
-    var preview = [];
-    preview.startTime = 0;
-    preview.endTime = 1000;
-    this.setData(preview);
+    // Populate this overview with some dummy initial data.
+    this.setData({ interval: { startTime: 0, endTime: 1000 }, markers: [] });
   });
 }
 
-Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
   fixedHeight: OVERVIEW_HEADER_HEIGHT + OVERVIEW_BODY_HEIGHT,
   clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
   selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
   selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR,
   selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR,
 
   /**
    * List of names and colors used to paint this overview.
@@ -79,83 +79,97 @@ Overview.prototype = Heritage.extend(Abs
 
     for (let type in blueprint) {
       this._paintBatches.set(type, { style: blueprint[type], batch: [] });
       this._lastGroup = Math.max(this._lastGroup, blueprint[type].group);
     }
   },
 
   /**
+   * Disables selection and empties this graph.
+   */
+  clearView: function() {
+    this.selectionEnabled = false;
+    this.dropSelection();
+    this.setData({ interval: { startTime: 0, endTime: 0 }, markers: [] });
+  },
+
+  /**
    * Renders the graph's data source.
    * @see AbstractCanvasGraph.prototype.buildGraphImage
    */
   buildGraphImage: function() {
+    let { interval, markers } = this._data;
+    let { startTime, endTime } = interval;
+
     let { canvas, ctx } = this._getNamedCanvas("overview-data");
     let canvasWidth = this._width;
     let canvasHeight = this._height;
     let safeBounds = OVERVIEW_HEADER_SAFE_BOUNDS * this._pixelRatio;
     let availableWidth = canvasWidth - safeBounds;
 
     // Group markers into separate paint batches. This is necessary to
     // draw all markers sharing the same style at once.
 
-    for (let marker of this._data) {
+    for (let marker of markers) {
       this._paintBatches.get(marker.name).batch.push(marker);
     }
 
     // Calculate each group's height, and the time-based scaling.
 
     let totalGroups = this._lastGroup + 1;
     let headerHeight = OVERVIEW_HEADER_HEIGHT * this._pixelRatio;
     let groupHeight = OVERVIEW_BODY_HEIGHT * this._pixelRatio / totalGroups;
     let groupPadding = OVERVIEW_GROUP_VERTICAL_PADDING * this._pixelRatio;
 
-    let totalTime = (this._data.endTime - this._data.startTime) || 0;
+    let totalTime = (endTime - startTime) || 0;
     let dataScale = this.dataScaleX = availableWidth / totalTime;
 
     // Draw the header and overview background.
 
     ctx.fillStyle = OVERVIEW_HEADER_BACKGROUND;
     ctx.fillRect(0, 0, canvasWidth, headerHeight);
 
     ctx.fillStyle = OVERVIEW_BACKGROUND_COLOR;
     ctx.fillRect(0, headerHeight, canvasWidth, canvasHeight);
 
     // Draw the alternating odd/even group backgrounds.
 
     ctx.fillStyle = OVERVIEW_GROUP_ALTERNATING_BACKGROUND;
     ctx.beginPath();
 
-    for (let i = 1; i < totalGroups; i += 2) {
+    for (let i = 0; i < totalGroups; i += 2) {
       let top = headerHeight + i * groupHeight;
       ctx.rect(0, top, canvasWidth, groupHeight);
     }
 
     ctx.fill();
 
     // Draw the timeline header ticks.
 
-    ctx.textBaseline = "middle";
     let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
     let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
+    let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
+    let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
+    let tickInterval = this._findOptimalTickInterval(dataScale);
+
+    ctx.textBaseline = "middle";
     ctx.font = fontSize + "px " + fontFamily;
     ctx.fillStyle = OVERVIEW_HEADER_TEXT_COLOR;
     ctx.strokeStyle = OVERVIEW_TIMELINE_STROKES;
     ctx.beginPath();
 
-    let tickInterval = this._findOptimalTickInterval(dataScale);
-    let headerTextPadding = OVERVIEW_HEADER_TEXT_PADDING * this._pixelRatio;
-
     for (let x = 0; x < availableWidth; x += tickInterval) {
-      let left = x + headerTextPadding;
+      let lineLeft = x;
+      let textLeft = lineLeft + textPaddingLeft;
       let time = Math.round(x / dataScale);
       let label = L10N.getFormatStr("timeline.tick", time);
-      ctx.fillText(label, left, headerHeight / 2 + 1);
-      ctx.moveTo(x, 0);
-      ctx.lineTo(x, canvasHeight);
+      ctx.fillText(label, textLeft, headerHeight / 2 + textPaddingTop);
+      ctx.moveTo(lineLeft, 0);
+      ctx.lineTo(lineLeft, canvasHeight);
     }
 
     ctx.stroke();
 
     // Draw the timeline markers.
 
     for (let [, { style, batch }] of this._paintBatches) {
       let top = headerHeight + style.group * groupHeight + groupPadding / 2;
@@ -165,18 +179,18 @@ Overview.prototype = Heritage.extend(Abs
       gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[0], style.stroke);
       gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[1], style.fill);
       gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[2], style.fill);
       gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[3], style.stroke);
       ctx.fillStyle = gradient;
       ctx.beginPath();
 
       for (let { start, end } of batch) {
-        start -= this._data.startTime;
-        end -= this._data.startTime;
+        start -= interval.startTime;
+        end -= interval.startTime;
 
         let left = start * dataScale;
         let duration = Math.max(end - start, OVERVIEW_MARKER_DURATION_MIN);
         let width = Math.max(duration * dataScale, this._pixelRatio);
         ctx.rect(left, top, width, height);
       }
 
       ctx.fill();
@@ -203,9 +217,9 @@ Overview.prototype = Heritage.extend(Abs
         timingStep <<= 1;
         continue;
       }
       return scaledStep;
     }
   }
 });
 
-exports.Overview = Overview;
+exports.MarkersOverview = MarkersOverview;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/widgets/memory-overview.js
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * This file contains the "memory overview" graph, a simple representation of
+ * of all the memory measurements taken while streaming the timeline data.
+ */
+
+const {Cc, Ci, Cu, Cr} = require("chrome");
+
+Cu.import("resource:///modules/devtools/Graphs.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+loader.lazyRequireGetter(this, "L10N",
+  "devtools/timeline/global", true);
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const OVERVIEW_HEIGHT = 30; // px
+
+const OVERVIEW_BACKGROUND_COLOR = "#fff";
+const OVERVIEW_BACKGROUND_GRADIENT_START = "rgba(0,136,204,0.1)";
+const OVERVIEW_BACKGROUND_GRADIENT_END = "rgba(0,136,204,0.0)";
+const OVERVIEW_STROKE_WIDTH = 1; // px
+const OVERVIEW_STROKE_COLOR = "rgba(0,136,204,1)";
+const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
+const OVERVIEW_SELECTION_LINE_COLOR = "#555";
+const OVERVIEW_MAXIMUM_LINE_COLOR = "rgba(0,136,204,0.4)";
+const OVERVIEW_AVERAGE_LINE_COLOR = "rgba(0,136,204,0.7)";
+const OVERVIEW_MINIMUM_LINE_COLOR = "rgba(0,136,204,0.9)";
+
+const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)";
+const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
+
+/**
+ * An overview for the memory data.
+ *
+ * @param nsIDOMNode parent
+ *        The parent node holding the overview.
+ */
+function MemoryOverview(parent) {
+  LineGraphWidget.call(this, parent, L10N.getStr("graphs.memory"));
+
+  this.once("ready", () => {
+    // Populate this overview with some dummy initial data.
+    this.setData({ interval: { startTime: 0, endTime: 1000 }, memory: [] });
+  });
+}
+
+MemoryOverview.prototype = Heritage.extend(LineGraphWidget.prototype, {
+  dampenValuesFactor: 0.95,
+  fixedHeight: OVERVIEW_HEIGHT,
+  backgroundColor: OVERVIEW_BACKGROUND_COLOR,
+  backgroundGradientStart: OVERVIEW_BACKGROUND_GRADIENT_START,
+  backgroundGradientEnd: OVERVIEW_BACKGROUND_GRADIENT_END,
+  strokeColor: OVERVIEW_STROKE_COLOR,
+  strokeWidth: OVERVIEW_STROKE_WIDTH,
+  maximumLineColor: OVERVIEW_MAXIMUM_LINE_COLOR,
+  averageLineColor: OVERVIEW_AVERAGE_LINE_COLOR,
+  minimumLineColor: OVERVIEW_MINIMUM_LINE_COLOR,
+  clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
+  selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
+  selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR,
+  selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR,
+  withTooltipArrows: false,
+  withFixedTooltipPositions: true,
+
+  /**
+   * Disables selection and empties this graph.
+   */
+  clearView: function() {
+    this.selectionEnabled = false;
+    this.dropSelection();
+    this.setData({ interval: { startTime: 0, endTime: 0 }, memory: [] });
+  },
+
+  /**
+   * Sets the data source for this graph.
+   */
+  setData: function({ interval, memory }) {
+    this.dataOffsetX = interval.startTime;
+    LineGraphWidget.prototype.setData.call(this, memory);
+  }
+});
+
+exports.MemoryOverview = MemoryOverview;
--- a/browser/devtools/timeline/widgets/waterfall.js
+++ b/browser/devtools/timeline/widgets/waterfall.js
@@ -17,51 +17,51 @@ loader.lazyRequireGetter(this, "TIMELINE
 
 loader.lazyImporter(this, "setNamedTimeout",
   "resource:///modules/devtools/ViewHelpers.jsm");
 loader.lazyImporter(this, "clearNamedTimeout",
   "resource:///modules/devtools/ViewHelpers.jsm");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
-const TIMELINE_IMMEDIATE_DRAW_MARKERS_COUNT = 30;
-const TIMELINE_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms
+const WATERFALL_SIDEBAR_WIDTH = 150; // px
 
-const TIMELINE_HEADER_TICKS_MULTIPLE = 5; // ms
-const TIMELINE_HEADER_TICKS_SPACING_MIN = 50; // px
-const TIMELINE_HEADER_TEXT_PADDING = 3; // px
+const WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT = 30;
+const WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms
 
-const TIMELINE_MARKER_SIDEBAR_WIDTH = 150; // px
-const TIMELINE_MARKER_BAR_WIDTH_MIN = 5; // px
+const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
+const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px
+const WATERFALL_HEADER_TEXT_PADDING = 3; // px
 
 const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
 const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
 const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
 const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
 const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
 const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
+const WATERFALL_MARKER_BAR_WIDTH_MIN = 5; // px
 
 /**
  * A detailed waterfall view for the timeline data.
  *
  * @param nsIDOMNode parent
  *        The parent node holding the waterfall.
  */
 function Waterfall(parent) {
   this._parent = parent;
   this._document = parent.ownerDocument;
   this._fragment = this._document.createDocumentFragment();
   this._outstandingMarkers = [];
 
   this._headerContents = this._document.createElement("hbox");
-  this._headerContents.className = "timeline-header-contents";
+  this._headerContents.className = "waterfall-header-contents";
   this._parent.appendChild(this._headerContents);
 
   this._listContents = this._document.createElement("vbox");
-  this._listContents.className = "timeline-list-contents";
+  this._listContents.className = "waterfall-list-contents";
   this._listContents.setAttribute("flex", "1");
   this._parent.appendChild(this._listContents);
 
   this._isRTL = this._getRTL();
 
   // Lazy require is a bit slow, and these are hot objects.
   this._l10n = L10N;
   this._blueprint = TIMELINE_BLUEPRINT;
@@ -70,28 +70,31 @@ function Waterfall(parent) {
 }
 
 Waterfall.prototype = {
   /**
    * Populates this view with the provided data source.
    *
    * @param array markers
    *        A list of markers received from the controller.
+   * @param number timeEpoch
+   *        The absolute time (in milliseconds) when the recording started.
    * @param number timeStart
    *        The time (in milliseconds) to start drawing from.
    * @param number timeEnd
    *        The time (in milliseconds) to end drawing at.
    */
-  setData: function(markers, timeStart, timeEnd) {
+  setData: function(markers, timeEpoch, timeStart, timeEnd) {
     this.clearView();
 
     let dataScale = this._waterfallWidth / (timeEnd - timeStart);
     this._drawWaterfallBackground(dataScale);
+
     // Label the header as if the first possible marker was at T=0.
-    this._buildHeader(this._headerContents, timeStart - markers.startTime, dataScale);
+    this._buildHeader(this._headerContents, timeStart - timeEpoch, dataScale);
     this._buildMarkers(this._listContents, markers, timeStart, timeEnd, dataScale);
   },
 
   /**
    * Depopulates this view.
    */
   clearView: function() {
     while (this._headerContents.hasChildNodes()) {
@@ -106,66 +109,66 @@ Waterfall.prototype = {
   },
 
   /**
    * Calculates and stores the available width for the waterfall.
    * This should be invoked every time the container window is resized.
    */
   recalculateBounds: function() {
     let bounds = this._parent.getBoundingClientRect();
-    this._waterfallWidth = bounds.width - TIMELINE_MARKER_SIDEBAR_WIDTH;
+    this._waterfallWidth = bounds.width - WATERFALL_SIDEBAR_WIDTH;
   },
 
   /**
    * Creates the header part of this view.
    *
    * @param nsIDOMNode parent
    *        The parent node holding the header.
    * @param number timeStart
    *        @see Waterfall.prototype.setData
    * @param number dataScale
    *        The time scale of the data source.
    */
   _buildHeader: function(parent, timeStart, dataScale) {
     let container = this._document.createElement("hbox");
-    container.className = "timeline-header-container";
+    container.className = "waterfall-header-container";
     container.setAttribute("flex", "1");
 
     let sidebar = this._document.createElement("hbox");
-    sidebar.className = "timeline-header-sidebar theme-sidebar";
-    sidebar.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
+    sidebar.className = "waterfall-sidebar theme-sidebar";
+    sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
     sidebar.setAttribute("align", "center");
     container.appendChild(sidebar);
 
     let name = this._document.createElement("label");
-    name.className = "plain timeline-header-name";
+    name.className = "plain waterfall-header-name";
     name.setAttribute("value", this._l10n.getStr("timeline.records"));
     sidebar.appendChild(name);
 
     let ticks = this._document.createElement("hbox");
-    ticks.className = "timeline-header-ticks";
+    ticks.className = "waterfall-header-ticks waterfall-background-ticks";
     ticks.setAttribute("align", "center");
     ticks.setAttribute("flex", "1");
     container.appendChild(ticks);
 
     let offset = this._isRTL ? this._waterfallWidth : 0;
     let direction = this._isRTL ? -1 : 1;
     let tickInterval = this._findOptimalTickInterval({
-      ticksMultiple: TIMELINE_HEADER_TICKS_MULTIPLE,
-      ticksSpacingMin: TIMELINE_HEADER_TICKS_SPACING_MIN,
+      ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
+      ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
       dataScale: dataScale
     });
 
     for (let x = 0; x < this._waterfallWidth; x += tickInterval) {
-      let start = x + direction * TIMELINE_HEADER_TEXT_PADDING;
+      let start = x + direction * WATERFALL_HEADER_TEXT_PADDING;
       let time = Math.round(timeStart + x / dataScale);
       let label = this._l10n.getFormatStr("timeline.tick", time);
 
       let node = this._document.createElement("label");
-      node.className = "plain timeline-header-tick";
+      node.className = "plain waterfall-header-tick";
       node.style.transform = "translateX(" + (start - offset) + "px)";
       node.setAttribute("value", label);
       ticks.appendChild(node);
     }
 
     parent.appendChild(container);
   },
 
@@ -185,32 +188,32 @@ Waterfall.prototype = {
     for (let marker of markers) {
       if (!isMarkerInRange(marker, timeStart, timeEnd)) {
         continue;
       }
       // Only build and display a finite number of markers initially, to
       // preserve a snappy UI. After a certain delay, continue building the
       // outstanding markers while there's (hopefully) no user interaction.
       let arguments_ = [this._fragment, marker, timeStart, dataScale];
-      if (processed++ < TIMELINE_IMMEDIATE_DRAW_MARKERS_COUNT) {
+      if (processed++ < WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT) {
         this._buildMarker.apply(this, arguments_);
       } else {
         this._outstandingMarkers.push(arguments_);
       }
     }
 
     // If there are no outstanding markers, add a dummy "spacer" at the end
     // to fill up any remaining available space in the UI.
     if (!this._outstandingMarkers.length) {
       this._buildMarker(this._fragment, null);
     }
     // Otherwise prepare flushing the outstanding markers after a small delay.
     else {
       this._setNamedTimeout("flush-outstanding-markers",
-        TIMELINE_FLUSH_OUTSTANDING_MARKERS_DELAY,
+        WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY,
         () => this._buildOutstandingMarkers(parent));
     }
 
     parent.appendChild(this._fragment);
   },
 
   /**
    * Finishes building the outstanding markers in this view.
@@ -236,17 +239,17 @@ Waterfall.prototype = {
    *        The { name, start, end } marker in the data source.
    * @param timeStart
    *        @see Waterfall.prototype.setData
    * @param number dataScale
    *        @see Waterfall.prototype._buildMarkers
    */
   _buildMarker: function(parent, marker, timeStart, dataScale) {
     let container = this._document.createElement("hbox");
-    container.className = "timeline-marker-container";
+    container.className = "waterfall-marker-container";
 
     if (marker) {
       this._buildMarkerSidebar(container, marker);
       this._buildMarkerWaterfall(container, marker, timeStart, dataScale);
     } else {
       this._buildMarkerSpacer(container);
       container.setAttribute("flex", "1");
       container.setAttribute("is-spacer", "");
@@ -262,31 +265,31 @@ Waterfall.prototype = {
    *        The container node representing the marker in this view.
    * @param object marker
    *        @see Waterfall.prototype._buildMarker
    */
   _buildMarkerSidebar: function(container, marker) {
     let blueprint = this._blueprint[marker.name];
 
     let sidebar = this._document.createElement("hbox");
-    sidebar.className = "timeline-marker-sidebar theme-sidebar";
-    sidebar.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
+    sidebar.className = "waterfall-sidebar theme-sidebar";
+    sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
     sidebar.setAttribute("align", "center");
 
     let bullet = this._document.createElement("hbox");
-    bullet.className = "timeline-marker-bullet";
+    bullet.className = "waterfall-marker-bullet";
     bullet.style.backgroundColor = blueprint.fill;
     bullet.style.borderColor = blueprint.stroke;
     bullet.setAttribute("type", marker.name);
     sidebar.appendChild(bullet);
 
     let name = this._document.createElement("label");
     name.setAttribute("crop", "end");
     name.setAttribute("flex", "1");
-    name.className = "plain timeline-marker-name";
+    name.className = "plain waterfall-marker-name";
 
     let label;
     if (marker.detail && marker.detail.causeName) {
       label = this._l10n.getFormatStr("timeline.markerDetailFormat",
                                       blueprint.label,
                                       marker.detail.causeName);
     } else {
       label = blueprint.label;
@@ -309,48 +312,49 @@ Waterfall.prototype = {
    *        @see Waterfall.prototype.setData
    * @param number dataScale
    *        @see Waterfall.prototype._buildMarkers
    */
   _buildMarkerWaterfall: function(container, marker, timeStart, dataScale) {
     let blueprint = this._blueprint[marker.name];
 
     let waterfall = this._document.createElement("hbox");
-    waterfall.className = "timeline-marker-waterfall";
+    waterfall.className = "waterfall-marker-item waterfall-background-ticks";
+    waterfall.setAttribute("align", "center");
     waterfall.setAttribute("flex", "1");
 
     let start = (marker.start - timeStart) * dataScale;
     let width = (marker.end - marker.start) * dataScale;
     let offset = this._isRTL ? this._waterfallWidth : 0;
 
     let bar = this._document.createElement("hbox");
-    bar.className = "timeline-marker-bar";
+    bar.className = "waterfall-marker-bar";
     bar.style.backgroundColor = blueprint.fill;
     bar.style.borderColor = blueprint.stroke;
     bar.style.transform = "translateX(" + (start - offset) + "px)";
     bar.setAttribute("type", marker.name);
-    bar.setAttribute("width", Math.max(width, TIMELINE_MARKER_BAR_WIDTH_MIN));
+    bar.setAttribute("width", Math.max(width, WATERFALL_MARKER_BAR_WIDTH_MIN));
     waterfall.appendChild(bar);
 
     container.appendChild(waterfall);
   },
 
   /**
    * Creates a dummy spacer as an empty marker.
    *
    * @param nsIDOMNode container
    *        The container node representing the marker.
    */
   _buildMarkerSpacer: function(container) {
     let sidebarSpacer = this._document.createElement("spacer");
-    sidebarSpacer.className = "timeline-marker-sidebar theme-sidebar";
-    sidebarSpacer.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
+    sidebarSpacer.className = "waterfall-sidebar theme-sidebar";
+    sidebarSpacer.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
 
     let waterfallSpacer = this._document.createElement("spacer");
-    waterfallSpacer.className = "timeline-marker-waterfall";
+    waterfallSpacer.className = "waterfall-marker-item waterfall-background-ticks";
     waterfallSpacer.setAttribute("flex", "1");
 
     container.appendChild(sidebarSpacer);
     container.appendChild(waterfallSpacer);
   },
 
   /**
    * Creates the background displayed on the marker's waterfall.
--- a/browser/locales/en-US/chrome/browser/devtools/timeline.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/timeline.dtd
@@ -10,20 +10,29 @@
   - You want to make that choice consistent across the developer tools.
   - A good criteria is the language in which you'd find the best
   - documentation on web development on the web. -->
 
 <!-- LOCALIZATION NOTE (timelineUI.recordButton): This string is displayed
   -  on a button that starts a new recording. -->
 <!ENTITY timelineUI.recordButton.tooltip "Record timeline operations">
 
-<!-- LOCALIZATION NOTE (timelineUI.recordButton): This string is displayed
+<!-- LOCALIZATION NOTE (timelineUI.recordLabel): This string is displayed
   -  as a label to signal that a recording is in progress. -->
 <!ENTITY timelineUI.recordLabel "Recording…">
 
+<!-- LOCALIZATION NOTE (timelineUI.timelineUI.memoryCheckbox.label): This string
+  -  is displayed next to a checkbox determining whether or not memory
+  -  measurements are enabled. -->
+<!ENTITY timelineUI.memoryCheckbox.label "Memory">
+
+<!-- LOCALIZATION NOTE (timelineUI.timelineUI.memoryCheckbox.tooltip): This string
+  -  is displayed next to the memory checkbox -->
+<!ENTITY timelineUI.memoryCheckbox.tooltip "Enable memory measurements">
+
 <!-- LOCALIZATION NOTE (timelineUI.emptyNotice1/2): This is the label shown
   -  in the timeline view when empty. -->
 <!ENTITY timelineUI.emptyNotice1    "Click on the">
 <!ENTITY timelineUI.emptyNotice2    "button to start recording timeline events.">
 
 <!-- LOCALIZATION NOTE (timelineUI.stopNotice1/2): This is the label shown
   -  in the timeline view while recording. -->
 <!ENTITY timelineUI.stopNotice1    "Click on the">
--- a/browser/locales/en-US/chrome/browser/devtools/timeline.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/timeline.properties
@@ -36,13 +36,19 @@ timeline.records=RECORDS
 # LOCALIZATION NOTE (timeline.label.*):
 # These strings are displayed in the timeline waterfall, identifying markers.
 timeline.label.styles=Styles
 timeline.label.reflow=Reflow
 timeline.label.paint=Paint
 timeline.label.domevent=DOM Event
 timeline.label.consoleTime=Console
 
+# LOCALIZATION NOTE (graphs.memory):
+# This string is displayed in the memory graph of the Performance tool,
+# as the unit used to memory consumption. This label should be kept
+# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
+graphs.memory=MB
+
 # LOCALIZATION NOTE (timeline.markerDetailFormat):
 # Some timeline markers come with details, like a size, a name, a js function.
 # %1$S is replaced with one of the above label (timeline.label.*) and %2$S
 # with the details. For examples: Paint (200x100), or console.time (FOO)
 timeline.markerDetailFormat=%1$S (%2$S)
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -41,17 +41,17 @@ invitee_expire_hours_label=Invitation wi
 display_name_guest=Guest
 display_name_dnd_status=Do Not Disturb
 display_name_available_status=Available
 
 # Error bars
 ## LOCALIZATION NOTE(unable_retrieve_url,session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
 ## These may be displayed at the top of the panel here:
 ## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#error
-unable_retrieve_url=Sorry, we were unable to retrieve a call url.
+unable_retrieve_url=Sorry, we were unable to retrieve a call URL.
 session_expired_error_description=Session expired. All URLs you have previously created and shared will no longer work.
 could_not_authenticate=Could Not Authenticate
 password_changed_question=Did you change your password?
 try_again_later=Please try again later
 could_not_connect=Could Not Connect To The Server
 check_internet_connection=Please check your internet connection
 login_expired=Your Login Has Expired
 service_not_available=Service Unavailable At This Time
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -20,16 +20,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
   "resource://gre/modules/UITelemetry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
   "resource:///modules/BrowserUITelemetry.jsm");
 
 
+// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
+const PREF_LOG_LEVEL      = "browser.uitour.loglevel";
 const PREF_SEENPAGEIDS    = "browser.uitour.seenPageIDs";
 const MAX_BUTTONS         = 4;
 
 const BUCKET_NAME         = "UITour";
 const BUCKET_TIMESTEPS    = [
   1 * 60 * 1000, // Until 1 minute after tab is closed/inactive.
   3 * 60 * 1000, // Until 3 minutes after tab is closed/inactive.
   10 * 60 * 1000, // Until 10 minutes after tab is closed/inactive.
@@ -37,16 +39,26 @@ const BUCKET_TIMESTEPS    = [
 ];
 
 // Time after which seen Page IDs expire.
 const SEENPAGEID_EXPIRY  = 8 * 7 * 24 * 60 * 60 * 1000; // 8 weeks.
 
 // Prefix for any target matching a search engine.
 const TARGET_SEARCHENGINE_PREFIX = "searchEngine-";
 
+// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let ConsoleAPI = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).ConsoleAPI;
+  let consoleOptions = {
+    // toLowerCase is because the loglevel values use title case to be compatible with Log.jsm.
+    maxLogLevel: Services.prefs.getCharPref(PREF_LOG_LEVEL).toLowerCase(),
+    prefix: "UITour",
+  };
+  return new ConsoleAPI(consoleOptions);
+});
 
 this.UITour = {
   url: null,
   seenPageIDs: null,
   pageIDSourceTabs: new WeakMap(),
   pageIDSourceWindows: new WeakMap(),
   /* Map from browser windows to a set of tabs in which a tour is open */
   originTabs: new WeakMap(),
@@ -136,16 +148,17 @@ this.UITour = {
     }],
     ["urlbar",      {
       query: "#urlbar",
       widgetName: "urlbar-container",
     }],
   ]),
 
   init: function() {
+    log.debug("Initializing UITour");
     // Lazy getter is initialized here so it can be replicated any time
     // in a test.
     delete this.seenPageIDs;
     Object.defineProperty(this, "seenPageIDs", {
       get: this.restoreSeenPageIDs.bind(this),
       configurable: true,
     });
 
@@ -219,99 +232,113 @@ this.UITour = {
     }
 
     Services.prefs.setCharPref(PREF_SEENPAGEIDS,
                                JSON.stringify([...this.seenPageIDs]));
   },
 
   onPageEvent: function(aMessage, aEvent) {
     let contentDocument = null;
-
     let browser = aMessage.target;
     let window = browser.ownerDocument.defaultView;
     let tab = window.gBrowser.getTabForBrowser(browser);
     let messageManager = browser.messageManager;
 
-    if (typeof aEvent.detail != "object")
+    log.debug("onPageEvent:", aEvent.detail);
+
+    if (typeof aEvent.detail != "object") {
+      log.warn("Malformed event - detail not an object");
       return false;
+    }
 
     let action = aEvent.detail.action;
-    if (typeof action != "string" || !action)
+    if (typeof action != "string" || !action) {
+      log.warn("Action not defined");
       return false;
+    }
 
     let data = aEvent.detail.data;
-    if (typeof data != "object")
+    if (typeof data != "object") {
+      log.warn("Malformed event - data not an object");
       return false;
+    }
 
     // Do this before bailing if there's no tab, so later we can pick up the pieces:
     window.gBrowser.tabContainer.addEventListener("TabSelect", this);
 
     if (!window.gMultiProcessBrowser) { // Non-e10s. See bug 1089000.
       contentDocument = browser.contentWindow.document;
       if (!tab) {
         // This should only happen while detaching a tab:
         if (this._detachingTab) {
+          log.debug("Got event while detatching a tab");
           this._queuedEvents.push(aEvent);
           this._pendingDoc = Cu.getWeakReference(contentDocument);
           return;
         }
-        Cu.reportError("Discarding tabless UITour event (" + action + ") while not detaching a tab." +
+        log.error("Discarding tabless UITour event (" + action + ") while not detaching a tab." +
                        "This shouldn't happen!");
         return;
       }
     }
 
     switch (action) {
       case "registerPageID": {
         // This is only relevant if Telemtry is enabled.
-        if (!UITelemetry.enabled)
+        if (!UITelemetry.enabled) {
+          log.debug("registerPageID: Telemery disabled, not doing anything");
           break;
+        }
 
         // We don't want to allow BrowserUITelemetry.BUCKET_SEPARATOR in the
         // pageID, as it could make parsing the telemetry bucket name difficult.
-        if (typeof data.pageID == "string" &&
-            !data.pageID.contains(BrowserUITelemetry.BUCKET_SEPARATOR)) {
-          this.addSeenPageID(data.pageID);
+        if (typeof data.pageID != "string" ||
+            data.pageID.contains(BrowserUITelemetry.BUCKET_SEPARATOR)) {
+          log.warn("registerPageID: Invalid page ID specified");
+          break;
+        }
+
+        this.addSeenPageID(data.pageID);
 
-          // Store tabs and windows separately so we don't need to loop over all
-          // tabs when a window is closed.
-          this.pageIDSourceTabs.set(tab, data.pageID);
-          this.pageIDSourceWindows.set(window, data.pageID);
+        // Store tabs and windows separately so we don't need to loop over all
+        // tabs when a window is closed.
+        this.pageIDSourceTabs.set(tab, data.pageID);
+        this.pageIDSourceWindows.set(window, data.pageID);
 
-          this.setTelemetryBucket(data.pageID);
-        }
+        this.setTelemetryBucket(data.pageID);
+
         break;
       }
 
       case "showHighlight": {
         let targetPromise = this.getTarget(window, data.target);
         targetPromise.then(target => {
           if (!target.node) {
-            Cu.reportError("UITour: Target could not be resolved: " + data.target);
+            log.error("UITour: Target could not be resolved: " + data.target);
             return;
           }
           let effect = undefined;
           if (this.highlightEffects.indexOf(data.effect) !== -1) {
             effect = data.effect;
           }
           this.showHighlight(target, effect);
-        }).then(null, Cu.reportError);
+        }).catch(log.error);
         break;
       }
 
       case "hideHighlight": {
         this.hideHighlight(window);
         break;
       }
 
       case "showInfo": {
         let targetPromise = this.getTarget(window, data.target, true);
         targetPromise.then(target => {
           if (!target.node) {
-            Cu.reportError("UITour: Target could not be resolved: " + data.target);
+            log.error("UITour: Target could not be resolved: " + data.target);
             return;
           }
 
           let iconURL = null;
           if (typeof data.icon == "string")
             iconURL = this.resolveURL(browser, data.icon);
 
           let buttons = [];
@@ -328,31 +355,33 @@ this.UITour = {
                 if (typeof buttonData.icon == "string")
                   button.iconURL = this.resolveURL(browser, buttonData.icon);
 
                 if (typeof buttonData.style == "string")
                   button.style = buttonData.style;
 
                 buttons.push(button);
 
-                if (buttons.length == MAX_BUTTONS)
+                if (buttons.length == MAX_BUTTONS) {
+                  log.warn("showInfo: Reached limit of allowed number of buttons");
                   break;
+                }
               }
             }
           }
 
           let infoOptions = {};
 
           if (typeof data.closeButtonCallbackID == "string")
             infoOptions.closeButtonCallbackID = data.closeButtonCallbackID;
           if (typeof data.targetCallbackID == "string")
             infoOptions.targetCallbackID = data.targetCallbackID;
 
           this.showInfo(messageManager, target, data.title, data.text, iconURL, buttons, infoOptions);
-        }).then(null, Cu.reportError);
+        }).catch(log.error);
         break;
       }
 
       case "hideInfo": {
         this.hideInfo(window);
         break;
       }
 
@@ -387,46 +416,50 @@ this.UITour = {
       case "hideMenu": {
         this.hideMenu(window, data.name);
         break;
       }
 
       case "startUrlbarCapture": {
         if (typeof data.text != "string" || !data.text ||
             typeof data.url != "string" || !data.url) {
+          log.warn("startUrlbarCapture: Text or URL not specified");
           return false;
         }
 
         let uri = null;
         try {
           uri = Services.io.newURI(data.url, null, null);
         } catch (e) {
+          log.warn("startUrlbarCapture: Malformed URL specified");
           return false;
         }
 
         let secman = Services.scriptSecurityManager;
         let principal = contentDocument.nodePrincipal;
         let flags = secman.DISALLOW_INHERIT_PRINCIPAL;
         try {
           secman.checkLoadURIWithPrincipal(principal, uri, flags);
         } catch (e) {
+          log.warn("startUrlbarCapture: Orginating page doesn't have permission to open specified URL");
           return false;
         }
 
         this.startUrlbarCapture(window, data.text, data.url);
         break;
       }
 
       case "endUrlbarCapture": {
         this.endUrlbarCapture(window);
         break;
       }
 
       case "getConfiguration": {
         if (typeof data.configuration != "string") {
+          log.warn("getConfiguration: No configuration option specified");
           return false;
         }
 
         this.getConfiguration(messageManager, window, data.configuration, data.callbackID);
         break;
       }
 
       case "showFirefoxAccounts": {
@@ -443,17 +476,17 @@ this.UITour = {
         break;
       }
 
       case "addNavBarWidget": {
         // Add a widget to the toolbar
         let targetPromise = this.getTarget(window, data.name);
         targetPromise.then(target => {
           this.addNavBarWidget(target, messageManager, data.callbackID);
-        }).then(null, Cu.reportError);
+        }).catch(log.error);
         break;
       }
     }
 
     if (!window.gMultiProcessBrowser) { // Non-e10s. See bug 1089000.
       if (!this.originTabs.has(window)) {
         this.originTabs.set(window, new Set());
       }
@@ -524,17 +557,17 @@ this.UITour = {
             }
             this.originTabs.get(window).add(selectedTab);
             this.pendingDoc = null;
             this._detachingTab = false;
             while (this._queuedEvents.length) {
               try {
                 this.onPageEvent(this._queuedEvents.shift());
               } catch (ex) {
-                Cu.reportError(ex);
+                log.error(ex);
               }
             }
             break;
           }
         }
 
         this.teardownTour(window);
         break;
@@ -578,16 +611,17 @@ this.UITour = {
   // can remain lazy-loaded on-demand.
   getTelemetry: function() {
     return {
       seenPageIDs: [...this.seenPageIDs.keys()],
     };
   },
 
   teardownTour: function(aWindow, aWindowClosing = false) {
+    log.debug("teardownTour: aWindowClosing = " + aWindowClosing);
     aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this);
     aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hidePanelAnnotations);
     aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hidePanelAnnotations);
     aWindow.removeEventListener("SSWindowClosing", this);
 
     let originTabs = this.originTabs.get(aWindow);
     if (originTabs) {
       for (let tab of originTabs) {
@@ -622,18 +656,20 @@ this.UITour = {
   },
 
   // This function is copied to UITourListener.
   isSafeScheme: function(aURI) {
     let allowedSchemes = new Set(["https", "about"]);
     if (!Services.prefs.getBoolPref("browser.uitour.requireSecure"))
       allowedSchemes.add("http");
 
-    if (!allowedSchemes.has(aURI.scheme))
+    if (!allowedSchemes.has(aURI.scheme)) {
+      log.error("Unsafe scheme:", aURI.scheme);
       return false;
+    }
 
     return true;
   },
 
   resolveURL: function(aBrowser, aURL) {
     try {
       let uri = Services.io.newURI(aURL, null, aBrowser.currentURI);
 
@@ -643,27 +679,30 @@ this.UITour = {
       return uri.spec;
     } catch (e) {}
 
     return null;
   },
 
   sendPageCallback: function(aMessageManager, aCallbackID, aData = {}) {
     let detail = {data: aData, callbackID: aCallbackID};
+    log.debug("sendPageCallback", detail);
     aMessageManager.sendAsyncMessage("UITour:SendPageCallback", detail);
   },
 
   isElementVisible: function(aElement) {
     let targetStyle = aElement.ownerDocument.defaultView.getComputedStyle(aElement);
     return (targetStyle.display != "none" && targetStyle.visibility == "visible");
   },
 
   getTarget: function(aWindow, aTargetName, aSticky = false) {
+    log.debug("getTarget:", aTargetName);
     let deferred = Promise.defer();
     if (typeof aTargetName != "string" || !aTargetName) {
+      log.warn("getTarget: Invalid target name specified");
       deferred.reject("Invalid target name specified");
       return deferred.promise;
     }
 
     if (aTargetName == "pinnedTab") {
       deferred.resolve({
           targetName: aTargetName,
           node: this.ensurePinnedTab(aWindow, aSticky)
@@ -673,42 +712,44 @@ this.UITour = {
 
     if (aTargetName.startsWith(TARGET_SEARCHENGINE_PREFIX)) {
       let engineID = aTargetName.slice(TARGET_SEARCHENGINE_PREFIX.length);
       return this.getSearchEngineTarget(aWindow, engineID);
     }
 
     let targetObject = this.targets.get(aTargetName);
     if (!targetObject) {
+      log.warn("getTarget: The specified target name is not in the allowed set");
       deferred.reject("The specified target name is not in the allowed set");
       return deferred.promise;
     }
 
     let targetQuery = targetObject.query;
     aWindow.PanelUI.ensureReady().then(() => {
       let node;
       if (typeof targetQuery == "function") {
         try {
           node = targetQuery(aWindow.document);
         } catch (ex) {
+          log.warn("getTarget: Error running target query:", ex);
           node = null;
         }
       } else {
         node = aWindow.document.querySelector(targetQuery);
       }
 
       deferred.resolve({
         addTargetListener: targetObject.addTargetListener,
         node: node,
         removeTargetListener: targetObject.removeTargetListener,
         targetName: aTargetName,
         widgetName: targetObject.widgetName,
         allowAdd: targetObject.allowAdd,
       });
-    }).then(null, Cu.reportError);
+    }).catch(log.error);
     return deferred.promise;
   },
 
   targetIsInAppMenu: function(aTarget) {
     let placement = CustomizableUI.getPlacementOfWidget(aTarget.widgetName || aTarget.node.id);
     if (placement && placement.area == CustomizableUI.AREA_PANEL) {
       return true;
     }
@@ -724,41 +765,48 @@ this.UITour = {
              && targetElement.id != "PanelUI-button";
   },
 
   /**
    * Called before opening or after closing a highlight or info panel to see if
    * we need to open or close the appMenu to see the annotation's anchor.
    */
   _setAppMenuStateForAnnotation: function(aWindow, aAnnotationType, aShouldOpenForHighlight, aCallback = null) {
+    log.debug("_setAppMenuStateForAnnotation:", aAnnotationType);
+    log.debug("_setAppMenuStateForAnnotation: Menu is exptected to be:", aShouldOpenForHighlight ? "open" : "closed");
+
     // If the panel is in the desired state, we're done.
     let panelIsOpen = aWindow.PanelUI.panel.state != "closed";
     if (aShouldOpenForHighlight == panelIsOpen) {
+      log.debug("_setAppMenuStateForAnnotation: Panel already in expected state");
       if (aCallback)
         aCallback();
       return;
     }
 
     // Don't close the menu if it wasn't opened by us (e.g. via showmenu instead).
     if (!aShouldOpenForHighlight && !this.appMenuOpenForAnnotation.has(aAnnotationType)) {
+      log.debug("_setAppMenuStateForAnnotation: Menu not opened by us, not closing");
       if (aCallback)
         aCallback();
       return;
     }
 
     if (aShouldOpenForHighlight) {
       this.appMenuOpenForAnnotation.add(aAnnotationType);
     } else {
       this.appMenuOpenForAnnotation.delete(aAnnotationType);
     }
 
     // Actually show or hide the menu
     if (this.appMenuOpenForAnnotation.size) {
+      log.debug("_setAppMenuStateForAnnotation: Opening the menu");
       this.showMenu(aWindow, "appMenu", aCallback);
     } else {
+      log.debug("_setAppMenuStateForAnnotation: Closing the menu");
       this.hideMenu(aWindow, "appMenu");
       if (aCallback)
         aCallback();
     }
 
   },
 
   previewTheme: function(aTheme) {
@@ -865,16 +913,17 @@ this.UITour = {
         highlighter.style.borderRadius = "";
       }
 
       highlighter.style.height = highlightHeight + "px";
       highlighter.style.width = highlightWidth + "px";
 
       // Close a previous highlight so we can relocate the panel.
       if (highlighter.parentElement.state == "showing" || highlighter.parentElement.state == "open") {
+        log.debug("showHighlight: Closing previous highlight first");
         highlighter.parentElement.hidePopup();
       }
       /* The "overlap" position anchors from the top-left but we want to centre highlights at their
          minimum size. */
       let highlightWindow = aTarget.node.ownerDocument.defaultView;
       let containerStyle = highlightWindow.getComputedStyle(highlighter.parentElement);
       let paddingTopPx = 0 - parseFloat(containerStyle.paddingTop);
       let paddingLeftPx = 0 - parseFloat(containerStyle.paddingLeft);
@@ -885,18 +934,20 @@ this.UITour = {
                       - (Math.max(0, highlightWidthWithMin - targetRect.width) / 2);
       let offsetY = paddingLeftPx
                       - (Math.max(0, highlightHeightWithMin - targetRect.height) / 2);
       this._addAnnotationPanelMutationObserver(highlighter.parentElement);
       highlighter.parentElement.openPopup(highlightAnchor, "overlap", offsetX, offsetY);
     }
 
     // Prevent showing a panel at an undefined position.
-    if (!this.isElementVisible(aTarget.node))
+    if (!this.isElementVisible(aTarget.node)) {
+      log.warn("showHighlight: Not showing a highlight since the target isn't visible", aTarget);
       return;
+    }
 
     this._setAppMenuStateForAnnotation(aTarget.node.ownerDocument.defaultView, "highlight",
                                        this.targetIsInAppMenu(aTarget),
                                        showHighlightPanel.bind(this));
   },
 
   hideHighlight: function(aWindow) {
     let tabData = this.pinnedTabs.get(aWindow);
@@ -1015,16 +1066,22 @@ this.UITour = {
         }
       });
 
       tooltip.setAttribute("targetName", aAnchor.targetName);
       tooltip.hidden = false;
       let alignment = "bottomcenter topright";
       this._addAnnotationPanelMutationObserver(tooltip);
       tooltip.openPopup(aAnchorEl, alignment);
+      if (tooltip.state == "closed") {
+        document.defaultView.addEventListener("endmodalstate", function endModalStateHandler() {
+          document.defaultView.removeEventListener("endmodalstate", endModalStateHandler);
+          tooltip.openPopup(aAnchorEl, alignment);
+        }, false);
+      }
     }
 
     // Prevent showing a panel at an undefined position.
     if (!this.isElementVisible(aAnchor.node))
       return;
 
     // Due to a platform limitation, we can't anchor a panel to an element in a
     // <menupopup>. So we can't support showing info panels for search engines.
@@ -1078,17 +1135,17 @@ this.UITour = {
       }
       aWindow.PanelUI.show();
     } else if (aMenuName == "bookmarks") {
       let menuBtn = aWindow.document.getElementById("bookmarks-menu-button");
       openMenuButton(menuBtn);
     } else if (aMenuName == "searchEngines") {
       this.getTarget(aWindow, "searchProvider").then(target => {
         openMenuButton(target.node);
-      }).catch(Cu.reportError);
+      }).catch(log.error);
     }
   },
 
   hideMenu: function(aWindow, aMenuName) {
     function closeMenuButton(aMenuBtn) {
       if (aMenuBtn && aMenuBtn.boxObject)
         aMenuBtn.boxObject.openMenu(false);
     }
@@ -1120,17 +1177,17 @@ this.UITour = {
           // Since getTarget is async, we need to make sure that the target hasn't
           // changed since it may have just moved to somewhere outside of the app menu.
           if (annotationElement.getAttribute("targetName") != aTarget.targetName ||
               annotationElement.state == "closed" ||
               !UITour.targetIsInAppMenu(aTarget)) {
             return;
           }
           hideMethod(win);
-        }).then(null, Cu.reportError);
+        }).catch(log.error);
       }
     });
     UITour.appMenuOpenForAnnotation.clear();
   },
 
   recreatePopup: function(aPanel) {
     // After changing popup attributes that relate to how the native widget is created
     // (e.g. @noautohide) we need to re-create the frame/widget for it to take effect.
@@ -1192,26 +1249,27 @@ this.UITour = {
         break;
       case "appinfo":
         let props = ["defaultUpdateChannel", "version"];
         let appinfo = {};
         props.forEach(property => appinfo[property] = Services.appinfo[property]);
         this.sendPageCallback(aMessageManager, aCallbackID, appinfo);
         break;
       default:
-        Cu.reportError("getConfiguration: Unknown configuration requested: " + aConfiguration);
+        log.error("getConfiguration: Unknown configuration requested: " + aConfiguration);
         break;
     }
   },
 
   getAvailableTargets: function(aMessageManager, aChromeWindow, aCallbackID) {
     Task.spawn(function*() {
       let window = aChromeWindow;
       let data = this.availableTargetsCache.get(window);
       if (data) {
+        log.debug("getAvailableTargets: Using cached targets list", data.targets.join(","));
         this.sendPageCallback(aMessageManager, aCallbackID, data);
         return;
       }
 
       let promises = [];
       for (let targetName of this.targets.keys()) {
         promises.push(this.getTarget(window, targetName));
       }
@@ -1231,34 +1289,34 @@ this.UITour = {
       );
 
       data = {
         targets: targetNames,
       };
       this.availableTargetsCache.set(window, data);
       this.sendPageCallback(aMessageManager, aCallbackID, data);
     }.bind(this)).catch(err => {
-      Cu.reportError(err);
+      log.error(err);
       this.sendPageCallback(aMessageManager, aCallbackID, {
         targets: [],
       });
     });
   },
 
   addNavBarWidget: function (aTarget, aMessageManager, aCallbackID) {
     if (aTarget.node) {
-      Cu.reportError("UITour: can't add a widget already present: " + data.target);
+      log.error("UITour: can't add a widget already present: " + data.target);
       return;
     }
     if (!aTarget.allowAdd) {
-      Cu.reportError("UITour: not allowed to add this widget: " + data.target);
+      log.error("UITour: not allowed to add this widget: " + data.target);
       return;
     }
     if (!aTarget.widgetName) {
-      Cu.reportError("UITour: can't add a widget without a widgetName property: " + data.target);
+      log.error("UITour: can't add a widget without a widgetName property: " + data.target);
       return;
     }
 
     CustomizableUI.addWidgetToArea(aTarget.widgetName, CustomizableUI.AREA_NAVBAR);
     this.sendPageCallback(aMessageManager, aCallbackID);
   },
 
   _addAnnotationPanelMutationObserver: function(aPanelEl) {
--- a/browser/modules/test/browser.ini
+++ b/browser/modules/test/browser.ini
@@ -23,16 +23,19 @@ skip-if = e10s # Bug 941428 - UITour.jsm
 [browser_UITour3.js]
 skip-if = os == "linux" || e10s # Linux: Bug 986760, Bug 989101; e10s: Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_availableTargets.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_detach_tab.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_annotation_size_attributes.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly.
+[browser_UITour_modalDialog.js]
+run-if = os == "mac" # modal dialog disabling only working on OS X
+skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_panel_close_annotation.js]
 skip-if = true # Disabled due to frequent failures, bugs 1026310 and 1032137
 [browser_UITour_registerPageID.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_sync.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_resetProfile.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_UITour_modalDialog.js
@@ -0,0 +1,106 @@
+"use strict";
+
+let gTestTab;
+let gContentAPI;
+let gContentWindow;
+let handleDialog;
+
+// Modified from toolkit/components/passwordmgr/test/prompt_common.js
+var didDialog;
+
+var timer; // keep in outer scope so it's not GC'd before firing
+function startCallbackTimer() {
+    didDialog = false;
+
+    // Delay before the callback twiddles the prompt.
+    const dialogDelay = 10;
+
+    // Use a timer to invoke a callback to twiddle the authentication dialog
+    timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    timer.init(observer, dialogDelay, Ci.nsITimer.TYPE_ONE_SHOT);
+}
+
+
+var observer = SpecialPowers.wrapCallbackObject({
+    QueryInterface : function (iid) {
+        const interfaces = [Ci.nsIObserver,
+                            Ci.nsISupports, Ci.nsISupportsWeakReference];
+
+        if (!interfaces.some( function(v) { return iid.equals(v) } ))
+            throw SpecialPowers.Components.results.NS_ERROR_NO_INTERFACE;
+        return this;
+    },
+
+    observe : function (subject, topic, data) {
+        var doc = getDialogDoc();
+        if (doc)
+            handleDialog(doc);
+        else
+            startCallbackTimer(); // try again in a bit
+    }
+});
+
+function getDialogDoc() {
+  // Find the <browser> which contains notifyWindow, by looking
+  // through all the open windows and all the <browsers> in each.
+  var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+           getService(Ci.nsIWindowMediator);
+  //var enumerator = wm.getEnumerator("navigator:browser");
+  var enumerator = wm.getXULWindowEnumerator(null);
+
+  while (enumerator.hasMoreElements()) {
+    var win = enumerator.getNext();
+    var windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell;
+
+    var containedDocShells = windowDocShell.getDocShellEnumerator(
+                                      Ci.nsIDocShellTreeItem.typeChrome,
+                                      Ci.nsIDocShell.ENUMERATE_FORWARDS);
+    while (containedDocShells.hasMoreElements()) {
+        // Get the corresponding document for this docshell
+        var childDocShell = containedDocShells.getNext();
+        // We don't want it if it's not done loading.
+        if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE)
+          continue;
+        var childDoc = childDocShell.QueryInterface(Ci.nsIDocShell)
+                                    .contentViewer
+                                    .DOMDocument;
+
+        //ok(true, "Got window: " + childDoc.location.href);
+        if (childDoc.location.href == "chrome://global/content/commonDialog.xul")
+          return childDoc;
+    }
+  }
+
+  return null;
+}
+
+Components.utils.import("resource:///modules/UITour.jsm");
+
+function test() {
+  UITourTest();
+}
+
+
+let tests = [
+  taskify(function* test_modal_dialog_while_opening_tooltip(done) {
+    let panelShown;
+    let popup;
+
+    handleDialog = (doc) => {
+      popup = document.getElementById("UITourTooltip");
+      gContentAPI.showInfo("appMenu", "test title", "test text");
+      doc.defaultView.setTimeout(function() {
+        is(popup.state, "closed", "Popup shouldn't be shown while dialog is up");
+        panelShown = promisePanelElementShown(window, popup);
+        let dialog = doc.getElementById("commonDialog");
+        dialog.acceptDialog();
+      }, 1000);
+    };
+    startCallbackTimer();
+    executeSoon(() => alert("test"));
+    yield waitForConditionPromise(() => panelShown, "Timed out waiting for panel promise to be assigned", 100);
+    yield panelShown;
+
+    yield hideInfoPromise();
+  })
+];
--- a/browser/modules/test/head.js
+++ b/browser/modules/test/head.js
@@ -3,21 +3,21 @@
 
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource:///modules/UITour.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 const SINGLE_TRY_TIMEOUT = 100;
 const NUMBER_OF_TRIES = 30;
 
-function waitForConditionPromise(condition, timeoutMsg) {
+function waitForConditionPromise(condition, timeoutMsg, tryCount=NUMBER_OF_TRIES) {
   let defer = Promise.defer();
   let tries = 0;
   function checkCondition() {
-    if (tries >= NUMBER_OF_TRIES) {
+    if (tries >= tryCount) {
       defer.reject(timeoutMsg);
     }
     var conditionPassed;
     try {
       conditionPassed = condition();
     } catch (e) {
       return defer.reject(e);
     }
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -239,17 +239,16 @@ browser.jar:
   skin/classic/browser/devtools/alerticon-warning.png (../shared/devtools/images/alerticon-warning.png)
   skin/classic/browser/devtools/alerticon-warning@2x.png      (../shared/devtools/images/alerticon-warning@2x.png)
 * skin/classic/browser/devtools/ruleview.css          (../shared/devtools/ruleview.css)
 * skin/classic/browser/devtools/webconsole.css                  (devtools/webconsole.css)
   skin/classic/browser/devtools/webconsole_networkpanel.css     (devtools/webconsole_networkpanel.css)
   skin/classic/browser/devtools/webconsole.png                  (../shared/devtools/images/webconsole.png)
   skin/classic/browser/devtools/webconsole@2x.png               (../shared/devtools/images/webconsole@2x.png)
   skin/classic/browser/devtools/commandline.css              (devtools/commandline.css)
-  skin/classic/browser/devtools/highlighter.css              (../shared/devtools/highlighter.css)
   skin/classic/browser/devtools/markup-view.css       (../shared/devtools/markup-view.css)
   skin/classic/browser/devtools/editor-error.png       (../shared/devtools/images/editor-error.png)
   skin/classic/browser/devtools/editor-breakpoint.png  (../shared/devtools/images/editor-breakpoint.png)
   skin/classic/browser/devtools/editor-debug-location.png (../shared/devtools/images/editor-debug-location.png)
   skin/classic/browser/devtools/editor-debug-location@2x.png (../shared/devtools/images/editor-debug-location@2x.png)
   skin/classic/browser/devtools/breadcrumbs-divider@2x.png      (../shared/devtools/images/breadcrumbs-divider@2x.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton.png    (../shared/devtools/images/breadcrumbs-scrollbutton.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -97,16 +97,22 @@
     margin-left: 7px;
   }
 }
 
 #main-window:not(:-moz-lwtheme) > #titlebar {
   -moz-appearance: -moz-window-titlebar;
 }
 
+@media (-moz-mac-yosemite-theme) {
+  #main-window:not(:-moz-lwtheme) > #titlebar {
+    -moz-appearance: -moz-mac-vibrancy-light;
+  }
+}
+
 #main-window:not([tabsintitlebar]) > #titlebar {
   height: 22px; /* The native titlebar on OS X is 22px tall. */
 }
 
 /**
  * For tabs in titlebar on OS X, we stretch the titlebar down so that the
  * tabstrip can overlap it.
  */
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -177,16 +177,17 @@ browser.jar:
 * skin/classic/browser/customizableui/panelUIOverlay.css    (customizableui/panelUIOverlay.css)
   skin/classic/browser/customizableui/whimsy.png          (../shared/customizableui/whimsy.png)
   skin/classic/browser/customizableui/whimsy@2x.png       (../shared/customizableui/whimsy@2x.png)
   skin/classic/browser/customizableui/whimsy-bw.png       (../shared/customizableui/whimsy-bw.png)
   skin/classic/browser/customizableui/whimsy-bw@2x.png    (../shared/customizableui/whimsy-bw@2x.png)
   skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css)
   skin/classic/browser/downloads/buttons.png                (downloads/buttons.png)
   skin/classic/browser/downloads/buttons@2x.png             (downloads/buttons@2x.png)
+* skin/classic/browser/downloads/contentAreaDownloadsView.css (downloads/contentAreaDownloadsView.css)
   skin/classic/browser/downloads/download-glow-menuPanel.png (downloads/download-glow-menuPanel.png)
   skin/classic/browser/downloads/download-glow-menuPanel@2x.png (downloads/download-glow-menuPanel@2x.png)
   skin/classic/browser/downloads/download-notification-finish.png  (downloads/download-notification-finish.png)
   skin/classic/browser/downloads/download-notification-finish@2x.png  (downloads/download-notification-finish@2x.png)
   skin/classic/browser/downloads/download-notification-start.png  (downloads/download-notification-start.png)
   skin/classic/browser/downloads/download-notification-start@2x.png  (downloads/download-notification-start@2x.png)
   skin/classic/browser/downloads/download-summary.png       (downloads/download-summary.png)
   skin/classic/browser/downloads/download-summary@2x.png    (downloads/download-summary@2x.png)
@@ -356,17 +357,16 @@ browser.jar:
   skin/classic/browser/devtools/command-console.png           (../shared/devtools/images/command-console.png)
   skin/classic/browser/devtools/command-console@2x.png        (../shared/devtools/images/command-console@2x.png)
   skin/classic/browser/devtools/command-eyedropper.png        (../shared/devtools/images/command-eyedropper.png)
   skin/classic/browser/devtools/command-eyedropper@2x.png     (../shared/devtools/images/command-eyedropper@2x.png)
   skin/classic/browser/devtools/alerticon-warning.png         (../shared/devtools/images/alerticon-warning.png)
   skin/classic/browser/devtools/alerticon-warning@2x.png      (../shared/devtools/images/alerticon-warning@2x.png)
 * skin/classic/browser/devtools/ruleview.css                (../shared/devtools/ruleview.css)
   skin/classic/browser/devtools/commandline.css             (devtools/commandline.css)
-  skin/classic/browser/devtools/highlighter.css              (../shared/devtools/highlighter.css)
   skin/classic/browser/devtools/markup-view.css             (../shared/devtools/markup-view.css)
   skin/classic/browser/devtools/editor-error.png             (../shared/devtools/images/editor-error.png)
   skin/classic/browser/devtools/editor-breakpoint.png        (../shared/devtools/images/editor-breakpoint.png)
   skin/classic/browser/devtools/editor-breakpoint@2x.png        (../shared/devtools/images/editor-breakpoint@2x.png)
   skin/classic/browser/devtools/editor-debug-location.png    (../shared/devtools/images/editor-debug-location.png)
   skin/classic/browser/devtools/editor-debug-location@2x.png    (../shared/devtools/images/editor-debug-location@2x.png)
 * skin/classic/browser/devtools/webconsole.css                  (devtools/webconsole.css)
   skin/classic/browser/devtools/webconsole_networkpanel.css     (devtools/webconsole_networkpanel.css)
--- a/browser/themes/shared/devtools/timeline.inc.css
+++ b/browser/themes/shared/devtools/timeline.inc.css
@@ -43,117 +43,113 @@
   list-style-image: url(profiler-stopwatch-checked.svg);
 }
 
 #empty-notice button .button-text,
 #recording-notice button .button-text {
   display: none;
 }
 
-.theme-dark #timeline-overview {
-  border-bottom: 1px solid #000;
+.theme-dark #timeline-pane {
+  border-top: 1px solid #000;
 }
 
-.theme-light #timeline-overview {
-  border-bottom: 1px solid #aaa;
+.theme-light #timeline-pane {
+  border-top: 1px solid #aaa;
 }
 
-.timeline-list-contents {
+.waterfall-list-contents {
   /* Hack: force hardware acceleration */
   transform: translateZ(1px);
   overflow-x: hidden;
   overflow-y: auto;
 }
 
-.timeline-header-ticks,
-.timeline-marker-waterfall {
+.waterfall-background-ticks {
   /* Background created on a <canvas> in js. */
   /* @see browser/devtools/timeline/widgets/waterfall.js */
   background-image: -moz-element(#waterfall-background);
   background-repeat: repeat-y;
   background-position: -1px center;
 }
 
-.timeline-marker-waterfall {
-  overflow: hidden;
-}
-
-.timeline-marker-container[is-spacer] {
+.waterfall-marker-container[is-spacer] {
   pointer-events: none;
 }
 
-.theme-dark .timeline-marker-container:not([is-spacer]):nth-child(2n) {
+.theme-dark .waterfall-marker-container:not([is-spacer]):nth-child(2n) {
   background-color: rgba(255,255,255,0.03);
 }
 
-.theme-light .timeline-marker-container:not([is-spacer]):nth-child(2n) {
+.theme-light .waterfall-marker-container:not([is-spacer]):nth-child(2n) {
   background-color: rgba(128,128,128,0.03);
 }
 
-.theme-dark .timeline-marker-container:hover {
+.theme-dark .waterfall-marker-container:hover {
   background-color: rgba(255,255,255,0.1) !important;
 }
 
-.theme-light .timeline-marker-container:hover {
+.theme-light .waterfall-marker-container:hover {
   background-color: rgba(128,128,128,0.1) !important;
 }
 
-.timeline-header-sidebar,
-.timeline-marker-sidebar {
+.waterfall-marker-item {
+  overflow: hidden;
+}
+
+.waterfall-sidebar {
   -moz-border-end: 1px solid;
 }
 
-.theme-dark .timeline-header-sidebar,
-.theme-dark .timeline-marker-sidebar {
+.theme-dark .waterfall-sidebar {
   -moz-border-end-color: #000;
 }
 
-.theme-light .timeline-header-sidebar,
-.theme-light .timeline-marker-sidebar {
+.theme-light .waterfall-sidebar {
   -moz-border-end-color: #aaa;
 }
 
-.timeline-header-sidebar {
-  padding: 5px;
-}
-
-.timeline-marker-sidebar {
-  padding: 2px;
-}
-
-.timeline-marker-container:hover > .timeline-marker-sidebar {
+.waterfall-marker-container:hover > .waterfall-sidebar {
   background-color: transparent;
 }
 
-.timeline-header-tick {
+.waterfall-header-name {
+  padding: 4px;
+}
+
+.waterfall-header-tick {
   width: 100px;
   font-size: 9px;
   transform-origin: left center;
 }
 
-.theme-dark .timeline-header-tick {
+.theme-dark .waterfall-header-tick {
   color: #a9bacb;
 }
 
-.theme-light .timeline-header-tick {
+.theme-light .waterfall-header-tick {
   color: #292e33;
 }
 
-.timeline-header-tick:not(:first-child) {
+.waterfall-header-tick:not(:first-child) {
   -moz-margin-start: -100px !important; /* Don't affect layout. */
 }
 
-.timeline-marker-bullet {
+.waterfall-marker-bullet {
   width: 8px;
   height: 8px;
   -moz-margin-start: 8px;
   -moz-margin-end: 6px;
   border: 1px solid;
   border-radius: 1px;
 }
 
-.timeline-marker-bar {
-  margin-top: 4px;
-  margin-bottom: 4px;
+.waterfall-marker-name {
+  font-size: 95%;
+  padding-bottom: 1px !important;
+}
+
+.waterfall-marker-bar {
+  height: 9px;
   border: 1px solid;
   border-radius: 1px;
   transform-origin: left center;
 }
--- a/browser/themes/shared/devtools/widgets.inc.css
+++ b/browser/themes/shared/devtools/widgets.inc.css
@@ -933,36 +933,31 @@
 }
 
 .graph-widget-canvas[input=dragging-selection-contents] {
   cursor: grabbing;
 }
 
 /* Line graph widget */
 
-.line-graph-widget-canvas {
-  background: #0088cc;
-}
-
 .line-graph-widget-gutter {
   position: absolute;
   background: rgba(255,255,255,0.75);
   width: 10px;
   height: 100%;
   top: 0;
   left: 0;
   border-right: 1px solid rgba(255,255,255,0.25);
   pointer-events: none;
 }
 
 .line-graph-widget-gutter-line {
   position: absolute;
   width: 100%;
   border-top: 1px solid;
-  transform: translateY(-1px);
 }
 
 .line-graph-widget-gutter-line[type=maximum] {
   border-color: #2cbb0f;
 }
 
 .line-graph-widget-gutter-line[type=minimum] {
   border-color: #ed2655;
@@ -970,55 +965,66 @@
 
 .line-graph-widget-gutter-line[type=average] {
   border-color: #d97e00;
 }
 
 .line-graph-widget-tooltip {
   position: absolute;
   background: rgba(255,255,255,0.75);
-  box-shadow: 0 2px 1px rgba(0,0,0,0.1);
   border-radius: 2px;
   line-height: 15px;
   -moz-padding-start: 6px;
   -moz-padding-end: 6px;
   transform: translateY(-50%);
   font-size: 80%;
   z-index: 1;
   pointer-events: none;
 }
 
-.line-graph-widget-tooltip::before {
+.line-graph-widget-tooltip[with-arrows=true]::before {
   content: "";
   position: absolute;
   border-top: 3px solid transparent;
   border-bottom: 3px solid transparent;
   top: calc(50% - 3px);
 }
 
-.line-graph-widget-tooltip[arrow=start]::before {
+.line-graph-widget-tooltip[arrow=start][with-arrows=true]::before {
   -moz-border-end: 3px solid rgba(255,255,255,0.75);
   left: -3px;
 }
 
-.line-graph-widget-tooltip[arrow=end]::before {
+.line-graph-widget-tooltip[arrow=end][with-arrows=true]::before {
   -moz-border-start: 3px solid rgba(255,255,255,0.75);
   right: -3px;
 }
 
 .line-graph-widget-tooltip[type=maximum] {
-  left: calc(10px + 6px);
+  left: -1px;
 }
 
 .line-graph-widget-tooltip[type=minimum] {
-  left: calc(10px + 6px);
+  left: -1px;
 }
 
 .line-graph-widget-tooltip[type=average] {
-  right: 6px;
+  right: -1px;
+}
+
+.line-graph-widget-tooltip[type=maximum][with-arrows=true] {
+  left: 14px;
+}
+
+.line-graph-widget-tooltip[type=minimum][with-arrows=true] {
+  left: 14px;
+}
+
+.line-graph-widget-tooltip[type=average][with-arrows=true] {
+  right: 4px;
 }
 
 .line-graph-widget-tooltip > [text=info] {
   color: #18191a;
 }
 
 .line-graph-widget-tooltip > [text=value] {
   -moz-margin-start: 3px;
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -270,17 +270,16 @@ browser.jar:
         skin/classic/browser/devtools/command-pick.png              (../shared/devtools/images/command-pick.png)
         skin/classic/browser/devtools/command-pick@2x.png           (../shared/devtools/images/command-pick@2x.png)
         skin/classic/browser/devtools/command-frames.png            (../shared/devtools/images/command-frames.png)
         skin/classic/browser/devtools/command-frames@2x.png         (../shared/devtools/images/command-frames@2x.png)
         skin/classic/browser/devtools/command-console.png           (../shared/devtools/images/command-console.png)
         skin/classic/browser/devtools/command-console@2x.png        (../shared/devtools/images/command-console@2x.png)
         skin/classic/browser/devtools/command-eyedropper.png        (../shared/devtools/images/command-eyedropper.png)
         skin/classic/browser/devtools/command-eyedropper@2x.png     (../shared/devtools/images/command-eyedropper@2x.png)
-        skin/classic/browser/devtools/highlighter.css               (../shared/devtools/highlighter.css)
         skin/classic/browser/devtools/markup-view.css               (../shared/devtools/markup-view.css)
         skin/classic/browser/devtools/editor-error.png              (../shared/devtools/images/editor-error.png)
         skin/classic/browser/devtools/editor-breakpoint.png         (../shared/devtools/images/editor-breakpoint.png)
         skin/classic/browser/devtools/editor-breakpoint@2x.png         (../shared/devtools/images/editor-breakpoint@2x.png)
         skin/classic/browser/devtools/editor-debug-location.png     (../shared/devtools/images/editor-debug-location.png)
         skin/classic/browser/devtools/editor-debug-location@2x.png     (../shared/devtools/images/editor-debug-location@2x.png)
 *       skin/classic/browser/devtools/webconsole.css                (devtools/webconsole.css)
         skin/classic/browser/devtools/webconsole_networkpanel.css   (devtools/webconsole_networkpanel.css)
@@ -707,17 +706,16 @@ browser.jar:
         skin/classic/aero/browser/devtools/command-console.png       (../shared/devtools/images/command-console.png)
         skin/classic/aero/browser/devtools/command-console@2x.png    (../shared/devtools/images/command-console@2x.png)
         skin/classic/aero/browser/devtools/command-eyedropper.png        (../shared/devtools/images/command-eyedropper.png)
         skin/classic/aero/browser/devtools/command-eyedropper@2x.png     (../shared/devtools/images/command-eyedropper@2x.png)
         skin/classic/aero/browser/devtools/alerticon-warning.png     (../shared/devtools/images/alerticon-warning.png)
         skin/classic/aero/browser/devtools/alerticon-warning@2x.png  (../shared/devtools/images/alerticon-warning@2x.png)
 *       skin/classic/aero/browser/devtools/ruleview.css              (../shared/devtools/ruleview.css)
         skin/classic/aero/browser/devtools/commandline.css           (devtools/commandline.css)
-        skin/classic/aero/browser/devtools/highlighter.css           (../shared/devtools/highlighter.css)
         skin/classic/aero/browser/devtools/markup-view.css           (../shared/devtools/markup-view.css)
         skin/classic/aero/browser/devtools/editor-error.png           (../shared/devtools/images/editor-error.png)
         skin/classic/aero/browser/devtools/editor-breakpoint.png      (../shared/devtools/images/editor-breakpoint.png)
         skin/classic/aero/browser/devtools/editor-breakpoint@2x.png      (../shared/devtools/images/editor-breakpoint@2x.png)
         skin/classic/aero/browser/devtools/editor-debug-location.png  (../shared/devtools/images/editor-debug-location.png)
         skin/classic/aero/browser/devtools/editor-debug-location@2x.png  (../shared/devtools/images/editor-debug-location@2x.png)
 *       skin/classic/aero/browser/devtools/webconsole.css                  (devtools/webconsole.css)
         skin/classic/aero/browser/devtools/webconsole_networkpanel.css     (devtools/webconsole_networkpanel.css)
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -8818,16 +8818,26 @@ nsGlobalWindow::LeaveModalState()
       mSuspendedDoc = nullptr;
     }
   }
 
   // Remember the time of the last dialog quit.
   nsGlobalWindow *inner = topWin->GetCurrentInnerWindowInternal();
   if (inner)
     inner->mLastDialogQuitTime = TimeStamp::Now();
+
+  if (topWin->mModalStateDepth == 0) {
+    nsCOMPtr<nsIDOMEvent> event;
+    NS_NewDOMEvent(getter_AddRefs(event), topWin, nullptr, nullptr);
+    event->InitEvent(NS_LITERAL_STRING("endmodalstate"), true, false);
+    event->SetTrusted(true);
+    event->GetInternalNSEvent()->mFlags.mOnlyChromeDispatch = true;
+    bool dummy;
+    topWin->DispatchEvent(event, &dummy);
+  }
 }
 
 bool
 nsGlobalWindow::IsInModalState()
 {
   nsGlobalWindow *topWin = GetScriptableTop();
 
   if (!topWin) {
--- a/dom/geolocation/nsGeolocation.cpp
+++ b/dom/geolocation/nsGeolocation.cpp
@@ -428,25 +428,33 @@ nsGeolocationRequest::GetElement(nsIDOME
   NS_ENSURE_ARG_POINTER(aRequestingElement);
   *aRequestingElement = nullptr;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsGeolocationRequest::Cancel()
 {
+  if (mLocator->ClearPendingRequest(this)) {
+    return NS_OK;
+  }
+
   NotifyError(nsIDOMGeoPositionError::PERMISSION_DENIED);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsGeolocationRequest::Allow(JS::HandleValue aChoices)
 {
   MOZ_ASSERT(aChoices.isUndefined());
 
+  if (mLocator->ClearPendingRequest(this)) {
+    return NS_OK;
+  }
+
   // Kick off the geo device, if it isn't already running
   nsRefPtr<nsGeolocationService> gs = nsGeolocationService::GetGeolocationService();
   nsresult rv = gs->StartDevice(GetPrincipal());
 
   if (NS_FAILED(rv)) {
     // Location provider error
     NotifyError(nsIDOMGeoPositionError::POSITION_UNAVAILABLE);
     return NS_OK;
@@ -1299,16 +1307,38 @@ Geolocation::NotifyError(uint16_t aError
   // notify everyone that is watching
   for (uint32_t i = 0; i < mWatchingCallbacks.Length(); i++) {
     mWatchingCallbacks[i]->NotifyErrorAndShutdown(aErrorCode);
   }
 
   return NS_OK;
 }
 
+bool
+Geolocation::IsAlreadyCleared(nsGeolocationRequest* aRequest)
+{
+  for (uint32_t i = 0, length = mClearedWatchIDs.Length(); i < length; ++i) {
+    if (mClearedWatchIDs[i] == aRequest->WatchId()) {
+      return true;
+    }
+  }
+  return false;
+}
+
+bool
+Geolocation::ClearPendingRequest(nsGeolocationRequest* aRequest)
+{
+  if (aRequest->IsWatch() && this->IsAlreadyCleared(aRequest)) {
+    this->NotifyAllowedRequest(aRequest);
+    this->ClearWatch(aRequest->WatchId());
+    return true;
+  }
+  return false;
+}
+
 void
 Geolocation::GetCurrentPosition(PositionCallback& aCallback,
                                 PositionErrorCallback* aErrorCallback,
                                 const PositionOptions& aOptions,
                                 ErrorResult& aRv)
 {
   GeoPositionCallback successCallback(&aCallback);
   GeoPositionErrorCallback errorCallback(aErrorCallback);
@@ -1484,20 +1514,25 @@ Geolocation::WatchPositionReady(nsGeoloc
 
 NS_IMETHODIMP
 Geolocation::ClearWatch(int32_t aWatchId)
 {
   if (aWatchId < 0) {
     return NS_OK;
   }
 
+  if (!mClearedWatchIDs.Contains(aWatchId)) {
+    mClearedWatchIDs.AppendElement(aWatchId);
+  }
+
   for (uint32_t i = 0, length = mWatchingCallbacks.Length(); i < length; ++i) {
     if (mWatchingCallbacks[i]->WatchId() == aWatchId) {
       mWatchingCallbacks[i]->Shutdown();
       RemoveRequest(mWatchingCallbacks[i]);
+      mClearedWatchIDs.RemoveElement(aWatchId);
       break;
     }
   }
 
   // make sure we also search through the pending requests lists for
   // watches to clear...
   for (uint32_t i = 0, length = mPendingRequests.Length(); i < length; ++i) {
     if (mPendingRequests[i]->IsWatch() &&
--- a/dom/geolocation/nsGeolocation.h
+++ b/dom/geolocation/nsGeolocation.h
@@ -149,16 +149,20 @@ public:
   bool HasActiveCallbacks();
 
   // Register an allowed request
   void NotifyAllowedRequest(nsGeolocationRequest* aRequest);
 
   // Remove request from all callbacks arrays
   void RemoveRequest(nsGeolocationRequest* request);
 
+  // Check if there is already ClearWatch called for current
+  // request & clear if yes
+  bool ClearPendingRequest(nsGeolocationRequest* aRequest);
+
   // Shutting down.
   void Shutdown();
 
   // Getter for the principal that this Geolocation was loaded from
   nsIPrincipal* GetPrincipal() { return mPrincipal; }
 
   // Getter for the window that this Geolocation is owned by
   nsIWeakReference* GetOwner() { return mOwner; }
@@ -180,16 +184,19 @@ private:
   nsresult WatchPosition(GeoPositionCallback& aCallback, GeoPositionErrorCallback& aErrorCallback, PositionOptions* aOptions, int32_t* aRv);
 
   bool RegisterRequestWithPrompt(nsGeolocationRequest* request);
 
   // Methods for the service when it's ready to process requests:
   nsresult GetCurrentPositionReady(nsGeolocationRequest* aRequest);
   nsresult WatchPositionReady(nsGeolocationRequest* aRequest);
 
+  // Check if clearWatch is already called
+  bool IsAlreadyCleared(nsGeolocationRequest* aRequest);
+
   // Two callback arrays.  The first |mPendingCallbacks| holds objects for only
   // one callback and then they are released/removed from the array.  The second
   // |mWatchingCallbacks| holds objects until the object is explictly removed or
   // there is a page change. All requests held by either array are active, that
   // is, they have been allowed and expect to be fulfilled.
 
   nsTArray<nsRefPtr<nsGeolocationRequest> > mPendingCallbacks;
   nsTArray<nsRefPtr<nsGeolocationRequest> > mWatchingCallbacks;
@@ -203,16 +210,19 @@ private:
   // owning back pointer.
   nsRefPtr<nsGeolocationService> mService;
 
   // Watch ID
   uint32_t mLastWatchId;
 
   // Pending requests are used when the service is not ready
   nsTArray<nsRefPtr<nsGeolocationRequest> > mPendingRequests;
+
+  // Array containing already cleared watch IDs
+  nsTArray<int32_t> mClearedWatchIDs;
 };
 
 class PositionError MOZ_FINAL : public nsIDOMGeoPositionError,
                                 public nsWrapperCache
 {
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(PositionError)
--- a/dom/tests/mochitest/geolocation/mochitest.ini
+++ b/dom/tests/mochitest/geolocation/mochitest.ini
@@ -12,16 +12,18 @@ skip-if = buildapp == 'b2g' || toolkit =
 [test_cachedPosition.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' || e10s #TIMED_OUT
 [test_cancelCurrent.html]
 skip-if = buildapp == 'b2g'
 [test_cancelWatch.html]
 skip-if = buildapp == 'b2g'
 [test_clearWatch.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' #TIMED_OUT
+[test_clearWatchBeforeAllowing.html]
+skip-if = buildapp == 'b2g' || toolkit == 'android' #TIMED_OUT
 [test_clearWatch_invalid.html]
 skip-if = buildapp == 'b2g'
 [test_errorcheck.html]
 skip-if = toolkit=='gonk' || toolkit == 'android' || e10s #TIMED_OUT # b2g-debug(debug-only timeout)
 [test_geolocation_is_undefined_when_pref_is_off.html]
 [test_handlerSpinsEventLoop.html]
 skip-if = buildapp == 'mulet' || buildapp == 'b2g' || toolkit == 'android' || e10s #Don't run modal tests on Android # b2g(showmodaldialog) b2g-debug(showmodaldialog) b2g-desktop(showmodaldialog)
 [test_manyCurrentConcurrent.html]
@@ -42,9 +44,9 @@ skip-if = buildapp == 'b2g' || toolkit =
 skip-if = buildapp == 'b2g'
 [test_shutdown.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' #TIMED_OUT
 [test_timerRestartWatch.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' || e10s #TIMED_OUT
 [test_windowClose.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' #TIMED_OUT
 [test_worseAccuracyDoesNotBlockCallback.html]
-skip-if = buildapp == 'b2g' || toolkit == 'android' #TIMED_OUT
+skip-if = buildapp == 'b2g' || toolkit == 'android' #TIMED_OUT
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/geolocation/test_clearWatchBeforeAllowing.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=886026
+-->
+<head>
+  <title>Test for getCurrentPosition </title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="geolocation_common.js"></script>
+
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank"
+href="https://bugzilla.mozilla.org/show_bug.cgi?id=886026">Mozilla Bug 886026</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+resume_geolocationProvider(function() {
+  force_prompt(true, run_test);
+});
+
+function run_test() {
+  var successCallbackCalled = false,
+  errorCallbackCalled = false;
+
+  var watchId = navigator.geolocation.watchPosition(
+    function(pos) {
+      successCallbackCalled = true;
+    }, function(err) {
+         errorCallbackCalled = true;
+       }
+  );
+
+  navigator.geolocation.getCurrentPosition(
+    function(pos) {
+      SimpleTest.executeSoon(function() {
+        ok(successCallbackCalled == false,
+        "getCurrentPosition : Success callback should not have been called");
+
+        ok(errorCallbackCalled == false,
+        "getCurrentPosition : Error callback should not have been called");
+
+        SimpleTest.finish();
+      });
+    }
+  );
+
+  navigator.geolocation.clearWatch(watchId);
+}
+</script>
+</pre>
+</body>
+</html>
\ No newline at end of file
--- a/layout/xul/nsXULPopupManager.cpp
+++ b/layout/xul/nsXULPopupManager.cpp
@@ -11,16 +11,17 @@
 #include "nsMenuBarListener.h"
 #include "nsContentUtils.h"
 #include "nsIDOMDocument.h"
 #include "nsIDOMEvent.h"
 #include "nsIDOMXULElement.h"
 #include "nsIXULDocument.h"
 #include "nsIXULTemplateBuilder.h"
 #include "nsCSSFrameConstructor.h"
+#include "nsGlobalWindow.h"
 #include "nsLayoutUtils.h"
 #include "nsViewManager.h"
 #include "nsIComponentManager.h"
 #include "nsITimer.h"
 #include "nsFocusManager.h"
 #include "nsIDocShell.h"
 #include "nsPIDOMWindow.h"
 #include "nsIInterfaceRequestorUtils.h"
@@ -1589,28 +1590,28 @@ nsXULPopupManager::MayShowPopup(nsMenuPo
   if (widget && widget->GetLastRollup() == aPopup->GetContent())
       return false;
 
   nsCOMPtr<nsIDocShellTreeItem> dsti = aPopup->PresContext()->GetDocShell();
   nsCOMPtr<nsIBaseWindow> baseWin = do_QueryInterface(dsti);
   if (!baseWin)
     return false;
 
+  nsCOMPtr<nsIDocShellTreeItem> root;
+  dsti->GetRootTreeItem(getter_AddRefs(root));
+  if (!root) {
+    return false;
+  }
+
+  nsCOMPtr<nsIDOMWindow> rootWin = root->GetWindow();
+
   // chrome shells can always open popups, but other types of shells can only
   // open popups when they are focused and visible
   if (dsti->ItemType() != nsIDocShellTreeItem::typeChrome) {
     // only allow popups in active windows
-    nsCOMPtr<nsIDocShellTreeItem> root;
-    dsti->GetRootTreeItem(getter_AddRefs(root));
-    if (!root) {
-      return false;
-    }
-
-    nsCOMPtr<nsIDOMWindow> rootWin = root->GetWindow();
-
     nsIFocusManager* fm = nsFocusManager::GetFocusManager();
     if (!fm || !rootWin)
       return false;
 
     nsCOMPtr<nsIDOMWindow> activeWindow;
     fm->GetActiveWindow(getter_AddRefs(activeWindow));
     if (activeWindow != rootWin)
       return false;
@@ -1625,16 +1626,25 @@ nsXULPopupManager::MayShowPopup(nsMenuPo
   // platforms respond differently when an popup is opened in a minimized
   // window, so this is always disabled.
   nsCOMPtr<nsIWidget> mainWidget;
   baseWin->GetMainWidget(getter_AddRefs(mainWidget));
   if (mainWidget && mainWidget->SizeMode() == nsSizeMode_Minimized) {
     return false;
   }
 
+#ifdef XP_MACOSX
+  if (rootWin) {
+    nsGlobalWindow *globalWin = static_cast<nsGlobalWindow *>(rootWin.get());
+    if (globalWin->IsInModalState()) {
+      return false;
+    }
+  }
+#endif
+
   // cannot open a popup that is a submenu of a menupopup that isn't open.
   nsMenuFrame* menuFrame = do_QueryFrame(aPopup->GetParent());
   if (menuFrame) {
     nsMenuParent* parentPopup = menuFrame->GetMenuParent();
     if (parentPopup && !parentPopup->IsOpen())
       return false;
   }
 
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -524,16 +524,17 @@ public class BrowserApp extends GeckoApp
         tabHistoryController = new TabHistoryController(new OnShowTabHistory() {
             @Override
             public void onShowHistory(final List<TabHistoryPage> historyPageList, final int toIndex) {
                 runOnUiThread(new Runnable() {
                     @Override
                     public void run() {
                         final TabHistoryFragment fragment = TabHistoryFragment.newInstance(historyPageList, toIndex);
                         final FragmentManager fragmentManager = getSupportFragmentManager();
+                        GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getInteger(R.integer.long_press_vibrate_msec));
                         fragment.show(R.id.tab_history_panel, fragmentManager.beginTransaction(), TAB_HISTORY_FRAGMENT_TAG);
                     }
                 });
             }
         });
         mBrowserToolbar.setTabHistoryController(tabHistoryController);
 
         final String action = intent.getAction();
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -1410,16 +1410,23 @@ public class GeckoAppShell
         }
     }
 
     private static Vibrator vibrator() {
         LayerView layerView = getLayerView();
         return (Vibrator) layerView.getContext().getSystemService(Context.VIBRATOR_SERVICE);
     }
 
+    // Vibrate only if haptic feedback is enabled.
+    public static void vibrateOnHapticFeedbackEnabled(long milliseconds) {
+        if (Settings.System.getInt(getContext().getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) > 0) {
+            vibrate(milliseconds);
+        }
+    }
+
     @WrapElementForJNI(stubName = "Vibrate1")
     public static void vibrate(long milliseconds) {
         sVibrationEndTime = System.nanoTime() + milliseconds * 1000000;
         sVibrationMaybePlaying = true;
         vibrator().vibrate(milliseconds);
     }
 
     @WrapElementForJNI(stubName = "VibrateA")
--- a/mobile/android/base/resources/values/integers.xml
+++ b/mobile/android/base/resources/values/integers.xml
@@ -3,10 +3,11 @@
    - 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/. -->
 
 <resources>
 
     <integer name="number_of_top_sites">6</integer>
     <integer name="number_of_top_sites_cols">2</integer>
     <integer name="max_icon_grid_columns">4</integer>
+    <integer name="long_press_vibrate_msec">100</integer>
 
 </resources>
--- a/mobile/android/base/tabs/TabStripView.java
+++ b/mobile/android/base/tabs/TabStripView.java
@@ -6,17 +6,17 @@
 package org.mozilla.gecko.tabs;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
-import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.ViewTreeObserver.OnPreDrawListener;
 
 import com.nineoldandroids.animation.Animator;
 import com.nineoldandroids.animation.AnimatorSet;
 import com.nineoldandroids.animation.ObjectAnimator;
@@ -28,18 +28,18 @@ import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.widget.TwoWayView;
 
 public class TabStripView extends TwoWayView {
     private static final String LOGTAG = "GeckoTabStrip";
 
     private static final int ANIM_TIME_MS = 200;
-    private static final AccelerateDecelerateInterpolator ANIM_INTERPOLATOR =
-            new AccelerateDecelerateInterpolator();
+    private static final DecelerateInterpolator ANIM_INTERPOLATOR =
+            new DecelerateInterpolator();
 
     private final TabStripAdapter adapter;
     private final Drawable divider;
 
     // Filled by calls to ShapeDrawable.getPadding();
     // saved to prevent allocation in draw().
     private final Rect dividerPadding = new Rect();
 
--- a/toolkit/components/downloads/nsDownloadManager.cpp
+++ b/toolkit/components/downloads/nsDownloadManager.cpp
@@ -3357,19 +3357,19 @@ nsDownload::FixTargetPermissions()
 
   // Set perms according to umask.
   nsCOMPtr<nsIPropertyBag2> infoService =
       do_GetService("@mozilla.org/system-info;1");
   uint32_t gUserUmask = 0;
   rv = infoService->GetPropertyAsUint32(NS_LITERAL_STRING("umask"),
                                         &gUserUmask);
   if (NS_SUCCEEDED(rv)) {
-    rv = target->SetPermissions(0666 & ~gUserUmask);
+    (void)target->SetPermissions(0666 & ~gUserUmask);
   }
-  return rv;
+  return NS_OK;
 }
 
 nsresult
 nsDownload::MoveTempToTarget()
 {
   nsCOMPtr<nsIFile> target;
   nsresult rv = GetTargetFile(getter_AddRefs(target));
   NS_ENSURE_SUCCESS(rv, rv);
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -13,16 +13,17 @@ const { classes: Cc, interfaces: Ci, res
 
 const PREF_BRANCH = "browser.urlbar.";
 
 // Prefs are defined as [pref name, default value].
 const PREF_ENABLED =                [ "autocomplete.enabled",   true ];
 const PREF_AUTOFILL =               [ "autoFill",               true ];
 const PREF_AUTOFILL_TYPED =         [ "autoFill.typed",         true ];
 const PREF_AUTOFILL_SEARCHENGINES = [ "autoFill.searchEngines", true ];
+const PREF_RESTYLESEARCHES        = [ "restyleSearches",        false ];
 const PREF_DELAY =                  [ "delay",                  50 ];
 const PREF_BEHAVIOR =               [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ];
 const PREF_FILTER_JS =              [ "filter.javascript",      true ];
 const PREF_MAXRESULTS =             [ "maxRichResults",         25 ];
 const PREF_RESTRICT_HISTORY =       [ "restrict.history",       "^" ];
 const PREF_RESTRICT_BOOKMARKS =     [ "restrict.bookmark",      "*" ];
 const PREF_RESTRICT_TYPED =         [ "restrict.typed",         "~" ];
 const PREF_RESTRICT_TAG =           [ "restrict.tag",           "+" ];
@@ -379,16 +380,17 @@ XPCOMUtils.defineLazyGetter(this, "Prefs
     }
   }
 
   function loadPrefs(subject, topic, data) {
     store.enabled = prefs.get(...PREF_ENABLED);
     store.autofill = prefs.get(...PREF_AUTOFILL);
     store.autofillTyped = prefs.get(...PREF_AUTOFILL_TYPED);
     store.autofillSearchEngines = prefs.get(...PREF_AUTOFILL_SEARCHENGINES);
+    store.restyleSearches = prefs.get(...PREF_RESTYLESEARCHES);
     store.delay = prefs.get(...PREF_DELAY);
     store.matchBehavior = prefs.get(...PREF_BEHAVIOR);
     store.filterJavaScript = prefs.get(...PREF_FILTER_JS);
     store.maxRichResults = prefs.get(...PREF_MAXRESULTS);
     store.restrictHistoryToken = prefs.get(...PREF_RESTRICT_HISTORY);
     store.restrictBookmarkToken = prefs.get(...PREF_RESTRICT_BOOKMARKS);
     store.restrictTypedToken = prefs.get(...PREF_RESTRICT_TYPED);
     store.restrictTagToken = prefs.get(...PREF_RESTRICT_TAG);
@@ -1104,17 +1106,17 @@ Search.prototype = {
         this._usedPlaceIds.add(match.placeId);
       this._usedURLs.add(urlMapKey);
 
       if (!match.style) {
         match.style = "favicon";
       }
 
       // Restyle past searches, unless they are bookmarks or special results.
-      if (match.style == "favicon") {
+      if (Prefs.restyleSearches && match.style == "favicon") {
         this._maybeRestyleSearchMatch(match);
       }
 
       this._result.appendMatch(match.value,
                                match.comment,
                                match.icon || PlacesUtils.favicons.defaultFavicon.spec,
                                match.style,
                                match.finalCompleteValue || "");
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -15,41 +15,16 @@
 #include "nsUnicharUtils.h"
 #include "nsPrintfCString.h"
 #include "prprf.h"
 #include "mozilla/storage.h"
 
 #include "GeckoProfiler.h"
 
 #define BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_LENGTH 32
-#define RECENT_BOOKMARKS_INITIAL_CACHE_LENGTH 10
-// Threshold to expire old bookmarks if the initial cache size is exceeded.
-#define RECENT_BOOKMARKS_THRESHOLD PRTime((int64_t)1 * 60 * PR_USEC_PER_SEC)
-
-#define BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(_itemId_) \
-  mUncachableBookmarks.PutEntry(_itemId_); \
-  mRecentBookmarksCache.RemoveEntry(_itemId_)
-
-#define END_CRITICAL_BOOKMARK_CACHE_SECTION(_itemId_) \
-  MOZ_ASSERT(!mRecentBookmarksCache.GetEntry(_itemId_)); \
-  MOZ_ASSERT(mUncachableBookmarks.GetEntry(_itemId_)); \
-  mUncachableBookmarks.RemoveEntry(_itemId_)
-
-#define ADD_TO_BOOKMARK_CACHE(_itemId_, _data_) \
-  PR_BEGIN_MACRO \
-  ExpireNonrecentBookmarks(&mRecentBookmarksCache); \
-  if (!mUncachableBookmarks.GetEntry(_itemId_)) { \
-    BookmarkKeyClass* key = mRecentBookmarksCache.PutEntry(_itemId_); \
-    if (key) { \
-      key->bookmark = _data_; \
-    } \
-  } \
-  PR_END_MACRO
-
-#define TOPIC_PLACES_MAINTENANCE "places-maintenance-finished"
 
 using namespace mozilla;
 
 // These columns sit to the right of the kGetInfoIndex_* columns.
 const int32_t nsNavBookmarks::kGetChildrenIndex_Guid = 15;
 const int32_t nsNavBookmarks::kGetChildrenIndex_Position = 16;
 const int32_t nsNavBookmarks::kGetChildrenIndex_Type = 17;
 const int32_t nsNavBookmarks::kGetChildrenIndex_PlaceID = 18;
@@ -150,73 +125,31 @@ public:
   }
 
 private:
   nsRefPtr<nsNavBookmarks> mBookmarksSvc;
   Method mCallback;
   DataType mData;
 };
 
-static PLDHashOperator
-ExpireNonrecentBookmarksCallback(BookmarkKeyClass* aKey,
-                                 void* userArg)
-{
-  int64_t* threshold = reinterpret_cast<int64_t*>(userArg);
-  if (aKey->creationTime < *threshold) {
-    return PL_DHASH_REMOVE;
-  }
-  return PL_DHASH_NEXT;
-}
-
-static void
-ExpireNonrecentBookmarks(nsTHashtable<BookmarkKeyClass>* hashTable)
-{
-  if (hashTable->Count() > RECENT_BOOKMARKS_INITIAL_CACHE_LENGTH) {
-    int64_t threshold = PR_Now() - RECENT_BOOKMARKS_THRESHOLD;
-    (void)hashTable->EnumerateEntries(ExpireNonrecentBookmarksCallback,
-                                      reinterpret_cast<void*>(&threshold));
-  }
-}
-
-static PLDHashOperator
-ExpireRecentBookmarksByParentCallback(BookmarkKeyClass* aKey,
-                                      void* userArg)
-{
-  int64_t* parentId = reinterpret_cast<int64_t*>(userArg);
-  if (aKey->bookmark.parentId == *parentId) {
-    return PL_DHASH_REMOVE;
-  }
-  return PL_DHASH_NEXT;
-}
-
-static void
-ExpireRecentBookmarksByParent(nsTHashtable<BookmarkKeyClass>* hashTable,
-                              int64_t aParentId)
-{
-  (void)hashTable->EnumerateEntries(ExpireRecentBookmarksByParentCallback,
-                                    reinterpret_cast<void*>(&aParentId));
-}
-
 } // Anonymous namespace.
 
 
 nsNavBookmarks::nsNavBookmarks()
   : mItemCount(0)
   , mRoot(0)
   , mMenuRoot(0)
   , mTagsRoot(0)
   , mUnfiledRoot(0)
   , mToolbarRoot(0)
   , mCanNotify(false)
   , mCacheObservers("bookmark-observers")
   , mBatching(false)
   , mBookmarkToKeywordHash(BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_LENGTH)
   , mBookmarkToKeywordHashInitialized(false)
-  , mRecentBookmarksCache(RECENT_BOOKMARKS_INITIAL_CACHE_LENGTH)
-  , mUncachableBookmarks(RECENT_BOOKMARKS_INITIAL_CACHE_LENGTH)
 {
   NS_ASSERTION(!gBookmarksService,
                "Attempting to create two instances of the service!");
   gBookmarksService = this;
 }
 
 
 nsNavBookmarks::~nsNavBookmarks()
@@ -240,17 +173,16 @@ NS_IMPL_ISUPPORTS(nsNavBookmarks
 nsresult
 nsNavBookmarks::Init()
 {
   mDB = Database::GetDatabase();
   NS_ENSURE_STATE(mDB);
 
   nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
   if (os) {
-    (void)os->AddObserver(this, TOPIC_PLACES_MAINTENANCE, true);
     (void)os->AddObserver(this, TOPIC_PLACES_SHUTDOWN, true);
     (void)os->AddObserver(this, TOPIC_PLACES_CONNECTION_CLOSED, true);
   }
 
   nsresult rv = ReadRoots();
   NS_ENSURE_SUCCESS(rv, rv);
 
   mCanNotify = true;
@@ -341,20 +273,16 @@ nsresult
 nsNavBookmarks::AdjustIndices(int64_t aFolderId,
                               int32_t aStartIndex,
                               int32_t aEndIndex,
                               int32_t aDelta)
 {
   NS_ASSERTION(aStartIndex >= 0 && aEndIndex <= INT32_MAX &&
                aStartIndex <= aEndIndex, "Bad indices");
 
-  // Expire all cached items for this parent, since all positions are going to
-  // change.
-  ExpireRecentBookmarksByParent(&mRecentBookmarksCache, aFolderId);
-
   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
     "UPDATE moz_bookmarks SET position = position + :delta "
       "WHERE parent = :parent "
         "AND position BETWEEN :from_index AND :to_index"
   );
   NS_ENSURE_STATE(stmt);
   mozStorageStatementScoper scoper(stmt);
 
@@ -547,18 +475,16 @@ nsNavBookmarks::InsertBookmarkInDB(int64
     bookmark.lastModified = aDateAdded;
   if (aURI) {
     rv = aURI->GetSpec(bookmark.url);
     NS_ENSURE_SUCCESS(rv, rv);
   }
   bookmark.parentGuid = aParentGuid;
   bookmark.grandParentId = aGrandParentId;
 
-  ADD_TO_BOOKMARK_CACHE(*_itemId, bookmark);
-
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::InsertBookmark(int64_t aFolder,
                                nsIURI* aURI,
                                int32_t aIndex,
@@ -679,18 +605,16 @@ nsNavBookmarks::RemoveItem(int64_t aItem
   }
 
   if (bookmark.type == TYPE_FOLDER) {
     // Remove all of the folder's children.
     rv = RemoveFolderChildren(bookmark.id);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
-
   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
     "DELETE FROM moz_bookmarks WHERE id = :item_id"
   );
   NS_ENSURE_STATE(stmt);
   mozStorageStatementScoper scoper(stmt);
 
   rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
   NS_ENSURE_SUCCESS(rv, rv);
@@ -707,18 +631,16 @@ nsNavBookmarks::RemoveItem(int64_t aItem
   bookmark.lastModified = PR_Now();
   rv = SetItemDateInternal(LAST_MODIFIED, bookmark.parentId,
                            bookmark.lastModified);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  END_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
-
   nsCOMPtr<nsIURI> uri;
   if (bookmark.type == TYPE_BOOKMARK) {
     // If not a tag, recalculate frecency for this entry, since it changed.
     if (bookmark.grandParentId != mTagsRoot) {
       nsNavHistory* history = nsNavHistory::GetHistoryService();
       NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
       rv = history->UpdateFrecency(bookmark.placeId);
       NS_ENSURE_SUCCESS(rv, rv);
@@ -1149,18 +1071,16 @@ nsNavBookmarks::RemoveFolderChildren(int
   nsCString foldersToRemove;
   for (uint32_t i = 0; i < folderChildrenArray.Length(); ++i) {
     BookmarkData& child = folderChildrenArray[i];
 
     if (child.type == TYPE_FOLDER) {
       foldersToRemove.Append(',');
       foldersToRemove.AppendInt(child.id);
     }
-
-    BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(child.id);
   }
 
   // Delete items from the database now.
   mozStorageTransaction transaction(mDB->MainConn(), false);
 
   nsCOMPtr<mozIStorageStatement> deleteStatement = mDB->GetStatement(
     NS_LITERAL_CSTRING(
       "DELETE FROM moz_bookmarks "
@@ -1197,17 +1117,16 @@ nsNavBookmarks::RemoveFolderChildren(int
         NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
         rv = history->UpdateFrecency(child.placeId);
         NS_ENSURE_SUCCESS(rv, rv);
       }
 
       rv = UpdateKeywordsHashForRemovedBookmark(child.id);
       NS_ENSURE_SUCCESS(rv, rv);
     }
-    END_CRITICAL_BOOKMARK_CACHE_SECTION(child.id);
   }
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Call observers in reverse order to serve children before their parent.
   for (int32_t i = folderChildrenArray.Length() - 1; i >= 0; --i) {
     BookmarkData& child = folderChildrenArray[i];
@@ -1346,18 +1265,16 @@ nsNavBookmarks::MoveItem(int64_t aItemId
     // First, fill the hole from the removal from the old parent.
     rv = AdjustIndices(bookmark.parentId, bookmark.position + 1, INT32_MAX, -1);
     NS_ENSURE_SUCCESS(rv, rv);
     // Now, make room in the new parent for the insertion.
     rv = AdjustIndices(aNewParent, newIndex, INT32_MAX, 1);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
-
   {
     // Update parent and position.
     nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
       "UPDATE moz_bookmarks SET parent = :parent, position = :item_index "
       "WHERE id = :item_id "
     );
     NS_ENSURE_STATE(stmt);
     mozStorageStatementScoper scoper(stmt);
@@ -1376,18 +1293,16 @@ nsNavBookmarks::MoveItem(int64_t aItemId
   rv = SetItemDateInternal(LAST_MODIFIED, bookmark.parentId, now);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = SetItemDateInternal(LAST_MODIFIED, aNewParent, now);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  END_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
-
   NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                    nsINavBookmarkObserver,
                    OnItemMoved(bookmark.id,
                                bookmark.parentId,
                                bookmark.position,
                                aNewParent,
                                newIndex,
                                bookmark.type,
@@ -1396,24 +1311,16 @@ nsNavBookmarks::MoveItem(int64_t aItemId
                                newParentGuid));
   return NS_OK;
 }
 
 nsresult
 nsNavBookmarks::FetchItemInfo(int64_t aItemId,
                               BookmarkData& _bookmark)
 {
-  // Check if the requested id is in the recent cache and avoid the database
-  // lookup if so.  Invalidate the cache after getting data if requested.
-  BookmarkKeyClass* key = mRecentBookmarksCache.GetEntry(aItemId);
-  if (key) {
-    _bookmark = key->bookmark;
-    return NS_OK;
-  }
-
   // LEFT JOIN since not all bookmarks have an associated place.
   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
     "SELECT b.id, h.url, b.title, b.position, b.fk, b.parent, b.type, "
            "b.dateAdded, b.lastModified, b.guid, t.guid, t.parent "
     "FROM moz_bookmarks b "
     "LEFT JOIN moz_bookmarks t ON t.id = b.parent "
     "LEFT JOIN moz_places h ON h.id = b.fk "
     "WHERE b.id = :item_id"
@@ -1466,18 +1373,16 @@ nsNavBookmarks::FetchItemInfo(int64_t aI
     NS_ENSURE_SUCCESS(rv, rv);
     rv = stmt->GetInt64(11, &_bookmark.grandParentId);
     NS_ENSURE_SUCCESS(rv, rv);
   }
   else {
     _bookmark.grandParentId = -1;
   }
 
-  ADD_TO_BOOKMARK_CACHE(aItemId, _bookmark);
-
   return NS_OK;
 }
 
 nsresult
 nsNavBookmarks::SetItemDateInternal(enum BookmarkDate aDateType,
                                     int64_t aItemId,
                                     PRTime aValue)
 {
@@ -1502,26 +1407,16 @@ nsNavBookmarks::SetItemDateInternal(enum
   nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("date"), aValue);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = stmt->Execute();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // Update the cache entry, if needed.
-  BookmarkKeyClass* key = mRecentBookmarksCache.GetEntry(aItemId);
-  if (key) {
-    if (aDateType == DATE_ADDED) {
-      key->bookmark.dateAdded = aValue;
-    }
-    // Set lastModified in both cases.
-    key->bookmark.lastModified = aValue;
-  }
-
   // note, we are not notifying the observers
   // that the item has changed.
 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
@@ -1645,28 +1540,16 @@ nsNavBookmarks::SetItemTitle(int64_t aIt
                                   bookmark.lastModified);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = statement->Execute();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // Update the cache entry, if needed.
-  BookmarkKeyClass* key = mRecentBookmarksCache.GetEntry(aItemId);
-  if (key) {
-    if (title.IsVoid()) {
-      key->bookmark.title.SetIsVoid(true);
-    }
-    else {
-      key->bookmark.title.Assign(title);
-    }
-    key->bookmark.lastModified = bookmark.lastModified;
-  }
-
   NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                    nsINavBookmarkObserver,
                    OnItemChanged(bookmark.id,
                                  NS_LITERAL_CSTRING("title"),
                                  false,
                                  title,
                                  bookmark.lastModified,
                                  bookmark.type,
@@ -2104,18 +1987,16 @@ nsNavBookmarks::ChangeBookmarkURI(int64_
   NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
   int64_t newPlaceId;
   nsAutoCString newPlaceGuid;
   rv = history->GetOrCreateIdForPage(aNewURI, &newPlaceId, newPlaceGuid);
   NS_ENSURE_SUCCESS(rv, rv);
   if (!newPlaceId)
     return NS_ERROR_INVALID_ARG;
 
-  BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
-
   nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
     "UPDATE moz_bookmarks SET fk = :page_id, lastModified = :date "
     "WHERE id = :item_id "
   );
   NS_ENSURE_STATE(statement);
   mozStorageStatementScoper scoper(statement);
 
   rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), newPlaceId);
@@ -2127,18 +2008,16 @@ nsNavBookmarks::ChangeBookmarkURI(int64_
   rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = statement->Execute();
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  END_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
-
   rv = history->UpdateFrecency(newPlaceId);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Upon changing the URI for a bookmark, update the frecency for the old
   // place as well.
   rv = history->UpdateFrecency(bookmark.placeId);
   NS_ENSURE_SUCCESS(rv, rv);
 
@@ -2340,34 +2219,30 @@ nsNavBookmarks::SetItemIndex(int64_t aIt
   int64_t grandParentId;
   nsAutoCString folderGuid;
   rv = FetchFolderInfo(bookmark.parentId, &folderCount, folderGuid, &grandParentId);
   NS_ENSURE_SUCCESS(rv, rv);
   NS_ENSURE_TRUE(aNewIndex < folderCount, NS_ERROR_INVALID_ARG);
   // Check the parent's guid is the expected one.
   MOZ_ASSERT(bookmark.parentGuid == folderGuid);
 
-  BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
-
   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
     "UPDATE moz_bookmarks SET position = :item_index WHERE id = :item_id"
   );
   NS_ENSURE_STATE(stmt);
   mozStorageStatementScoper scoper(stmt);
 
   rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), aNewIndex);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = stmt->Execute();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  END_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
-
   NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                    nsINavBookmarkObserver,
                    OnItemMoved(bookmark.id,
                                bookmark.parentId,
                                bookmark.position,
                                bookmark.parentId,
                                aNewIndex,
                                bookmark.type,
@@ -2490,22 +2365,16 @@ nsNavBookmarks::SetKeywordForBookmark(in
                                            bookmark.id);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = updateBookmarkStmt->Execute();
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // Update the cache entry, if needed.
-  BookmarkKeyClass* key = mRecentBookmarksCache.GetEntry(aBookmarkId);
-  if (key) {
-    key->bookmark.lastModified = bookmark.lastModified;
-  }
-
   NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                    nsINavBookmarkObserver,
                    OnItemChanged(bookmark.id,
                                  NS_LITERAL_CSTRING("keyword"),
                                  false,
                                  NS_ConvertUTF16toUTF8(keyword),
                                  bookmark.lastModified,
                                  bookmark.type,
@@ -2753,22 +2622,17 @@ nsNavBookmarks::NotifyItemChanged(const 
 //// nsIObserver
 
 NS_IMETHODIMP
 nsNavBookmarks::Observe(nsISupports *aSubject, const char *aTopic,
                         const char16_t *aData)
 {
   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
 
-  if (strcmp(aTopic, TOPIC_PLACES_MAINTENANCE) == 0) {
-    // Maintenance can execute direct writes to the database, thus clear all
-    // the cached bookmarks.
-    mRecentBookmarksCache.Clear();
-  }
-  else if (strcmp(aTopic, TOPIC_PLACES_SHUTDOWN) == 0) {
+  if (strcmp(aTopic, TOPIC_PLACES_SHUTDOWN) == 0) {
     // Stop Observing annotations.
     nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
     if (annosvc) {
       annosvc->RemoveObserver(this);
     }
   }
   else if (strcmp(aTopic, TOPIC_PLACES_CONNECTION_CLOSED) == 0) {
     // Don't even try to notify observers from this point on, the category
--- a/toolkit/components/places/nsNavBookmarks.h
+++ b/toolkit/components/places/nsNavBookmarks.h
@@ -56,34 +56,16 @@ namespace places {
     nsCString property;
     bool isAnnotation;
     nsCString newValue;
   };
 
   typedef void (nsNavBookmarks::*ItemVisitMethod)(const ItemVisitData&);
   typedef void (nsNavBookmarks::*ItemChangeMethod)(const ItemChangeData&);
 
-  class BookmarkKeyClass : public nsTrimInt64HashKey
-  {
-    public:
-    explicit BookmarkKeyClass(const int64_t* aItemId)
-    : nsTrimInt64HashKey(aItemId)
-    , creationTime(PR_Now())
-    {
-    }
-    BookmarkKeyClass(const BookmarkKeyClass& aOther)
-    : nsTrimInt64HashKey(aOther)
-    , creationTime(PR_Now())
-    {
-      NS_NOTREACHED("Do not call me!");
-    }
-    BookmarkData bookmark;
-    PRTime creationTime;
-  };
-
   enum BookmarkDate {
     DATE_ADDED = 0
   , LAST_MODIFIED
   };
 
 } // namespace places
 } // namespace mozilla
 
@@ -119,17 +101,16 @@ public:
       NS_ENSURE_TRUE(serv, nullptr);
       NS_ASSERTION(gBookmarksService,
                    "Should have static instance pointer now");
     }
     return gBookmarksService;
   }
 
   typedef mozilla::places::BookmarkData BookmarkData;
-  typedef mozilla::places::BookmarkKeyClass BookmarkKeyClass;
   typedef mozilla::places::ItemVisitData ItemVisitData;
   typedef mozilla::places::ItemChangeData ItemChangeData;
   typedef mozilla::places::BookmarkStatementId BookmarkStatementId;
 
   nsresult ResultNodeForContainer(int64_t aID,
                                   nsNavHistoryQueryOptions* aOptions,
                                   nsNavHistoryResultNode** aNode);
 
@@ -450,23 +431,11 @@ private:
 
   /**
    * This function must be called every time a bookmark is removed.
    *
    * @param aURI
    *        Uri to test.
    */
   nsresult UpdateKeywordsHashForRemovedBookmark(int64_t aItemId);
-
-  /**
-   * Cache for the last fetched BookmarkData entries.
-   * This is used to speed up repeated requests to the same item id.
-   */
-  nsTHashtable<BookmarkKeyClass> mRecentBookmarksCache;
-
-  /**
-   * Tracks bookmarks in the cache critical path.  Items should not be
-   * added to the cache till they are removed from this hash.
-   */
-  nsTHashtable<nsTrimInt64HashKey> mUncachableBookmarks;
 };
 
 #endif // nsNavBookmarks_h_
--- a/toolkit/components/places/nsNavHistoryResult.cpp
+++ b/toolkit/components/places/nsNavHistoryResult.cpp
@@ -3503,36 +3503,48 @@ nsNavHistoryFolderResultNode::OnItemAdde
                                           nsIURI* aURI,
                                           const nsACString& aTitle,
                                           PRTime aDateAdded,
                                           const nsACString& aGUID,
                                           const nsACString& aParentGUID)
 {
   NS_ASSERTION(aParentFolder == mItemId, "Got wrong bookmark update");
 
+  RESTART_AND_RETURN_IF_ASYNC_PENDING();
+
+  {
+    uint32_t index;
+    nsNavHistoryResultNode* node = FindChildById(aItemId, &index);
+    // Bug 1097528.
+    // It's possible our result registered due to a previous notification, for
+    // example the Library left pane could have refreshed and replaced the
+    // right pane as a consequence. In such a case our contents are already
+    // up-to-date.  That's OK.
+    if (node)
+      return NS_OK;
+  }
+
   bool excludeItems = (mResult && mResult->mRootNode->mOptions->ExcludeItems()) ||
-                        (mParent && mParent->mOptions->ExcludeItems()) ||
-                        mOptions->ExcludeItems();
+                      (mParent && mParent->mOptions->ExcludeItems()) ||
+                      mOptions->ExcludeItems();
 
   // here, try to do something reasonable if the bookmark service gives us
   // a bogus index.
   if (aIndex < 0) {
     NS_NOTREACHED("Invalid index for item adding: <0");
     aIndex = 0;
   }
   else if (aIndex > mChildren.Count()) {
     if (!excludeItems) {
       // Something wrong happened while updating indexes.
       NS_NOTREACHED("Invalid index for item adding: greater than count");
     }
     aIndex = mChildren.Count();
   }
 
-  RESTART_AND_RETURN_IF_ASYNC_PENDING();
-
   nsresult rv;
 
   // Check for query URIs, which are bookmarks, but treated as containers
   // in results and views.
   bool isQuery = false;
   if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK) {
     NS_ASSERTION(aURI, "Got a null URI when we are a bookmark?!");
     nsAutoCString itemURISpec;
@@ -3601,33 +3613,33 @@ nsNavHistoryFolderResultNode::OnItemRemo
   // its list.
   if (mItemId == aItemId)
     return NS_OK;
 
   NS_ASSERTION(aParentFolder == mItemId, "Got wrong bookmark update");
 
   RESTART_AND_RETURN_IF_ASYNC_PENDING();
 
-  bool excludeItems = (mResult && mResult->mRootNode->mOptions->ExcludeItems()) ||
-                        (mParent && mParent->mOptions->ExcludeItems()) ||
-                        mOptions->ExcludeItems();
-
   // don't trust the index from the bookmark service, find it ourselves.  The
   // sorting could be different, or the bookmark services indices and ours might
   // be out of sync somehow.
   uint32_t index;
   nsNavHistoryResultNode* node = FindChildById(aItemId, &index);
+    // Bug 1097528.
+    // It's possible our result registered due to a previous notification, for
+    // example the Library left pane could have refreshed and replaced the
+    // right pane as a consequence. In such a case our contents are already
+    // up-to-date.  That's OK.
   if (!node) {
-    if (excludeItems)
-      return NS_OK;
-
-    NS_NOTREACHED("Removing item we don't have");
-    return NS_ERROR_FAILURE;
+    return NS_OK;
   }
 
+  bool excludeItems = (mResult && mResult->mRootNode->mOptions->ExcludeItems()) ||
+                        (mParent && mParent->mOptions->ExcludeItems()) ||
+                        mOptions->ExcludeItems();
   if ((node->IsURI() || node->IsSeparator()) && excludeItems) {
     // don't update items when we aren't displaying them, but we do need to
     // adjust everybody's bookmark indices to account for the removal
     ReindexRange(aIndex, INT32_MAX, -1);
     return NS_OK;
   }
 
   if (!StartIncrementalUpdate())
@@ -3849,34 +3861,52 @@ nsNavHistoryFolderResultNode::OnItemMove
                                           const nsACString& aOldParentGUID,
                                           const nsACString& aNewParentGUID)
 {
   NS_ASSERTION(aOldParent == mItemId || aNewParent == mItemId,
                "Got a bookmark message that doesn't belong to us");
 
   RESTART_AND_RETURN_IF_ASYNC_PENDING();
 
+  uint32_t index;
+  nsNavHistoryResultNode* node = FindChildById(aItemId, &index);
+  // Bug 1097528.
+  // It's possible our result registered due to a previous notification, for
+  // example the Library left pane could have refreshed and replaced the
+  // right pane as a consequence. In such a case our contents are already
+  // up-to-date.  That's OK.
+  if (node && aNewParent == mItemId && index == static_cast<uint32_t>(aNewIndex))
+    return NS_OK;
+  if (!node && aOldParent == mItemId)
+    return NS_OK;
+
+  bool excludeItems = (mResult && mResult->mRootNode->mOptions->ExcludeItems()) ||
+                      (mParent && mParent->mOptions->ExcludeItems()) ||
+                      mOptions->ExcludeItems();
+  if (node && excludeItems && (node->IsURI() || node->IsSeparator())) {
+    // Don't update items when we aren't displaying them.
+    return NS_OK;
+  }
+
   if (!StartIncrementalUpdate())
     return NS_OK; // entire container was refreshed for us
 
   if (aOldParent == aNewParent) {
     // getting moved within the same folder, we don't want to do a remove and
     // an add because that will lose your tree state.
 
     // adjust bookmark indices
     ReindexRange(aOldIndex + 1, INT32_MAX, -1);
     ReindexRange(aNewIndex, INT32_MAX, 1);
 
-    uint32_t index;
-    nsNavHistoryResultNode* node = FindChildById(aItemId, &index);
+    MOZ_ASSERT(node, "Can't find folder that is moving!");
     if (!node) {
-      NS_NOTREACHED("Can't find folder that is moving!");
       return NS_ERROR_FAILURE;
     }
-    NS_ASSERTION(index < uint32_t(mChildren.Count()), "Invalid index!");
+    MOZ_ASSERT(index < uint32_t(mChildren.Count()), "Invalid index!");
     node->mBookmarkIndex = aNewIndex;
 
     // adjust position
     EnsureItemPosition(index);
     return NS_OK;
   } else {
     // moving between two different folders, just do a remove and an add
     nsCOMPtr<nsIURI> itemURI;
--- a/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_restyle.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_restyle.js
@@ -10,16 +10,25 @@ add_task(function* test_searchEngine() {
   do_register_cleanup(() => Services.search.removeEngine(engine));
 
   let uri1 = NetUtil.newURI("http://s.example.com/search?q=Terms&client=1");
   let uri2 = NetUtil.newURI("http://s.example.com/search?q=Terms&client=2");
   yield promiseAddVisits({ uri: uri1, title: "Terms - SearchEngine Search" });
   addBookmark({ uri: uri2, title: "Terms - SearchEngine Search" });
 
   do_log_info("Past search terms should be styled, unless bookmarked");
+  Services.prefs.setBoolPref("browser.urlbar.restyleSearches", true);
   yield check_autocomplete({
     search: "term",
     matches: [ { uri: uri1, title: "Terms", searchEngine: "SearchEngine", style: ["favicon", "search"] },
                { uri: uri2, title: "Terms - SearchEngine Search", style: ["bookmark"] } ]
   });
 
+  do_log_info("Past search terms should not be styled if restyling is disabled");
+  Services.prefs.setBoolPref("browser.urlbar.restyleSearches", false);
+  yield check_autocomplete({
+    search: "term",
+    matches: [ { uri: uri1, title: "Terms - SearchEngine Search" },
+               { uri: uri2, title: "Terms - SearchEngine Search", style: ["bookmark"] } ]
+  });
+
   yield cleanup();
 });
--- a/toolkit/devtools/moz.build
+++ b/toolkit/devtools/moz.build
@@ -40,8 +40,12 @@ EXTRA_JS_MODULES.devtools += [
 
 EXTRA_JS_MODULES.devtools += [
     'Console.jsm',
     'DevToolsUtils.jsm',
     'LayoutHelpers.jsm',
     'Loader.jsm',
     'Require.jsm',
 ]
+
+EXTRA_JS_MODULES.devtools.server.actors += [
+    'server/actors/highlighter.css'
+]
rename from browser/themes/shared/devtools/highlighter.css
rename to toolkit/devtools/server/actors/highlighter.css
--- a/toolkit/devtools/server/actors/highlighter.js
+++ b/toolkit/devtools/server/actors/highlighter.js
@@ -17,17 +17,17 @@ Cu.import("resource://gre/modules/devtoo
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 // FIXME: add ":visited" and ":link" after bug 713106 is fixed
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
 const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"];
 const BOX_MODEL_SIDES = ["top", "right", "bottom", "left"];
 const SVG_NS = "http://www.w3.org/2000/svg";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-const HIGHLIGHTER_STYLESHEET_URI = "chrome://browser/skin/devtools/highlighter.css";
+const HIGHLIGHTER_STYLESHEET_URI = "resource://gre/modules/devtools/server/actors/highlighter.css";
 const HIGHLIGHTER_PICKED_TIMER = 1000;
 // How high is the nodeinfobar
 const NODE_INFOBAR_HEIGHT = 40; //px
 const NODE_INFOBAR_ARROW_SIZE = 15; // px
 // Width of boxmodelhighlighter guides
 const GUIDE_STROKE_WIDTH = 1;
 // The minimum distance a line should be before it has an arrow marker-end
 const ARROW_LINE_MIN_DISTANCE = 10;
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -172,16 +172,37 @@ BreakpointStore.prototype = {
           delete this._wholeLineBreakpoints[url][line];
           this._size--;
         }
       }
     }
   },
 
   /**
+   * Move the breakpoint to the new location.
+   *
+   * @param Object aBreakpoint
+   *        The breakpoint being moved. See `addBreakpoint` for a description of
+   *        its expected properties.
+   * @param Object aNewLocation
+   *        The location to move the breakpoint to. Properties:
+   *          - line
+   *          - column (optional; omission implies whole line breakpoint)
+   */
+  moveBreakpoint: function (aBreakpoint, aNewLocation) {
+    const existingBreakpoint = this.getBreakpoint(aBreakpoint);
+    this.removeBreakpoint(existingBreakpoint);
+
+    const { line, column } = aNewLocation;
+    existingBreakpoint.line = line;
+    existingBreakpoint.column = column;
+    this.addBreakpoint(existingBreakpoint);
+  },
+
+  /**
    * Get a breakpoint from the breakpoint store. Will throw an error if the
    * breakpoint is not found.
    *
    * @param Object aLocation
    *        The location of the breakpoint you are retrieving. It is an object
    *        with the following properties:
    *          - url
    *          - line
@@ -1421,222 +1442,291 @@ ThreadActor.prototype = {
    */
   _createAndStoreBreakpoint: function (aLocation) {
     // Add the breakpoint to the store for later reuse, in case it belongs to a
     // script that hasn't appeared yet.
     this.breakpointStore.addBreakpoint(aLocation);
     return this._setBreakpoint(aLocation);
   },
 
+
   /**
-   * Set a breakpoint using the jsdbg2 API. If the line on which the breakpoint
-   * is being set contains no code, then the breakpoint will slide down to the
-   * next line that has runnable code. In this case the server breakpoint cache
-   * will be updated, so callers that iterate over the breakpoint cache should
-   * take that into account.
+   * Get or create the BreakpointActor for the breakpoint at the given location.
+   *
+   * NB: This will override a pre-existing BreakpointActor's condition with
+   * the given the location's condition.
+   *
+   * @param Object location
+   *        The breakpoint location. See BreakpointStore.prototype.addBreakpoint
+   *        for more information.
+   * @returns BreakpointActor
+   */
+  _getOrCreateBreakpointActor: function (location) {
+    let actor;
+    const storedBp = this.breakpointStore.getBreakpoint(location);
+
+    if (storedBp.actor) {
+      actor = storedBp.actor;
+      actor.condition = location.condition;
+      return actor;
+    }
+
+    storedBp.actor = actor = new BreakpointActor(this, {
+      url: location.url,
+      line: location.line,
+      column: location.column,
+      condition: location.condition
+    });
+    this.threadLifetimePool.addActor(actor);
+    return actor;
+  },
+
+  /**
+   * Set breakpoints at the offsets closest to our target location's column.
+   *
+   * @param Array scripts
+   *        The set of Debugger.Script instances to consider.
+   * @param Object location
+   *        The target location.
+   * @param BreakpointActor actor
+   *        The BreakpointActor to handle hitting the breakpoints we set.
+   * @returns Object
+   *          The RDP response.
+   */
+  _setBreakpointAtColumn: function (scripts, location, actor) {
+    // Debugger.Script -> array of offset mappings
+    const scriptsAndOffsetMappings = new Map();
+
+    for (let script of scripts) {
+      this._findClosestOffsetMappings(location, script, scriptsAndOffsetMappings);
+    }
+
+    for (let [script, mappings] of scriptsAndOffsetMappings) {
+      for (let offsetMapping of mappings) {
+        script.setBreakpoint(offsetMapping.offset, actor);
+      }
+      actor.addScript(script, this);
+    }
+
+    return {
+      actor: actor.actorID
+    };
+  },
+
+  /**
+   * Find the scripts which contain offsets that are an entry point to the given
+   * line.
+   *
+   * @param Array scripts
+   *        The set of Debugger.Scripts to consider.
+   * @param Number line
+   *        The line we are searching for entry points into.
+   * @returns Array of objects of the form { script, offsets } where:
+   *          - script is a Debugger.Script
+   *          - offsets is an array of offsets that are entry points into the
+   *            given line.
+   */
+  _findEntryPointsForLine: function (scripts, line) {
+    const entryPoints = [];
+    for (let script of scripts) {
+      const offsets = script.getLineOffsets(line);
+      if (offsets.length) {
+        entryPoints.push({ script, offsets });
+      }
+    }
+    return entryPoints;
+  },
+
+  /**
+   * Find the first line that is associated with bytecode offsets, and is
+   * greater than or equal to the given start line.
+   *
+   * @param Array scripts
+   *        The set of Debugger.Script instances to consider.
+   * @param Number startLine
+   *        The target line.
+   * @return Object|null
+   *         If we can't find a line matching our constraints, return
+   *         null. Otherwise, return an object of the form:
+   *           {
+   *             line: Number,
+   *             entryPoints: [
+   *               { script: Debugger.Script, offsets: [offset, ...] },
+   *               ...
+   *             ]
+   *           }
+   */
+  _findNextLineWithOffsets: function (scripts, startLine) {
+    const maxLine = Math.max(...scripts.map(s => s.startLine + s.lineCount));
+
+    for (let line = startLine; line < maxLine; line++) {
+      const entryPoints = this._findEntryPointsForLine(scripts, line);
+      if (entryPoints.length) {
+        return { line, entryPoints };
+      }
+    }
+
+    return null;
+  },
+
+  /**
+   * Set a breakpoint using the Debugger API. If the line on which the
+   * breakpoint is being set contains no code, then the breakpoint will slide
+   * down to the next line that has runnable code. In this case the server
+   * breakpoint cache will be updated, so callers that iterate over the
+   * breakpoint cache should take that into account.
    *
    * @param object aLocation
    *        The location of the breakpoint (in the generated source, if source
    *        mapping).
    * @param Debugger.Script aOnlyThisScript [optional]
    *        If provided, only set breakpoints in this Debugger.Script, and
    *        nowhere else.
    */
   _setBreakpoint: function (aLocation, aOnlyThisScript=null) {
-    let location = {
+    const location = {
       url: aLocation.url,
       line: aLocation.line,
       column: aLocation.column,
       condition: aLocation.condition
     };
-
-    let actor;
-    let storedBp = this.breakpointStore.getBreakpoint(location);
-    if (storedBp.actor) {
-      actor = storedBp.actor;
-      actor.condition = location.condition;
-    } else {
-      storedBp.actor = actor = new BreakpointActor(this, {
-        url: location.url,
-        line: location.line,
-        column: location.column,
-        condition: location.condition
-      });
-      this.threadLifetimePool.addActor(actor);
-    }
-
-    // Find all scripts matching the given location
-    let scripts = this.dbg.findScripts(location);
-    if (scripts.length == 0) {
+    const actor = location.actor = this._getOrCreateBreakpointActor(location);
+    const scripts = this.dbg.findScripts({
+      url: location.url,
+      // Although we will automatically slide the breakpoint down to the first
+      // line with code when the requested line doesn't have any, we want to
+      // restrict the sliding to within functions that contain the requested
+      // line.
+      line: location.line
+    });
+
+    if (scripts.length === 0) {
       // Since we did not find any scripts to set the breakpoint on now, return
       // early. When a new script that matches this breakpoint location is
-      // introduced, the breakpoint actor will already be in the breakpoint store
-      // and will be set at that time.
-      return {
-        actor: actor.actorID
-      };
-    }
-
-   /**
-    * For each script, if the given line has at least one entry point, set a
-    * breakpoint on the bytecode offets for each of them.
-    */
-
-    // Debugger.Script -> array of offset mappings
-    let scriptsAndOffsetMappings = new Map();
-
-    for (let script of scripts) {
-      this._findClosestOffsetMappings(location,
-                                      script,
-                                      scriptsAndOffsetMappings);
-    }
-
-    if (scriptsAndOffsetMappings.size > 0) {
-      for (let [script, mappings] of scriptsAndOffsetMappings) {
-        if (aOnlyThisScript && script !== aOnlyThisScript) {
-          continue;
-        }
-
-        for (let offsetMapping of mappings) {
-          script.setBreakpoint(offsetMapping.offset, actor);
-        }
-        actor.addScript(script, this);
-      }
-
+      // introduced, the breakpoint actor will already be in the breakpoint
+      // store and the breakpoint will be set at that time. This is similar to
+      // GDB's "pending" breakpoints for shared libraries that aren't loaded
+      // yet.
       return {
         actor: actor.actorID
       };
     }
 
-   /**
-    * If we get here, no breakpoint was set. This is because the given line
-    * has no entry points, for example because it is empty. As a fallback
-    * strategy, we try to set the breakpoint on the smallest line greater
-    * than or equal to the given line that as at least one entry point.
-    */
-
-    // Find all innermost scripts matching the given location
-    scripts = this.dbg.findScripts({
-      url: aLocation.url,
-      line: aLocation.line,
-      innermost: true
-    });
-
-    /**
-     * For each innermost script, look for the smallest line greater than or
-     * equal to the given line that has one or more entry points. If found, set
-     * a breakpoint on the bytecode offset for each of its entry points.
-     */
-    let actualLocation;
-    let found = false;
-    for (let script of scripts) {
-      let offsets = script.getAllOffsets();
-
-      for (let line = location.line; line < offsets.length; ++line) {
-        if (offsets[line]) {
-          if (!aOnlyThisScript || script === aOnlyThisScript) {
-            for (let offset of offsets[line]) {
-              script.setBreakpoint(offset, actor);
-            }
-            actor.addScript(script, this);
-          }
-
-          if (!actualLocation) {
-            actualLocation = {
-              url: location.url,
-              line: line
-            };
-          }
-
-          found = true;
-          break;
-        }
-      }
-    }
-
-    if (found) {
-      let existingBp = this.breakpointStore.hasBreakpoint(actualLocation);
-
-      if (existingBp && existingBp.actor) {
-        /**
-         * We already have a breakpoint actor for the actual location, so actor
-         * we created earlier is now redundant. Delete it, update the breakpoint
-         * store, and return the actor for the actual location.
-         */
+    if (location.column) {
+      return this._setBreakpointAtColumn(scripts, location, actor);
+    }
+
+    // Select the first line that has offsets, and is greater than or equal to
+    // the requested line. Set breakpoints on each of the offsets that is an
+    // entry point to our selected line.
+
+    const result = this._findNextLineWithOffsets(scripts, location.line);
+    if (!result) {
+      return {
+        error: "noCodeAtLineColumn",
+        actor: actor.actorID
+      };
+    }
+
+    const { line, entryPoints } = result;
+    const actualLocation = line !== location.line
+      ? { url: location.url, line }
+      : undefined;
+
+    if (actualLocation) {
+      // Check whether we already have a breakpoint actor for the actual
+      // location. If we do have an existing actor, then the actor we created
+      // above is redundant and must be destroyed. If we do not have an existing
+      // actor, we need to update the breakpoint store with the new location.
+
+      const existingBreakpoint = this.breakpointStore.hasBreakpoint(actualLocation);
+      if (existingBreakpoint && existingBreakpoint.actor) {
         actor.onDelete();
         this.breakpointStore.removeBreakpoint(location);
         return {
-          actor: existingBp.actor.actorID,
-          actualLocation: actualLocation
+          actor: existingBreakpoint.actor.actorID,
+          actualLocation
         };
+      } else {
+        actor.location = actualLocation;
+        this.breakpointStore.moveBreakpoint(location, actualLocation);
       }
-
-      /**
-       * We don't have a breakpoint actor for the actual location yet. Instead
-       * or creating a new actor, reuse the actor we created earlier, and update
-       * the breakpoint store.
-       */
-      actor.location = actualLocation;
-      this.breakpointStore.addBreakpoint({
-        actor: actor,
-        url: actualLocation.url,
-        line: actualLocation.line,
-        column: actualLocation.column
-      });
-      this.breakpointStore.removeBreakpoint(location);
-      return {
-        actor: actor.actorID,
-        actualLocation: actualLocation
-      };
-    }
-
-    /**
-     * If we get here, no line matching the given line was found, so just fail
-     * epically.
-     */
+    }
+
+    this._setBreakpointOnEntryPoints(
+      actor,
+      aOnlyThisScript
+        ? entryPoints.filter(o => o.script === aOnlyThisScript)
+        : entryPoints
+    );
+
     return {
-      error: "noCodeAtLineColumn",
-      actor: actor.actorID
+      actor: actor.actorID,
+      actualLocation
     };
   },
 
   /**
+   * Set breakpoints on all the given entry points with the given
+   * BreakpointActor as the handler.
+   *
+   * @param BreakpointActor actor
+   *        The actor handling the breakpoint hits.
+   * @param Array entryPoints
+   *        An array of objects of the form `{ script, offsets }`.
+   */
+  _setBreakpointOnEntryPoints: function (actor, entryPoints) {
+    for (let { script, offsets } of entryPoints) {
+      for (let offset of offsets) {
+        script.setBreakpoint(offset, actor);
+      }
+      actor.addScript(script, this);
+    }
+  },
+
+  /**
    * Find all of the offset mappings associated with `aScript` that are closest
    * to `aTargetLocation`. If new offset mappings are found that are closer to
    * `aTargetOffset` than the existing offset mappings inside
    * `aScriptsAndOffsetMappings`, we empty that map and only consider the
-   * closest offset mappings. If there is no column in `aTargetLocation`, we add
-   * all offset mappings that are on the given line.
+   * closest offset mappings.
+   *
+   * In many cases, but not all, this method finds only one closest offset.
+   * Consider the following case, where multiple offsets will be found:
+   *
+   *     0         1         2         3
+   *     0123456789012345678901234567890
+   *    +-------------------------------
+   *   1|function f() {
+   *   2|  return g() + h();
+   *   3|}
+   *
+   * The Debugger reports three offsets on line 2 upon which we could set a
+   * breakpoint: the `return` statement at column 2, the call expression `g()`
+   * at column 9, and the call expression `h()` at column 15. (Careful readers
+   * will note that complete source location information isn't saved by
+   * SpiderMonkey's frontend, and we don't get an offset associated specifically
+   * with the `+` operation.)
+   *
+   * If our target location is line 2 column 12, the offset for the call to `g`
+   * is 3 columns to the left and the offset for the call to `h` is 3 columns to
+   * the right. Because they are equally close, we will return both offsets to
+   * have breakpoints set upon them.
    *
    * @param Object aTargetLocation
    *        An object of the form { url, line[, column] }.
    * @param Debugger.Script aScript
    *        The script in which we are searching for offsets.
    * @param Map aScriptsAndOffsetMappings
    *        A Map object which maps Debugger.Script instances to arrays of
    *        offset mappings. This is an out param.
    */
   _findClosestOffsetMappings: function (aTargetLocation,
                                         aScript,
                                         aScriptsAndOffsetMappings) {
-    // If we are given a column, we will try and break only at that location,
-    // otherwise we will break anytime we get on that line.
-
-    if (aTargetLocation.column == null) {
-      let offsetMappings = aScript.getLineOffsets(aTargetLocation.line)
-        .map(o => ({
-          line: aTargetLocation.line,
-          offset: o
-        }));
-      if (offsetMappings.length) {
-        aScriptsAndOffsetMappings.set(aScript, offsetMappings);
-      }
-      return;
-    }
-
     let offsetMappings = aScript.getAllColumnOffsets()
       .filter(({ lineNumber }) => lineNumber === aTargetLocation.line);
 
     // Attempt to find the current closest offset distance from the target
     // location by grabbing any offset mapping in the map by doing one iteration
     // and then breaking (they all have the same distance from the target
     // location).
     let closestDistance = Infinity;
--- a/toolkit/devtools/server/tests/unit/test_breakpoint-20.js
+++ b/toolkit/devtools/server/tests/unit/test_breakpoint-20.js
@@ -11,20 +11,20 @@ var gClient;
 var gTraceClient;
 var gThreadClient;
 
 Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
 
 function run_test()
 {
   initTestTracerServer();
-  gDebuggee = addTestGlobal("test-tracer-actor");
+  gDebuggee = addTestGlobal("test-breakpoints");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect(function() {
-    attachTestThread(gClient, "test-tracer-actor", testBreakpoint);
+    attachTestThread(gClient, "test-breakpoints", testBreakpoint);
   });
   do_test_pending();
 }
 
 const testBreakpoint = Task.async(function* (threadResponse, tabClient, threadClient, tabResponse) {
   evalSetupCode();
 
   // Load the test source once.
--- a/toolkit/devtools/server/tests/unit/test_breakpointstore.js
+++ b/toolkit/devtools/server/tests/unit/test_breakpointstore.js
@@ -11,16 +11,17 @@ function run_test()
   Cu.import("resource://gre/modules/jsdebugger.jsm");
   addDebuggerToGlobal(this);
 
   test_has_breakpoint();
   test_add_breakpoint();
   test_remove_breakpoint();
   test_find_breakpoints();
   test_duplicate_breakpoints();
+  test_move_breakpoint();
 }
 
 function test_has_breakpoint() {
   let bpStore = new BreakpointStore();
   let location = {
     url: "http://example.com/foo.js",
     line: 3
   };
@@ -175,8 +176,36 @@ function test_duplicate_breakpoints() {
     url: "http://example.com/foo.js",
     line: 15
   };
   bpStore.addBreakpoint(location);
   bpStore.addBreakpoint(location);
   do_check_eq(bpStore.size, 1, "We should have only 1 whole line breakpoint");
   bpStore.removeBreakpoint(location);
 }
+
+function test_move_breakpoint() {
+  let bpStore = new BreakpointStore();
+
+  let oldLocation = {
+    url: "http://example.com/foo.js",
+    line: 10
+  };
+
+  let newLocation = {
+    url: "http://example.com/foo.js",
+    line: 12
+  };
+
+  bpStore.addBreakpoint(oldLocation);
+  bpStore.moveBreakpoint(oldLocation, newLocation);
+
+  equal(bpStore.size, 1, "Moving a breakpoint maintains the correct size.");
+
+  let bp = bpStore.getBreakpoint(newLocation);
+  ok(bp, "We should be able to get a breakpoint at the new location.");
+  equal(bp.line, newLocation.line,
+        "We should get the moved line.");
+
+  equal(bpStore.hasBreakpoint({ url: "http://example.com/foo.js", line: 10 }),
+        null,
+        "And we shouldn't be able to get any BP at the old location.");
+}
--- a/toolkit/modules/Promise-backend.js
+++ b/toolkit/modules/Promise-backend.js
@@ -35,16 +35,19 @@ const STATUS_REJECTED = 2;
 // properties are inaccessible by other code, but provide enough protection to
 // avoid using them by mistake.
 const salt = Math.floor(Math.random() * 100);
 const N_INTERNALS = "{private:internals:" + salt + "}";
 
 const JS_HAS_SYMBOLS = typeof Symbol === "function";
 const ITERATOR_SYMBOL = JS_HAS_SYMBOLS ? Symbol.iterator : "@@iterator";
 
+// We use DOM Promise for scheduling the walker loop.
+const DOMPromise = Promise;
+
 /////// Warn-upon-finalization mechanism
 //
 // One of the difficult problems with promises is locating uncaught
 // rejections. We adopt the following strategy: if a promise is rejected
 // at the time of its garbage-collection *and* if the promise is at the
 // end of a promise chain (i.e. |thatPromise.then| has never been
 // called), then we print a warning.
 //
@@ -680,18 +683,17 @@ this.PromiseWalker = {
   },
 
   /**
    * Sets up the PromiseWalker loop to start on the next tick of the event loop
    */
   scheduleWalkerLoop: function()
   {
     this.walkerLoopScheduled = true;
-    Services.tm.currentThread.dispatch(this.walkerLoop,
-                                       Ci.nsIThread.DISPATCH_NORMAL);
+    DOMPromise.resolve().then(() => this.walkerLoop());
   },
 
   /**
    * Schedules the resolution or rejection handlers registered on the provided
    * promise for processing.
    *
    * @param aPromise
    *        Resolved or rejected promise whose handlers should be processed.  It
--- a/toolkit/mozapps/extensions/test/browser/browser_bug562797.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug562797.js
@@ -254,34 +254,36 @@ add_test(function() {
         if (event.target.location != "about:addons")
           return;
         gBrowser.removeEventListener("pageshow", arguments.callee, true);
 
         wait_for_view_load(gBrowser.contentWindow.wrappedJSObject, function(aManager) {
           info("Part 3");
           is_in_list(aManager, "addons://list/extension", true, false);
 
-          go_back(aManager);
+          executeSoon(() => go_back(aManager));
           gBrowser.addEventListener("pageshow", function() {
             gBrowser.removeEventListener("pageshow", arguments.callee, false);
             info("Part 4");
-            is(gBrowser.currentURI.spec, "http://example.com/", "Should be showing the webpage");
-            ok(!gBrowser.canGoBack, "Should not be able to go back");
-            ok(gBrowser.canGoForward, "Should be able to go forward");
+            executeSoon(() => executeSoon(function () {
+              is(gBrowser.currentURI.spec, "http://example.com/", "Should be showing the webpage");
+              ok(!gBrowser.canGoBack, "Should not be able to go back");
+              ok(gBrowser.canGoForward, "Should be able to go forward");
 
-            go_forward(aManager);
-            gBrowser.addEventListener("pageshow", function() {
-              gBrowser.removeEventListener("pageshow", arguments.callee, false);
-              wait_for_view_load(gBrowser.contentWindow.wrappedJSObject, function(aManager) {
-                info("Part 5");
-                is_in_list(aManager, "addons://list/extension", true, false);
+              go_forward(aManager);
+              gBrowser.addEventListener("pageshow", function() {
+                gBrowser.removeEventListener("pageshow", arguments.callee, false);
+                wait_for_view_load(gBrowser.contentWindow.wrappedJSObject, function(aManager) {
+                  info("Part 5");
+                  is_in_list(aManager, "addons://list/extension", true, false);
 
-                close_manager(aManager, run_next_test);
-              });
-            }, false);
+                  close_manager(aManager, run_next_test);
+                });
+              }, false);
+            }));
           }, false);
         });
       }, true);
     });
   }, false);
 });
 
 // Tests simple forward and back navigation and that the right heading and
@@ -433,17 +435,17 @@ add_test(function() {
           if (event.target.location != "about:addons")
             return;
           gBrowser.removeEventListener("pageshow", arguments.callee, false);
 
           wait_for_view_load(gBrowser.contentWindow.wrappedJSObject, function(aManager) {
             info("Part 3");
             is_in_list(aManager, "addons://list/plugin", false, true);
 
-            go_forward(aManager);
+            executeSoon(() => go_forward(aManager));
             gBrowser.addEventListener("pageshow", function(event) {
               if (event.target.location != "http://example.com/")
                 return;
               gBrowser.removeEventListener("pageshow", arguments.callee, false);
               info("Part 4");
 
               executeSoon(function() {
                 ok(gBrowser.canGoBack, "Should be able to go back");
--- a/toolkit/themes/linux/global/inContentUI.css
+++ b/toolkit/themes/linux/global/inContentUI.css
@@ -14,16 +14,23 @@
   -moz-appearance: none;
   padding: 18px;
   background-color: Window;
   background-image: /* Texture */
                     url("chrome://global/skin/inContentUI/background-texture.png");
   color: WindowText;
 }
 
+/* Use the new in-content colors for #contentAreaDownloadsView. After landing
+   of bug 989469 the colors can be moved to *|*:root */
+*|*#contentAreaDownloadsView {
+  background: #f1f1f1;
+  color: #424e5a;
+}
+
 html|html {
   font: message-box;
 }
 
 /* Content */
 *|*.main-content {
   /* Needed to allow the radius to clip the inner content, see bug 595656 */
   overflow: hidden;
--- a/toolkit/themes/osx/global/inContentUI.css
+++ b/toolkit/themes/osx/global/inContentUI.css
@@ -16,16 +16,23 @@
   -moz-appearance: none;
   padding: 18px;
   background-image: /* Texture */
                     url("chrome://global/skin/inContentUI/background-texture.png"),
                     /* Gradient */
                     linear-gradient(#ADB5C2, #BFC6D1);
 }
 
+/* Use the new in-content colors for #contentAreaDownloadsView. After landing
+   of bug 989469 the colors can be moved to *|*:root */
+*|*#contentAreaDownloadsView {
+  background: #f1f1f1;
+  color: #424e5a;
+}
+
 html|html {
   font: message-box;
 }
 
 /* Content */
 *|*.main-content {
   /* Needed to allow the radius to clip the inner content, see bug 595656 */
   overflow: hidden;
--- a/toolkit/themes/shared/in-content/common.inc.css
+++ b/toolkit/themes/shared/in-content/common.inc.css
@@ -72,16 +72,17 @@ xul|groupbox xul|label {
 
 xul|tabpanels {
   -moz-appearance: none;
   font-size: 1.25rem;
   line-height: 22px;
   border: none;
   padding: 0;
   background-color: transparent;
+  color: inherit;
 }
 
 xul|tabs {
   margin-bottom: 15px;
   border-top: 1px solid #c1c1c1;
   border-bottom: 1px solid #c1c1c1;
   background-color: #fbfbfb;
 }
--- a/toolkit/themes/windows/global/inContentUI.css
+++ b/toolkit/themes/windows/global/inContentUI.css
@@ -55,16 +55,23 @@ html|html {
                                            /* Third light beam */
                                            transparent 87%, rgba(255,255,255,0.2) 90%),
                       /* Texture */
                       url("chrome://global/skin/inContentUI/background-texture.png");
   }
 }
 %endif
 
+/* Use the new in-content colors for #contentAreaDownloadsView. After landing
+   of bug 989469 the colors can be moved to *|*:root */
+*|*#contentAreaDownloadsView {
+  background: #f1f1f1;
+  color: #424e5a;
+}
+
 /* Content */
 *|*.main-content {
   /* Needed to allow the radius to clip the inner content, see bug 595656 */
   overflow: hidden;
   background-color: rgba(255, 255, 255, 0.35);
   background-image: linear-gradient(rgba(255, 255, 255, 0),
                                     rgba(255, 255, 255, 0.75));
   border: 1px solid #C3CEDF;