Merge m-c to b2g-inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 13 Nov 2014 15:46:11 -0500
changeset 231824 a859d43e7e1c22ccd704815f9b53f0d83162abf3
parent 231823 3207096151fe56c4add4defb95a9d0999bdeb788 (current diff)
parent 231821 7f0d92595432f9c0a8e83c21f7a7ed4bd43d2b9d (diff)
child 231825 7c04369f39a8a1e54bd629fc4d4f040f79678fe9
push id7326
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:58:42 +0000
treeherdermozilla-aurora@d3a3b2a0f2f8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone36.0a1
Merge m-c to b2g-inbound. 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
--- 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/nsCCUncollectableMarker.cpp
+++ b/dom/base/nsCCUncollectableMarker.cpp
@@ -45,17 +45,17 @@ NS_IMPL_ISUPPORTS(nsCCUncollectableMarke
 
 /* static */
 nsresult
 nsCCUncollectableMarker::Init()
 {
   if (sInited) {
     return NS_OK;
   }
-  
+
   nsCOMPtr<nsIObserver> marker = new nsCCUncollectableMarker;
   NS_ENSURE_TRUE(marker, NS_ERROR_OUT_OF_MEMORY);
 
   nsCOMPtr<nsIObserverService> obs =
     mozilla::services::GetObserverService();
   if (!obs)
     return NS_ERROR_FAILURE;
 
@@ -196,17 +196,17 @@ MarkContentViewer(nsIContentViewer* aVie
         elm = win->GetExistingListenerManager();
         if (elm) {
           elm->MarkForCC();
         }
         static_cast<nsGlobalWindow*>(win.get())->UnmarkGrayTimers();
       }
     } else if (aPrepareForCC) {
       // Unfortunately we need to still mark user data just before running CC so
-      // that it has the right generation. 
+      // that it has the right generation.
       doc->PropertyTable(DOM_USER_DATA)->
         EnumerateAll(MarkUserData, &nsCCUncollectableMarker::sGeneration);
     }
   }
   if (doc) {
     nsPIDOMWindow* inner = doc->GetInnerWindow();
     if (inner) {
       inner->MarkUncollectableForCCGeneration(nsCCUncollectableMarker::sGeneration);
@@ -242,17 +242,17 @@ MarkSHEntry(nsISHEntry* aSHEntry, bool a
   nsCOMPtr<nsISHContainer> shCont = do_QueryInterface(aSHEntry);
   int32_t count;
   shCont->GetChildCount(&count);
   for (i = 0; i < count; ++i) {
     nsCOMPtr<nsISHEntry> childEntry;
     shCont->GetChildAt(i, getter_AddRefs(childEntry));
     MarkSHEntry(childEntry, aCleanupJS, aPrepareForCC);
   }
-  
+
 }
 
 void
 MarkDocShell(nsIDocShellTreeItem* aNode, bool aCleanupJS, bool aPrepareForCC)
 {
   nsCOMPtr<nsIDocShell> shell = do_QueryInterface(aNode);
   if (!shell) {
     return;
@@ -312,19 +312,19 @@ nsCCUncollectableMarker::Observe(nsISupp
       mozilla::services::GetObserverService();
     if (!obs)
       return NS_ERROR_FAILURE;
 
     // No need for kungFuDeathGrip here, yay observerservice!
     obs->RemoveObserver(this, "xpcom-shutdown");
     obs->RemoveObserver(this, "cycle-collector-begin");
     obs->RemoveObserver(this, "cycle-collector-forget-skippable");
-    
+
     sGeneration = 0;
-    
+
     return NS_OK;
   }
 
   NS_ASSERTION(!strcmp(aTopic, "cycle-collector-begin") ||
                !strcmp(aTopic, "cycle-collector-forget-skippable"), "wrong topic");
 
   // JS cleanup can be slow. Do it only if there has been a GC.
   bool cleanupJS =
@@ -360,17 +360,17 @@ nsCCUncollectableMarker::Observe(nsISupp
     do_GetService(NS_WINDOWWATCHER_CONTRACTID);
   if (ww) {
     rv = ww->GetWindowEnumerator(getter_AddRefs(windowList));
     NS_ENSURE_SUCCESS(rv, rv);
 
     MarkWindowList(windowList, cleanupJS, prepareForCC);
   }
 
-  nsCOMPtr<nsIAppShellService> appShell = 
+  nsCOMPtr<nsIAppShellService> appShell =
     do_GetService(NS_APPSHELLSERVICE_CONTRACTID);
   if (appShell) {
     nsCOMPtr<nsIXULWindow> hw;
     appShell->GetHiddenWindow(getter_AddRefs(hw));
     if (hw) {
       nsCOMPtr<nsIDocShell> shell;
       hw->GetDocShell(getter_AddRefs(shell));
       MarkDocShell(shell, cleanupJS, prepareForCC);
@@ -389,29 +389,66 @@ nsCCUncollectableMarker::Observe(nsISupp
 
 #ifdef MOZ_XUL
   nsXULPrototypeCache* xulCache = nsXULPrototypeCache::GetInstance();
   if (xulCache) {
     xulCache->MarkInCCGeneration(sGeneration);
   }
 #endif
 
-  static bool previousWasJSCleanup = false;
-  if (cleanupJS) {
-    nsContentUtils::UnmarkGrayJSListenersInCCGenerationDocuments(sGeneration);
-    MarkMessageManagers();
+  enum ForgetSkippableCleanupState
+  {
+    eInitial = 0,
+    eUnmarkJSEventListeners = 1,
+    eUnmarkMessageManagers = 2,
+    eUnmarkStrongObservers = 3,
+    eUnmarkJSHolders = 4,
+    eDone = 5
+  };
+
+  static_assert(eDone == NS_MAJOR_FORGET_SKIPPABLE_CALLS,
+                "There must be one forgetSkippable call per cleanup state.");
+
+  static uint32_t sFSState = eDone;
+  if (prepareForCC) {
+    sFSState = eDone;
+    return NS_OK;
+  }
 
-    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
-    static_cast<nsObserverService *>(obs.get())->UnmarkGrayStrongObservers();
+  if (cleanupJS) {
+    // After a GC we start clean up phases from the beginning,
+    // but we don't want to do the additional clean up phases here
+    // since we have done already plenty of gray unmarking while going through
+    // frame message managers and docshells.
+    sFSState = eInitial;
+    return NS_OK;
+  } else {
+    ++sFSState;
+  }
 
-    previousWasJSCleanup = true;
-  } else if (previousWasJSCleanup) {
-    previousWasJSCleanup = false;
-    if (!prepareForCC) {
+  switch(sFSState) {
+    case eUnmarkJSEventListeners: {
+      nsContentUtils::UnmarkGrayJSListenersInCCGenerationDocuments(sGeneration);
+      break;
+    }
+    case eUnmarkMessageManagers: {
+      MarkMessageManagers();
+      break;
+    }
+    case eUnmarkStrongObservers: {
+      nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+      static_cast<nsObserverService *>(obs.get())->UnmarkGrayStrongObservers();
+      break;
+    }
+    case eUnmarkJSHolders: {
       xpc_UnmarkSkippableJSHolders();
+      break;
+    }
+    default: {
+      break;
     }
   }
 
   return NS_OK;
 }
 
 struct TraceClosure
 {
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -439,21 +439,21 @@ nsDOMWindowUtils::SetDisplayPortMarginsF
     return NS_ERROR_INVALID_ARG;
   }
 
   if (content->GetCurrentDoc() != presShell->GetDocument()) {
     return NS_ERROR_INVALID_ARG;
   }
 
   // Note order change of arguments between our function signature and
-  // LayerMargin constructor.
-  LayerMargin displayportMargins(aTopMargin,
-                                 aRightMargin,
-                                 aBottomMargin,
-                                 aLeftMargin);
+  // ScreenMargin constructor.
+  ScreenMargin displayportMargins(aTopMargin,
+                                  aRightMargin,
+                                  aBottomMargin,
+                                  aLeftMargin);
 
   nsLayoutUtils::SetDisplayPortMargins(content, presShell, displayportMargins,
                                        aAlignmentX, aAlignmentY, aPriority);
 
   return NS_OK;
 }
 
 
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -1127,17 +1127,18 @@ nsGlobalWindow::nsGlobalWindow(nsGlobalW
     mSetOpenerWindowCalled(false),
 #endif
 #ifdef MOZ_B2G
     mNetworkUploadObserverEnabled(false),
     mNetworkDownloadObserverEnabled(false),
 #endif
     mCleanedUp(false),
     mDialogAbuseCount(0),
-    mAreDialogsEnabled(true)
+    mAreDialogsEnabled(true),
+    mCanSkipCCGeneration(0)
 {
   nsLayoutStatics::AddRef();
 
   // Initialize the PRCList (this).
   PR_INIT_CLIST(this);
 
   if (aOuterWindow) {
     // |this| is an inner window, add this inner window to the outer
@@ -1678,16 +1679,20 @@ static PLDHashOperator
 MarkXBLHandlers(nsXBLPrototypeHandler* aKey, JS::Heap<JSObject*>& aData, void* aClosure)
 {
   JS::ExposeObjectToActiveJS(aData);
   return PL_DHASH_NEXT;
 }
 
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(nsGlobalWindow)
   if (tmp->IsBlackForCC(false)) {
+    if (nsCCUncollectableMarker::InGeneration(tmp->mCanSkipCCGeneration)) {
+      return true;
+    }
+    tmp->mCanSkipCCGeneration = nsCCUncollectableMarker::sGeneration;
     if (tmp->mCachedXBLPrototypeHandlers) {
       tmp->mCachedXBLPrototypeHandlers->Enumerate(MarkXBLHandlers, nullptr);
     }
     if (EventListenerManager* elm = tmp->GetExistingListenerManager()) {
       elm->MarkForCC();
     }
     tmp->UnmarkGrayTimers();
     return true;
@@ -8813,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/base/nsGlobalWindow.h
+++ b/dom/base/nsGlobalWindow.h
@@ -1623,16 +1623,19 @@ protected:
 
   nsTArray<uint32_t> mEnabledSensors;
 
 #ifdef MOZ_WEBSPEECH
   // mSpeechSynthesis is only used on inner windows.
   nsRefPtr<mozilla::dom::SpeechSynthesis> mSpeechSynthesis;
 #endif
 
+  // This is the CC generation the last time we called CanSkip.
+  uint32_t mCanSkipCCGeneration;
+
   friend class nsDOMScriptableHelper;
   friend class nsDOMWindowUtils;
   friend class PostMessageEvent;
   friend class DesktopNotification;
 
   static WindowByIdTable* sWindowsById;
   static bool sWarnedAboutWindowInternal;
 };
--- a/dom/base/nsJSEnvironment.cpp
+++ b/dom/base/nsJSEnvironment.cpp
@@ -112,17 +112,17 @@ const size_t gStackSize = 8192;
 
 // If we haven't painted in 100ms, we allow for a longer GC budget
 #define NS_INTERSLICE_GC_BUDGET     40 // ms
 
 // The amount of time we wait between a request to CC (after GC ran)
 // and doing the actual CC.
 #define NS_CC_DELAY                 6000 // ms
 
-#define NS_CC_SKIPPABLE_DELAY       400 // ms
+#define NS_CC_SKIPPABLE_DELAY       250 // ms
 
 // Maximum amount of time that should elapse between incremental CC slices
 static const int64_t kICCIntersliceDelay = 32; // ms
 
 // Time budget for an incremental CC slice
 static const int64_t kICCSliceBudget = 5; // ms
 
 // Maximum total duration for an ICC
@@ -139,18 +139,16 @@ static const uint32_t kMaxICCDuration = 
 // Trigger a CC if the purple buffer exceeds this size when we check it.
 #define NS_CC_PURPLE_LIMIT          200
 
 #define JAVASCRIPT nsIProgrammingLanguage::JAVASCRIPT
 
 // Large value used to specify that a script should run essentially forever
 #define NS_UNLIMITED_SCRIPT_RUNTIME (0x40000000LL << 32)
 
-#define NS_MAJOR_FORGET_SKIPPABLE_CALLS 2
-
 // if you add statics here, add them to the list in StartupJSEnvironment
 
 static nsITimer *sGCTimer;
 static nsITimer *sShrinkGCBuffersTimer;
 static nsITimer *sCCTimer;
 static nsITimer *sICCTimer;
 static nsITimer *sFullGCTimer;
 static nsITimer *sInterSliceGCTimer;
@@ -1780,20 +1778,18 @@ nsJSContext::BeginCycleCollectionCallbac
   gCCStats.mSuspected = nsCycleCollector_suspectedCount();
 
   KillCCTimer();
 
   gCCStats.RunForgetSkippable();
 
   MOZ_ASSERT(!sICCTimer, "Tried to create a new ICC timer when one already existed.");
 
-  if (!sIncrementalCC) {
-    return;
-  }
-
+  // Create an ICC timer even if ICC is globally disabled, because we could be manually triggering
+  // an incremental collection, and we want to be sure to finish it.
   CallCreateInstance("@mozilla.org/timer;1", &sICCTimer);
   if (sICCTimer) {
     sICCTimer->InitWithFuncCallback(ICCTimerFired,
                                     nullptr,
                                     kICCIntersliceDelay,
                                     nsITimer::TYPE_REPEATING_SLACK);
   }
 }
@@ -2033,18 +2029,20 @@ CCTimerFired(nsITimer *aTimer, void *aCl
         return;
       }
     } else {
       // We are in the final timer fire and still meet the conditions for
       // triggering a CC. Let RunCycleCollectorSlice finish the current IGC, if
       // any because that will allow us to include the GC time in the CC pause.
       nsJSContext::RunCycleCollectorSlice();
     }
-  } else if ((sPreviousSuspectedCount + 100) <= suspected) {
-      // Only do a forget skippable if there are more than a few new objects.
+  } else if (((sPreviousSuspectedCount + 100) <= suspected) ||
+             (sCleanupsSinceLastGC < NS_MAJOR_FORGET_SKIPPABLE_CALLS)) {
+      // Only do a forget skippable if there are more than a few new objects
+      // or we're doing the initial forget skippables.
       FireForgetSkippable(suspected, false);
   }
 
   if (isLateTimerFire) {
     ccDelay = NS_CC_DELAY;
 
     // We have either just run the CC or decided we don't want to run the CC
     // next time, so kill the timer.
--- a/dom/base/nsJSEnvironment.h
+++ b/dom/base/nsJSEnvironment.h
@@ -31,16 +31,18 @@ namespace mozilla {
 template <class> class Maybe;
 struct CycleCollectorResults;
 }
 
 // The amount of time we wait between a request to GC (due to leaving
 // a page) and doing the actual GC.
 #define NS_GC_DELAY                 4000 // ms
 
+#define NS_MAJOR_FORGET_SKIPPABLE_CALLS 5
+
 class nsJSContext : public nsIScriptContext
 {
 public:
   nsJSContext(bool aGCOnDestruction, nsIScriptGlobalObject* aGlobalObject);
 
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(nsJSContext,
                                                          nsIScriptContext)
--- 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/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -176,17 +176,17 @@ interface nsIDOMWindowUtils : nsISupport
    * If both a displayport rect and displayport margins with corresponding base
    * rect are set with the same priority then the margins will take precendence.
    *
    * Specifying an alignment value will ensure that after the base rect has
    * been expanded by the displayport margins, it will be further expanded so
    * that each edge is located at a multiple of the "alignment" value.
    *
    * Note that both the margin values and alignment are treated as values in
-   * LayerPixels. Refer to layout/base/Units.h for a description of this unit.
+   * ScreenPixels. Refer to layout/base/Units.h for a description of this unit.
    * The base rect values are in app units.
    */
   void setDisplayPortMarginsForElement(in float aLeftMargin,
                                        in float aTopMargin,
                                        in float aRightMargin,
                                        in float aBottomMargin,
                                        in uint32_t aAlignmentX,
                                        in uint32_t aAlignmentY,
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -547,86 +547,16 @@ ContentChild::~ContentChild()
 {
 }
 
 NS_INTERFACE_MAP_BEGIN(ContentChild)
   NS_INTERFACE_MAP_ENTRY(nsIContentChild)
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
-#if defined(XP_MACOSX) && defined(MOZ_CONTENT_SANDBOX)
-static bool
-GetAppPaths(nsCString &aAppPath, nsCString &aAppBinaryPath)
-{
-  nsAutoCString appPath;
-  nsAutoCString appBinaryPath(
-    (CommandLine::ForCurrentProcess()->argv()[0]).c_str());
-
-  nsAutoCString::const_iterator start, end;
-  appBinaryPath.BeginReading(start);
-  appBinaryPath.EndReading(end);
-  if (RFindInReadable(NS_LITERAL_CSTRING(".app/Contents/MacOS/"), start, end)) {
-    end = start;
-    ++end; ++end; ++end; ++end;
-    appBinaryPath.BeginReading(start);
-    appPath.Assign(Substring(start, end));
-  } else {
-    return false;
-  }
-
-  nsCOMPtr<nsIFile> app, appBinary;
-  nsresult rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(appPath),
-                                true, getter_AddRefs(app));
-  if (NS_FAILED(rv)) {
-    return false;
-  }
-  rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(appBinaryPath),
-                       true, getter_AddRefs(appBinary));
-  if (NS_FAILED(rv)) {
-    return false;
-  }
-
-  bool isLink;
-  app->IsSymlink(&isLink);
-  if (isLink) {
-    app->GetNativeTarget(aAppPath);
-  } else {
-    app->GetNativePath(aAppPath);
-  }
-  appBinary->IsSymlink(&isLink);
-  if (isLink) {
-    appBinary->GetNativeTarget(aAppBinaryPath);
-  } else {
-    appBinary->GetNativePath(aAppBinaryPath);
-  }
-
-  return true;
-}
-
-void
-ContentChild::OnChannelConnected(int32_t aPid)
-{
-  nsAutoCString appPath, appBinaryPath;
-  if (!GetAppPaths(appPath, appBinaryPath)) {
-    MOZ_CRASH("Error resolving child process path");
-  }
-
-  MacSandboxInfo info;
-  info.type = MacSandboxType_Content;
-  info.appPath.Assign(appPath);
-  info.appBinaryPath.Assign(appBinaryPath);
-
-  nsAutoCString err;
-  if (!mozilla::StartMacSandbox(info, err)) {
-    NS_WARNING(err.get());
-    MOZ_CRASH("sandbox_init() failed");
-  }
-}
-#endif
-
 bool
 ContentChild::Init(MessageLoop* aIOLoop,
                    base::ProcessHandle aParentHandle,
                    IPC::Channel* aChannel)
 {
 #ifdef MOZ_WIDGET_GTK
     // sigh
     gtk_init(nullptr, nullptr);
@@ -1115,16 +1045,86 @@ ContentChild::CleanUpSandboxEnvironment(
     }
 
     // Don't check the return value as the directory will only have been created
     // if it has been used.
     unused << lowIntegrityTemp->Remove(/* aRecursive */ true);
 }
 #endif
 
+#if defined(XP_MACOSX) && defined(MOZ_CONTENT_SANDBOX)
+static bool
+GetAppPaths(nsCString &aAppPath, nsCString &aAppBinaryPath)
+{
+  nsAutoCString appPath;
+  nsAutoCString appBinaryPath(
+    (CommandLine::ForCurrentProcess()->argv()[0]).c_str());
+
+  nsAutoCString::const_iterator start, end;
+  appBinaryPath.BeginReading(start);
+  appBinaryPath.EndReading(end);
+  if (RFindInReadable(NS_LITERAL_CSTRING(".app/Contents/MacOS/"), start, end)) {
+    end = start;
+    ++end; ++end; ++end; ++end;
+    appBinaryPath.BeginReading(start);
+    appPath.Assign(Substring(start, end));
+  } else {
+    return false;
+  }
+
+  nsCOMPtr<nsIFile> app, appBinary;
+  nsresult rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(appPath),
+                                true, getter_AddRefs(app));
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+  rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(appBinaryPath),
+                       true, getter_AddRefs(appBinary));
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
+  bool isLink;
+  app->IsSymlink(&isLink);
+  if (isLink) {
+    app->GetNativeTarget(aAppPath);
+  } else {
+    app->GetNativePath(aAppPath);
+  }
+  appBinary->IsSymlink(&isLink);
+  if (isLink) {
+    appBinary->GetNativeTarget(aAppBinaryPath);
+  } else {
+    appBinary->GetNativePath(aAppBinaryPath);
+  }
+
+  return true;
+}
+
+static void
+StartMacOSContentSandbox()
+{
+  nsAutoCString appPath, appBinaryPath;
+  if (!GetAppPaths(appPath, appBinaryPath)) {
+    MOZ_CRASH("Error resolving child process path");
+  }
+
+  MacSandboxInfo info;
+  info.type = MacSandboxType_Content;
+  info.appPath.Assign(appPath);
+  info.appBinaryPath.Assign(appBinaryPath);
+
+  nsAutoCString err;
+  if (!mozilla::StartMacSandbox(info, err)) {
+    NS_WARNING(err.get());
+    MOZ_CRASH("sandbox_init() failed");
+  }
+}
+#endif
+
 bool
 ContentChild::RecvSetProcessSandbox()
 {
     // We may want to move the sandbox initialization somewhere else
     // at some point; see bug 880808.
 #if defined(MOZ_CONTENT_SANDBOX)
 #if defined(XP_LINUX)
 #if defined(MOZ_WIDGET_GONK) && ANDROID_VERSION >= 19
@@ -1141,16 +1141,18 @@ ContentChild::RecvSetProcessSandbox()
 #elif defined(XP_WIN)
     nsAdoptingString contentSandboxPref =
         Preferences::GetString("browser.tabs.remote.sandbox");
     if (contentSandboxPref.EqualsLiteral("on")
         || contentSandboxPref.EqualsLiteral("warn")) {
         mozilla::SandboxTarget::Instance()->StartSandbox();
         SetUpSandboxEnvironment();
     }
+#elif defined(XP_MACOSX)
+    StartMacOSContentSandbox();
 #endif
 #endif
     return true;
 }
 
 bool
 ContentChild::RecvSpeakerManagerNotify()
 {
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -71,20 +71,16 @@ public:
         nsCString version;
         nsCString buildID;
         nsCString name;
         nsCString UAName;
         nsCString ID;
         nsCString vendor;
     };
 
-#if defined(XP_MACOSX) && defined(MOZ_CONTENT_SANDBOX)
-    void OnChannelConnected(int32_t aPid);
-#endif
-
     bool Init(MessageLoop* aIOLoop,
               base::ProcessHandle aParentHandle,
               IPC::Channel* aChannel);
     void InitProcessAttributes();
     void InitXPCOM();
 
     static ContentChild* GetSingleton() {
         return sSingleton;
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -236,26 +236,26 @@ void
 TabChildBase::InitializeRootMetrics()
 {
   // Calculate a really simple resolution that we probably won't
   // be keeping, as well as putting the scroll offset back to
   // the top-left of the page.
   mLastRootMetrics.SetViewport(CSSRect(CSSPoint(), kDefaultViewportSize));
   mLastRootMetrics.mCompositionBounds = ParentLayerRect(
       ParentLayerPoint(),
-      ParentLayerSize(ViewAs<ParentLayerPixel>(mInnerSize, PixelCastJustification::ScreenToParentLayerForRoot)));
+      ParentLayerSize(ViewAs<ParentLayerPixel>(mInnerSize, PixelCastJustification::ScreenIsParentLayerForRoot)));
   mLastRootMetrics.SetZoom(mLastRootMetrics.CalculateIntrinsicScale());
   mLastRootMetrics.mDevPixelsPerCSSPixel = WebWidget()->GetDefaultScale();
-  // We use ScreenToLayerScale(1) below in order to turn the
+  // We use ParentLayerToLayerScale(1) below in order to turn the
   // async zoom amount into the gecko zoom amount.
   mLastRootMetrics.mCumulativeResolution =
-    mLastRootMetrics.GetZoom() / mLastRootMetrics.mDevPixelsPerCSSPixel * ScreenToLayerScale(1);
+    mLastRootMetrics.GetZoom() / mLastRootMetrics.mDevPixelsPerCSSPixel * ParentLayerToLayerScale(1);
   // This is the root layer, so the cumulative resolution is the same
   // as the resolution.
-  mLastRootMetrics.mResolution = mLastRootMetrics.mCumulativeResolution / LayoutDeviceToParentLayerScale(1);
+  mLastRootMetrics.mPresShellResolution = mLastRootMetrics.mCumulativeResolution.scale;
   mLastRootMetrics.SetScrollOffset(CSSPoint(0, 0));
 
   TABC_LOG("After InitializeRootMetrics, mLastRootMetrics is %s\n",
     Stringify(mLastRootMetrics).c_str());
 }
 
 void
 TabChildBase::SetCSSViewport(const CSSSize& aSize)
@@ -289,16 +289,30 @@ TabChildBase::GetPageSize(nsCOMPtr<nsIDo
   if (bodyDOMElement) {
     bodyWidth = bodyDOMElement->ScrollWidth();
     bodyHeight = bodyDOMElement->ScrollHeight();
   }
   return CSSSize(std::max(htmlWidth, bodyWidth),
                  std::max(htmlHeight, bodyHeight));
 }
 
+// For the root frame, Screen and ParentLayer pixels are interchangeable.
+// nsViewportInfo stores zoom values as CSSToScreenScale (because it's a
+// data structure specific to the root frame), while FrameMetrics and
+// ZoomConstraints store zoom values as CSSToParentLayerScale (because they
+// are not specific to the root frame). We define convenience functions for
+// converting between the two. As the name suggests, they should only be used
+// when dealing with the root frame!
+CSSToScreenScale ConvertScaleForRoot(CSSToParentLayerScale aScale) {
+  return ViewTargetAs<ScreenPixel>(aScale, PixelCastJustification::ScreenIsParentLayerForRoot);
+}
+CSSToParentLayerScale ConvertScaleForRoot(CSSToScreenScale aScale) {
+  return ViewTargetAs<ParentLayerPixel>(aScale, PixelCastJustification::ScreenIsParentLayerForRoot);
+}
+
 bool
 TabChildBase::HandlePossibleViewportChange(const ScreenIntSize& aOldScreenSize)
 {
   if (!IsAsyncPanZoomEnabled()) {
     return false;
   }
 
   TABC_LOG("HandlePossibleViewportChange aOldScreenSize=%s mInnerSize=%s\n",
@@ -311,18 +325,18 @@ TabChildBase::HandlePossibleViewportChan
   uint32_t presShellId = 0;
   mozilla::layers::FrameMetrics::ViewID viewId = FrameMetrics::NULL_SCROLL_ID;
   bool scrollIdentifiersValid = APZCCallbackHelper::GetOrCreateScrollIdentifiers(
         document->GetDocumentElement(), &presShellId, &viewId);
   if (scrollIdentifiersValid) {
     ZoomConstraints constraints(
       viewportInfo.IsZoomAllowed(),
       viewportInfo.IsDoubleTapZoomAllowed(),
-      viewportInfo.GetMinZoom(),
-      viewportInfo.GetMaxZoom());
+      ConvertScaleForRoot(viewportInfo.GetMinZoom()),
+      ConvertScaleForRoot(viewportInfo.GetMaxZoom()));
     DoUpdateZoomConstraints(presShellId,
                             viewId,
                             /* isRoot = */ true,
                             constraints);
   }
 
   float screenW = mInnerSize.width;
   float screenH = mInnerSize.height;
@@ -361,17 +375,17 @@ TabChildBase::HandlePossibleViewportChan
   if (oldScreenSize == ScreenIntSize()) {
     oldScreenSize = mInnerSize;
   }
 
   FrameMetrics metrics(mLastRootMetrics);
   metrics.SetViewport(CSSRect(CSSPoint(), viewport));
   metrics.mCompositionBounds = ParentLayerRect(
       ParentLayerPoint(),
-      ParentLayerSize(ViewAs<ParentLayerPixel>(mInnerSize, PixelCastJustification::ScreenToParentLayerForRoot)));
+      ParentLayerSize(ViewAs<ParentLayerPixel>(mInnerSize, PixelCastJustification::ScreenIsParentLayerForRoot)));
   metrics.SetRootCompositionSize(
       ScreenSize(mInnerSize) * ScreenToLayoutDeviceScale(1.0f) / metrics.mDevPixelsPerCSSPixel);
 
   // This change to the zoom accounts for all types of changes I can conceive:
   // 1. screen size changes, CSS viewport does not (pages with no meta viewport
   //    or a fixed size viewport)
   // 2. screen size changes, CSS viewport also does (pages with a device-width
   //    viewport)
@@ -391,39 +405,41 @@ TabChildBase::HandlePossibleViewportChan
   // by AsyncPanZoomController and causes a blurry flash.
   bool isFirstPaint;
   nsresult rv = utils->GetIsFirstPaint(&isFirstPaint);
   if (NS_FAILED(rv) || isFirstPaint) {
     // FIXME/bug 799585(?): GetViewportInfo() returns a defaultZoom of
     // 0.0 to mean "did not calculate a zoom".  In that case, we default
     // it to the intrinsic scale.
     if (viewportInfo.GetDefaultZoom().scale < 0.01f) {
-      viewportInfo.SetDefaultZoom(metrics.CalculateIntrinsicScale());
+      viewportInfo.SetDefaultZoom(ConvertScaleForRoot(metrics.CalculateIntrinsicScale()));
     }
 
     CSSToScreenScale defaultZoom = viewportInfo.GetDefaultZoom();
     MOZ_ASSERT(viewportInfo.GetMinZoom() <= defaultZoom &&
                defaultZoom <= viewportInfo.GetMaxZoom());
-    metrics.SetZoom(defaultZoom);
+    metrics.SetZoom(ConvertScaleForRoot(defaultZoom));
 
     metrics.SetScrollId(viewId);
   }
 
   if (nsIPresShell* shell = document->GetShell()) {
     if (nsPresContext* context = shell->GetPresContext()) {
       metrics.mDevPixelsPerCSSPixel = CSSToLayoutDeviceScale(
         (float)nsPresContext::AppUnitsPerCSSPixel() / context->AppUnitsPerDevPixel());
     }
   }
 
-  metrics.mCumulativeResolution = metrics.GetZoom() / metrics.mDevPixelsPerCSSPixel * ScreenToLayerScale(1);
+  metrics.mCumulativeResolution = metrics.GetZoom()
+                                / metrics.mDevPixelsPerCSSPixel
+                                * ParentLayerToLayerScale(1);
   // This is the root layer, so the cumulative resolution is the same
   // as the resolution.
-  metrics.mResolution = metrics.mCumulativeResolution / LayoutDeviceToParentLayerScale(1);
-  utils->SetResolution(metrics.mResolution.scale, metrics.mResolution.scale);
+  metrics.mPresShellResolution = metrics.mCumulativeResolution.scale;
+  utils->SetResolution(metrics.mPresShellResolution, metrics.mPresShellResolution);
 
   CSSSize scrollPort = metrics.CalculateCompositedSizeInCssPixels();
   utils->SetScrollPositionClampingScrollPortSize(scrollPort.width, scrollPort.height);
 
   // The call to GetPageSize forces a resize event to content, so we need to
   // make sure that we have the right CSS viewport and
   // scrollPositionClampingScrollPortSize set up before that happens.
 
@@ -435,35 +451,35 @@ TabChildBase::HandlePossibleViewportChan
   metrics.mScrollableRect = CSSRect(CSSPoint(), pageSize);
 
   // Calculate a display port _after_ having a scrollable rect because the
   // display port is clamped to the scrollable rect.
   metrics.SetDisplayPortMargins(APZCTreeManager::CalculatePendingDisplayPort(
     // The page must have been refreshed in some way such as a new document or
     // new CSS viewport, so we know that there's no velocity, acceleration, and
     // we have no idea how long painting will take.
-    metrics, ScreenPoint(0.0f, 0.0f), 0.0));
+    metrics, ParentLayerPoint(0.0f, 0.0f), 0.0));
   metrics.SetUseDisplayPortMargins();
 
   // Force a repaint with these metrics. This, among other things, sets the
   // displayport, so we start with async painting.
   mLastRootMetrics = ProcessUpdateFrame(metrics);
 
   if (viewportInfo.IsZoomAllowed() && scrollIdentifiersValid) {
     // If the CSS viewport is narrower than the screen (i.e. width <= device-width)
     // then we disable double-tap-to-zoom behaviour.
     bool allowDoubleTapZoom = (viewport.width > screenW / metrics.mDevPixelsPerCSSPixel.scale);
     if (allowDoubleTapZoom != viewportInfo.IsDoubleTapZoomAllowed()) {
       viewportInfo.SetAllowDoubleTapZoom(allowDoubleTapZoom);
 
       ZoomConstraints constraints(
         viewportInfo.IsZoomAllowed(),
         viewportInfo.IsDoubleTapZoomAllowed(),
-        viewportInfo.GetMinZoom(),
-        viewportInfo.GetMaxZoom());
+        ConvertScaleForRoot(viewportInfo.GetMinZoom()),
+        ConvertScaleForRoot(viewportInfo.GetMaxZoom()));
       DoUpdateZoomConstraints(presShellId,
                               viewId,
                               /* isRoot = */ true,
                               constraints);
     }
   }
 
   return true;
@@ -902,18 +918,18 @@ TabChild::Observe(nsISupports *aSubject,
         mContentDocumentIsDisplayed = true;
 
         // In some cases before-first-paint gets called before
         // RecvUpdateDimensions is called and therefore before we have an
         // mInnerSize value set. In such cases defer initializing the viewport
         // until we we get an inner size.
         if (HasValidInnerSize()) {
           InitializeRootMetrics();
-          utils->SetResolution(mLastRootMetrics.mResolution.scale,
-                               mLastRootMetrics.mResolution.scale);
+          utils->SetResolution(mLastRootMetrics.mPresShellResolution,
+                               mLastRootMetrics.mPresShellResolution);
           HandlePossibleViewportChange(mInnerSize);
         }
       }
     }
   }
 
   return NS_OK;
 }
--- a/dom/media/DOMMediaStream.cpp
+++ b/dom/media/DOMMediaStream.cpp
@@ -102,20 +102,17 @@ private:
   // These fields may only be accessed on the main thread
   DOMMediaStream* mStream;
 };
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(DOMMediaStream)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DOMMediaStream,
                                                 DOMEventTargetHelper)
-  if (tmp->mListener) {
-    // Make sure |mListener| cannot call back after |mTracks| is collected
-    tmp->mListener->Forget();
-  }
+  tmp->Destroy();
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mTracks)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsumersToKeepAlive)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DOMMediaStream,
                                                   DOMEventTargetHelper)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
--- a/dom/media/fmp4/gonk/GonkAudioDecoderManager.cpp
+++ b/dom/media/fmp4/gonk/GonkAudioDecoderManager.cpp
@@ -200,22 +200,18 @@ GonkAudioDecoderManager::Output(int64_t 
     }
   }
 
   return NS_OK;
 }
 
 void GonkAudioDecoderManager::ReleaseAudioBuffer() {
   if (mAudioBuffer) {
-    sp<MetaData> metaData = mAudioBuffer->meta_data();
-    int32_t index;
-    metaData->findInt32(android::MediaCodecProxy::kKeyBufferIndex, &index);
-    mAudioBuffer->release();
+    mDecoder->ReleaseMediaBuffer(mAudioBuffer);
     mAudioBuffer = nullptr;
-    mDecoder->releaseOutputBuffer(index);
   }
 }
 
 nsresult
 GonkAudioDecoderManager::Input(mp4_demuxer::MP4Sample* aSample)
 {
   if (mDecoder == nullptr) {
     ALOG("Decoder is not inited");
--- a/dom/media/fmp4/gonk/GonkVideoDecoderManager.cpp
+++ b/dom/media/fmp4/gonk/GonkVideoDecoderManager.cpp
@@ -17,16 +17,20 @@
 #include "stagefright/MediaBuffer.h"
 #include "stagefright/MetaData.h"
 #include "stagefright/MediaErrors.h"
 #include <stagefright/foundation/ADebug.h>
 #include <stagefright/foundation/AMessage.h>
 #include <stagefright/foundation/AString.h>
 #include <stagefright/foundation/ALooper.h>
 #include "mp4_demuxer/AnnexB.h"
+#include "GonkNativeWindow.h"
+#include "GonkNativeWindowClient.h"
+#include "mozilla/layers/GrallocTextureClient.h"
+#include "mozilla/layers/TextureClient.h"
 
 #define READ_OUTPUT_BUFFER_TIMEOUT_US  3000
 
 #define LOG_TAG "GonkVideoDecoderManager"
 #include <android/log.h>
 #define ALOG(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
 
 #ifdef PR_LOGGING
@@ -41,22 +45,24 @@ typedef android::MediaCodecProxy MediaCo
 
 namespace mozilla {
 enum {
   kNotifyCodecReserved = 'core',
   kNotifyCodecCanceled = 'coca',
 };
 
 GonkVideoDecoderManager::GonkVideoDecoderManager(
-                                  mozilla::layers::ImageContainer* aImageContainer,
-		                  const mp4_demuxer::VideoDecoderConfig& aConfig)
+                           mozilla::layers::ImageContainer* aImageContainer,
+		           const mp4_demuxer::VideoDecoderConfig& aConfig)
   : mImageContainer(aImageContainer)
   , mConfig(aConfig)
   , mReaderCallback(nullptr)
   , mColorConverterBufferSize(0)
+  , mNativeWindow(nullptr)
+  , mPendingVideoBuffersLock("GonkVideoDecoderManager::mPendingVideoBuffersLock")
 {
   NS_ASSERTION(!NS_IsMainThread(), "Should not be on main thread.");
   MOZ_ASSERT(mImageContainer);
   MOZ_COUNT_CTOR(GonkVideoDecoderManager);
   mVideoWidth  = aConfig.display_width;
   mVideoHeight = aConfig.display_height;
   mDisplayWidth = aConfig.display_width;
   mDisplayHeight = aConfig.display_height;
@@ -93,35 +99,42 @@ GonkVideoDecoderManager::Init(MediaDataD
 
   mReaderCallback = aCallback;
 
   if (mLooper.get() != nullptr) {
     return nullptr;
   }
   // Create ALooper
   mLooper = new ALooper;
-  mLooper->setName("GonkVideoDecoderManager");
+  mManagerLooper = new ALooper;
+  mManagerLooper->setName("GonkVideoDecoderManager");
   // Register AMessage handler to ALooper.
-  mLooper->registerHandler(mHandler);
+  mManagerLooper->registerHandler(mHandler);
   // Start ALooper thread.
-  if (mLooper->start() != OK) {
+  if (mLooper->start() != OK || mManagerLooper->start() != OK ) {
     return nullptr;
   }
   mDecoder = MediaCodecProxy::CreateByType(mLooper, "video/avc", false, true, mVideoListener);
+  uint32_t capability = MediaCodecProxy::kEmptyCapability;
+  if (mDecoder->getCapability(&capability) == OK && (capability &
+      MediaCodecProxy::kCanExposeGraphicBuffer)) {
+    mNativeWindow = new GonkNativeWindow();
+  }
+
   return mDecoder;
 }
 
 nsresult
 GonkVideoDecoderManager::CreateVideoData(int64_t aStreamOffset, VideoData **v)
 {
   *v = nullptr;
   int64_t timeUs;
   int32_t keyFrame;
 
-  if (!(mVideoBuffer != nullptr && mVideoBuffer->data() != nullptr)) {
+  if (mVideoBuffer == nullptr) {
     ALOG("Video Buffer is not valid!");
     return NS_ERROR_UNEXPECTED;
   }
 
   if (!mVideoBuffer->meta_data()->findInt64(kKeyTime, &timeUs)) {
     ALOG("Decoder did not return frame time");
     return NS_ERROR_UNEXPECTED;
   }
@@ -145,80 +158,107 @@ GonkVideoDecoderManager::CreateVideoData
     // and we will preserve the ratio of the crop rectangle as it
     // was reported relative to the picture size reported by the container.
     picture.x = (mPicture.x * mFrameInfo.mWidth) / mInitialFrame.width;
     picture.y = (mPicture.y * mFrameInfo.mHeight) / mInitialFrame.height;
     picture.width = (mFrameInfo.mWidth * mPicture.width) / mInitialFrame.width;
     picture.height = (mFrameInfo.mHeight * mPicture.height) / mInitialFrame.height;
   }
 
-  uint8_t *yuv420p_buffer = (uint8_t *)mVideoBuffer->data();
-  int32_t stride = mFrameInfo.mStride;
-  int32_t slice_height = mFrameInfo.mSliceHeight;
+  RefPtr<mozilla::layers::TextureClient> textureClient;
 
-  // Converts to OMX_COLOR_FormatYUV420Planar
-  if (mFrameInfo.mColorFormat != OMX_COLOR_FormatYUV420Planar) {
-    ARect crop;
-    crop.top = 0;
-    crop.bottom = mFrameInfo.mHeight;
-    crop.left = 0;
-    crop.right = mFrameInfo.mWidth;
-    yuv420p_buffer = GetColorConverterBuffer(mFrameInfo.mWidth, mFrameInfo.mHeight);
-    if (mColorConverter.convertDecoderOutputToI420(mVideoBuffer->data(),
-        mFrameInfo.mWidth, mFrameInfo.mHeight, crop, yuv420p_buffer) != OK) {
-        ReleaseVideoBuffer();
-        ALOG("Color conversion failed!");
-        return NS_ERROR_UNEXPECTED;
-    }
-      stride = mFrameInfo.mWidth;
-      slice_height = mFrameInfo.mHeight;
+  if ((mVideoBuffer->graphicBuffer().get())) {
+    textureClient = mNativeWindow->getTextureClientFromBuffer(mVideoBuffer->graphicBuffer().get());
   }
 
-  size_t yuv420p_y_size = stride * slice_height;
-  size_t yuv420p_u_size = ((stride + 1) / 2) * ((slice_height + 1) / 2);
-  uint8_t *yuv420p_y = yuv420p_buffer;
-  uint8_t *yuv420p_u = yuv420p_y + yuv420p_y_size;
-  uint8_t *yuv420p_v = yuv420p_u + yuv420p_u_size;
+  if (textureClient) {
+    GrallocTextureClientOGL* grallocClient = static_cast<GrallocTextureClientOGL*>(textureClient.get());
+    grallocClient->SetMediaBuffer(mVideoBuffer);
+    textureClient->SetRecycleCallback(GonkVideoDecoderManager::RecycleCallback, this);
+
+    *v = VideoData::Create(mInfo.mVideo,
+                          mImageContainer,
+                          aStreamOffset,
+                          timeUs,
+                          1, // We don't know the duration.
+                          textureClient,
+                          keyFrame,
+                          -1,
+                          picture);
 
-  // This is the approximate byte position in the stream.
-  int64_t pos = aStreamOffset;
+  } else {
+    if (!mVideoBuffer->data()) {
+      ALOG("No data in Video Buffer!");
+      return NS_ERROR_UNEXPECTED;
+    }
+    uint8_t *yuv420p_buffer = (uint8_t *)mVideoBuffer->data();
+    int32_t stride = mFrameInfo.mStride;
+    int32_t slice_height = mFrameInfo.mSliceHeight;
 
-  VideoData::YCbCrBuffer b;
-  b.mPlanes[0].mData = yuv420p_y;
-  b.mPlanes[0].mWidth = mFrameInfo.mWidth;
-  b.mPlanes[0].mHeight = mFrameInfo.mHeight;
-  b.mPlanes[0].mStride = stride;
-  b.mPlanes[0].mOffset = 0;
-  b.mPlanes[0].mSkip = 0;
+    // Converts to OMX_COLOR_FormatYUV420Planar
+    if (mFrameInfo.mColorFormat != OMX_COLOR_FormatYUV420Planar) {
+      ARect crop;
+      crop.top = 0;
+      crop.bottom = mFrameInfo.mHeight;
+      crop.left = 0;
+      crop.right = mFrameInfo.mWidth;
+      yuv420p_buffer = GetColorConverterBuffer(mFrameInfo.mWidth, mFrameInfo.mHeight);
+      if (mColorConverter.convertDecoderOutputToI420(mVideoBuffer->data(),
+          mFrameInfo.mWidth, mFrameInfo.mHeight, crop, yuv420p_buffer) != OK) {
+          ReleaseVideoBuffer();
+          ALOG("Color conversion failed!");
+          return NS_ERROR_UNEXPECTED;
+      }
+        stride = mFrameInfo.mWidth;
+        slice_height = mFrameInfo.mHeight;
+    }
 
-  b.mPlanes[1].mData = yuv420p_u;
-  b.mPlanes[1].mWidth = (mFrameInfo.mWidth + 1) / 2;
-  b.mPlanes[1].mHeight = (mFrameInfo.mHeight + 1) / 2;
-  b.mPlanes[1].mStride = (stride + 1) / 2;
-  b.mPlanes[1].mOffset = 0;
-  b.mPlanes[1].mSkip = 0;
+    size_t yuv420p_y_size = stride * slice_height;
+    size_t yuv420p_u_size = ((stride + 1) / 2) * ((slice_height + 1) / 2);
+    uint8_t *yuv420p_y = yuv420p_buffer;
+    uint8_t *yuv420p_u = yuv420p_y + yuv420p_y_size;
+    uint8_t *yuv420p_v = yuv420p_u + yuv420p_u_size;
+
+    // This is the approximate byte position in the stream.
+    int64_t pos = aStreamOffset;
+
+    VideoData::YCbCrBuffer b;
+    b.mPlanes[0].mData = yuv420p_y;
+    b.mPlanes[0].mWidth = mFrameInfo.mWidth;
+    b.mPlanes[0].mHeight = mFrameInfo.mHeight;
+    b.mPlanes[0].mStride = stride;
+    b.mPlanes[0].mOffset = 0;
+    b.mPlanes[0].mSkip = 0;
 
-  b.mPlanes[2].mData = yuv420p_v;
-  b.mPlanes[2].mWidth =(mFrameInfo.mWidth + 1) / 2;
-  b.mPlanes[2].mHeight = (mFrameInfo.mHeight + 1) / 2;
-  b.mPlanes[2].mStride = (stride + 1) / 2;
-  b.mPlanes[2].mOffset = 0;
-  b.mPlanes[2].mSkip = 0;
+    b.mPlanes[1].mData = yuv420p_u;
+    b.mPlanes[1].mWidth = (mFrameInfo.mWidth + 1) / 2;
+    b.mPlanes[1].mHeight = (mFrameInfo.mHeight + 1) / 2;
+    b.mPlanes[1].mStride = (stride + 1) / 2;
+    b.mPlanes[1].mOffset = 0;
+    b.mPlanes[1].mSkip = 0;
 
-  *v = VideoData::Create(
-      mInfo.mVideo,
-      mImageContainer,
-      pos,
-      timeUs,
-      1, // We don't know the duration.
-      b,
-      keyFrame,
-      -1,
-      picture);
-  ReleaseVideoBuffer();
+    b.mPlanes[2].mData = yuv420p_v;
+    b.mPlanes[2].mWidth =(mFrameInfo.mWidth + 1) / 2;
+    b.mPlanes[2].mHeight = (mFrameInfo.mHeight + 1) / 2;
+    b.mPlanes[2].mStride = (stride + 1) / 2;
+    b.mPlanes[2].mOffset = 0;
+    b.mPlanes[2].mSkip = 0;
+
+    *v = VideoData::Create(
+        mInfo.mVideo,
+        mImageContainer,
+        pos,
+        timeUs,
+        1, // We don't know the duration.
+        b,
+        keyFrame,
+        -1,
+        picture);
+    ReleaseVideoBuffer();
+  }
   return NS_OK;
 }
 
 bool
 GonkVideoDecoderManager::SetVideoFormat()
 {
   // read video metadata from MediaCodec
   sp<AMessage> codecFormat;
@@ -249,18 +289,20 @@ GonkVideoDecoderManager::SetVideoFormat(
     mFrameInfo.mSliceHeight = slice_height;
     mFrameInfo.mColorFormat = color_format;
 
     nsIntSize displaySize(width, height);
     if (!IsValidVideoRegion(mInitialFrame, mPicture, displaySize)) {
       ALOG("It is not a valid region");
       return false;
     }
+    return true;
   }
-  return true;
+  ALOG("Fail to get output format");
+  return false;
 }
 
 // Blocks until decoded sample is produced by the deoder.
 nsresult
 GonkVideoDecoderManager::Output(int64_t aStreamOffset,
                                 nsAutoPtr<MediaData>& aOutData)
 {
   aOutData = nullptr;
@@ -329,22 +371,18 @@ GonkVideoDecoderManager::Output(int64_t 
     }
   }
 
   return NS_OK;
 }
 
 void GonkVideoDecoderManager::ReleaseVideoBuffer() {
   if (mVideoBuffer) {
-    sp<MetaData> metaData = mVideoBuffer->meta_data();
-    int32_t index;
-    metaData->findInt32(android::MediaCodecProxy::kKeyBufferIndex, &index);
-    mVideoBuffer->release();
+    mDecoder->ReleaseMediaBuffer(mVideoBuffer);
     mVideoBuffer = nullptr;
-    mDecoder->releaseOutputBuffer(index);
   }
 }
 
 nsresult
 GonkVideoDecoderManager::Input(mp4_demuxer::MP4Sample* aSample)
 {
   if (mDecoder == nullptr) {
     ALOG("Decoder is not inited");
@@ -366,22 +404,26 @@ GonkVideoDecoderManager::Input(mp4_demux
   }
   return (rv == OK) ? NS_OK : NS_ERROR_FAILURE;
 }
 
 void
 GonkVideoDecoderManager::codecReserved()
 {
   sp<AMessage> format = new AMessage;
+  sp<Surface> surface;
+
   // Fixed values
   format->setString("mime", "video/avc");
   format->setInt32("width", mVideoWidth);
   format->setInt32("height", mVideoHeight);
-
-  mDecoder->configure(format, nullptr, nullptr, 0);
+  if (mNativeWindow != nullptr) {
+    surface = new Surface(mNativeWindow->getBufferQueue());
+  }
+  status_t err = mDecoder->configure(format, surface, nullptr, 0);
   mDecoder->Prepare();
   SetVideoFormat();
 
   if (mHandler != nullptr) {
     // post kNotifyCodecReserved to Looper thread.
     sp<AMessage> notify = new AMessage(kNotifyCodecReserved, mHandler->id());
     notify->post();
   }
@@ -394,17 +436,17 @@ GonkVideoDecoderManager::codecCanceled()
   if (mHandler != nullptr) {
     // post kNotifyCodecCanceled to Looper thread.
     sp<AMessage> notify = new AMessage(kNotifyCodecCanceled, mHandler->id());
     notify->post();
   }
 
 }
 
-// Called on GonkVideoDecoderManager::mLooper thread.
+// Called on GonkVideoDecoderManager::mManagerLooper thread.
 void
 GonkVideoDecoderManager::onMessageReceived(const sp<AMessage> &aMessage)
 {
   switch (aMessage->what()) {
     case kNotifyCodecReserved:
     {
       // Our decode may have acquired the hardware resource that it needs
       // to start. Notify the state machine to resume loading metadata.
@@ -413,16 +455,22 @@ GonkVideoDecoderManager::onMessageReceiv
     }
 
     case kNotifyCodecCanceled:
     {
       mReaderCallback->ReleaseMediaResources();
       break;
     }
 
+    case kNotifyPostReleaseBuffer:
+    {
+      ReleaseAllPendingVideoBuffersLocked();
+      break;
+    }
+
     default:
       TRESPASS();
       break;
   }
 }
 
 GonkVideoDecoderManager::MessageHandler::MessageHandler(GonkVideoDecoderManager *aManager)
   : mManager(aManager)
@@ -479,9 +527,56 @@ GonkVideoDecoderManager::GetColorConvert
   if (mColorConverterBufferSize != yuv420p_size) {
     mColorConverterBuffer = nullptr; // release the previous buffer first
     mColorConverterBuffer = new uint8_t[yuv420p_size];
     mColorConverterBufferSize = yuv420p_size;
   }
   return mColorConverterBuffer.get();
 }
 
+/* static */
+void
+GonkVideoDecoderManager::RecycleCallback(TextureClient* aClient, void* aClosure)
+{
+  GonkVideoDecoderManager* videoManager = static_cast<GonkVideoDecoderManager*>(aClosure);
+  GrallocTextureClientOGL* client = static_cast<GrallocTextureClientOGL*>(aClient);
+  aClient->ClearRecycleCallback();
+  videoManager->PostReleaseVideoBuffer(client->GetMediaBuffer());
+}
+
+void GonkVideoDecoderManager::PostReleaseVideoBuffer(
+                                android::MediaBuffer *aBuffer)
+{
+  {
+    MutexAutoLock autoLock(mPendingVideoBuffersLock);
+    if (aBuffer) {
+      mPendingVideoBuffers.append(aBuffer);
+    }
+  }
+  sp<AMessage> notify =
+            new AMessage(kNotifyPostReleaseBuffer, mHandler->id());
+  notify->post();
+
+}
+
+void GonkVideoDecoderManager::ReleaseAllPendingVideoBuffersLocked()
+{
+  Vector<android::MediaBuffer*> releasingVideoBuffers;
+  {
+    MutexAutoLock autoLock(mPendingVideoBuffersLock);
+    int size = mPendingVideoBuffers.length();
+    for (int i = 0; i < size; i++) {
+      releasingVideoBuffers.append(mPendingVideoBuffers[i]);
+    }
+    mPendingVideoBuffers.clear();
+  }
+  // Free all pending video buffers without holding mPendingVideoBuffersLock.
+  int size = releasingVideoBuffers.length();
+  for (int i = 0; i < size; i++) {
+    android::MediaBuffer *buffer;
+    buffer = releasingVideoBuffers[i];
+    mDecoder->ReleaseMediaBuffer(buffer);
+    buffer = nullptr;
+  }
+  releasingVideoBuffers.clear();
+}
+
 } // namespace mozilla
--- a/dom/media/fmp4/gonk/GonkVideoDecoderManager.h
+++ b/dom/media/fmp4/gonk/GonkVideoDecoderManager.h
@@ -2,50 +2,60 @@
 /* vim:set ts=2 sw=2 sts=2 et cindent: */
 /* 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/. */
 
 #if !defined(GonkVideoDecoderManager_h_)
 #define GonkVideoDecoderManager_h_
 
+#include <set>
 #include "MP4Reader.h"
 #include "nsRect.h"
 #include "GonkMediaDataDecoder.h"
 #include "mozilla/RefPtr.h"
 #include "I420ColorConverterHelper.h"
 #include "MediaCodecProxy.h"
 #include <stagefright/foundation/AHandler.h>
+#include "GonkNativeWindow.h"
+#include "GonkNativeWindowClient.h"
 
 using namespace android;
 
 namespace android {
 struct MOZ_EXPORT ALooper;
 class MOZ_EXPORT MediaBuffer;
 struct MOZ_EXPORT AString;
+class GonkNativeWindow;
 } // namespace android
 
 namespace mozilla {
 
+namespace layers {
+class TextureClient;
+} // namespace mozilla::layers
+
 class GonkVideoDecoderManager : public GonkDecoderManager {
 typedef android::MediaCodecProxy MediaCodecProxy;
+typedef mozilla::layers::TextureClient TextureClient;
 
 public:
   GonkVideoDecoderManager(mozilla::layers::ImageContainer* aImageContainer,
 		          const mp4_demuxer::VideoDecoderConfig& aConfig);
 
   ~GonkVideoDecoderManager();
 
   virtual android::sp<MediaCodecProxy> Init(MediaDataDecoderCallback* aCallback) MOZ_OVERRIDE;
 
   virtual nsresult Input(mp4_demuxer::MP4Sample* aSample) MOZ_OVERRIDE;
 
   virtual nsresult Output(int64_t aStreamOffset,
                           nsAutoPtr<MediaData>& aOutput) MOZ_OVERRIDE;
 
+  static void RecycleCallback(TextureClient* aClient, void* aClosure);
 private:
   struct FrameInfo
   {
     int32_t mWidth = 0;
     int32_t mHeight = 0;
     int32_t mStride = 0;
     int32_t mSliceHeight = 0;
     int32_t mColorFormat = 0;
@@ -97,16 +107,19 @@ private:
   void ReleaseVideoBuffer();
   uint8_t* GetColorConverterBuffer(int32_t aWidth, int32_t aHeight);
 
   // For codec resource management
   void codecReserved();
   void codecCanceled();
   void onMessageReceived(const sp<AMessage> &aMessage);
 
+  void ReleaseAllPendingVideoBuffersLocked();
+  void PostReleaseVideoBuffer(android::MediaBuffer *aBuffer);
+
   const mp4_demuxer::VideoDecoderConfig& mConfig;
   uint32_t mVideoWidth;
   uint32_t mVideoHeight;
   uint32_t mDisplayWidth;
   uint32_t mDisplayHeight;
   nsIntRect mPicture;
   nsIntSize mInitialFrame;
 
@@ -116,19 +129,32 @@ private:
 
   android::MediaBuffer* mVideoBuffer;
 
   MediaDataDecoderCallback*  mReaderCallback;
   MediaInfo mInfo;
   android::sp<VideoResourceListener> mVideoListener;
   android::sp<MessageHandler> mHandler;
   android::sp<ALooper> mLooper;
+  android::sp<ALooper> mManagerLooper;
   FrameInfo mFrameInfo;
 
   // color converter
   android::I420ColorConverterHelper mColorConverter;
   nsAutoArrayPtr<uint8_t> mColorConverterBuffer;
   size_t mColorConverterBufferSize;
+
+  android::sp<android::GonkNativeWindow> mNativeWindow;
+  enum {
+    kNotifyPostReleaseBuffer = 'nprb',
+  };
+
+  // Hold video's MediaBuffers that are released.
+  // The holded MediaBuffers are released soon after flush.
+  Vector<android::MediaBuffer*> mPendingVideoBuffers;
+  // The lock protects mPendingVideoBuffers.
+  Mutex mPendingVideoBuffersLock;
+
 };
 
 } // namespace mozilla
 
 #endif // GonkVideoDecoderManager_h_
--- a/dom/media/omx/MediaCodecProxy.cpp
+++ b/dom/media/omx/MediaCodecProxy.cpp
@@ -11,16 +11,17 @@
 #include <stagefright/foundation/ADebug.h>
 #include <stagefright/MetaData.h>
 #include "stagefright/MediaErrors.h"
 
 #define LOG_TAG "MediaCodecProxy"
 #include <android/log.h>
 #define ALOG(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
 #define TIMEOUT_DEQUEUE_INPUTBUFFER_MS 1000000ll
+
 namespace android {
 
 // General Template: MediaCodec::getOutputGraphicBufferFromIndex(...)
 template <typename T, bool InterfaceSupported>
 struct OutputGraphicBufferStub
 {
   static status_t GetOutputGraphicBuffer(T *aMediaCodec,
                                          size_t aIndex,
@@ -219,16 +220,17 @@ status_t
 MediaCodecProxy::start()
 {
   // Read Lock for mCodec
   RWLock::AutoRLock arl(mCodecLock);
 
   if (mCodec == nullptr) {
     return NO_INIT;
   }
+
   return mCodec->start();
 }
 
 status_t
 MediaCodecProxy::stop()
 {
   // Read Lock for mCodec
   RWLock::AutoRLock arl(mCodecLock);
@@ -555,18 +557,24 @@ status_t MediaCodecProxy::Output(MediaBu
   status_t err = dequeueOutputBuffer(&index, &offset, &size,
                                       &timeUs, &flags, aTimeoutUs);
   if (err != OK) {
     ALOG("Output returned %d", err);
     return err;
   }
 
   MediaBuffer *buffer;
+  sp<GraphicBuffer> graphicBuffer;
 
-  buffer = new MediaBuffer(mOutputBuffers.itemAt(index));
+  if (getOutputGraphicBufferFromIndex(index, &graphicBuffer) == OK &&
+      graphicBuffer != nullptr) {
+    buffer = new MediaBuffer(graphicBuffer);
+  } else {
+    buffer = new MediaBuffer(mOutputBuffers.itemAt(index));
+  }
   sp<MetaData> metaData = buffer->meta_data();
   metaData->setInt32(kKeyBufferIndex, index);
   metaData->setInt64(kKeyTime, timeUs);
   buffer->set_range(buffer->range_offset(), size);
   *aBuffer = buffer;
   if (flags & MediaCodec::BUFFER_FLAG_EOS) {
     return ERROR_END_OF_STREAM;
   }
@@ -587,9 +595,19 @@ void MediaCodecProxy::ReleaseMediaResour
 {
   if (mCodec.get()) {
     mCodec->stop();
     mCodec->release();
     mCodec.clear();
   }
 }
 
+void MediaCodecProxy::ReleaseMediaBuffer(MediaBuffer* aBuffer) {
+  if (aBuffer) {
+    sp<MetaData> metaData = aBuffer->meta_data();
+    int32_t index;
+    metaData->findInt32(kKeyBufferIndex, &index);
+    aBuffer->release();
+    releaseOutputBuffer(index);
+  }
+}
+
 } // namespace android
--- a/dom/media/omx/MediaCodecProxy.h
+++ b/dom/media/omx/MediaCodecProxy.h
@@ -3,21 +3,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef MEDIA_CODEC_PROXY_H
 #define MEDIA_CODEC_PROXY_H
 
 #include <nsString.h>
-
 #include <stagefright/MediaCodec.h>
 #include <stagefright/MediaBuffer.h>
 #include <utils/threads.h>
-
 #include "MediaResourceHandler.h"
 
 namespace android {
 // This class is intended to be a proxy for MediaCodec with codec resource
 // management. Basically user can use it like MediaCodec, but need to handle
 // the listener when Codec is reserved for Async case. A good example is
 // MediaCodecReader.cpp. Another useage is to use configure(), Prepare(),
 // Input(), and Output(). It is used in GonkVideoDecoderManager.cpp which
@@ -130,16 +128,18 @@ public:
   status_t Input(const uint8_t* aData, uint32_t aDataSize,
                  int64_t aTimestampUsecs, uint64_t flags);
   status_t Output(MediaBuffer** aBuffer, int64_t aTimeoutUs);
   bool Prepare();
   bool IsWaitingResources();
   bool IsDormantNeeded();
   void ReleaseMediaResources();
 
+  void ReleaseMediaBuffer(MediaBuffer* abuffer);
+
 protected:
   virtual ~MediaCodecProxy();
 
   // MediaResourceHandler::EventListener::resourceReserved()
   virtual void resourceReserved();
   // MediaResourceHandler::EventListener::resourceCanceled()
   virtual void resourceCanceled();
 
--- a/dom/media/omx/MediaCodecReader.cpp
+++ b/dom/media/omx/MediaCodecReader.cpp
@@ -6,29 +6,32 @@
 
 #include "MediaCodecReader.h"
 
 #include <OMX_IVCommon.h>
 
 #include <gui/Surface.h>
 #include <ICrypto.h>
 
+#include "GonkNativeWindow.h"
+
 #include <stagefright/foundation/ABuffer.h>
 #include <stagefright/foundation/ADebug.h>
 #include <stagefright/foundation/ALooper.h>
 #include <stagefright/foundation/AMessage.h>
 #include <stagefright/MediaBuffer.h>
 #include <stagefright/MediaCodec.h>
 #include <stagefright/MediaDefs.h>
 #include <stagefright/MediaExtractor.h>
 #include <stagefright/MediaSource.h>
 #include <stagefright/MetaData.h>
 #include <stagefright/Utils.h>
 
 #include "mozilla/TimeStamp.h"
+#include "mozilla/layers/GrallocTextureClient.h"
 
 #include "gfx2DGlue.h"
 
 #include "MediaStreamSource.h"
 #include "MediaTaskQueue.h"
 #include "MP3FrameParser.h"
 #include "nsThreadUtils.h"
 #include "ImageContainer.h"
@@ -107,16 +110,20 @@ MediaCodecReader::VideoResourceListener:
 void
 MediaCodecReader::VideoResourceListener::codecCanceled()
 {
   if (mReader) {
     mReader->codecCanceled(mReader->mVideoTrack);
   }
 }
 
+MediaCodecReader::TrackInputCopier::~TrackInputCopier()
+{
+}
+
 bool
 MediaCodecReader::TrackInputCopier::Copy(MediaBuffer* aSourceBuffer,
                                          sp<ABuffer> aCodecBuffer)
 {
   if (aSourceBuffer == nullptr ||
       aCodecBuffer == nullptr ||
       aSourceBuffer->range_length() > aCodecBuffer->capacity()) {
     return false;
@@ -125,28 +132,30 @@ MediaCodecReader::TrackInputCopier::Copy
   aCodecBuffer->setRange(0, aSourceBuffer->range_length());
   memcpy(aCodecBuffer->data(),
          (uint8_t*)aSourceBuffer->data() + aSourceBuffer->range_offset(),
          aSourceBuffer->range_length());
 
   return true;
 }
 
-MediaCodecReader::Track::Track()
-  : mSourceIsStopped(true)
+MediaCodecReader::Track::Track(Type type)
+  : mType(type)
+  , mSourceIsStopped(true)
   , mDurationLock("MediaCodecReader::Track::mDurationLock")
   , mDurationUs(INT64_C(0))
   , mInputIndex(sInvalidInputIndex)
   , mInputEndOfStream(false)
   , mOutputEndOfStream(false)
   , mSeekTimeUs(sInvalidTimestampUs)
   , mFlushed(false)
   , mDiscontinuity(false)
   , mTaskQueue(nullptr)
 {
+  MOZ_ASSERT(mType != kUnknown, "Should have a valid Track::Type");
 }
 
 // Append the value of |kKeyValidSamples| to the end of each vorbis buffer.
 // https://github.com/mozilla-b2g/platform_frameworks_av/blob/master/media/libstagefright/OMXCodec.cpp#L3128
 // https://github.com/mozilla-b2g/platform_frameworks_av/blob/master/media/libstagefright/NuMediaExtractor.cpp#L472
 bool
 MediaCodecReader::VorbisInputCopier::Copy(MediaBuffer* aSourceBuffer,
                                           sp<ABuffer> aCodecBuffer)
@@ -168,21 +177,23 @@ MediaCodecReader::VorbisInputCopier::Cop
          aSourceBuffer->range_length());
   memcpy(aCodecBuffer->data() + aSourceBuffer->range_length(),
          &numPageSamples, sizeof(numPageSamples));
 
   return true;
 }
 
 MediaCodecReader::AudioTrack::AudioTrack()
+  : Track(kAudio)
 {
 }
 
 MediaCodecReader::VideoTrack::VideoTrack()
-  : mWidth(0)
+  : Track(kVideo)
+  , mWidth(0)
   , mHeight(0)
   , mStride(0)
   , mSliceHeight(0)
   , mColorFormat(0)
   , mRotation(0)
 {
 }
 
@@ -274,23 +285,24 @@ MediaCodecReader::ProcessCachedDataTask:
   nsRefPtr<ReferenceKeeperRunnable<MediaCodecReader>> runnable(
       new ReferenceKeeperRunnable<MediaCodecReader>(mReader));
   mReader = nullptr;
   NS_DispatchToMainThread(runnable.get());
 }
 
 MediaCodecReader::MediaCodecReader(AbstractMediaDecoder* aDecoder)
   : MediaOmxCommonReader(aDecoder)
+  , mExtractor(nullptr)
+  , mIsWaitingResources(false)
+  , mTextureClientIndexesLock("MediaCodecReader::mTextureClientIndexesLock")
   , mColorConverterBufferSize(0)
-  , mExtractor(nullptr)
   , mParserMonitor("MediaCodecReader::mParserMonitor")
   , mParseDataFromCache(true)
   , mNextParserPosition(INT64_C(0))
   , mParsedDataLength(INT64_C(0))
-  , mIsWaitingResources(false)
 {
   mHandler = new MessageHandler(this);
   mVideoListener = new VideoResourceListener(this);
 }
 
 MediaCodecReader::~MediaCodecReader()
 {
   MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
@@ -321,21 +333,21 @@ MediaCodecReader::IsDormantNeeded()
   return mVideoTrack.mSource != nullptr;
 }
 
 void
 MediaCodecReader::ReleaseMediaResources()
 {
   // Stop the mSource because we are in the dormant state and the stop function
   // will rewind the mSource to the beginning of the stream.
-  if (mVideoTrack.mSource != nullptr) {
+  if (mVideoTrack.mSource != nullptr && !mVideoTrack.mSourceIsStopped) {
     mVideoTrack.mSource->stop();
     mVideoTrack.mSourceIsStopped = true;
   }
-  if (mAudioTrack.mSource != nullptr) {
+  if (mAudioTrack.mSource != nullptr && !mAudioTrack.mSourceIsStopped) {
     mAudioTrack.mSource->stop();
     mAudioTrack.mSourceIsStopped = true;
   }
   ReleaseCriticalResources();
 }
 
 void
 MediaCodecReader::Shutdown()
@@ -747,16 +759,92 @@ MediaCodecReader::ResetDecode()
     mVideoTrack.mTaskQueue->Flush();
     FlushCodecData(mVideoTrack);
     mVideoTrack.mDiscontinuity = true;
   }
 
   return MediaDecoderReader::ResetDecode();
 }
 
+void
+MediaCodecReader::TextureClientRecycleCallback(TextureClient* aClient,
+                                               void* aClosure)
+{
+  nsRefPtr<MediaCodecReader> reader = static_cast<MediaCodecReader*>(aClosure);
+  MOZ_ASSERT(reader, "reader should not be nullptr in TextureClientRecycleCallback()");
+
+  reader->TextureClientRecycleCallback(aClient);
+}
+
+void
+MediaCodecReader::TextureClientRecycleCallback(TextureClient* aClient)
+{
+  MOZ_ASSERT(aClient, "aClient should not be nullptr in RecycleCallback()");
+
+  size_t index = 0;
+
+  {
+    MutexAutoLock al(mTextureClientIndexesLock);
+
+    aClient->ClearRecycleCallback();
+
+    // aClient has been removed from mTextureClientIndexes by
+    // ReleaseAllTextureClients() on another thread.
+    if (!mTextureClientIndexes.Get(aClient, &index)) {
+      return;
+    }
+    mTextureClientIndexes.Remove(aClient);
+  }
+
+  if (mVideoTrack.mCodec != nullptr) {
+    mVideoTrack.mCodec->releaseOutputBuffer(index);
+  }
+}
+
+PLDHashOperator
+MediaCodecReader::ReleaseTextureClient(TextureClient* aClient,
+                                       size_t& aIndex,
+                                       void* aUserArg)
+{
+  nsRefPtr<MediaCodecReader> reader = static_cast<MediaCodecReader*>(aUserArg);
+  MOZ_ASSERT(reader, "reader should not be nullptr in ReleaseTextureClient()");
+
+  return reader->ReleaseTextureClient(aClient, aIndex);
+}
+
+PLDHashOperator
+MediaCodecReader::ReleaseTextureClient(TextureClient* aClient,
+                                       size_t& aIndex)
+{
+  MOZ_ASSERT(aClient, "TextureClient should be a valid pointer");
+
+  aClient->ClearRecycleCallback();
+
+  if (mVideoTrack.mCodec != nullptr) {
+    mVideoTrack.mCodec->releaseOutputBuffer(aIndex);
+  }
+
+  return PL_DHASH_REMOVE;
+}
+
+void
+MediaCodecReader::ReleaseAllTextureClients()
+{
+  MutexAutoLock al(mTextureClientIndexesLock);
+  MOZ_ASSERT(mTextureClientIndexes.Count(), "All TextureClients should be released already");
+
+  if (mTextureClientIndexes.Count() == 0) {
+    return;
+  }
+  printf_stderr("All TextureClients should be released already");
+
+  mTextureClientIndexes.Enumerate(MediaCodecReader::ReleaseTextureClient, this);
+  mTextureClientIndexes.Clear();
+}
+
 bool
 MediaCodecReader::DecodeVideoFrameSync(int64_t aTimeThreshold)
 {
   if (mVideoTrack.mCodec == nullptr || !mVideoTrack.mCodec->allocated() ||
       mVideoTrack.mOutputEndOfStream) {
     return false;
   }
 
@@ -790,99 +878,123 @@ MediaCodecReader::DecodeVideoFrameSync(i
         return false;
       }
     } else {
       return false;
     }
   }
 
   bool result = false;
-  if (bufferInfo.mBuffer != nullptr && bufferInfo.mSize > 0 &&
-      bufferInfo.mBuffer->data() != nullptr) {
-    uint8_t* yuv420p_buffer = bufferInfo.mBuffer->data();
-    int32_t stride = mVideoTrack.mStride;
-    int32_t slice_height = mVideoTrack.mSliceHeight;
-
-    // Converts to OMX_COLOR_FormatYUV420Planar
-    if (mVideoTrack.mColorFormat != OMX_COLOR_FormatYUV420Planar) {
-      ARect crop;
-      crop.top = 0;
-      crop.bottom = mVideoTrack.mHeight;
-      crop.left = 0;
-      crop.right = mVideoTrack.mWidth;
-
-      yuv420p_buffer = GetColorConverterBuffer(mVideoTrack.mWidth,
-                                               mVideoTrack.mHeight);
-      if (mColorConverter.convertDecoderOutputToI420(
-            bufferInfo.mBuffer->data(), mVideoTrack.mWidth, mVideoTrack.mHeight,
-            crop, yuv420p_buffer) != OK) {
-        mVideoTrack.mCodec->releaseOutputBuffer(bufferInfo.mIndex);
-        NS_WARNING("Unable to convert color format");
-        return false;
-      }
-
-      stride = mVideoTrack.mWidth;
-      slice_height = mVideoTrack.mHeight;
-    }
-
-    size_t yuv420p_y_size = stride * slice_height;
-    size_t yuv420p_u_size = ((stride + 1) / 2) * ((slice_height + 1) / 2);
-    uint8_t* yuv420p_y = yuv420p_buffer;
-    uint8_t* yuv420p_u = yuv420p_y + yuv420p_y_size;
-    uint8_t* yuv420p_v = yuv420p_u + yuv420p_u_size;
-
+  VideoData *v = nullptr;
+  RefPtr<TextureClient> textureClient;
+  sp<GraphicBuffer> graphicBuffer;
+  if (bufferInfo.mBuffer != nullptr) {
     // This is the approximate byte position in the stream.
     int64_t pos = mDecoder->GetResource()->Tell();
 
-    VideoData::YCbCrBuffer b;
-    b.mPlanes[0].mData = yuv420p_y;
-    b.mPlanes[0].mWidth = mVideoTrack.mWidth;
-    b.mPlanes[0].mHeight = mVideoTrack.mHeight;
-    b.mPlanes[0].mStride = stride;
-    b.mPlanes[0].mOffset = 0;
-    b.mPlanes[0].mSkip = 0;
+    if (mVideoTrack.mNativeWindow != nullptr &&
+        mVideoTrack.mCodec->getOutputGraphicBufferFromIndex(bufferInfo.mIndex, &graphicBuffer) == OK &&
+        graphicBuffer != nullptr) {
+      textureClient = mVideoTrack.mNativeWindow->getTextureClientFromBuffer(graphicBuffer.get());
+      v = VideoData::Create(mInfo.mVideo,
+                            mDecoder->GetImageContainer(),
+                            pos,
+                            bufferInfo.mTimeUs,
+                            1, // We don't know the duration.
+                            textureClient,
+                            bufferInfo.mFlags & MediaCodec::BUFFER_FLAG_SYNCFRAME,
+                            -1,
+                            mVideoTrack.mRelativePictureRect);
+    } else if (bufferInfo.mSize > 0 &&
+        bufferInfo.mBuffer->data() != nullptr) {
+      uint8_t* yuv420p_buffer = bufferInfo.mBuffer->data();
+      int32_t stride = mVideoTrack.mStride;
+      int32_t slice_height = mVideoTrack.mSliceHeight;
 
-    b.mPlanes[1].mData = yuv420p_u;
-    b.mPlanes[1].mWidth = (mVideoTrack.mWidth + 1) / 2;
-    b.mPlanes[1].mHeight = (mVideoTrack.mHeight + 1) / 2;
-    b.mPlanes[1].mStride = (stride + 1) / 2;
-    b.mPlanes[1].mOffset = 0;
-    b.mPlanes[1].mSkip = 0;
+      // Converts to OMX_COLOR_FormatYUV420Planar
+      if (mVideoTrack.mColorFormat != OMX_COLOR_FormatYUV420Planar) {
+        ARect crop;
+        crop.top = 0;
+        crop.bottom = mVideoTrack.mHeight;
+        crop.left = 0;
+        crop.right = mVideoTrack.mWidth;
+
+        yuv420p_buffer = GetColorConverterBuffer(mVideoTrack.mWidth,
+                                                 mVideoTrack.mHeight);
+        if (mColorConverter.convertDecoderOutputToI420(
+              bufferInfo.mBuffer->data(), mVideoTrack.mWidth, mVideoTrack.mHeight,
+              crop, yuv420p_buffer) != OK) {
+          mVideoTrack.mCodec->releaseOutputBuffer(bufferInfo.mIndex);
+          NS_WARNING("Unable to convert color format");
+          return false;
+        }
 
-    b.mPlanes[2].mData = yuv420p_v;
-    b.mPlanes[2].mWidth =(mVideoTrack.mWidth + 1) / 2;
-    b.mPlanes[2].mHeight = (mVideoTrack.mHeight + 1) / 2;
-    b.mPlanes[2].mStride = (stride + 1) / 2;
-    b.mPlanes[2].mOffset = 0;
-    b.mPlanes[2].mSkip = 0;
+        stride = mVideoTrack.mWidth;
+        slice_height = mVideoTrack.mHeight;
+      }
+
+      size_t yuv420p_y_size = stride * slice_height;
+      size_t yuv420p_u_size = ((stride + 1) / 2) * ((slice_height + 1) / 2);
+      uint8_t* yuv420p_y = yuv420p_buffer;
+      uint8_t* yuv420p_u = yuv420p_y + yuv420p_y_size;
+      uint8_t* yuv420p_v = yuv420p_u + yuv420p_u_size;
+
+      VideoData::YCbCrBuffer b;
+      b.mPlanes[0].mData = yuv420p_y;
+      b.mPlanes[0].mWidth = mVideoTrack.mWidth;
+      b.mPlanes[0].mHeight = mVideoTrack.mHeight;
+      b.mPlanes[0].mStride = stride;
+      b.mPlanes[0].mOffset = 0;
+      b.mPlanes[0].mSkip = 0;
 
-    VideoData *v = VideoData::Create(
-      mInfo.mVideo,
-      mDecoder->GetImageContainer(),
-      pos,
-      bufferInfo.mTimeUs,
-      1, // We don't know the duration.
-      b,
-      bufferInfo.mFlags & MediaCodec::BUFFER_FLAG_SYNCFRAME,
-      -1,
-      mVideoTrack.mRelativePictureRect);
+      b.mPlanes[1].mData = yuv420p_u;
+      b.mPlanes[1].mWidth = (mVideoTrack.mWidth + 1) / 2;
+      b.mPlanes[1].mHeight = (mVideoTrack.mHeight + 1) / 2;
+      b.mPlanes[1].mStride = (stride + 1) / 2;
+      b.mPlanes[1].mOffset = 0;
+      b.mPlanes[1].mSkip = 0;
+
+      b.mPlanes[2].mData = yuv420p_v;
+      b.mPlanes[2].mWidth =(mVideoTrack.mWidth + 1) / 2;
+      b.mPlanes[2].mHeight = (mVideoTrack.mHeight + 1) / 2;
+      b.mPlanes[2].mStride = (stride + 1) / 2;
+      b.mPlanes[2].mOffset = 0;
+      b.mPlanes[2].mSkip = 0;
+
+      v = VideoData::Create(mInfo.mVideo,
+                            mDecoder->GetImageContainer(),
+                            pos,
+                            bufferInfo.mTimeUs,
+                            1, // We don't know the duration.
+                            b,
+                            bufferInfo.mFlags & MediaCodec::BUFFER_FLAG_SYNCFRAME,
+                            -1,
+                            mVideoTrack.mRelativePictureRect);
+    }
 
     if (v) {
       result = true;
       VideoQueue().Push(v);
     } else {
       NS_WARNING("Unable to create VideoData");
     }
   }
 
   if ((bufferInfo.mFlags & MediaCodec::BUFFER_FLAG_EOS) ||
       (status == ERROR_END_OF_STREAM)) {
     VideoQueue().Finish();
   }
-  mVideoTrack.mCodec->releaseOutputBuffer(bufferInfo.mIndex);
+
+  if (v != nullptr && textureClient != nullptr && graphicBuffer != nullptr && result) {
+    MutexAutoLock al(mTextureClientIndexesLock);
+    mTextureClientIndexes.Put(textureClient.get(), bufferInfo.mIndex);
+    textureClient->SetRecycleCallback(MediaCodecReader::TextureClientRecycleCallback, this);
+  } else {
+    mVideoTrack.mCodec->releaseOutputBuffer(bufferInfo.mIndex);
+  }
 
   return result;
 }
 
 void
 MediaCodecReader::Seek(int64_t aTime,
                        int64_t aStartTime,
                        int64_t aEndTime,
@@ -976,16 +1088,17 @@ MediaCodecReader::ReleaseCriticalResourc
 {
   ResetDecode();
   // Before freeing a video codec, all video buffers needed to be released
   // even from graphics pipeline.
   VideoFrameContainer* videoframe = mDecoder->GetVideoFrameContainer();
   if (videoframe) {
     videoframe->ClearCurrentFrame();
   }
+  ReleaseAllTextureClients();
 
   DestroyMediaCodecs();
 
   ClearColorConverterBuffer();
 }
 
 void
 MediaCodecReader::ReleaseResources()
@@ -1000,27 +1113,29 @@ MediaCodecReader::ReleaseResources()
 bool
 MediaCodecReader::CreateLooper()
 {
   if (mLooper != nullptr) {
     return true;
   }
 
   // Create ALooper
-  mLooper = new ALooper;
-  mLooper->setName("MediaCodecReader");
+  sp<ALooper> looper = new ALooper;
+  looper->setName("MediaCodecReader::mLooper");
 
   // Register AMessage handler to ALooper.
-  mLooper->registerHandler(mHandler);
+  looper->registerHandler(mHandler);
 
   // Start ALooper thread.
-  if (mLooper->start() != OK) {
+  if (looper->start() != OK) {
     return false;
   }
 
+  mLooper = looper;
+
   return true;
 }
 
 void
 MediaCodecReader::DestroyLooper()
 {
   if (mLooper == nullptr) {
     return;
@@ -1197,79 +1312,92 @@ MediaCodecReader::CreateMediaCodec(sp<AL
     }
 
     if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_VORBIS)) {
       aTrack.mInputCopier = new VorbisInputCopier;
     } else {
       aTrack.mInputCopier = new TrackInputCopier;
     }
 
+    uint32_t capability = MediaCodecProxy::kEmptyCapability;
+    if (aTrack.mType == Track::kVideo &&
+        aTrack.mCodec->getCapability(&capability) == OK &&
+        (capability & MediaCodecProxy::kCanExposeGraphicBuffer) == MediaCodecProxy::kCanExposeGraphicBuffer) {
+      aTrack.mNativeWindow = new GonkNativeWindow();
+    }
+
     if (!aAsync) {
       // Pending configure() and start() to codecReserved() if the creation
       // should be asynchronous.
       if (!aTrack.mCodec->allocated() || !ConfigureMediaCodec(aTrack)){
         NS_WARNING("Couldn't create and configure MediaCodec synchronously");
-        aTrack.mCodec = nullptr;
+        DestroyMediaCodec(aTrack);
         return false;
       }
     }
   }
 
   return true;
 }
 
 bool
 MediaCodecReader::ConfigureMediaCodec(Track& aTrack)
 {
   if (aTrack.mSource != nullptr && aTrack.mCodec != nullptr) {
     if (!aTrack.mCodec->allocated()) {
       return false;
     }
 
+    sp<Surface> surface;
+    if (aTrack.mNativeWindow != nullptr) {
+      surface = new Surface(aTrack.mNativeWindow->getBufferQueue());
+    }
+
     sp<MetaData> sourceFormat = aTrack.mSource->getFormat();
     sp<AMessage> codecFormat;
     convertMetaDataToMessage(sourceFormat, &codecFormat);
 
     bool allpass = true;
-    if (allpass && aTrack.mCodec->configure(codecFormat, nullptr, nullptr, 0) != OK) {
+    if (allpass && aTrack.mCodec->configure(codecFormat, surface, nullptr, 0) != OK) {
       NS_WARNING("Couldn't configure MediaCodec");
       allpass = false;
     }
     if (allpass && aTrack.mCodec->start() != OK) {
       NS_WARNING("Couldn't start MediaCodec");
       allpass = false;
     }
     if (allpass && aTrack.mCodec->getInputBuffers(&aTrack.mInputBuffers) != OK) {
       NS_WARNING("Couldn't get input buffers from MediaCodec");
       allpass = false;
     }
     if (allpass && aTrack.mCodec->getOutputBuffers(&aTrack.mOutputBuffers) != OK) {
       NS_WARNING("Couldn't get output buffers from MediaCodec");
       allpass = false;
     }
     if (!allpass) {
-      aTrack.mCodec = nullptr;
+      DestroyMediaCodec(aTrack);
       return false;
     }
   }
 
   return true;
 }
 
 void
 MediaCodecReader::DestroyMediaCodecs()
 {
-  DestroyMediaCodecs(mAudioTrack);
-  DestroyMediaCodecs(mVideoTrack);
+  DestroyMediaCodec(mAudioTrack);
+  DestroyMediaCodec(mVideoTrack);
 }
 
 void
-MediaCodecReader::DestroyMediaCodecs(Track& aTrack)
+MediaCodecReader::DestroyMediaCodec(Track& aTrack)
 {
   aTrack.mCodec = nullptr;
+  aTrack.mNativeWindow = nullptr;
 }
 
 bool
 MediaCodecReader::TriggerIncrementalParser()
 {
   if (mMetaData == nullptr) {
     return false;
   }
@@ -1507,16 +1635,23 @@ MediaCodecReader::UpdateVideoInfo()
   mVideoTrack.mRelativePictureRect = relative_picture_rect;
 
   return true;
 }
 
 status_t
 MediaCodecReader::FlushCodecData(Track& aTrack)
 {
+  if (aTrack.mType == Track::kVideo) {
+    // TODO: if we do release TextureClient on a separate thread in the future,
+    // we will have to explicitly cleanup TextureClients which have been
+    // recycled through TextureClient::mRecycleCallback.
+    // Just NO-OP for now.
+  }
+
   if (aTrack.mSource == nullptr || aTrack.mCodec == nullptr ||
       !aTrack.mCodec->allocated()) {
     return UNKNOWN_ERROR;
   }
 
   status_t status = aTrack.mCodec->flush();
   aTrack.mFlushed = (status == OK);
   if (aTrack.mFlushed) {
@@ -1655,17 +1790,16 @@ MediaCodecReader::GetCodecOutputData(Tra
         break;
       } else {
         aTrack.mCodec->releaseOutputBuffer(info.mIndex);
       }
     } else if (status == INFO_OUTPUT_BUFFERS_CHANGED) {
       // Update output buffers of MediaCodec.
       if (aTrack.mCodec->getOutputBuffers(&aTrack.mOutputBuffers) != OK) {
         NS_WARNING("Couldn't get output buffers from MediaCodec");
-        aTrack.mCodec = nullptr;
         return UNKNOWN_ERROR;
       }
     }
 
     if (TimeStamp::Now() > aTimeout) {
       // Don't let this loop run for too long. Try it again later.
       return -EAGAIN;
     }
@@ -1706,17 +1840,27 @@ MediaCodecReader::EnsureCodecFormatParse
   size_t offset = 0;
   size_t size = 0;
   int64_t timeUs = INT64_C(0);
   uint32_t flags = 0;
   while ((status = aTrack.mCodec->dequeueOutputBuffer(&index, &offset, &size,
                      &timeUs, &flags)) != INFO_FORMAT_CHANGED) {
     if (status == OK) {
       aTrack.mCodec->releaseOutputBuffer(index);
+    } else if (status == INFO_OUTPUT_BUFFERS_CHANGED) {
+      // Update output buffers of MediaCodec.
+      if (aTrack.mCodec->getOutputBuffers(&aTrack.mOutputBuffers) != OK) {
+        NS_WARNING("Couldn't get output buffers from MediaCodec");
+        return false;
+      }
+    } else if (status != -EAGAIN && status != INVALID_OPERATION){
+      // FIXME: let INVALID_OPERATION pass?
+      return false; // something wrong!!!
     }
+
     status = FillCodecInputData(aTrack);
     if (status == INFO_FORMAT_CHANGED) {
       break;
     } else if (status != OK) {
       return false;
     }
   }
   return aTrack.mCodec->getOutputFormat(&format) == OK;
@@ -1771,32 +1915,32 @@ MediaCodecReader::onMessageReceived(cons
   }
 }
 
 // Called on Binder thread.
 void
 MediaCodecReader::codecReserved(Track& aTrack)
 {
   if (!ConfigureMediaCodec(aTrack)) {
-    DestroyMediaCodecs(aTrack);
+    DestroyMediaCodec(aTrack);
     return;
   }
 
   if (mHandler != nullptr) {
     // post kNotifyCodecReserved to MediaCodecReader::mLooper thread.
     sp<AMessage> notify = new AMessage(kNotifyCodecReserved, mHandler->id());
     notify->post();
   }
 }
 
 // Called on Binder thread.
 void
 MediaCodecReader::codecCanceled(Track& aTrack)
 {
-  DestroyMediaCodecs(aTrack);
+  DestroyMediaCodec(aTrack);
 
   if (mHandler != nullptr) {
     // post kNotifyCodecCanceled to MediaCodecReader::mLooper thread.
     sp<AMessage> notify = new AMessage(kNotifyCodecCanceled, mHandler->id());
     notify->post();
   }
 }
 
--- a/dom/media/omx/MediaCodecReader.h
+++ b/dom/media/omx/MediaCodecReader.h
@@ -10,39 +10,49 @@
 #include <utils/threads.h>
 
 #include <base/message_loop.h>
 
 #include <mozilla/CheckedInt.h>
 #include <mozilla/Mutex.h>
 #include <mozilla/Monitor.h>
 
+#include <nsDataHashtable.h>
+
 #include "MediaData.h"
 
 #include "I420ColorConverterHelper.h"
 #include "MediaCodecProxy.h"
 #include "MediaOmxCommonReader.h"
 
 namespace android {
 struct ALooper;
 struct AMessage;
 
 class MOZ_EXPORT MediaExtractor;
 class MOZ_EXPORT MetaData;
 class MOZ_EXPORT MediaBuffer;
 struct MOZ_EXPORT MediaSource;
+
+class GonkNativeWindow;
 } // namespace android
 
 namespace mozilla {
 
 class MediaTaskQueue;
 class MP3FrameParser;
 
+namespace layers {
+class TextureClient;
+} // namespace mozilla::layers
+
 class MediaCodecReader : public MediaOmxCommonReader
 {
+  typedef mozilla::layers::TextureClient TextureClient;
+
 public:
   MediaCodecReader(AbstractMediaDecoder* aDecoder);
   virtual ~MediaCodecReader();
 
   // Initializes the reader, returns NS_OK on success, or NS_ERROR_FAILURE
   // on failure.
   virtual nsresult Init(MediaDecoderReader* aCloneDonor);
 
@@ -96,30 +106,42 @@ public:
 
   virtual bool IsMediaSeekable() MOZ_OVERRIDE;
 
   virtual android::sp<android::MediaSource> GetAudioOffloadTrack();
 
 protected:
   struct TrackInputCopier
   {
+    virtual ~TrackInputCopier();
+
     virtual bool Copy(android::MediaBuffer* aSourceBuffer,
                       android::sp<android::ABuffer> aCodecBuffer);
   };
 
   struct Track
   {
-    Track();
+    enum Type
+    {
+      kUnknown = 0,
+      kAudio,
+      kVideo,
+    };
+
+    Track(Type type=kUnknown);
+
+    const Type mType;
 
     // pipeline parameters
     android::sp<android::MediaSource> mSource;
     bool mSourceIsStopped;
     android::sp<android::MediaCodecProxy> mCodec;
     android::Vector<android::sp<android::ABuffer> > mInputBuffers;
     android::Vector<android::sp<android::ABuffer> > mOutputBuffers;
+    android::sp<android::GonkNativeWindow> mNativeWindow;
 
     // pipeline copier
     nsAutoPtr<TrackInputCopier> mInputCopier;
 
     // media parameters
     Mutex mDurationLock; // mDurationUs might be read or updated from multiple
                          // threads.
     int64_t mDurationUs;
@@ -365,17 +387,17 @@ private:
 
   bool CreateMediaCodecs();
   static bool CreateMediaCodec(android::sp<android::ALooper>& aLooper,
                                Track& aTrack,
                                bool aAsync,
                                android::wp<android::MediaCodecProxy::CodecResourceListener> aListener);
   static bool ConfigureMediaCodec(Track& aTrack);
   void DestroyMediaCodecs();
-  static void DestroyMediaCodecs(Track& aTrack);
+  static void DestroyMediaCodec(Track& aTrack);
 
   bool CreateTaskQueues();
   void ShutdownTaskQueues();
   bool DecodeVideoFrameTask(int64_t aTimeThreshold);
   bool DecodeVideoFrameSync(int64_t aTimeThreshold);
   bool DecodeAudioDataTask();
   bool DecodeAudioDataSync();
   void DispatchVideoTask(int64_t aTimeThreshold);
@@ -391,39 +413,55 @@ private:
   }
 
   bool TriggerIncrementalParser();
 
   bool UpdateDuration();
   bool UpdateAudioInfo();
   bool UpdateVideoInfo();
 
-  static android::status_t FlushCodecData(Track& aTrack);
-  static android::status_t FillCodecInputData(Track& aTrack);
-  static android::status_t GetCodecOutputData(Track& aTrack,
-                                              CodecBufferInfo& aBuffer,
-                                              int64_t aThreshold,
-                                              const TimeStamp& aTimeout);
-  static bool EnsureCodecFormatParsed(Track& aTrack);
+  android::status_t FlushCodecData(Track& aTrack);
+  android::status_t FillCodecInputData(Track& aTrack);
+  android::status_t GetCodecOutputData(Track& aTrack,
+                                       CodecBufferInfo& aBuffer,
+                                       int64_t aThreshold,
+                                       const TimeStamp& aTimeout);
+  bool EnsureCodecFormatParsed(Track& aTrack);
 
   uint8_t* GetColorConverterBuffer(int32_t aWidth, int32_t aHeight);
   void ClearColorConverterBuffer();
 
   int64_t ProcessCachedData(int64_t aOffset,
                             nsRefPtr<SignalObject> aSignal);
   bool ParseDataSegment(const char* aBuffer,
                         uint32_t aLength,
                         int64_t aOffset);
 
+  static void TextureClientRecycleCallback(TextureClient* aClient,
+                                           void* aClosure);
+  void TextureClientRecycleCallback(TextureClient* aClient);
+
+  void ReleaseRecycledTextureClients();
+  static PLDHashOperator ReleaseTextureClient(TextureClient* aClient,
+                                              size_t& aIndex,
+                                              void* aUserArg);
+  PLDHashOperator ReleaseTextureClient(TextureClient* aClient,
+                                       size_t& aIndex);
+
+  void ReleaseAllTextureClients();
+
   android::sp<MessageHandler> mHandler;
   android::sp<VideoResourceListener> mVideoListener;
 
   android::sp<android::ALooper> mLooper;
   android::sp<android::MetaData> mMetaData;
 
+  Mutex mTextureClientIndexesLock;
+  nsDataHashtable<nsPtrHashKey<TextureClient>, size_t> mTextureClientIndexes;
+
   // media tracks
   AudioTrack mAudioTrack;
   VideoTrack mVideoTrack;
   AudioTrack mAudioOffloadTrack; // only Track::mSource is valid
 
   // color converter
   android::I420ColorConverterHelper mColorConverter;
   nsAutoArrayPtr<uint8_t> mColorConverterBuffer;
--- 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/dom/xul/XULDocument.cpp
+++ b/dom/xul/XULDocument.cpp
@@ -3574,33 +3574,33 @@ XULDocument::ExecuteScript(nsXULPrototyp
     NS_ENSURE_TRUE(scriptObject, NS_ERROR_UNEXPECTED);
 
     // Execute the precompiled script with the given version
     nsAutoMicroTask mt;
 
     // We're about to run script via JS::CloneAndExecuteScript, so we need an
     // AutoEntryScript. This is Gecko specific and not in any spec.
     AutoEntryScript aes(mScriptGlobalObject);
+    aes.TakeOwnershipOfErrorReporting();
     JSContext* cx = aes.cx();
     JS::Rooted<JSObject*> baseGlobal(cx, JS::CurrentGlobalOrNull(cx));
     NS_ENSURE_TRUE(nsContentUtils::GetSecurityManager()->ScriptAllowed(baseGlobal), NS_OK);
 
     JSAddonId* addonId = mCurrentPrototype ? MapURIToAddonID(mCurrentPrototype->GetURI()) : nullptr;
     JS::Rooted<JSObject*> global(cx, xpc::GetAddonScope(cx, baseGlobal, addonId));
     NS_ENSURE_TRUE(global, NS_ERROR_FAILURE);
 
     JS::ExposeObjectToActiveJS(global);
     xpc_UnmarkGrayScript(scriptObject);
     JSAutoCompartment ac(cx, global);
 
     // The script is in the compilation scope. Clone it into the target scope
-    // and execute it.
-    if (!JS::CloneAndExecuteScript(cx, global, scriptObject)) {
-        nsJSUtils::ReportPendingException(cx);
-    }
+    // and execute it. On failure, ~AutoScriptEntry will handle exceptions, so
+    // there is no need to manually check the return value.
+    JS::CloneAndExecuteScript(cx, global, scriptObject);
 
     return NS_OK;
 }
 
 
 nsresult
 XULDocument::CreateElementFromPrototype(nsXULPrototypeElement* aPrototype,
                                         Element** aResult,
--- a/dom/xul/test/chrome.ini
+++ b/dom/xul/test/chrome.ini
@@ -21,9 +21,10 @@ support-files =
 [test_bug445177.xul]
 [test_bug449457.xul]
 [test_bug468176.xul]
 [test_bug497875.xul]
 [test_bug583948.xul]
 [test_bug640158_overlay_persist.xul]
 [test_bug757137.xul]
 [test_bug775972.xul]
+[test_bug1070049_throw_from_script.xul]
 [test_import_xul_to_content.xul]
new file mode 100644
--- /dev/null
+++ b/dom/xul/test/test_bug1070049_throw_from_script.xul
@@ -0,0 +1,41 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1070049
+-->
+<window title="Mozilla Bug 1070049"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+  <!-- test code goes here -->
+  <script type="application/javascript">
+  <![CDATA[
+
+  /** Test for Bug 1070049 **/
+  SimpleTest.waitForExplicitFinish();
+  addLoadEvent(function() {
+    // Prevent the test from failing when the exception hits onerror.
+    SimpleTest.expectUncaughtException();
+
+    // Tell the test to expect exactly one console error with the given parameters,
+    // with SimpleTest.finish as a continuation function.
+    SimpleTest.monitorConsole(SimpleTest.finish, [{errorMessage: new RegExp('flimfniffle')}]);
+
+    // Schedule the console accounting (and continuation) to run next, right
+    // after we throw (below).
+    SimpleTest.executeSoon(SimpleTest.endMonitorConsole);
+
+    // Throw.
+    throw "flimfniffle";
+  });
+  ]]>
+  </script>
+
+  <!-- test results are displayed in the html:body -->
+  <body xmlns="http://www.w3.org/1999/xhtml">
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1070049"
+     target="_blank">Mozilla Bug 1070049</a>
+  </body>
+</window>
--- a/gfx/ipc/GfxMessageUtils.h
+++ b/gfx/ipc/GfxMessageUtils.h
@@ -736,28 +736,28 @@ struct ParamTraits<mozilla::layers::Fram
     WriteParam(aMsg, aParam.mDisplayPort);
     WriteParam(aMsg, aParam.mDisplayPortMargins);
     WriteParam(aMsg, aParam.mUseDisplayPortMargins);
     WriteParam(aMsg, aParam.mCriticalDisplayPort);
     WriteParam(aMsg, aParam.mCompositionBounds);
     WriteParam(aMsg, aParam.mRootCompositionSize);
     WriteParam(aMsg, aParam.mScrollId);
     WriteParam(aMsg, aParam.mScrollParentId);
-    WriteParam(aMsg, aParam.mResolution);
+    WriteParam(aMsg, aParam.mPresShellResolution);
     WriteParam(aMsg, aParam.mCumulativeResolution);
     WriteParam(aMsg, aParam.mZoom);
     WriteParam(aMsg, aParam.mDevPixelsPerCSSPixel);
     WriteParam(aMsg, aParam.mMayHaveTouchListeners);
     WriteParam(aMsg, aParam.mMayHaveTouchCaret);
     WriteParam(aMsg, aParam.mPresShellId);
     WriteParam(aMsg, aParam.mIsRoot);
     WriteParam(aMsg, aParam.mHasScrollgrab);
     WriteParam(aMsg, aParam.mUpdateScrollOffset);
     WriteParam(aMsg, aParam.mScrollGeneration);
-    WriteParam(aMsg, aParam.mTransformScale);
+    WriteParam(aMsg, aParam.mExtraResolution);
     WriteParam(aMsg, aParam.mBackgroundColor);
     WriteParam(aMsg, aParam.mDoSmoothScroll);
     WriteParam(aMsg, aParam.mSmoothScrollOffset);
     WriteParam(aMsg, aParam.GetContentDescription());
   }
 
   static bool ReadContentDescription(const Message* aMsg, void** aIter, paramType* aResult)
   {
@@ -777,28 +777,28 @@ struct ParamTraits<mozilla::layers::Fram
             ReadParam(aMsg, aIter, &aResult->mDisplayPort) &&
             ReadParam(aMsg, aIter, &aResult->mDisplayPortMargins) &&
             ReadParam(aMsg, aIter, &aResult->mUseDisplayPortMargins) &&
             ReadParam(aMsg, aIter, &aResult->mCriticalDisplayPort) &&
             ReadParam(aMsg, aIter, &aResult->mCompositionBounds) &&
             ReadParam(aMsg, aIter, &aResult->mRootCompositionSize) &&
             ReadParam(aMsg, aIter, &aResult->mScrollId) &&
             ReadParam(aMsg, aIter, &aResult->mScrollParentId) &&
-            ReadParam(aMsg, aIter, &aResult->mResolution) &&
+            ReadParam(aMsg, aIter, &aResult->mPresShellResolution) &&
             ReadParam(aMsg, aIter, &aResult->mCumulativeResolution) &&
             ReadParam(aMsg, aIter, &aResult->mZoom) &&
             ReadParam(aMsg, aIter, &aResult->mDevPixelsPerCSSPixel) &&
             ReadParam(aMsg, aIter, &aResult->mMayHaveTouchListeners) &&
             ReadParam(aMsg, aIter, &aResult->mMayHaveTouchCaret) &&
             ReadParam(aMsg, aIter, &aResult->mPresShellId) &&
             ReadParam(aMsg, aIter, &aResult->mIsRoot) &&
             ReadParam(aMsg, aIter, &aResult->mHasScrollgrab) &&
             ReadParam(aMsg, aIter, &aResult->mUpdateScrollOffset) &&
             ReadParam(aMsg, aIter, &aResult->mScrollGeneration) &&
-            ReadParam(aMsg, aIter, &aResult->mTransformScale) &&
+            ReadParam(aMsg, aIter, &aResult->mExtraResolution) &&
             ReadParam(aMsg, aIter, &aResult->mBackgroundColor) &&
             ReadParam(aMsg, aIter, &aResult->mDoSmoothScroll) &&
             ReadParam(aMsg, aIter, &aResult->mSmoothScrollOffset) &&
             ReadContentDescription(aMsg, aIter, aResult));
   }
 };
 
 template<>
--- a/gfx/layers/FrameMetrics.h
+++ b/gfx/layers/FrameMetrics.h
@@ -16,48 +16,16 @@
 #include "nsString.h"
 
 namespace IPC {
 template <typename T> struct ParamTraits;
 } // namespace IPC
 
 namespace mozilla {
 
-// The layer coordinates of the parent layer.
-// This can be arrived at in two ways:
-//   - Start with the CSS coordinates of the parent layer (note: NOT the
-//     CSS coordinates of the current layer, that will give you the wrong
-//     answer), multiply by the device scale and the resolutions of all
-//     layers from the root down to and including the parent.
-//   - Start with global screen coordinates and unapply all CSS and async
-//     transforms from the root down to and including the parent.
-// It's helpful to look at https://wiki.mozilla.org/Platform/GFX/APZ#Coordinate_systems
-// to get a picture of how the various coordinate systems relate to each other.
-struct ParentLayerPixel {};
-
-template<> struct IsPixel<ParentLayerPixel> : TrueType {};
-
-typedef gfx::MarginTyped<ParentLayerPixel> ParentLayerMargin;
-typedef gfx::PointTyped<ParentLayerPixel> ParentLayerPoint;
-typedef gfx::RectTyped<ParentLayerPixel> ParentLayerRect;
-typedef gfx::SizeTyped<ParentLayerPixel> ParentLayerSize;
-
-typedef gfx::IntMarginTyped<ParentLayerPixel> ParentLayerIntMargin;
-typedef gfx::IntPointTyped<ParentLayerPixel> ParentLayerIntPoint;
-typedef gfx::IntRectTyped<ParentLayerPixel> ParentLayerIntRect;
-typedef gfx::IntSizeTyped<ParentLayerPixel> ParentLayerIntSize;
-
-typedef gfx::ScaleFactor<CSSPixel, ParentLayerPixel> CSSToParentLayerScale;
-typedef gfx::ScaleFactor<LayoutDevicePixel, ParentLayerPixel> LayoutDeviceToParentLayerScale;
-typedef gfx::ScaleFactor<ScreenPixel, ParentLayerPixel> ScreenToParentLayerScale;
-
-typedef gfx::ScaleFactor<ParentLayerPixel, LayerPixel> ParentLayerToLayerScale;
-typedef gfx::ScaleFactor<ParentLayerPixel, ScreenPixel> ParentLayerToScreenScale;
-
-
 namespace layers {
 
 /**
  * The viewport and displayport metrics for the painted frame at the
  * time of a layer-tree transaction.  These metrics are especially
  * useful for shadow layers, because the metrics values are updated
  * atomically with new pixels.
  */
@@ -71,19 +39,18 @@ public:
                                         // will begin at.
   static const FrameMetrics sNullMetrics;   // We often need an empty metrics
 
   FrameMetrics()
     : mCompositionBounds(0, 0, 0, 0)
     , mDisplayPort(0, 0, 0, 0)
     , mCriticalDisplayPort(0, 0, 0, 0)
     , mScrollableRect(0, 0, 0, 0)
-    , mResolution(1)
+    , mPresShellResolution(1)
     , mCumulativeResolution(1)
-    , mTransformScale(1)
     , mDevPixelsPerCSSPixel(1)
     , mMayHaveTouchListeners(false)
     , mMayHaveTouchCaret(false)
     , mIsRoot(false)
     , mHasScrollgrab(false)
     , mScrollId(NULL_SCROLL_ID)
     , mScrollParentId(NULL_SCROLL_ID)
     , mScrollOffset(0, 0)
@@ -92,45 +59,47 @@ public:
     , mScrollGeneration(0)
     , mDoSmoothScroll(false)
     , mSmoothScrollOffset(0, 0)
     , mRootCompositionSize(0, 0)
     , mDisplayPortMargins(0, 0, 0, 0)
     , mUseDisplayPortMargins(false)
     , mPresShellId(-1)
     , mViewport(0, 0, 0, 0)
+    , mExtraResolution(1)
     , mBackgroundColor(0, 0, 0, 0)
   {
   }
 
   // Default copy ctor and operator= are fine
 
   bool operator==(const FrameMetrics& aOther) const
   {
     return mCompositionBounds.IsEqualEdges(aOther.mCompositionBounds) &&
            mRootCompositionSize == aOther.mRootCompositionSize &&
            mDisplayPort.IsEqualEdges(aOther.mDisplayPort) &&
            mDisplayPortMargins == aOther.mDisplayPortMargins &&
            mUseDisplayPortMargins == aOther.mUseDisplayPortMargins &&
            mCriticalDisplayPort.IsEqualEdges(aOther.mCriticalDisplayPort) &&
            mViewport.IsEqualEdges(aOther.mViewport) &&
            mScrollableRect.IsEqualEdges(aOther.mScrollableRect) &&
-           mResolution == aOther.mResolution &&
+           mPresShellResolution == aOther.mPresShellResolution &&
            mCumulativeResolution == aOther.mCumulativeResolution &&
            mDevPixelsPerCSSPixel == aOther.mDevPixelsPerCSSPixel &&
            mMayHaveTouchListeners == aOther.mMayHaveTouchListeners &&
            mMayHaveTouchCaret == aOther.mMayHaveTouchCaret &&
            mPresShellId == aOther.mPresShellId &&
            mIsRoot == aOther.mIsRoot &&
            mScrollId == aOther.mScrollId &&
            mScrollParentId == aOther.mScrollParentId &&
            mScrollOffset == aOther.mScrollOffset &&
            mSmoothScrollOffset == aOther.mSmoothScrollOffset &&
            mHasScrollgrab == aOther.mHasScrollgrab &&
            mUpdateScrollOffset == aOther.mUpdateScrollOffset &&
+           mExtraResolution == aOther.mExtraResolution &&
            mBackgroundColor == aOther.mBackgroundColor &&
            mDoSmoothScroll == aOther.mDoSmoothScroll;
   }
   bool operator!=(const FrameMetrics& aOther) const
   {
     return !operator==(aOther);
   }
 
@@ -147,29 +116,37 @@ public:
     return mIsRoot;
   }
 
   bool IsScrollable() const
   {
     return mScrollId != NULL_SCROLL_ID;
   }
 
+  CSSToScreenScale DisplayportPixelsPerCSSPixel() const
+  {
+    // Note: use 'mZoom * ParentLayerToLayerScale(1.0f)' as the CSS-to-Layer scale
+    // instead of LayersPixelsPerCSSPixel(), because displayport calculations
+    // are done in the context of a repaint request, where we ask Layout to
+    // repaint at a new resolution that includes any async zoom. Until this
+    // repaint request is processed, LayersPixelsPerCSSPixel() does not yet
+    // include the async zoom, but it will when the displayport is interpreted
+    // for the repaint.
+    return mZoom * ParentLayerToLayerScale(1.0f) / mExtraResolution;
+  }
+
   CSSToLayerScale LayersPixelsPerCSSPixel() const
   {
     return mCumulativeResolution * mDevPixelsPerCSSPixel;
   }
 
-  LayerPoint GetScrollOffsetInLayerPixels() const
+  // Get the amount by which this frame has been zoomed since the last repaint.
+  LayerToParentLayerScale GetAsyncZoom() const
   {
-    return GetScrollOffset() * LayersPixelsPerCSSPixel();
-  }
-
-  LayoutDeviceToParentLayerScale GetParentResolution() const
-  {
-    return mCumulativeResolution / mResolution;
+    return mZoom / LayersPixelsPerCSSPixel();
   }
 
   // Ensure the scrollableRect is at least as big as the compositionBounds
   // because the scrollableRect can be smaller if the content is not large
   // and the scrollableRect hasn't been updated yet.
   // We move the scrollableRect up because we don't know if we can move it
   // down. i.e. we know that scrollableRect can go back as far as zero.
   // but we don't know how much further ahead it can go.
@@ -189,41 +166,31 @@ public:
       scrollableRect.height = compSize.height;
     }
 
     return scrollableRect;
   }
 
   // Return the scale factor needed to fit the viewport
   // into its composition bounds.
-  CSSToScreenScale CalculateIntrinsicScale() const
+  CSSToParentLayerScale CalculateIntrinsicScale() const
   {
-    return CSSToScreenScale(
+    return CSSToParentLayerScale(
         std::max(mCompositionBounds.width / mViewport.width,
                  mCompositionBounds.height / mViewport.height));
   }
 
-  // Return the scale factor for converting from CSS pixels (for this layer)
-  // to layer pixels of our parent layer. Much as mZoom is used to interface
-  // between inputs we get in screen pixels and quantities in CSS pixels,
-  // this is used to interface between mCompositionBounds and quantities
-  // in CSS pixels.
-  CSSToParentLayerScale GetZoomToParent() const
-  {
-    return mZoom * mTransformScale;
-  }
-
   CSSSize CalculateCompositedSizeInCssPixels() const
   {
-    return mCompositionBounds.Size() / GetZoomToParent();
+    return mCompositionBounds.Size() / GetZoom();
   }
 
   CSSRect CalculateCompositedRectInCssPixels() const
   {
-    return mCompositionBounds / GetZoomToParent();
+    return mCompositionBounds / GetZoom();
   }
 
   CSSSize CalculateBoundedCompositedSizeInCssPixels() const
   {
     CSSSize size = CalculateCompositedSizeInCssPixels();
     size.width = std::min(size.width, mRootCompositionSize.width);
     size.height = std::min(size.height, mRootCompositionSize.height);
     return size;
@@ -324,37 +291,31 @@ public:
   //
   // This is valid on any layer unless it has no content.
   CSSRect mScrollableRect;
 
   // ---------------------------------------------------------------------------
   // The following metrics are dimensionless.
   //
 
-  // The incremental resolution that the current frame has been painted at
-  // relative to the parent frame's resolution. This information is provided
-  // by Gecko at layout/paint time.
-  ParentLayerToLayerScale mResolution;
+  // The pres-shell resolution that has been induced on the document containing
+  // this scroll frame as a result of zooming this scroll frame (whether via
+  // user action, or choosing an initial zoom level on page load). This can
+  // only be different from 1.0 for frames that are zoomable, which currently
+  // is just the root content document's root scroll frame (mIsRoot = true).
+  // This is a plain float rather than a ScaleFactor because in and of itself
+  // it does not convert between any coordinate spaces for which we have names.
+  float mPresShellResolution;
 
   // The cumulative resolution that the current frame has been painted at.
-  // This is the product of our mResolution and the mResolutions of our parent frames.
-  // This information is provided by Gecko at layout/paint time.
+  // This is the product of the pres-shell resolutions of the document
+  // containing this scroll frame and its ancestors, and any css-driven
+  // resolution. This information is provided by Gecko at layout/paint time.
   LayoutDeviceToLayerScale mCumulativeResolution;
 
-  // The conversion factor between local screen pixels (the coordinate
-  // system in which APZCs receive input events) and our parent layer's
-  // layer pixels (the coordinate system of mCompositionBounds).
-  // This consists of the scale of the local CSS transform and the
-  // nontransient async transform.
-  // TODO: APZ does not currently work well if there is a CSS transform
-  //       on the layer being scrolled that's not just a scale that's
-  //       the same in both directions. When we fix this, mTransformScale
-  //       will probably need to turn into a matrix.
-  ScreenToParentLayerScale mTransformScale;
-
   // The conversion factor between CSS pixels and device pixels for this frame.
   // This can vary based on a variety of things, such as reflowing-zoom. The
   // conversion factor for device pixels to layers pixels is just the
   // resolution.
   CSSToLayoutDeviceScale mDevPixelsPerCSSPixel;
 
 public:
   void SetIsRoot(bool aIsRoot)
@@ -392,22 +353,22 @@ public:
     mSmoothScrollOffset = aSmoothScrollDestination;
   }
 
   const CSSPoint& GetSmoothScrollOffset() const
   {
     return mSmoothScrollOffset;
   }
 
-  void SetZoom(const CSSToScreenScale& aZoom)
+  void SetZoom(const CSSToParentLayerScale& aZoom)
   {
     mZoom = aZoom;
   }
 
-  CSSToScreenScale GetZoom() const
+  CSSToParentLayerScale GetZoom() const
   {
     return mZoom;
   }
 
   void SetScrollOffsetUpdated(uint32_t aScrollGeneration)
   {
     mUpdateScrollOffset = true;
     mScrollGeneration = aScrollGeneration;
@@ -459,22 +420,22 @@ public:
     mRootCompositionSize = aRootCompositionSize;
   }
 
   const CSSSize& GetRootCompositionSize() const
   {
     return mRootCompositionSize;
   }
 
-  void SetDisplayPortMargins(const LayerMargin& aDisplayPortMargins)
+  void SetDisplayPortMargins(const ScreenMargin& aDisplayPortMargins)
   {
     mDisplayPortMargins = aDisplayPortMargins;
   }
 
-  const LayerMargin& GetDisplayPortMargins() const
+  const ScreenMargin& GetDisplayPortMargins() const
   {
     return mDisplayPortMargins;
   }
 
   void SetUseDisplayPortMargins()
   {
     mUseDisplayPortMargins = true;
   }
@@ -499,16 +460,26 @@ public:
     mViewport = aViewport;
   }
 
   const CSSRect& GetViewport() const
   {
     return mViewport;
   }
 
+  void SetExtraResolution(const ScreenToLayerScale& aExtraResolution)
+  {
+    mExtraResolution = aExtraResolution;
+  }
+
+  ScreenToLayerScale GetExtraResolution() const
+  {
+    return mExtraResolution;
+  }
+
   const gfxRGBA& GetBackgroundColor() const
   {
     return mBackgroundColor;
   }
 
   void SetBackgroundColor(const gfxRGBA& aBackgroundColor)
   {
     mBackgroundColor = aBackgroundColor;
@@ -582,17 +553,17 @@ private:
   // This is valid for any layer, but is always relative to this frame and
   // not any parents, regardless of parent transforms.
   CSSPoint mScrollOffset;
 
   // The "user zoom". Content is painted by gecko at mResolution * mDevPixelsPerCSSPixel,
   // but will be drawn to the screen at mZoom. In the steady state, the
   // two will be the same, but during an async zoom action the two may
   // diverge. This information is initialized in Gecko but updated in the APZC.
-  CSSToScreenScale mZoom;
+  CSSToParentLayerScale mZoom;
 
   // Whether mScrollOffset was updated by something other than the APZ code, and
   // if the APZC receiving this metrics should update its local copy.
   bool mUpdateScrollOffset;
   // The scroll generation counter used to acknowledge the scroll offset update.
   uint32_t mScrollGeneration;
 
   // When mDoSmoothScroll, the scroll offset should be animated to
@@ -600,17 +571,17 @@ private:
   bool mDoSmoothScroll;
   CSSPoint mSmoothScrollOffset;
 
   // The size of the root scrollable's composition bounds, but in local CSS pixels.
   CSSSize mRootCompositionSize;
 
   // A display port expressed as layer margins that apply to the rect of what
   // is drawn of the scrollable element.
-  LayerMargin mDisplayPortMargins;
+  ScreenMargin mDisplayPortMargins;
 
   // If this is true then we use the display port margins on this metrics,
   // otherwise use the display port rect.
   bool mUseDisplayPortMargins;
 
   uint32_t mPresShellId;
 
   // The CSS viewport, which is the dimensions we're using to constrain the
@@ -619,16 +590,20 @@ private:
   // method layout uses to scroll content.
   //
   // This is mainly useful on the root layer, however nested iframes can have
   // their own viewport, which will just be the size of the window of the
   // iframe. For layers that don't correspond to a document, this metric is
   // meaningless and invalid.
   CSSRect mViewport;
 
+  // The extra resolution at which content in this scroll frame is drawn beyond
+  // that necessary to draw one Layer pixel per Screen pixel.
+  ScreenToLayerScale mExtraResolution;
+
   // The background color to use when overscrolling.
   gfxRGBA mBackgroundColor;
 
   // A description of the content element corresponding to this frame.
   // This is empty unless this is a scrollable layer and the
   // apz.printtree pref is turned on.
   nsCString mContentDescription;
 };
@@ -716,30 +691,30 @@ struct ScrollableLayerGuid {
 template <int LogLevel>
 gfx::Log<LogLevel>& operator<<(gfx::Log<LogLevel>& log, const ScrollableLayerGuid& aGuid) {
   return log << '(' << aGuid.mLayersId << ',' << aGuid.mPresShellId << ',' << aGuid.mScrollId << ')';
 }
 
 struct ZoomConstraints {
   bool mAllowZoom;
   bool mAllowDoubleTapZoom;
-  CSSToScreenScale mMinZoom;
-  CSSToScreenScale mMaxZoom;
+  CSSToParentLayerScale mMinZoom;
+  CSSToParentLayerScale mMaxZoom;
 
   ZoomConstraints()
     : mAllowZoom(true)
     , mAllowDoubleTapZoom(true)
   {
     MOZ_COUNT_CTOR(ZoomConstraints);
   }
 
   ZoomConstraints(bool aAllowZoom,
                   bool aAllowDoubleTapZoom,
-                  const CSSToScreenScale& aMinZoom,
-                  const CSSToScreenScale& aMaxZoom)
+                  const CSSToParentLayerScale& aMinZoom,
+                  const CSSToParentLayerScale& aMaxZoom)
     : mAllowZoom(aAllowZoom)
     , mAllowDoubleTapZoom(aAllowDoubleTapZoom)
     , mMinZoom(aMinZoom)
     , mMaxZoom(aMaxZoom)
   {
     MOZ_COUNT_CTOR(ZoomConstraints);
   }
 
--- a/gfx/layers/ImageContainer.cpp
+++ b/gfx/layers/ImageContainer.cpp
@@ -491,21 +491,43 @@ CairoImage::GetTextureClient(Compositabl
   }
 
   RefPtr<SourceSurface> surface = GetAsSourceSurface();
   MOZ_ASSERT(surface);
   if (!surface) {
     return nullptr;
   }
 
-  // gfx::BackendType::NONE means default to content backend
-  textureClient = aClient->CreateTextureClientForDrawing(surface->GetFormat(),
-                                                         surface->GetSize(),
-                                                         gfx::BackendType::NONE,
-                                                         TextureFlags::DEFAULT);
+
+// XXX windows' TextureClients do not hold ISurfaceAllocator,
+// recycler does not work on windows.
+#ifndef XP_WIN
+
+// XXX only gonk ensure when TextureClient is recycled,
+// TextureHost is not used by CompositableHost.
+#ifdef MOZ_WIDGET_GONK
+  RefPtr<TextureClientRecycleAllocator> recycler =
+    aClient->GetTextureClientRecycler();
+  if (recycler) {
+    textureClient =
+      recycler->CreateOrRecycleForDrawing(surface->GetFormat(),
+                                          surface->GetSize(),
+                                          gfx::BackendType::NONE,
+                                          aClient->GetTextureFlags());
+  }
+#endif
+
+#endif
+  if (!textureClient) {
+    // gfx::BackendType::NONE means default to content backend
+    textureClient = aClient->CreateTextureClientForDrawing(surface->GetFormat(),
+                                                           surface->GetSize(),
+                                                           gfx::BackendType::NONE,
+                                                           TextureFlags::DEFAULT);
+  }
   if (!textureClient) {
     return nullptr;
   }
   MOZ_ASSERT(textureClient->CanExposeDrawTarget());
   if (!textureClient->Lock(OpenMode::OPEN_WRITE_ONLY)) {
     return nullptr;
   }
 
--- a/gfx/layers/ImageContainer.h
+++ b/gfx/layers/ImageContainer.h
@@ -806,17 +806,17 @@ public:
 
   virtual TemporaryRef<gfx::SourceSurface> GetAsSourceSurface()
   {
     return mSourceSurface.get();
   }
 
   virtual ISharedImage* AsSharedImage() { return this; }
   virtual uint8_t* GetBuffer() { return nullptr; }
-  virtual TextureClient* GetTextureClient(CompositableClient* aClient);
+  virtual TextureClient* GetTextureClient(CompositableClient* aClient) MOZ_OVERRIDE;
 
   gfx::IntSize GetSize() { return mSize; }
 
   CairoImage();
   ~CairoImage();
 
   gfx::IntSize mSize;
 
--- a/gfx/layers/LayersLogging.cpp
+++ b/gfx/layers/LayersLogging.cpp
@@ -153,20 +153,20 @@ AppendToString(std::stringstream& aStrea
       AppendToString(aStream, m.GetScrollParentId(), " scrollParent=");
     }
     aStream << nsPrintfCString(" z=%.3f }", m.GetZoom().scale).get();
   } else {
     AppendToString(aStream, m.GetDisplayPortMargins(), " dpm=");
     aStream << nsPrintfCString(" um=%d", m.GetUseDisplayPortMargins()).get();
     AppendToString(aStream, m.GetRootCompositionSize(), " rcs=");
     AppendToString(aStream, m.GetViewport(), " v=");
-    aStream << nsPrintfCString(" z=(ld=%.3f r=%.3f cr=%.3f z=%.3f ts=%.3f)",
-            m.mDevPixelsPerCSSPixel.scale, m.mResolution.scale,
+    aStream << nsPrintfCString(" z=(ld=%.3f r=%.3f cr=%.3f z=%.3f er=%.3f)",
+            m.mDevPixelsPerCSSPixel.scale, m.mPresShellResolution,
             m.mCumulativeResolution.scale, m.GetZoom().scale,
-            m.mTransformScale.scale).get();
+            m.GetExtraResolution().scale).get();
     aStream << nsPrintfCString(" u=(%d %d %lu)",
             m.GetScrollOffsetUpdated(), m.GetDoSmoothScroll(),
             m.GetScrollGeneration()).get();
     AppendToString(aStream, m.GetScrollParentId(), " p=");
     aStream << nsPrintfCString(" i=(%ld %lld) }",
             m.GetPresShellId(), m.GetScrollId()).get();
   }
   aStream << sfx;
--- a/gfx/layers/apz/src/APZCTreeManager.cpp
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -56,20 +56,20 @@ struct APZCTreeManager::TreeBuildingStat
   const uint64_t mOriginatingLayersId;
   const APZPaintLogHelper mPaintLogger;
 
   // State that is updated as we perform the tree build
   nsTArray< nsRefPtr<AsyncPanZoomController> > mApzcsToDestroy;
   std::map<ScrollableLayerGuid, AsyncPanZoomController*> mApzcMap;
 };
 
-/*static*/ const LayerMargin
+/*static*/ const ScreenMargin
 APZCTreeManager::CalculatePendingDisplayPort(
   const FrameMetrics& aFrameMetrics,
-  const ScreenPoint& aVelocity,
+  const ParentLayerPoint& aVelocity,
   double aEstimatedPaintDuration)
 {
   return AsyncPanZoomController::CalculatePendingDisplayPort(
     aFrameMetrics, aVelocity, aEstimatedPaintDuration);
 }
 
 APZCTreeManager::APZCTreeManager()
     : mInputQueue(new InputQueue()),
@@ -192,22 +192,28 @@ ComputeTouchSensitiveRegion(GeckoContent
 {
   // Use the composition bounds as the hit test region.
   // Optionally, the GeckoContentController can provide a touch-sensitive
   // region that constrains all frames associated with the controller.
   // In this case we intersect the composition bounds with that region.
   ParentLayerRect visible(aMetrics.mCompositionBounds);
   CSSRect touchSensitiveRegion;
   if (aController->GetTouchSensitiveRegion(&touchSensitiveRegion)) {
-    // Note: we assume here that touchSensitiveRegion is in the CSS pixels
-    // of our parent layer, which makes this coordinate conversion
-    // correct.
+    // Here we assume 'touchSensitiveRegion' is in the CSS pixels of the
+    // parent frame. To convert it to ParentLayer pixels, we therefore need
+    // the cumulative resolution of the parent frame. We approximate this as
+    // the quotient of our cumulative resolution and our pres shell resolution;
+    // this approximation may not be accurate in the presence of a css-driven
+    // resolution.
+    LayoutDeviceToParentLayerScale parentCumulativeResolution =
+          aMetrics.mCumulativeResolution
+        / ParentLayerToLayerScale(aMetrics.mPresShellResolution);
     visible = visible.Intersect(touchSensitiveRegion
                                 * aMetrics.mDevPixelsPerCSSPixel
-                                * aMetrics.GetParentResolution());
+                                * parentCumulativeResolution);
   }
 
   // Not sure what rounding option is the most correct here, but if we ever
   // figure it out we can change this. For now I'm rounding in to minimize
   // the chances of getting a complex region.
   ParentLayerIntRect roundedVisible = RoundedIn(visible);
   nsIntRegion unobscured;
   unobscured.Sub(nsIntRect(roundedVisible.x, roundedVisible.y,
@@ -479,46 +485,16 @@ APZCTreeManager::UpdatePanZoomController
     return apzc;
   }
   if (next) {
     return next;
   }
   return aNextSibling;
 }
 
-/*static*/ template<class T> void
-ApplyTransform(gfx::PointTyped<T>* aPoint, const Matrix4x4& aMatrix)
-{
-  Point result = aMatrix * aPoint->ToUnknownPoint();
-  *aPoint = ViewAs<T>(result);
-}
-
-/*static*/ template<class T> void
-ApplyTransform(gfx::IntPointTyped<T>* aPoint, const Matrix4x4& aMatrix)
-{
-  Point result = aMatrix * aPoint->ToUnknownPoint();
-  *aPoint = TruncatedToInt(ViewAs<T>(result));
-}
-
-/*static*/ void
-ApplyTransform(nsIntPoint* aPoint, const Matrix4x4& aMatrix)
-{
-  Point result = aMatrix * Point(aPoint->x, aPoint->y);
-  aPoint->x = NS_lround(result.x);
-  aPoint->y = NS_lround(result.y);
-}
-
-/*static*/ template<class T> void
-TransformScreenToGecko(T* aPoint, AsyncPanZoomController* aApzc, APZCTreeManager* aApzcTm)
-{
-  Matrix4x4 transformToApzc = aApzcTm->GetScreenToApzcTransform(aApzc);
-  Matrix4x4 transformToGecko = aApzcTm->GetApzcToGeckoTransform(aApzc);
-  ApplyTransform(aPoint, transformToApzc * transformToGecko);
-}
-
 nsEventStatus
 APZCTreeManager::ReceiveInputEvent(InputData& aEvent,
                                    ScrollableLayerGuid* aOutTargetGuid,
                                    uint64_t* aOutInputBlockId)
 {
   // Initialize aOutInputBlockId to a sane value, and then later we overwrite
   // it if the input event goes into a block.
   if (aOutInputBlockId) {
@@ -532,63 +508,63 @@ APZCTreeManager::ReceiveInputEvent(Input
       MultiTouchInput& touchInput = aEvent.AsMultiTouchInput();
       result = ProcessTouchInput(touchInput, aOutTargetGuid, aOutInputBlockId);
       break;
     } case PANGESTURE_INPUT: {
       PanGestureInput& panInput = aEvent.AsPanGestureInput();
       nsRefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(panInput.mPanStartPoint,
                                                             &inOverscrolledApzc);
       if (apzc) {
-        // When passing the event to the APZC, we need to apply a different
-        // transform than the one in TransformScreenToGecko, so we need to
-        // make a copy of the event.
-        PanGestureInput inputForApzc(panInput);
         transformToApzc = GetScreenToApzcTransform(apzc);
-        ApplyTransform(&(inputForApzc.mPanStartPoint), transformToApzc);
-        result = mInputQueue->ReceiveInputEvent(apzc, inputForApzc, aOutInputBlockId);
+        panInput.mLocalPanStartPoint = TransformTo<ParentLayerPixel>(
+            transformToApzc, panInput.mPanStartPoint);
+        panInput.mLocalPanDisplacement = TransformVector<ParentLayerPixel>(
+            transformToApzc, panInput.mPanDisplacement, panInput.mPanStartPoint);
+        result = mInputQueue->ReceiveInputEvent(apzc, panInput, aOutInputBlockId);
 
         // Update the out-parameters so they are what the caller expects.
         apzc->GetGuid(aOutTargetGuid);
-        TransformScreenToGecko(&(panInput.mPanStartPoint), apzc, this);
+        Matrix4x4 transformToGecko = transformToApzc * GetApzcToGeckoTransform(apzc);
+        panInput.mPanStartPoint = TransformTo<ScreenPixel>(
+            transformToGecko, panInput.mPanStartPoint);
+        panInput.mPanDisplacement = TransformVector<ScreenPixel>(
+            transformToGecko, panInput.mPanDisplacement, panInput.mPanStartPoint);
       }
       break;
-    } case PINCHGESTURE_INPUT: {
+    } case PINCHGESTURE_INPUT: {  // note: no one currently sends these
       PinchGestureInput& pinchInput = aEvent.AsPinchGestureInput();
       nsRefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(pinchInput.mFocusPoint,
                                                             &inOverscrolledApzc);
       if (apzc) {
-        // When passing the event to the APZC, we need to apply a different
-        // transform than the one in TransformScreenToGecko, so we need to
-        // make a copy of the event.
-        PinchGestureInput inputForApzc(pinchInput);
         transformToApzc = GetScreenToApzcTransform(apzc);
-        ApplyTransform(&(inputForApzc.mFocusPoint), transformToApzc);
-        result = mInputQueue->ReceiveInputEvent(apzc, inputForApzc, aOutInputBlockId);
+        pinchInput.mLocalFocusPoint = TransformTo<ParentLayerPixel>(
+            transformToApzc, pinchInput.mFocusPoint);
+        result = mInputQueue->ReceiveInputEvent(apzc, pinchInput, aOutInputBlockId);
 
         // Update the out-parameters so they are what the caller expects.
         apzc->GetGuid(aOutTargetGuid);
-        TransformScreenToGecko(&(pinchInput.mFocusPoint), apzc, this);
+        Matrix4x4 outTransform = transformToApzc * GetApzcToGeckoTransform(apzc);
+        pinchInput.mFocusPoint = TransformTo<ScreenPixel>(
+            outTransform, pinchInput.mFocusPoint);
       }
       break;
-    } case TAPGESTURE_INPUT: {
+    } case TAPGESTURE_INPUT: {  // note: no one currently sends these
       TapGestureInput& tapInput = aEvent.AsTapGestureInput();
-      nsRefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(ScreenPoint(tapInput.mPoint),
+      nsRefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(tapInput.mPoint,
                                                             &inOverscrolledApzc);
       if (apzc) {
-        // When passing the event to the APZC, we need to apply a different
-        // transform than the one in TransformScreenToGecko, so we need to
-        // make a copy of the event.
-        TapGestureInput inputForApzc(tapInput);
         transformToApzc = GetScreenToApzcTransform(apzc);
-        ApplyTransform(&(inputForApzc.mPoint), transformToApzc);
-        result = mInputQueue->ReceiveInputEvent(apzc, inputForApzc, aOutInputBlockId);
+        tapInput.mLocalPoint = TransformTo<ParentLayerPixel>(
+            transformToApzc, tapInput.mPoint);
+        result = mInputQueue->ReceiveInputEvent(apzc, tapInput, aOutInputBlockId);
 
         // Update the out-parameters so they are what the caller expects.
         apzc->GetGuid(aOutTargetGuid);
-        TransformScreenToGecko(&(tapInput.mPoint), apzc, this);
+        Matrix4x4 outTransform = transformToApzc * GetApzcToGeckoTransform(apzc);
+        tapInput.mPoint = TransformTo<ScreenPixel>(outTransform, tapInput.mPoint);
       }
       break;
     }
   }
   if (inOverscrolledApzc) {
     result = nsEventStatus_eConsumeNoDefault;
   }
   return result;
@@ -699,30 +675,33 @@ APZCTreeManager::ProcessTouchInput(Multi
 
   nsEventStatus result = nsEventStatus_eIgnore;
   if (mApzcForInputBlock) {
     mApzcForInputBlock->GetGuid(aOutTargetGuid);
     // For computing the input for the APZC, used the cached transform.
     // This ensures that the sequence of touch points an APZC sees in an
     // input block are all in the same coordinate space.
     Matrix4x4 transformToApzc = mCachedTransformToApzcForInputBlock;
-    MultiTouchInput inputForApzc(aInput);
-    for (size_t i = 0; i < inputForApzc.mTouches.Length(); i++) {
-      ApplyTransform(&(inputForApzc.mTouches[i].mScreenPoint), transformToApzc);
+    for (size_t i = 0; i < aInput.mTouches.Length(); i++) {
+      SingleTouchData& touchData = aInput.mTouches[i];
+      touchData.mLocalScreenPoint = TransformTo<ParentLayerPixel>(
+          transformToApzc, ScreenPoint(touchData.mScreenPoint));
     }
-    result = mInputQueue->ReceiveInputEvent(mApzcForInputBlock, inputForApzc, aOutInputBlockId);
+    result = mInputQueue->ReceiveInputEvent(mApzcForInputBlock, aInput, aOutInputBlockId);
 
     // For computing the event to pass back to Gecko, use the up-to-date transforms.
     // This ensures that transformToApzc and transformToGecko are in sync
     // (note that transformToGecko isn't cached).
     transformToApzc = GetScreenToApzcTransform(mApzcForInputBlock);
     Matrix4x4 transformToGecko = GetApzcToGeckoTransform(mApzcForInputBlock);
     Matrix4x4 outTransform = transformToApzc * transformToGecko;
     for (size_t i = 0; i < aInput.mTouches.Length(); i++) {
-      ApplyTransform(&(aInput.mTouches[i].mScreenPoint), outTransform);
+      SingleTouchData& touchData = aInput.mTouches[i];
+      touchData.mScreenPoint = TransformTo<ScreenPixel>(
+          outTransform, touchData.mScreenPoint);
     }
   }
   if (mInOverscrolledApzc) {
     result = nsEventStatus_eConsumeNoDefault;
   }
 
   if (aInput.mType == MultiTouchInput::MULTITOUCH_END) {
     if (mTouchCount >= aInput.mTouches.Length()) {
@@ -774,17 +753,17 @@ APZCTreeManager::ProcessEvent(WidgetInpu
   bool inOverscrolledApzc = false;
   nsRefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(ScreenPoint(aEvent.refPoint.x, aEvent.refPoint.y),
                                                         &inOverscrolledApzc);
   if (apzc) {
     apzc->GetGuid(aOutTargetGuid);
     Matrix4x4 transformToApzc = GetScreenToApzcTransform(apzc);
     Matrix4x4 transformToGecko = GetApzcToGeckoTransform(apzc);
     Matrix4x4 outTransform = transformToApzc * transformToGecko;
-    ApplyTransform(&(aEvent.refPoint), outTransform);
+    aEvent.refPoint = TransformTo<LayoutDevicePixel>(outTransform, aEvent.refPoint);
   }
   if (inOverscrolledApzc) {
     result = nsEventStatus_eConsumeNoDefault;
   }
   return result;
 }
 
 nsEventStatus
@@ -903,46 +882,46 @@ APZCTreeManager::ClearTree()
   Collect(mRootApzc, &apzcsToDestroy);
   for (size_t i = 0; i < apzcsToDestroy.Length(); i++) {
     apzcsToDestroy[i]->Destroy();
   }
   mRootApzc = nullptr;
 }
 
 /**
- * Transform a displacement from the screen coordinates of a source APZC to
- * the screen coordinates of a target APZC.
+ * Transform a displacement from the ParentLayer coordinates of a source APZC
+ * to the ParentLayer coordinates of a target APZC.
  * @param aTreeManager the tree manager for the APZC tree containing |aSource|
  *                     and |aTarget|
  * @param aSource the source APZC
  * @param aTarget the target APZC
  * @param aStartPoint the start point of the displacement
  * @param aEndPoint the end point of the displacement
  */
 static void
 TransformDisplacement(APZCTreeManager* aTreeManager,
                       AsyncPanZoomController* aSource,
                       AsyncPanZoomController* aTarget,
-                      ScreenPoint& aStartPoint,
-                      ScreenPoint& aEndPoint) {
-  // Convert start and end points to untransformed screen coordinates.
+                      ParentLayerPoint& aStartPoint,
+                      ParentLayerPoint& aEndPoint) {
+  // Convert start and end points to Screen coordinates.
   Matrix4x4 untransformToApzc = aTreeManager->GetScreenToApzcTransform(aSource).Inverse();
-  ApplyTransform(&aStartPoint, untransformToApzc);
-  ApplyTransform(&aEndPoint, untransformToApzc);
+  ScreenPoint screenStart = TransformTo<ScreenPixel>(untransformToApzc, aStartPoint);
+  ScreenPoint screenEnd = TransformTo<ScreenPixel>(untransformToApzc, aEndPoint);
 
-  // Convert start and end points to aTarget's transformed screen coordinates.
+  // Convert start and end points to aTarget's ParentLayer coordinates.
   Matrix4x4 transformToApzc = aTreeManager->GetScreenToApzcTransform(aTarget);
-  ApplyTransform(&aStartPoint, transformToApzc);
-  ApplyTransform(&aEndPoint, transformToApzc);
+  aStartPoint = TransformTo<ParentLayerPixel>(transformToApzc, screenStart);
+  aEndPoint = TransformTo<ParentLayerPixel>(transformToApzc, screenEnd);
 }
 
 bool
 APZCTreeManager::DispatchScroll(AsyncPanZoomController* aPrev,
-                                ScreenPoint aStartPoint,
-                                ScreenPoint aEndPoint,
+                                ParentLayerPoint aStartPoint,
+                                ParentLayerPoint aEndPoint,
                                 OverscrollHandoffState& aOverscrollHandoffState)
 {
   const OverscrollHandoffChain& overscrollHandoffChain = aOverscrollHandoffState.mChain;
   uint32_t overscrollHandoffChainIndex = aOverscrollHandoffState.mChainIndex;
   nsRefPtr<AsyncPanZoomController> next;
   // If we have reached the end of the overscroll handoff chain, there is
   // nothing more to scroll, so we ignore the rest of the pan gesture.
   if (overscrollHandoffChainIndex >= overscrollHandoffChain.Length()) {
@@ -967,76 +946,76 @@ APZCTreeManager::DispatchScroll(AsyncPan
 
   // Scroll |next|. If this causes overscroll, it will call DispatchScroll()
   // again with an incremented index.
   return next->AttemptScroll(aStartPoint, aEndPoint, aOverscrollHandoffState);
 }
 
 bool
 APZCTreeManager::DispatchFling(AsyncPanZoomController* aPrev,
-                               ScreenPoint aVelocity,
+                               ParentLayerPoint aVelocity,
                                nsRefPtr<const OverscrollHandoffChain> aOverscrollHandoffChain,
                                bool aHandoff)
 {
   nsRefPtr<AsyncPanZoomController> current;
   uint32_t aOverscrollHandoffChainLength = aOverscrollHandoffChain->Length();
   uint32_t startIndex;
-  
+
   // The fling's velocity needs to be transformed from the screen coordinates
   // of |aPrev| to the screen coordinates of |next|. To transform a velocity
   // correctly, we need to convert it to a displacement. For now, we do this
   // by anchoring it to a start point of (0, 0).
   // TODO: For this to be correct in the presence of 3D transforms, we should
   // use the end point of the touch that started the fling as the start point
   // rather than (0, 0).
-  ScreenPoint startPoint;  // (0, 0)
-  ScreenPoint endPoint;
-  ScreenPoint transformedVelocity = aVelocity;
-  
+  ParentLayerPoint startPoint;  // (0, 0)
+  ParentLayerPoint endPoint;
+  ParentLayerPoint transformedVelocity = aVelocity;
+
   if (aHandoff) {
     startIndex = aOverscrollHandoffChain->IndexOf(aPrev) + 1;
-    
+
     // IndexOf will return aOverscrollHandoffChain->Length() if
     // |aPrev| is not found.
     if (startIndex >= aOverscrollHandoffChainLength) {
       return false;
     }
   } else {
     startIndex = 0;
   }
-  
+
   for (; startIndex < aOverscrollHandoffChainLength; startIndex++) {
     current = aOverscrollHandoffChain->GetApzcAtIndex(startIndex);
-    
+
     // Make sure the apcz about to be handled can be handled
     if (current == nullptr || current->IsDestroyed()) {
       return false;
     }
-    
+
     endPoint = startPoint + transformedVelocity;
-    
+
     // Only transform when current apcz can be transformed with previous
     if (startIndex > 0) {
       TransformDisplacement(this,
                             aOverscrollHandoffChain->GetApzcAtIndex(startIndex - 1),
                             current,
                             startPoint,
                             endPoint);
     }
-    
+
     transformedVelocity = endPoint - startPoint;
-    
+
     bool handoff = (startIndex < 1) ? aHandoff : true;
     if (current->AttemptFling(transformedVelocity,
                               aOverscrollHandoffChain,
                               handoff)) {
       return true;
     }
   }
-  
+
   return false;
 }
 
 bool
 APZCTreeManager::HitTestAPZC(const ScreenIntPoint& aPoint)
 {
   nsRefPtr<AsyncPanZoomController> target = GetTargetAPZC(aPoint, nullptr);
   return target != nullptr;
--- a/gfx/layers/apz/src/APZCTreeManager.h
+++ b/gfx/layers/apz/src/APZCTreeManager.h
@@ -240,19 +240,19 @@ public:
    */
   bool HitTestAPZC(const ScreenIntPoint& aPoint);
 
   /**
    * See AsyncPanZoomController::CalculatePendingDisplayPort. This
    * function simply delegates to that one, so that non-layers code
    * never needs to include AsyncPanZoomController.h
    */
-  static const LayerMargin CalculatePendingDisplayPort(
+  static const ScreenMargin CalculatePendingDisplayPort(
     const FrameMetrics& aFrameMetrics,
-    const ScreenPoint& aVelocity,
+    const ParentLayerPoint& aVelocity,
     double aEstimatedPaintDuration);
 
   /**
    * Set the dpi value used by all AsyncPanZoomControllers.
    * DPI defaults to 72 if not set using SetDPI() at any point.
    */
   static void SetDPI(float aDpiValue) { sDPI = aDpiValue; }
 
@@ -322,18 +322,18 @@ public:
    *   - TM.DispatchScroll() calls A.AttemptScroll() (since A is at index 2 in the chain)
    *   - A.AttemptScroll() scrolls A. If there is overscroll, it calls TM.DispatchScroll() with index = 3.
    *   - TM.DispatchScroll() discards the rest of the scroll as there are no more elements in the chain.
    *
    * Note: this should be used for panning only. For handing off overscroll for
    *       a fling, use DispatchFling().
    */
   bool DispatchScroll(AsyncPanZoomController* aApzc,
-                      ScreenPoint aStartPoint,
-                      ScreenPoint aEndPoint,
+                      ParentLayerPoint aStartPoint,
+                      ParentLayerPoint aEndPoint,
                       OverscrollHandoffState& aOverscrollHandoffState);
 
   /**
    * This is a callback for AsyncPanZoomController to call when it wants to
    * start a fling in response to a touch-end event, or when it needs to hand
    * off a fling to the next APZC. Note that because of scroll grabbing, the
    * first APZC to fling may not be the one that is receiving the touch events.
    *
@@ -348,17 +348,17 @@ public:
    *                 start a fling (in this case the fling is given to the
    *                 first APZC in the chain)
    *
    * Returns true iff. an APZC accepted the fling. In the case of fling handoff,
    * the caller uses this return value to determine whether it should consume
    * the excess fling itself by going into an overscroll fling.
    */
   bool DispatchFling(AsyncPanZoomController* aApzc,
-                     ScreenPoint aVelocity,
+                     ParentLayerPoint aVelocity,
                      nsRefPtr<const OverscrollHandoffChain> aOverscrollHandoffChain,
                      bool aHandoff);
 
   /*
    * Build the chain of APZCs that will handle overscroll for a pan starting at |aInitialTarget|.
    */
   nsRefPtr<const OverscrollHandoffChain> BuildOverscrollHandoffChain(const nsRefPtr<AsyncPanZoomController>& aInitialTarget);
 
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -130,16 +130,17 @@ WidgetModifiersToDOMModifiers(mozilla::M
 
 namespace mozilla {
 namespace layers {
 
 typedef mozilla::layers::AllowedTouchBehavior AllowedTouchBehavior;
 typedef GeckoContentController::APZStateChange APZStateChange;
 typedef mozilla::gfx::Point Point;
 typedef mozilla::gfx::Matrix4x4 Matrix4x4;
+using mozilla::gfx::PointTyped;
 
 /**
  * \page APZCPrefs APZ preferences
  *
  * The following prefs are used to control the behaviour of the APZC.
  * The default values are provided in gfxPrefs.h.
  *
  * \li\b apz.allow_checkerboarding
@@ -384,22 +385,22 @@ StaticAutoPtr<ComputedTimingFunction> gZ
 /**
  * Computed time function used for curving up velocity when it gets high.
  */
 StaticAutoPtr<ComputedTimingFunction> gVelocityCurveFunction;
 
 /**
  * Maximum zoom amount, always used, even if a page asks for higher.
  */
-static const CSSToScreenScale MAX_ZOOM(8.0f);
+static const CSSToParentLayerScale MAX_ZOOM(8.0f);
 
 /**
  * Minimum zoom amount, always used, even if a page asks for lower.
  */
-static const CSSToScreenScale MIN_ZOOM(0.125f);
+static const CSSToParentLayerScale MIN_ZOOM(0.125f);
 
 /**
  * Is aAngle within the given threshold of the horizontal axis?
  * @param aAngle an angle in radians in the range [0, pi]
  * @param aThreshold an angle in radians in the range [0, pi/2]
  */
 static bool IsCloseToHorizontal(float aAngle, float aThreshold)
 {
@@ -492,17 +493,17 @@ public:
       if (!mApzc.mX.CanScroll()) {
         mApzc.mX.SetVelocity(0);
       }
       if (!mApzc.mY.CanScroll()) {
         mApzc.mY.SetVelocity(0);
       }
     }
 
-    ScreenPoint velocity(mApzc.mX.GetVelocity(), mApzc.mY.GetVelocity());
+    ParentLayerPoint velocity = mApzc.GetVelocityVector();
 
     // If the last fling was very recent and in the same direction as this one,
     // boost the velocity to be the sum of the two. Check separate axes separately
     // because we could have two vertical flings with small horizontal components
     // on the opposite side of zero, and we still want the y-fling to get accelerated.
     // Note that the acceleration code is only applied on the APZC that initiates
     // the fling; the accelerated velocities are then handed off using the
     // normal DispatchFling codepath.
@@ -565,28 +566,28 @@ public:
       mDeferredTasks.append(NewRunnableMethod(mOverscrollHandoffChain.get(),
                                               &OverscrollHandoffChain::SnapBackOverscrolledApzc,
                                               &mApzc));
       return false;
     }
 
     // AdjustDisplacement() zeroes out the Axis velocity if we're in overscroll.
     // Since we need to hand off the velocity to the tree manager in such a case,
-    // we save it here. Would be ScreenVector instead of ScreenPoint if we had
-    // vector classes.
-    ScreenPoint velocity(mApzc.mX.GetVelocity(), mApzc.mY.GetVelocity());
-
-    ScreenPoint offset = velocity * aDelta.ToMilliseconds();
+    // we save it here. Would be ParentLayerVector instead of ParentLayerPoint
+    // if we had vector classes.
+    ParentLayerPoint velocity = mApzc.GetVelocityVector();
+
+    ParentLayerPoint offset = velocity * aDelta.ToMilliseconds();
 
     // Ordinarily we might need to do a ScheduleComposite if either of
     // the following AdjustDisplacement calls returns true, but this
     // is already running as part of a FlingAnimation, so we'll be compositing
     // per frame of animation anyway.
-    ScreenPoint overscroll;
-    ScreenPoint adjustedOffset;
+    ParentLayerPoint overscroll;
+    ParentLayerPoint adjustedOffset;
     mApzc.mX.AdjustDisplacement(offset.x, adjustedOffset.x, overscroll.x);
     mApzc.mY.AdjustDisplacement(offset.y, adjustedOffset.y, overscroll.y);
 
     aFrameMetrics.ScrollBy(adjustedOffset / aFrameMetrics.GetZoom());
 
     // The fling may have caused us to reach the end of our scroll range.
     if (!IsZero(overscroll)) {
       // Hand off the fling to the next APZC in the overscroll handoff chain.
@@ -640,18 +641,18 @@ private:
   }
 
   AsyncPanZoomController& mApzc;
   nsRefPtr<const OverscrollHandoffChain> mOverscrollHandoffChain;
 };
 
 class ZoomAnimation: public AsyncPanZoomAnimation {
 public:
-  ZoomAnimation(CSSPoint aStartOffset, CSSToScreenScale aStartZoom,
-                CSSPoint aEndOffset, CSSToScreenScale aEndZoom)
+  ZoomAnimation(CSSPoint aStartOffset, CSSToParentLayerScale aStartZoom,
+                CSSPoint aEndOffset, CSSToParentLayerScale aEndZoom)
     : mTotalDuration(TimeDuration::FromMilliseconds(gfxPrefs::APZZoomAnimationDuration()))
     , mStartOffset(aStartOffset)
     , mStartZoom(aStartZoom)
     , mEndOffset(aEndOffset)
     , mEndZoom(aEndZoom)
   {}
 
   virtual bool Sample(FrameMetrics& aFrameMetrics,
@@ -667,17 +668,17 @@ public:
     }
 
     // Sample the zoom at the current time point.  The sampled zoom
     // will affect the final computed resolution.
     float sampledPosition = gZoomAnimationFunction->GetValue(animPosition);
 
     // We scale the scrollOffset linearly with sampledPosition, so the zoom
     // needs to scale inversely to match.
-    aFrameMetrics.SetZoom(CSSToScreenScale(1 /
+    aFrameMetrics.SetZoom(CSSToParentLayerScale(1 /
       (sampledPosition / mEndZoom.scale +
       (1 - sampledPosition) / mStartZoom.scale)));
 
     aFrameMetrics.SetScrollOffset(CSSPoint::FromUnknownPoint(gfx::Point(
       mEndOffset.x * sampledPosition + mStartOffset.x * (1 - sampledPosition),
       mEndOffset.y * sampledPosition + mStartOffset.y * (1 - sampledPosition)
     )));
 
@@ -688,28 +689,28 @@ private:
   TimeDuration mDuration;
   const TimeDuration mTotalDuration;
 
   // Old metrics from before we started a zoom animation. This is only valid
   // when we are in the "ANIMATED_ZOOM" state. This is used so that we can
   // interpolate between the start and end frames. We only use the
   // |mViewportScrollOffset| and |mResolution| fields on this.
   CSSPoint mStartOffset;
-  CSSToScreenScale mStartZoom;
+  CSSToParentLayerScale mStartZoom;
 
   // Target metrics for a zoom to animation. This is only valid when we are in
   // the "ANIMATED_ZOOM" state. We only use the |mViewportScrollOffset| and
   // |mResolution| fields on this.
   CSSPoint mEndOffset;
-  CSSToScreenScale mEndZoom;
+  CSSToParentLayerScale mEndZoom;
 };
 
 class OverscrollAnimation: public AsyncPanZoomAnimation {
 public:
-  explicit OverscrollAnimation(AsyncPanZoomController& aApzc, const ScreenPoint& aVelocity)
+  explicit OverscrollAnimation(AsyncPanZoomController& aApzc, const ParentLayerPoint& aVelocity)
     : mApzc(aApzc)
   {
     mApzc.mX.SetVelocity(aVelocity.x);
     mApzc.mY.SetVelocity(aVelocity.y);
   }
 
   virtual bool Sample(FrameMetrics& aFrameMetrics,
                       const TimeDuration& aDelta) MOZ_OVERRIDE
@@ -760,37 +761,37 @@ public:
     mYAxisModel.Simulate(aDelta);
 
     CSSPoint position = CSSPoint::FromAppUnits(nsPoint(mXAxisModel.GetPosition(),
                                                        mYAxisModel.GetPosition()));
     CSSPoint css_velocity = CSSPoint::FromAppUnits(nsPoint(mXAxisModel.GetVelocity(),
                                                            mYAxisModel.GetVelocity()));
 
     // Convert from points/second to points/ms
-    ScreenPoint velocity = ScreenPoint(css_velocity.x, css_velocity.y) / 1000.0f;
+    ParentLayerPoint velocity = ParentLayerPoint(css_velocity.x, css_velocity.y) / 1000.0f;
 
     // Keep the velocity updated for the Axis class so that any animations
     // chained off of the smooth scroll will inherit it.
     if (mXAxisModel.IsFinished()) {
       mApzc.mX.SetVelocity(0);
     } else {
       mApzc.mX.SetVelocity(velocity.x);
     }
     if (mYAxisModel.IsFinished()) {
       mApzc.mY.SetVelocity(0);
     } else {
       mApzc.mY.SetVelocity(velocity.y);
     }
     // If we overscroll, hand off to a fling animation that will complete the
     // spring back.
-    CSSToScreenScale zoom = aFrameMetrics.GetZoom();
-    ScreenPoint displacement = (position - aFrameMetrics.GetScrollOffset()) * zoom;
-
-    ScreenPoint overscroll;
-    ScreenPoint adjustedOffset;
+    CSSToParentLayerScale zoom = aFrameMetrics.GetZoom();
+    ParentLayerPoint displacement = (position - aFrameMetrics.GetScrollOffset()) * zoom;
+
+    ParentLayerPoint overscroll;
+    ParentLayerPoint adjustedOffset;
     mApzc.mX.AdjustDisplacement(displacement.x, adjustedOffset.x, overscroll.x);
     mApzc.mY.AdjustDisplacement(displacement.y, adjustedOffset.y, overscroll.y);
 
     aFrameMetrics.ScrollBy(adjustedOffset / zoom);
 
     // The smooth scroll may have caused us to reach the end of our scroll range.
     // This can happen if either the layout.css.scroll-behavior.damping-ratio
     // preference is set to less than 1 (underdamped) or if a smooth scroll
@@ -999,17 +1000,17 @@ AsyncPanZoomController::Destroy()
 }
 
 bool
 AsyncPanZoomController::IsDestroyed() const
 {
   return mTreeManager == nullptr;
 }
 
-/* static */float
+/* static */ScreenCoord
 AsyncPanZoomController::GetTouchStartTolerance()
 {
   return (gfxPrefs::APZTouchStartTolerance() * APZCTreeManager::GetDPI());
 }
 
 /* static */AsyncPanZoomController::AxisLockMode AsyncPanZoomController::GetAxisLockMode()
 {
   return static_cast<AxisLockMode>(gfxPrefs::APZAxisLockMode());
@@ -1126,17 +1127,17 @@ nsEventStatus AsyncPanZoomController::Ha
   }
 
   return rv;
 }
 
 nsEventStatus AsyncPanZoomController::OnTouchStart(const MultiTouchInput& aEvent) {
   APZC_LOG("%p got a touch-start in state %d\n", this, mState);
   mPanDirRestricted = false;
-  ScreenPoint point = GetFirstTouchScreenPoint(aEvent);
+  ParentLayerPoint point = GetFirstTouchPoint(aEvent);
 
   switch (mState) {
     case FLING:
     case ANIMATING_ZOOM:
     case SMOOTH_SCROLL:
       CurrentTouchBlock()->GetOverscrollHandoffChain()->CancelAnimations();
       // Fall through.
     case NOTHING: {
@@ -1180,17 +1181,17 @@ nsEventStatus AsyncPanZoomController::On
 
     case CROSS_SLIDING_X:
     case CROSS_SLIDING_Y:
       // While cross-sliding, we don't want to consume any touchmove events for
       // panning or zooming, and let the caller handle them instead.
       return nsEventStatus_eIgnore;
 
     case TOUCHING: {
-      float panThreshold = GetTouchStartTolerance();
+      ScreenCoord panThreshold = GetTouchStartTolerance();
       UpdateWithTouchAtDevicePoint(aEvent);
 
       if (PanDistance() < panThreshold) {
         return nsEventStatus_eIgnore;
       }
 
       if (gfxPrefs::TouchActionEnabled() && CurrentTouchBlock()->TouchActionAllowsPanningXY()) {
         // User tries to trigger a touch behavior. If allowed touch behavior is vertical pan
@@ -1265,17 +1266,17 @@ nsEventStatus AsyncPanZoomController::On
 
   case PANNING:
   case PANNING_LOCKED_X:
   case PANNING_LOCKED_Y:
   {
     CurrentTouchBlock()->GetOverscrollHandoffChain()->FlushRepaints();
     mX.EndTouch(aEvent.mTime);
     mY.EndTouch(aEvent.mTime);
-    ScreenPoint flingVelocity(mX.GetVelocity(), mY.GetVelocity());
+    ParentLayerPoint flingVelocity = GetVelocityVector();
     // Clear our velocities; if DispatchFling() gives the fling to us,
     // the fling velocity gets *added* to our existing velocity in
     // AcceptFling().
     mX.SetVelocity(0);
     mY.SetVelocity(0);
     // Clear our state so that we don't stay in the PANNING state
     // if DispatchFling() gives the fling to somone else. However,
     // don't send the state change notification until we've determined
@@ -1329,17 +1330,17 @@ nsEventStatus AsyncPanZoomController::On
     return nsEventStatus_eIgnore;
   }
 
   if (!mZoomConstraints.mAllowZoom) {
     return nsEventStatus_eConsumeNoDefault;
   }
 
   SetState(PINCHING);
-  mLastZoomFocus = ToParentLayerCoords(aEvent.mFocusPoint) - mFrameMetrics.mCompositionBounds.TopLeft();
+  mLastZoomFocus = aEvent.mLocalFocusPoint - mFrameMetrics.mCompositionBounds.TopLeft();
 
   return nsEventStatus_eConsumeNoDefault;
 }
 
 nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) {
   APZC_LOG("%p got a scale in state %d\n", this, mState);
 
   if (HasReadyTouchBlock() && !CurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
@@ -1356,34 +1357,34 @@ nsEventStatus AsyncPanZoomController::On
     return nsEventStatus_eConsumeNoDefault;
   }
 
   float spanRatio = aEvent.mCurrentSpan / aEvent.mPreviousSpan;
 
   {
     ReentrantMonitorAutoEnter lock(mMonitor);
 
-    CSSToParentLayerScale userZoom = mFrameMetrics.GetZoomToParent();
-    ParentLayerPoint focusPoint = ToParentLayerCoords(aEvent.mFocusPoint) - mFrameMetrics.mCompositionBounds.TopLeft();
-    CSSPoint cssFocusPoint = focusPoint / mFrameMetrics.GetZoomToParent();
+    CSSToParentLayerScale userZoom = mFrameMetrics.GetZoom();
+    ParentLayerPoint focusPoint = aEvent.mLocalFocusPoint - mFrameMetrics.mCompositionBounds.TopLeft();
+    CSSPoint cssFocusPoint = focusPoint / mFrameMetrics.GetZoom();
 
     CSSPoint focusChange = (mLastZoomFocus - focusPoint) / userZoom;
     // If displacing by the change in focus point will take us off page bounds,
     // then reduce the displacement such that it doesn't.
     focusChange.x -= mX.DisplacementWillOverscrollAmount(focusChange.x);
     focusChange.y -= mY.DisplacementWillOverscrollAmount(focusChange.y);
     ScrollBy(focusChange);
 
     // When we zoom in with focus, we can zoom too much towards the boundaries
     // that we actually go over them. These are the needed displacements along
     // either axis such that we don't overscroll the boundaries when zooming.
     CSSPoint neededDisplacement;
 
-    CSSToParentLayerScale realMinZoom = mZoomConstraints.mMinZoom * mFrameMetrics.mTransformScale;
-    CSSToParentLayerScale realMaxZoom = mZoomConstraints.mMaxZoom * mFrameMetrics.mTransformScale;
+    CSSToParentLayerScale realMinZoom = mZoomConstraints.mMinZoom;
+    CSSToParentLayerScale realMaxZoom = mZoomConstraints.mMaxZoom;
     realMinZoom.scale = std::max(realMinZoom.scale,
                                  mFrameMetrics.mCompositionBounds.width / mFrameMetrics.mScrollableRect.width);
     realMinZoom.scale = std::max(realMinZoom.scale,
                                  mFrameMetrics.mCompositionBounds.height / mFrameMetrics.mScrollableRect.height);
     if (realMaxZoom < realMinZoom) {
       realMaxZoom = realMinZoom;
     }
 
@@ -1448,38 +1449,37 @@ nsEventStatus AsyncPanZoomController::On
     RequestContentRepaint();
     UpdateSharedCompositorFrameMetrics();
   }
 
   return nsEventStatus_eConsumeNoDefault;
 }
 
 bool
-AsyncPanZoomController::ConvertToGecko(const ScreenPoint& aPoint, CSSPoint* aOut)
+AsyncPanZoomController::ConvertToGecko(const ParentLayerPoint& aPoint, CSSPoint* aOut)
 {
   if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
     Matrix4x4 transformToGecko = treeManagerLocal->GetApzcToGeckoTransform(this);
-    Point result = transformToGecko * Point(aPoint.x, aPoint.y);
     // NOTE: This isn't *quite* LayoutDevicePoint, we just don't have a name
     // for this coordinate space and it maps the closest to LayoutDevicePoint.
-    LayoutDevicePoint layoutPoint = LayoutDevicePoint(result.x, result.y);
+    LayoutDevicePoint layoutPoint = TransformTo<LayoutDevicePixel>(transformToGecko, aPoint);
     { // scoped lock to access mFrameMetrics
       ReentrantMonitorAutoEnter lock(mMonitor);
       *aOut = layoutPoint / mFrameMetrics.mDevPixelsPerCSSPixel;
     }
     return true;
   }
   return false;
 }
 
 nsEventStatus AsyncPanZoomController::OnPanMayBegin(const PanGestureInput& aEvent) {
   APZC_LOG("%p got a pan-maybegin in state %d\n", this, mState);
 
-  mX.StartTouch(aEvent.mPanStartPoint.x, aEvent.mTime);
-  mY.StartTouch(aEvent.mPanStartPoint.y, aEvent.mTime);
+  mX.StartTouch(aEvent.mLocalPanStartPoint.x, aEvent.mTime);
+  mY.StartTouch(aEvent.mLocalPanStartPoint.y, aEvent.mTime);
   if (mPanGestureState) {
     mPanGestureState->GetOverscrollHandoffChain()->CancelAnimations();
   } else {
     CancelAnimation();
   }
 
   return nsEventStatus_eConsumeNoDefault;
 }
@@ -1499,18 +1499,18 @@ nsEventStatus AsyncPanZoomController::On
 
   if (mState == SMOOTH_SCROLL) {
     // SMOOTH_SCROLL scrolls are cancelled by pan gestures.
     CancelAnimation();
   }
 
   mPanGestureState = MakeUnique<InputBlockState>(this);
 
-  mX.StartTouch(aEvent.mPanStartPoint.x, aEvent.mTime);
-  mY.StartTouch(aEvent.mPanStartPoint.y, aEvent.mTime);
+  mX.StartTouch(aEvent.mLocalPanStartPoint.x, aEvent.mTime);
+  mY.StartTouch(aEvent.mLocalPanStartPoint.y, aEvent.mTime);
 
   if (GetAxisLockMode() == FREE) {
     SetState(PANNING);
     return nsEventStatus_eConsumeNoDefault;
   }
 
   float dx = aEvent.mPanDisplacement.x, dy = aEvent.mPanDisplacement.y;
   double angle = atan2(dy, dx); // range [-pi, pi]
@@ -1537,29 +1537,28 @@ nsEventStatus AsyncPanZoomController::On
       CancelAnimation();
     }
   }
 
   // We need to update the axis velocity in order to get a useful display port
   // size and position. We need to do so even if this is a momentum pan (i.e.
   // aFingersOnTouchpad == false); in that case the "with touch" part is not
   // really appropriate, so we may want to rethink this at some point.
-  mX.UpdateWithTouchAtDevicePoint(aEvent.mPanStartPoint.x, aEvent.mTime);
-  mY.UpdateWithTouchAtDevicePoint(aEvent.mPanStartPoint.y, aEvent.mTime);
-
-  ScreenPoint panDisplacement = aEvent.mPanDisplacement;
-  ToGlobalScreenCoordinates(&panDisplacement, aEvent.mPanStartPoint);
-  HandlePanningUpdate(panDisplacement);
+  mX.UpdateWithTouchAtDevicePoint(aEvent.mLocalPanStartPoint.x, aEvent.mTime);
+  mY.UpdateWithTouchAtDevicePoint(aEvent.mLocalPanStartPoint.y, aEvent.mTime);
+
+  HandlePanningUpdate(aEvent.mPanDisplacement);
 
   // TODO: Handle pan events sent without pan begin / pan end events properly.
   if (mPanGestureState) {
-    ScreenPoint panDistance(fabs(panDisplacement.x), fabs(panDisplacement.y));
+    ScreenPoint panDistance(fabs(aEvent.mPanDisplacement.x), fabs(aEvent.mPanDisplacement.y));
     OverscrollHandoffState handoffState(
         *mPanGestureState->GetOverscrollHandoffChain(), panDistance);
-    CallDispatchScroll(aEvent.mPanStartPoint, aEvent.mPanStartPoint + aEvent.mPanDisplacement,
+    CallDispatchScroll(aEvent.mLocalPanStartPoint,
+                       aEvent.mLocalPanStartPoint + aEvent.mLocalPanDisplacement,
                        handoffState);
   }
 
   return nsEventStatus_eConsumeNoDefault;
 }
 
 nsEventStatus AsyncPanZoomController::OnPanEnd(const PanGestureInput& aEvent) {
   APZC_LOG("%p got a pan-end in state %d\n", this, mState);
@@ -1603,40 +1602,40 @@ nsEventStatus AsyncPanZoomController::On
 }
 
 nsEventStatus AsyncPanZoomController::OnLongPress(const TapGestureInput& aEvent) {
   APZC_LOG("%p got a long-press in state %d\n", this, mState);
   nsRefPtr<GeckoContentController> controller = GetGeckoContentController();
   if (controller) {
     int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers);
     CSSPoint geckoScreenPoint;
-    if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) {
+    if (ConvertToGecko(aEvent.mLocalPoint, &geckoScreenPoint)) {
       uint64_t blockId = GetInputQueue()->InjectNewTouchBlock(this);
       controller->HandleLongTap(geckoScreenPoint, modifiers, GetGuid(), blockId);
       return nsEventStatus_eConsumeNoDefault;
     }
   }
   return nsEventStatus_eIgnore;
 }
 
 nsEventStatus AsyncPanZoomController::OnLongPressUp(const TapGestureInput& aEvent) {
   APZC_LOG("%p got a long-tap-up in state %d\n", this, mState);
   nsRefPtr<GeckoContentController> controller = GetGeckoContentController();
   if (controller) {
     int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers);
     CSSPoint geckoScreenPoint;
-    if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) {
+    if (ConvertToGecko(aEvent.mLocalPoint, &geckoScreenPoint)) {
       controller->HandleLongTapUp(geckoScreenPoint, modifiers, GetGuid());
       return nsEventStatus_eConsumeNoDefault;
     }
   }
   return nsEventStatus_eIgnore;
 }
 
-nsEventStatus AsyncPanZoomController::GenerateSingleTap(const ScreenIntPoint& aPoint, mozilla::Modifiers aModifiers) {
+nsEventStatus AsyncPanZoomController::GenerateSingleTap(const ParentLayerPoint& aPoint, mozilla::Modifiers aModifiers) {
   nsRefPtr<GeckoContentController> controller = GetGeckoContentController();
   if (controller) {
     CSSPoint geckoScreenPoint;
     if (ConvertToGecko(aPoint, &geckoScreenPoint)) {
       if (!CurrentTouchBlock()->SetSingleTapOccurred()) {
         return nsEventStatus_eIgnore;
       }
       // Because this may be being running as part of APZCTreeManager::ReceiveInputEvent,
@@ -1662,94 +1661,84 @@ void AsyncPanZoomController::OnTouchEndO
   }
 }
 
 nsEventStatus AsyncPanZoomController::OnSingleTapUp(const TapGestureInput& aEvent) {
   APZC_LOG("%p got a single-tap-up in state %d\n", this, mState);
   // If mZoomConstraints.mAllowDoubleTapZoom is true we wait for a call to OnSingleTapConfirmed before
   // sending event to content
   if (!(mZoomConstraints.mAllowDoubleTapZoom && CurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) {
-    return GenerateSingleTap(aEvent.mPoint, aEvent.modifiers);
+    return GenerateSingleTap(aEvent.mLocalPoint, aEvent.modifiers);
   }
   return nsEventStatus_eIgnore;
 }
 
 nsEventStatus AsyncPanZoomController::OnSingleTapConfirmed(const TapGestureInput& aEvent) {
   APZC_LOG("%p got a single-tap-confirmed in state %d\n", this, mState);
-  return GenerateSingleTap(aEvent.mPoint, aEvent.modifiers);
+  return GenerateSingleTap(aEvent.mLocalPoint, aEvent.modifiers);
 }
 
 nsEventStatus AsyncPanZoomController::OnDoubleTap(const TapGestureInput& aEvent) {
   APZC_LOG("%p got a double-tap in state %d\n", this, mState);
   nsRefPtr<GeckoContentController> controller = GetGeckoContentController();
   if (controller) {
     if (mZoomConstraints.mAllowDoubleTapZoom && CurrentTouchBlock()->TouchActionAllowsDoubleTapZoom()) {
       int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers);
       CSSPoint geckoScreenPoint;
-      if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) {
+      if (ConvertToGecko(aEvent.mLocalPoint, &geckoScreenPoint)) {
         controller->HandleDoubleTap(geckoScreenPoint, modifiers, GetGuid());
       }
     }
     return nsEventStatus_eConsumeNoDefault;
   }
   return nsEventStatus_eIgnore;
 }
 
 nsEventStatus AsyncPanZoomController::OnCancelTap(const TapGestureInput& aEvent) {
   APZC_LOG("%p got a cancel-tap in state %d\n", this, mState);
   // XXX: Implement this.
   return nsEventStatus_eIgnore;
 }
 
-// Helper function for To[Global|Local]ScreenCoordinates().
-// TODO(botond): Generalize this into a template function in UnitTransforms.h.
-static void TransformVector(const Matrix4x4& aTransform,
-                            ScreenPoint* aVector,
-                            const ScreenPoint& aAnchor) {
-  ScreenPoint start = aAnchor;
-  ScreenPoint end = aAnchor + *aVector;
-  start = TransformTo<ScreenPixel>(aTransform, start);
-  end = TransformTo<ScreenPixel>(aTransform, end);
-  *aVector = end - start;
-}
-
-void AsyncPanZoomController::ToGlobalScreenCoordinates(ScreenPoint* aVector,
-                                                       const ScreenPoint& aAnchor) const {
+
+ScreenPoint AsyncPanZoomController::ToScreenCoordinates(const ParentLayerPoint& aVector,
+                                                        const ParentLayerPoint& aAnchor) const {
   if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
     Matrix4x4 apzcToScreen = treeManagerLocal->GetScreenToApzcTransform(this).Inverse();
-    TransformVector(apzcToScreen, aVector, aAnchor);
+    return TransformVector<ScreenPixel>(apzcToScreen, aVector, aAnchor);
   }
+  return ViewAs<ScreenPixel>(aVector, PixelCastJustification::TransformNotAvailable);
 }
 
-void AsyncPanZoomController::ToLocalScreenCoordinates(ScreenPoint* aVector,
-                                                      const ScreenPoint& aAnchor) const {
+ParentLayerPoint AsyncPanZoomController::ToParentLayerCoordinates(const ScreenPoint& aVector,
+                                                                  const ScreenPoint& aAnchor) const {
   if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
     Matrix4x4 transform = treeManagerLocal->GetScreenToApzcTransform(this);
-    TransformVector(transform, aVector, aAnchor);
+    return TransformVector<ParentLayerPixel>(transform, aVector, aAnchor);
   }
+  return ViewAs<ParentLayerPixel>(aVector, PixelCastJustification::TransformNotAvailable);
 }
 
-float AsyncPanZoomController::PanDistance() const {
-  ScreenPoint panVector;
-  ScreenPoint panStart;
+ScreenCoord AsyncPanZoomController::PanDistance() const {
+  ParentLayerPoint panVector;
+  ParentLayerPoint panStart;
   {
     ReentrantMonitorAutoEnter lock(mMonitor);
-    panVector = ScreenPoint(mX.PanDistance(), mY.PanDistance());
+    panVector = ParentLayerPoint(mX.PanDistance(), mY.PanDistance());
     panStart = PanStart();
   }
-  ToGlobalScreenCoordinates(&panVector, panStart);
-  return NS_hypot(panVector.x, panVector.y);
+  return ToScreenCoordinates(panVector, panStart).Length();
 }
 
-ScreenPoint AsyncPanZoomController::PanStart() const {
-  return ScreenPoint(mX.PanStart(), mY.PanStart());
+ParentLayerPoint AsyncPanZoomController::PanStart() const {
+  return ParentLayerPoint(mX.PanStart(), mY.PanStart());
 }
 
-const ScreenPoint AsyncPanZoomController::GetVelocityVector() const {
-  return ScreenPoint(mX.GetVelocity(), mY.GetVelocity());
+const ParentLayerPoint AsyncPanZoomController::GetVelocityVector() const {
+  return ParentLayerPoint(mX.GetVelocity(), mY.GetVelocity());
 }
 
 void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle) {
   // Handling of cross sliding will need to be added in this method after touch-action released
   // enabled by default.
   if (CurrentTouchBlock()->TouchActionAllowsPanningXY()) {
     if (mX.CanScrollNow() && mY.CanScrollNow()) {
       if (IsCloseToHorizontal(aAngle, gfxPrefs::APZAxisLockAngle())) {
@@ -1839,17 +1828,17 @@ void AsyncPanZoomController::HandlePanni
       }
     }
   }
 }
 
 nsEventStatus AsyncPanZoomController::StartPanning(const MultiTouchInput& aEvent) {
   ReentrantMonitorAutoEnter lock(mMonitor);
 
-  ScreenPoint point = GetFirstTouchScreenPoint(aEvent);
+  ParentLayerPoint point = GetFirstTouchPoint(aEvent);
   float dx = mX.PanDistance(point.x);
   float dy = mY.PanDistance(point.y);
 
   // When the touch move breaks through the pan threshold, reposition the touch down origin
   // so the page won't jump when we start panning.
   mX.StartTouch(point.x, aEvent.mTime);
   mY.StartTouch(point.y, aEvent.mTime);
 
@@ -1872,35 +1861,35 @@ nsEventStatus AsyncPanZoomController::St
     }
     return nsEventStatus_eConsumeNoDefault;
   }
   // Don't consume an event that didn't trigger a panning.
   return nsEventStatus_eIgnore;
 }
 
 void AsyncPanZoomController::UpdateWithTouchAtDevicePoint(const MultiTouchInput& aEvent) {
-  ScreenPoint point = GetFirstTouchScreenPoint(aEvent);
+  ParentLayerPoint point = GetFirstTouchPoint(aEvent);
   mX.UpdateWithTouchAtDevicePoint(point.x, aEvent.mTime);
   mY.UpdateWithTouchAtDevicePoint(point.y, aEvent.mTime);
 }
 
-bool AsyncPanZoomController::AttemptScroll(const ScreenPoint& aStartPoint,
-                                           const ScreenPoint& aEndPoint,
+bool AsyncPanZoomController::AttemptScroll(const ParentLayerPoint& aStartPoint,
+                                           const ParentLayerPoint& aEndPoint,
                                            OverscrollHandoffState& aOverscrollHandoffState) {
 
   // "start - end" rather than "end - start" because e.g. moving your finger
   // down (*positive* direction along y axis) causes the vertical scroll offset
   // to *decrease* as the page follows your finger.
-  ScreenPoint displacement = aStartPoint - aEndPoint;
-
-  ScreenPoint overscroll;  // will be used outside monitor block
+  ParentLayerPoint displacement = aStartPoint - aEndPoint;
+
+  ParentLayerPoint overscroll;  // will be used outside monitor block
   {
     ReentrantMonitorAutoEnter lock(mMonitor);
 
-    ScreenPoint adjustedDisplacement;
+    ParentLayerPoint adjustedDisplacement;
     bool xChanged = mX.AdjustDisplacement(displacement.x, adjustedDisplacement.x, overscroll.x);
     bool yChanged = mY.AdjustDisplacement(displacement.y, adjustedDisplacement.y, overscroll.y);
     if (xChanged || yChanged) {
       ScheduleComposite();
     }
 
     if (!IsZero(adjustedDisplacement)) {
       ScrollBy(adjustedDisplacement / mFrameMetrics.GetZoom());
@@ -1927,34 +1916,34 @@ bool AsyncPanZoomController::AttemptScro
 
   // If there is no APZC later in the handoff chain that accepted the
   // overscroll, try to accept it ourselves. We only accept it if we
   // are pannable.
   APZC_LOG("%p taking overscroll during panning\n", this);
   return OverscrollForPanning(overscroll, aOverscrollHandoffState.mPanDistance);
 }
 
-bool AsyncPanZoomController::OverscrollForPanning(ScreenPoint aOverscroll,
+bool AsyncPanZoomController::OverscrollForPanning(ParentLayerPoint aOverscroll,
                                                   const ScreenPoint& aPanDistance) {
   // Only allow entering overscroll along an axis if the pan distance along
   // that axis is greater than the pan distance along the other axis by a
   // configurable factor. If we are already overscrolled, don't check this.
   if (!IsOverscrolled()) {
     if (aPanDistance.x < gfxPrefs::APZMinPanDistanceRatio() * aPanDistance.y) {
       aOverscroll.x = 0;
     }
     if (aPanDistance.y < gfxPrefs::APZMinPanDistanceRatio() * aPanDistance.x) {
       aOverscroll.y = 0;
     }
   }
 
   return OverscrollBy(aOverscroll);
 }
 
-bool AsyncPanZoomController::OverscrollBy(const ScreenPoint& aOverscroll) {
+bool AsyncPanZoomController::OverscrollBy(const ParentLayerPoint& aOverscroll) {
   if (!gfxPrefs::APZOverscrollEnabled()) {
     return false;
   }
 
   ReentrantMonitorAutoEnter lock(mMonitor);
   // Do not go into overscroll in a direction in which we have no room to
   // scroll to begin with.
   bool xCanScroll = mX.CanScroll();
@@ -1981,118 +1970,118 @@ nsRefPtr<const OverscrollHandoffChain> A
 
   // This APZC IsDestroyed(). To avoid callers having to special-case this
   // scenario, just build a 1-element chain containing ourselves.
   OverscrollHandoffChain* result = new OverscrollHandoffChain;
   result->Add(this);
   return result;
 }
 
-void AsyncPanZoomController::AcceptFling(const ScreenPoint& aVelocity,
+void AsyncPanZoomController::AcceptFling(const ParentLayerPoint& aVelocity,
                                          const nsRefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
                                          bool aHandoff) {
   // We may have a pre-existing velocity for whatever reason (for example,