Merge m-c to autoland. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 01 Jun 2017 16:44:19 -0400
changeset 410005 51413258faef176d7ba0e9e52a97bed5c1078b21
parent 410004 9aa183c8533e23762ce56370ccd18128eb1df27c (current diff)
parent 409991 b138d2f271fdb598bf8a66c2dcb7fe391ca2a96f (diff)
child 410006 18b819dd5afc1b7af098076343487f4e5ef3d08e
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to autoland. a=merge
--- a/browser/base/content/test/performance/browser.ini
+++ b/browser/base/content/test/performance/browser.ini
@@ -1,8 +1,12 @@
 [DEFAULT]
 support-files =
   head.js
 [browser_startup.js]
+[browser_tabclose_grow_reflows.js]
 [browser_tabclose_reflows.js]
 [browser_tabopen_reflows.js]
+[browser_tabopen_squeeze_reflows.js]
+[browser_tabswitch_reflows.js]
 [browser_toolbariconcolor_restyles.js]
+[browser_windowclose_reflows.js]
 [browser_windowopen_reflows.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabclose_grow_reflows.js
@@ -0,0 +1,58 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS. This
+ * is a whitelist that should slowly go away as we improve the performance of
+ * the front-end. Instead of adding more reflows to the whitelist, you should
+ * be modifying your code to avoid the reflow.
+ *
+ * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+  /**
+   * Nothing here! Please don't add anything new!
+   */
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when closing a tab that will
+ * cause the existing tabs to grow bigger.
+ */
+add_task(async function() {
+  await ensureNoPreloadedBrowser();
+
+  // At the time of writing, there are no reflows on tab closing with
+  // tab growth. Mochitest will fail if we have no assertions, so we
+  // add one here to make sure nobody adds any new ones.
+  Assert.equal(EXPECTED_REFLOWS.length, 0,
+    "We shouldn't have added any new expected reflows.");
+
+  // Compute the number of tabs we can put into the strip without
+  // overflowing. If we remove one of the tabs, we know that the
+  // remaining tabs will grow to fill the remaining space in the
+  // tabstrip.
+  const TAB_COUNT_FOR_GROWTH = computeMaxTabCount();
+  await createTabs(TAB_COUNT_FOR_GROWTH);
+
+  // Because the tab strip is a scrollable frame, we can't use the
+  // default dirtying function from withReflowObserver and reliably
+  // get reflows for the strip. Instead, we provide a node that's
+  // already in the scrollable frame to dirty - in this case, the
+  // original tab.
+  let origTab = gBrowser.selectedTab;
+  let lastTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+  await BrowserTestUtils.switchTab(gBrowser, lastTab);
+
+  await withReflowObserver(async function() {
+    let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+    let tab = gBrowser.tabs[gBrowser.tabs.length - 1];
+    gBrowser.removeTab(tab, { animate: true });
+    await BrowserTestUtils.waitForEvent(tab, "transitionend",
+      false, e => e.propertyName === "max-width");
+    await switchDone;
+  }, EXPECTED_REFLOWS, window, origTab);
+
+  await removeAllButFirstTab();
+});
--- a/browser/base/content/test/performance/browser_tabclose_reflows.js
+++ b/browser/base/content/test/performance/browser_tabclose_reflows.js
@@ -5,55 +5,27 @@
  * is a whitelist that should slowly go away as we improve the performance of
  * the front-end. Instead of adding more reflows to the whitelist, you should
  * be modifying your code to avoid the reflow.
  *
  * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
  * for tips on how to do that.
  */
 const EXPECTED_REFLOWS = [
-  [
-    "_adjustFocusAfterTabSwitch@chrome://browser/content/tabbrowser.xml",
-  ],
+  /**
+   * Nothing here! Please don't add anything new!
+   */
 ];
 
-if (gMultiProcessBrowser) {
-  EXPECTED_REFLOWS.push(
-    [
-      "_adjustFocusAfterTabSwitch@chrome://browser/content/tabbrowser.xml",
-    ],
-  );
-}
-
 /*
  * This test ensures that there are no unexpected
  * uninterruptible reflows when closing new tabs.
  */
 add_task(async function() {
-  // If we've got a preloaded browser, get rid of it so that it
-  // doesn't interfere with the test if it's loading. We have to
-  // do this before we disable preloading or changing the new tab
-  // URL, otherwise _getPreloadedBrowser will return null, despite
-  // the preloaded browser existing.
-  let preloaded = gBrowser._getPreloadedBrowser();
-  if (preloaded) {
-    preloaded.remove();
-  }
-
-  await SpecialPowers.pushPrefEnv({
-    set: [["browser.newtab.preload", false]],
-  });
-
-  let aboutNewTabService = Cc["@mozilla.org/browser/aboutnewtab-service;1"]
-                             .getService(Ci.nsIAboutNewTabService);
-  aboutNewTabService.newTabURL = "about:blank";
-
-  registerCleanupFunction(() => {
-    aboutNewTabService.resetNewTabURL();
-  });
+  await ensureNoPreloadedBrowser();
 
   // Because the tab strip is a scrollable frame, we can't use the
   // default dirtying function from withReflowObserver and reliably
   // get reflows for the strip. Instead, we provide a node that's
   // already in the scrollable frame to dirty - in this case, the
   // original tab.
   let origTab = gBrowser.selectedTab;
 
@@ -63,9 +35,10 @@ add_task(async function() {
   // Add a reflow observer and open a new tab.
   await withReflowObserver(async function() {
     let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
     gBrowser.removeTab(tab, { animate: true });
     await BrowserTestUtils.waitForEvent(tab, "transitionend",
         false, e => e.propertyName === "max-width");
     await switchDone;
   }, EXPECTED_REFLOWS, window, origTab);
+  is(EXPECTED_REFLOWS.length, 0, "No reflows are expected when closing a tab");
 });
--- a/browser/base/content/test/performance/browser_tabopen_reflows.js
+++ b/browser/base/content/test/performance/browser_tabopen_reflows.js
@@ -12,61 +12,25 @@
  * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
  * for tips on how to do that.
  */
 const EXPECTED_REFLOWS = [
   // selection change notification may cause querying the focused editor content
   // by IME and that will cause reflow.
   [
     "select@chrome://global/content/bindings/textbox.xml",
-    "focusAndSelectUrlBar@chrome://browser/content/browser.js",
-    "_adjustFocusAfterTabSwitch@chrome://browser/content/tabbrowser.xml",
-  ],
-
-  // selection change notification may cause querying the focused editor content
-  // by IME and that will cause reflow.
-  [
-    "select@chrome://global/content/bindings/textbox.xml",
-    "focusAndSelectUrlBar@chrome://browser/content/browser.js",
-    "_adjustFocusAfterTabSwitch@chrome://browser/content/tabbrowser.xml",
-  ],
-
-  [
-    "select@chrome://global/content/bindings/textbox.xml",
-    "focusAndSelectUrlBar@chrome://browser/content/browser.js",
-    "_adjustFocusAfterTabSwitch@chrome://browser/content/tabbrowser.xml",
   ],
 ];
 
 /*
  * This test ensures that there are no unexpected
  * uninterruptible reflows when opening new tabs.
  */
 add_task(async function() {
-  // If we've got a preloaded browser, get rid of it so that it
-  // doesn't interfere with the test if it's loading. We have to
-  // do this before we disable preloading or changing the new tab
-  // URL, otherwise _getPreloadedBrowser will return null, despite
-  // the preloaded browser existing.
-  let preloaded = gBrowser._getPreloadedBrowser();
-  if (preloaded) {
-    preloaded.remove();
-  }
-
-  await SpecialPowers.pushPrefEnv({
-    set: [["browser.newtab.preload", false]],
-  });
-
-  let aboutNewTabService = Cc["@mozilla.org/browser/aboutnewtab-service;1"]
-                             .getService(Ci.nsIAboutNewTabService);
-  aboutNewTabService.newTabURL = "about:blank";
-
-  registerCleanupFunction(() => {
-    aboutNewTabService.resetNewTabURL();
-  });
+  await ensureNoPreloadedBrowser();
 
   // Because the tab strip is a scrollable frame, we can't use the
   // default dirtying function from withReflowObserver and reliably
   // get reflows for the strip. Instead, we provide a node that's
   // already in the scrollable frame to dirty - in this case, the
   // original tab.
   let origTab = gBrowser.selectedTab;
 
@@ -78,9 +42,8 @@ add_task(async function() {
         false, e => e.propertyName === "max-width");
     await switchDone;
   }, EXPECTED_REFLOWS, window, origTab);
 
   let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
   await switchDone;
 });
-
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabopen_squeeze_reflows.js
@@ -0,0 +1,52 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS. This
+ * is a whitelist that should slowly go away as we improve the performance of
+ * the front-end. Instead of adding more reflows to the whitelist, you should
+ * be modifying your code to avoid the reflow.
+ *
+ * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+  [
+    "select@chrome://global/content/bindings/textbox.xml",
+    "focusAndSelectUrlBar@chrome://browser/content/browser.js",
+    "_adjustFocusAfterTabSwitch@chrome://browser/content/tabbrowser.xml",
+  ],
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when opening a new tab that will
+ * cause the existing tabs to squeeze smaller.
+ */
+add_task(async function() {
+  await ensureNoPreloadedBrowser();
+
+  // Compute the number of tabs we can put into the strip without
+  // overflowing, and remove one, so that we can create
+  // TAB_COUNT_FOR_SQUEEE tabs, and then one more, which should
+  // cause the tab to squeeze to a smaller size rather than overflow.
+  const TAB_COUNT_FOR_SQUEEZE = computeMaxTabCount() - 1;
+
+  await createTabs(TAB_COUNT_FOR_SQUEEZE);
+
+  // Because the tab strip is a scrollable frame, we can't use the
+  // default dirtying function from withReflowObserver and reliably
+  // get reflows for the strip. Instead, we provide a node that's
+  // already in the scrollable frame to dirty - in this case, the
+  // original tab.
+  let origTab = gBrowser.selectedTab;
+
+  await withReflowObserver(async function() {
+    let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+    BrowserOpenTab();
+    await BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "transitionend",
+      false, e => e.propertyName === "max-width");
+    await switchDone;
+  }, EXPECTED_REFLOWS, window, origTab);
+
+  await removeAllButFirstTab();
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabswitch_reflows.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS. This
+ * is a whitelist that should slowly go away as we improve the performance of
+ * the front-end. Instead of adding more reflows to the whitelist, you should
+ * be modifying your code to avoid the reflow.
+ *
+ * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+  /**
+   * Nothing here! Please don't add anything new!
+   */
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when switching between two
+ * tabs that are both fully visible.
+ */
+add_task(async function() {
+  await ensureNoPreloadedBrowser();
+
+  // At the time of writing, there are no reflows on simple tab switching.
+  // Mochitest will fail if we have no assertions, so we add one here
+  // to make sure nobody adds any new ones.
+  Assert.equal(EXPECTED_REFLOWS.length, 0,
+    "We shouldn't have added any new expected reflows.");
+
+  // Because the tab strip is a scrollable frame, we can't use the
+  // default dirtying function from withReflowObserver and reliably
+  // get reflows for the strip. Instead, we provide a node that's
+  // already in the scrollable frame to dirty - in this case, the
+  // original tab.
+  let origTab = gBrowser.selectedTab;
+
+  let firstSwitchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+  let otherTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+  await firstSwitchDone;
+
+  await withReflowObserver(async function() {
+    let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+    gBrowser.selectedTab = origTab;
+    await switchDone;
+  }, EXPECTED_REFLOWS, window, origTab);
+
+  await BrowserTestUtils.removeTab(otherTab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/performance/browser_windowclose_reflows.js
@@ -0,0 +1,50 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS. This
+ * is a whitelist that should slowly go away as we improve the performance of
+ * the front-end. Instead of adding more reflows to the whitelist, you should
+ * be modifying your code to avoid the reflow.
+ *
+ * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+  /**
+   * Nothing here! Please don't add anything new!
+   */
+];
+
+/**
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when closing windows. When the
+ * window is closed, the test waits until the original window
+ * has activated.
+ */
+add_task(function*() {
+  // Ensure that this browser window starts focused. This seems to be
+  // necessary to avoid intermittent failures when running this test
+  // on repeat.
+  yield new Promise(resolve => {
+    waitForFocus(resolve, window);
+  });
+
+  let win = yield BrowserTestUtils.openNewBrowserWindow();
+  yield new Promise(resolve => {
+    waitForFocus(resolve, win);
+  });
+
+  // At the time of writing, there are no reflows on window closing.
+  // Mochitest will fail if we have no assertions, so we add one here
+  // to make sure nobody adds any new ones.
+  Assert.equal(EXPECTED_REFLOWS.length, 0,
+    "We shouldn't have added any new expected reflows for window close.");
+
+  yield withReflowObserver(async function() {
+    let promiseOrigBrowserFocused = BrowserTestUtils.waitForCondition(() => {
+      return Services.focus.activeWindow == window;
+    });
+    await BrowserTestUtils.closeWindow(win);
+    await promiseOrigBrowserFocused;
+  }, EXPECTED_REFLOWS, win);
+});
--- a/browser/base/content/test/performance/browser_windowopen_reflows.js
+++ b/browser/base/content/test/performance/browser_windowopen_reflows.js
@@ -8,24 +8,16 @@
  * is a whitelist that should slowly go away as we improve the performance of
  * the front-end. Instead of adding more reflows to the whitelist, you should
  * be modifying your code to avoid the reflow.
  *
  * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
  * for tips on how to do that.
  */
 const EXPECTED_REFLOWS = [
-  // Selecting the address bar causes two reflows, unfortunately.
-  [
-    "select@chrome://global/content/bindings/textbox.xml",
-    "focusAndSelectUrlBar@chrome://browser/content/browser.js",
-    "_delayedStartup@chrome://browser/content/browser.js",
-  ],
-
-  // Selecting the address bar causes two reflows, unfortunately.
   [
     "select@chrome://global/content/bindings/textbox.xml",
     "focusAndSelectUrlBar@chrome://browser/content/browser.js",
     "_delayedStartup@chrome://browser/content/browser.js",
   ],
 ];
 
 if (Services.appinfo.OS == "Darwin") {
--- a/browser/base/content/test/performance/head.js
+++ b/browser/base/content/test/performance/head.js
@@ -148,8 +148,92 @@ async function withReflowObserver(testFn
 
     els.removeListenerForAllEvents(win, dirtyFrameFn, true);
     docShell.removeWeakReflowObserver(observer);
 
     elemToDirty.style.margin = "";
   }
 }
 
+async function ensureNoPreloadedBrowser() {
+  // If we've got a preloaded browser, get rid of it so that it
+  // doesn't interfere with the test if it's loading. We have to
+  // do this before we disable preloading or changing the new tab
+  // URL, otherwise _getPreloadedBrowser will return null, despite
+  // the preloaded browser existing.
+  let preloaded = gBrowser._getPreloadedBrowser();
+  if (preloaded) {
+    preloaded.remove();
+  }
+
+  await SpecialPowers.pushPrefEnv({
+    set: [["browser.newtab.preload", false]],
+  });
+
+  let aboutNewTabService = Cc["@mozilla.org/browser/aboutnewtab-service;1"]
+                             .getService(Ci.nsIAboutNewTabService);
+  aboutNewTabService.newTabURL = "about:blank";
+
+  registerCleanupFunction(() => {
+    aboutNewTabService.resetNewTabURL();
+  });
+}
+
+/**
+ * Calculate and return how many additional tabs can be fit into the
+ * tabstrip without causing it to overflow.
+ *
+ * @return int
+ *         The maximum additional tabs that can be fit into the
+ *         tabstrip without causing it to overflow.
+ */
+function computeMaxTabCount() {
+  let currentTabCount = gBrowser.tabs.length;
+  let newTabButton =
+    document.getAnonymousElementByAttribute(gBrowser.tabContainer,
+                                            "class", "tabs-newtab-button");
+  let newTabRect = newTabButton.getBoundingClientRect();
+  let tabStripRect = gBrowser.tabContainer.mTabstrip.getBoundingClientRect();
+  let availableTabStripWidth = tabStripRect.width - newTabRect.width;
+
+  let tabMinWidth =
+    parseInt(getComputedStyle(gBrowser.selectedTab, null).minWidth, 10);
+
+  let maxTabCount = Math.floor(availableTabStripWidth / tabMinWidth) - currentTabCount;
+  Assert.ok(maxTabCount > 0,
+            "Tabstrip needs to be wide enough to accomodate at least 1 more tab " +
+            "without overflowing.");
+  return maxTabCount;
+}
+
+/**
+ * Helper function that opens up some number of about:blank tabs, and wait
+ * until they're all fully open.
+ *
+ * @param howMany (int)
+ *        How many about:blank tabs to open.
+ */
+async function createTabs(howMany) {
+  let uris = [];
+  while (howMany--) {
+    uris.push("about:blank");
+  }
+
+  gBrowser.loadTabs(uris, true, false);
+
+  await BrowserTestUtils.waitForCondition(() => {
+    return Array.from(gBrowser.tabs).every(tab => tab._fullyOpen);
+  });
+}
+
+/**
+ * Removes all of the tabs except the originally selected
+ * tab, and waits until all of the DOM nodes have been
+ * completely removed from the tab strip.
+ */
+async function removeAllButFirstTab() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["browser.tabs.warnOnCloseOtherTabs", false]],
+  });
+  gBrowser.removeAllTabsBut(gBrowser.tabs[0]);
+  await BrowserTestUtils.waitForCondition(() => gBrowser.tabs.length == 1);
+  await SpecialPowers.popPrefEnv();
+}
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -197,16 +197,22 @@ this.PanelMultiView = class {
     return this._panelViews;
   }
   get _dwu() {
     if (this.__dwu)
       return this.__dwu;
     return this.__dwu = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
                                    .getInterface(Ci.nsIDOMWindowUtils);
   }
+  get _screenManager() {
+    if (this.__screenManager)
+      return this.__screenManager;
+    return this.__screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
+                                    .getService(Ci.nsIScreenManager);
+  }
   get _currentSubView() {
     return this.panelViews ? this.panelViews.currentView : this.__currentSubView;
   }
   set _currentSubView(panel) {
     if (this.panelViews)
       this.panelViews.currentView = panel;
     else
       this.__currentSubView = panel;
@@ -769,50 +775,57 @@ this.PanelMultiView = class {
         // subview causes the panel to exceed the dimensions of the screen in the
         // direction that the panel originally opened in. This property resets
         // every time the popup closes, which is why we have to set it each time.
         this._panel.autoPosition = false;
         if (this.panelViews) {
           this.window.addEventListener("keydown", this);
           this._panel.addEventListener("mousemove", this);
         }
+
+        // Before opening the panel, we have to limit the maximum height of any
+        // view based on the space that will be available. We cannot just use
+        // window.screen.availTop and availHeight because these may return an
+        // incorrect value when the window spans multiple screens.
+        let anchorBox = this._panel.anchorNode.boxObject;
+        let screen = this._screenManager.screenForRect(anchorBox.screenX,
+                                                       anchorBox.screenY,
+                                                       anchorBox.width,
+                                                       anchorBox.height);
+        let availTop = {}, availHeight = {};
+        screen.GetAvailRect({}, availTop, {}, availHeight);
+        let cssAvailTop = availTop.value / screen.defaultCSSScaleFactor;
+
+        // The distance from the anchor to the available margin of the screen is
+        // based on whether the panel will open towards the top or the bottom.
+        let maxHeight;
+        if (this._panel.alignmentPosition.startsWith("before_")) {
+          maxHeight = anchorBox.screenY - cssAvailTop;
+        } else {
+          let anchorScreenBottom = anchorBox.screenY + anchorBox.height;
+          let cssAvailHeight = availHeight.value / screen.defaultCSSScaleFactor;
+          maxHeight = cssAvailTop + cssAvailHeight - anchorScreenBottom;
+        }
+
+        // To go from the maximum height of the panel to the maximum height of
+        // the view stack, we need to subtract the height of the arrow and the
+        // height of the opposite margin, but we cannot get their actual values
+        // because the panel is not visible yet. However, we know that this is
+        // currently 11px on Mac, 13px on Windows, and 13px on Linux. We also
+        // want an extra margin, both for visual reasons and to prevent glitches
+        // due to small rounding errors. So, we just use a value that makes
+        // sense for all platforms. If the arrow visuals change significantly,
+        // this value will be easy to adjust.
+        const EXTRA_MARGIN_PX = 20;
+        this._viewStack.style.maxHeight = (maxHeight - EXTRA_MARGIN_PX) + "px";
         break;
       case "popupshown":
         // Now that the main view is visible, we can check the height of the
         // description elements it contains.
         this.descriptionHeightWorkaround();
-
-        if (!this.panelViews) {
-          // Now that the panel has opened, we can compute the distance from its
-          // anchor to the available margin of the screen, based on whether the
-          // panel actually opened towards the top or the bottom. We use this to
-          // limit its maximum height, which is relevant when opening a subview.
-          let maxHeight;
-          if (this._panel.alignmentPosition.startsWith("before_")) {
-            maxHeight = this._panel.getOuterScreenRect().bottom -
-                        this.window.screen.availTop;
-          } else {
-            maxHeight = this.window.screen.availTop +
-                        this.window.screen.availHeight -
-                        this._panel.getOuterScreenRect().top;
-          }
-          // To go from the maximum height of the panel to the maximum height of
-          // the view stack, we start by subtracting the height of the arrow box.
-          // We don't need to trigger a new layout because this does not change.
-          let arrowBox = this.document.getAnonymousElementByAttribute(
-                                          this._panel, "anonid", "arrowbox");
-          maxHeight -= this._dwu.getBoundsWithoutFlushing(arrowBox).height;
-          // We subtract a fixed margin to account for variable borders. We don't
-          // try to measure this accurately so it's faster, we don't depend on
-          // the arrowpanel structure, and we don't hit rounding errors. Instead,
-          // we use a value that is much greater than the typical borders and
-          // makes sense visually.
-          const EXTRA_MARGIN_PX = 8;
-          this._viewStack.style.maxHeight = (maxHeight - EXTRA_MARGIN_PX) + "px";
-        }
         break;
       case "popuphidden":
         this.node.removeAttribute("panelopen");
         this.showMainView();
         if (this.panelViews) {
           this.window.removeEventListener("keydown", this);
           this._panel.removeEventListener("mousemove", this);
           this._resetKeyNavigation();
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -81,16 +81,17 @@ skip-if = debug # bug 1211084
 [browser_cleaner.js]
 [browser_crashedTabs.js]
 skip-if = !e10s || !crashreporter
 [browser_unrestored_crashedTabs.js]
 skip-if = !e10s || !crashreporter
 [browser_revive_crashed_bg_tabs.js]
 skip-if = !e10s || !crashreporter
 [browser_dying_cache.js]
+skip-if = (os == 'win') # bug 1331853
 [browser_dynamic_frames.js]
 [browser_form_restore_events.js]
 [browser_formdata.js]
 [browser_formdata_cc.js]
 [browser_formdata_format.js]
 [browser_formdata_xpath.js]
 [browser_frametree.js]
 [browser_frame_history.js]
--- a/browser/themes/shared/toolbarbuttons.inc.css
+++ b/browser/themes/shared/toolbarbuttons.inc.css
@@ -58,18 +58,18 @@ toolbar[brighttext] {
 .tabbrowser-arrowscrollbox > .scrollbutton-up[disabled=true],
 .tabbrowser-arrowscrollbox > .scrollbutton-down[disabled=true],
 /* specialcase the overflow and the hamburger button so they show up disabled in customize mode. */
 #nav-bar-overflow-button[disabled=true] > .toolbarbutton-icon,
 #PanelUI-menu-button[disabled=true] > .toolbarbutton-badge-stack > .toolbarbutton-icon,
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-icon,
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-menu-dropmarker,
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-menubutton-dropmarker,
-#main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-menubutton-button > .toolbarbutton-icon,
-#main-window:not([customizing]) .toolbarbutton-1 > .toolbarbutton-menubutton-button[disabled=true] > .toolbarbutton-icon {
+#main-window:not([customizing]) .toolbarbutton-1 > .toolbarbutton-menubutton-button[disabled=true] > .toolbarbutton-icon,
+#main-window:not([customizing]) .toolbarbutton-1[disabled=true] > :-moz-any(.toolbarbutton-menubutton-button, .toolbarbutton-badge-stack) > .toolbarbutton-icon {
   opacity: 0.4;
 }
 
 .toolbarbutton-1 > .toolbarbutton-menu-dropmarker,
 .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   list-style-image: url("chrome://browser/skin/toolbarbutton-dropdown-arrow.png");
 }
 
--- a/dom/base/nsFocusManager.cpp
+++ b/dom/base/nsFocusManager.cpp
@@ -1564,17 +1564,17 @@ nsFocusManager::CheckIfFocusable(nsICont
   // can't focus elements that are not in documents
   if (!doc) {
     LOGCONTENT("Cannot focus %s because content not in document", aContent)
     return nullptr;
   }
 
   // Make sure that our frames are up to date
   mEventHandlingNeedsFlush = false;
-  doc->FlushPendingNotifications(FlushType::Layout);
+  doc->FlushPendingNotifications(FlushType::Frames);
 
   nsIPresShell *shell = doc->GetShell();
   if (!shell)
     return nullptr;
 
   // the root content can always be focused,
   // except in userfocusignored context.
   if (aContent == doc->GetRootElement())
--- a/dom/workers/ServiceWorkerPrivate.cpp
+++ b/dom/workers/ServiceWorkerPrivate.cpp
@@ -352,18 +352,22 @@ private:
   }
 
   void
   RemovePromise(ExtendableEventResult aResult)
   {
     MOZ_ASSERT(mWorkerPrivate);
     mWorkerPrivate->AssertIsOnWorkerThread();
     MOZ_DIAGNOSTIC_ASSERT(mPendingPromisesCount > 0);
-    MOZ_ASSERT(mSelfRef);
-    MOZ_ASSERT(mKeepAliveToken);
+
+    // Note: mSelfRef and mKeepAliveToken can be nullptr here
+    //       if MaybeCleanup() was called just before a promise
+    //       settled.  This can happen, for example, if the
+    //       worker thread is being terminated for running too
+    //       long, browser shutdown, etc.
 
     mRejected |= (aResult == Rejected);
 
     --mPendingPromisesCount;
     if (mPendingPromisesCount) {
       return;
     }
 
--- a/gfx/2d/DrawTargetD2D1.cpp
+++ b/gfx/2d/DrawTargetD2D1.cpp
@@ -163,23 +163,24 @@ DrawTargetD2D1::DrawSurface(SourceSurfac
   RefPtr<ID2D1Image> image = GetImageForSurface(aSurface, transform, ExtendMode::CLAMP);
 
   if (!image) {
     gfxWarning() << *this << ": Unable to get D2D image for surface.";
     return;
   }
 
   RefPtr<ID2D1Bitmap> bitmap;
+  HRESULT hr;
   if (aSurface->GetType() == SurfaceType::D2D1_1_IMAGE) {
     // If this is called with a DataSourceSurface it might do a partial upload
     // that our DrawBitmap call doesn't support.
-    image->QueryInterface((ID2D1Bitmap**)getter_AddRefs(bitmap));
+    hr = image->QueryInterface((ID2D1Bitmap**)getter_AddRefs(bitmap));
   }
 
-  if (bitmap && aSurfOptions.mSamplingBounds == SamplingBounds::UNBOUNDED) {
+  if (SUCCEEDED(hr) && bitmap && aSurfOptions.mSamplingBounds == SamplingBounds::UNBOUNDED) {
     mDC->DrawBitmap(bitmap, D2DRect(aDest), aOptions.mAlpha,
                     D2DFilter(aSurfOptions.mSamplingFilter), D2DRect(aSource));
   } else {
     // This has issues ignoring the alpha channel on windows 7 with images marked opaque.
     MOZ_ASSERT(aSurface->GetFormat() != SurfaceFormat::B8G8R8X8);
 
     // Bug 1275478 - D2D1 cannot draw A8 surface correctly.
     MOZ_ASSERT(aSurface->GetFormat() != SurfaceFormat::A8);
@@ -396,19 +397,19 @@ DrawTargetD2D1::CopySurface(SourceSurfac
 
   IntRect sourceRect = aSourceRect;
   sourceRect.x += (aDestination.x - aSourceRect.x) - mat._31;
   sourceRect.width -= (aDestination.x - aSourceRect.x) - mat._31;
   sourceRect.y += (aDestination.y - aSourceRect.y) - mat._32;
   sourceRect.height -= (aDestination.y - aSourceRect.y) - mat._32;
 
   RefPtr<ID2D1Bitmap> bitmap;
-  image->QueryInterface((ID2D1Bitmap**)getter_AddRefs(bitmap));
+  HRESULT hr = image->QueryInterface((ID2D1Bitmap**)getter_AddRefs(bitmap));
 
-  if (bitmap && mFormat == SurfaceFormat::A8) {
+  if (SUCCEEDED(hr) && bitmap && mFormat == SurfaceFormat::A8) {
     RefPtr<ID2D1SolidColorBrush> brush;
     mDC->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White),
                                D2D1::BrushProperties(), getter_AddRefs(brush));
     mDC->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
     mDC->SetPrimitiveBlend(D2D1_PRIMITIVE_BLEND_COPY);
     mDC->FillOpacityMask(bitmap, brush, D2D1_OPACITY_MASK_CONTENT_GRAPHICS);
     mDC->SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
     mDC->SetPrimitiveBlend(D2D1_PRIMITIVE_BLEND_SOURCE_OVER);
@@ -416,17 +417,17 @@ DrawTargetD2D1::CopySurface(SourceSurfac
   }
 
   Rect srcRect(Float(sourceRect.x), Float(sourceRect.y),
                Float(aSourceRect.width), Float(aSourceRect.height));
 
   Rect dstRect(Float(aDestination.x), Float(aDestination.y),
                Float(aSourceRect.width), Float(aSourceRect.height));
 
-  if (bitmap) {
+  if (SUCCEEDED(hr) && bitmap) {
     mDC->SetPrimitiveBlend(D2D1_PRIMITIVE_BLEND_COPY);
     mDC->DrawBitmap(bitmap, D2DRect(dstRect), 1.0f,
                     D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR,
                     D2DRect(srcRect));
     mDC->SetPrimitiveBlend(D2D1_PRIMITIVE_BLEND_SOURCE_OVER);
     return;
   }
 
@@ -1777,18 +1778,18 @@ DrawTargetD2D1::CreateBrushForPattern(co
 
     RefPtr<ID2D1Image> image = GetImageForSurface(pat->mSurface, mat, pat->mExtendMode, !pat->mSamplingRect.IsEmpty() ? &pat->mSamplingRect : nullptr);
 
     if (pat->mSurface->GetFormat() == SurfaceFormat::A8) {
       // See bug 1251431, at least FillOpacityMask does not appear to allow a source bitmapbrush
       // with source format A8. This creates a BGRA surface with the same alpha values that
       // the A8 surface has.
       RefPtr<ID2D1Bitmap> bitmap;
-      image->QueryInterface((ID2D1Bitmap**)getter_AddRefs(bitmap));
-      if (bitmap) {
+      HRESULT hr = image->QueryInterface((ID2D1Bitmap**)getter_AddRefs(bitmap));
+      if (SUCCEEDED(hr) && bitmap) {
         RefPtr<ID2D1Image> oldTarget;
         RefPtr<ID2D1Bitmap1> tmpBitmap;
         mDC->CreateBitmap(D2D1::SizeU(pat->mSurface->GetSize().width, pat->mSurface->GetSize().height), nullptr, 0,
                           D2D1::BitmapProperties1(D2D1_BITMAP_OPTIONS_TARGET, D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED)),
                           getter_AddRefs(tmpBitmap));
         mDC->GetTarget(getter_AddRefs(oldTarget));
         mDC->SetTarget(tmpBitmap);
 
@@ -1801,18 +1802,18 @@ DrawTargetD2D1::CreateBrushForPattern(co
     }
 
     if (!image) {
       return CreateTransparentBlackBrush();
     }
 
     if (pat->mSamplingRect.IsEmpty()) {
       RefPtr<ID2D1Bitmap> bitmap;
-      image->QueryInterface((ID2D1Bitmap**)getter_AddRefs(bitmap));
-      if (bitmap) {
+      HRESULT hr = image->QueryInterface((ID2D1Bitmap**)getter_AddRefs(bitmap));
+      if (SUCCEEDED(hr) && bitmap) {
         /**
          * Create the brush with the proper repeat modes.
          */
         RefPtr<ID2D1BitmapBrush> bitmapBrush;
         D2D1_EXTEND_MODE xRepeat = D2DExtend(pat->mExtendMode, Axis::X_AXIS);
         D2D1_EXTEND_MODE yRepeat = D2DExtend(pat->mExtendMode, Axis::Y_AXIS);
 
         mDC->CreateBitmapBrush(bitmap,
--- a/gfx/thebes/gfxFont.h
+++ b/gfx/thebes/gfxFont.h
@@ -1702,18 +1702,18 @@ public:
     nsExpirationState *GetExpirationState() { return &mExpirationState; }
 
     // Get the glyphID of a space
     virtual uint32_t GetSpaceGlyph() = 0;
 
     gfxGlyphExtents *GetOrCreateGlyphExtents(int32_t aAppUnitsPerDevUnit);
 
     // You need to call SetupCairoFont on aDrawTarget just before calling this.
-    virtual void SetupGlyphExtents(DrawTarget* aDrawTarget, uint32_t aGlyphID,
-                                   bool aNeedTight, gfxGlyphExtents *aExtents);
+    void SetupGlyphExtents(DrawTarget* aDrawTarget, uint32_t aGlyphID,
+                           bool aNeedTight, gfxGlyphExtents *aExtents);
 
     // This is called by the default Draw() implementation above.
     virtual bool SetupCairoFont(DrawTarget* aDrawTarget) = 0;
 
     virtual bool AllowSubpixelAA() { return true; }
 
     bool IsSyntheticBold() { return mApplySyntheticBold; }
 
@@ -1884,17 +1884,17 @@ public:
     // method may be called to access the gfxMathTable data.
     bool          TryGetMathTable();
     gfxMathTable* MathTable() {
         MOZ_RELEASE_ASSERT(mMathTable, "A successful call to TryGetMathTable() must be performed before calling this function");
         return mMathTable.get();
     }
 
     // return a cloned font resized and offset to simulate sub/superscript glyphs
-    virtual already_AddRefed<gfxFont>
+    already_AddRefed<gfxFont>
     GetSubSuperscriptFont(int32_t aAppUnitsPerDevPixel);
 
     /**
      * Return the reference cairo_t object from aDT.
      */
     static cairo_t* RefCairo(mozilla::gfx::DrawTarget* aDT);
 
 protected:
@@ -1925,20 +1925,17 @@ protected:
     // set the font size and offset used for
     // synthetic subscript/superscript glyphs
     void CalculateSubSuperSizeAndOffset(int32_t aAppUnitsPerDevPixel,
                                         gfxFloat& aSubSuperSizeRatio,
                                         float& aBaselineOffset);
 
     // Return a font that is a "clone" of this one, but reduced to 80% size
     // (and with variantCaps set to normal).
-    // Default implementation relies on gfxFontEntry::CreateFontInstance;
-    // backends that don't implement that will need to override this and use
-    // an alternative technique. (gfxFontconfigFonts, I'm looking at you...)
-    virtual already_AddRefed<gfxFont> GetSmallCapsFont();
+    already_AddRefed<gfxFont> GetSmallCapsFont();
 
     // subclasses may provide (possibly hinted) glyph widths (in font units);
     // if they do not override this, harfbuzz will use unhinted widths
     // derived from the font tables
     virtual bool ProvidesGlyphWidths() const {
         return false;
     }
 
--- a/gfx/thebes/gfxFontEntry.h
+++ b/gfx/thebes/gfxFontEntry.h
@@ -391,20 +391,18 @@ protected:
     friend class gfxSingleFaceMacFontFamily;
     friend class gfxUserFontEntry;
 
     gfxFontEntry();
 
     // Protected destructor, to discourage deletion outside of Release():
     virtual ~gfxFontEntry();
 
-    virtual gfxFont *CreateFontInstance(const gfxFontStyle *aFontStyle, bool aNeedsBold) {
-        NS_NOTREACHED("oops, somebody didn't override CreateFontInstance");
-        return nullptr;
-    }
+    virtual gfxFont *CreateFontInstance(const gfxFontStyle *aFontStyle,
+                                        bool aNeedsBold) = 0;
 
     virtual void CheckForGraphiteTables();
 
     // Copy a font table into aBuffer.
     // The caller will be responsible for ownership of the data.
     virtual nsresult CopyFontTable(uint32_t aTableTag,
                                    nsTArray<uint8_t>& aBuffer) {
         NS_NOTREACHED("forgot to override either GetFontTable or CopyFontTable?");
--- a/gfx/thebes/gfxTextRun.h
+++ b/gfx/thebes/gfxTextRun.h
@@ -822,69 +822,69 @@ private:
     bool              mHasGlyphRunArray; // whether we're using an array or
                                          // just storing a single glyphrun
 
     // shaping state for handling variant fallback features
     // such as subscript/superscript variant glyphs
     ShapingState      mShapingState;
 };
 
-class gfxFontGroup : public gfxTextRunFactory {
+class gfxFontGroup final : public gfxTextRunFactory {
 public:
     typedef mozilla::unicode::Script Script;
 
     static void Shutdown(); // platform must call this to release the languageAtomService
 
     gfxFontGroup(const mozilla::FontFamilyList& aFontFamilyList,
                  const gfxFontStyle* aStyle,
                  gfxTextPerfMetrics* aTextPerf,
                  gfxUserFontSet* aUserFontSet,
                  gfxFloat aDevToCssSize);
 
     virtual ~gfxFontGroup();
 
     // Returns first valid font in the fontlist or default font.
     // Initiates userfont loads if userfont not loaded
-    virtual gfxFont* GetFirstValidFont(uint32_t aCh = 0x20);
+    gfxFont* GetFirstValidFont(uint32_t aCh = 0x20);
 
     // Returns the first font in the font-group that has an OpenType MATH table,
     // or null if no such font is available. The GetMathConstant methods may be
     // called on the returned font.
     gfxFont *GetFirstMathFont();
 
     const gfxFontStyle *GetStyle() const { return &mStyle; }
 
-    virtual gfxFontGroup *Copy(const gfxFontStyle *aStyle);
+    gfxFontGroup *Copy(const gfxFontStyle *aStyle);
 
     /**
      * The listed characters should be treated as invisible and zero-width
      * when creating textruns.
      */
     static bool IsInvalidChar(uint8_t ch);
     static bool IsInvalidChar(char16_t ch);
 
     /**
      * Make a textrun for a given string.
      * If aText is not persistent (aFlags & TEXT_IS_PERSISTENT), the
      * textrun will copy it.
      * This calls FetchGlyphExtents on the textrun.
      */
-    virtual already_AddRefed<gfxTextRun>
+    already_AddRefed<gfxTextRun>
     MakeTextRun(const char16_t *aString, uint32_t aLength,
                 const Parameters *aParams,
                 mozilla::gfx::ShapedTextFlags aFlags,
                 nsTextFrameUtils::Flags aFlags2,
                 gfxMissingFontRecorder *aMFR);
     /**
      * Make a textrun for a given string.
      * If aText is not persistent (aFlags & TEXT_IS_PERSISTENT), the
      * textrun will copy it.
      * This calls FetchGlyphExtents on the textrun.
      */
-    virtual already_AddRefed<gfxTextRun>
+    already_AddRefed<gfxTextRun>
     MakeTextRun(const uint8_t *aString, uint32_t aLength,
                 const Parameters *aParams,
                 mozilla::gfx::ShapedTextFlags aFlags,
                 nsTextFrameUtils::Flags aFlags2,
                 gfxMissingFontRecorder *aMFR);
 
     /**
      * Textrun creation helper for clients that don't want to pass
@@ -926,19 +926,19 @@ public:
 
     // This returns the preferred underline for this font group.
     // Some CJK fonts have wrong underline offset in its metrics.
     // If this group has such "bad" font, each platform's gfxFontGroup
     // initialized mUnderlineOffset. The value should be lower value of
     // first font's metrics and the bad font's metrics. Otherwise, this
     // returns from first font's metrics.
     enum { UNDERLINE_OFFSET_NOT_SET = INT16_MAX };
-    virtual gfxFloat GetUnderlineOffset();
+    gfxFloat GetUnderlineOffset();
 
-    virtual already_AddRefed<gfxFont>
+    already_AddRefed<gfxFont>
         FindFontForChar(uint32_t ch, uint32_t prevCh, uint32_t aNextCh,
                         Script aRunScript, gfxFont *aPrevMatchedFont,
                         uint8_t *aMatchType);
 
     gfxUserFontSet* GetUserFontSet();
 
     // With downloadable fonts, the composition of the font group can change as fonts are downloaded
     // for each change in state of the user font set, the generation value is bumped to avoid picking up
@@ -960,17 +960,17 @@ public:
         mUnderlineOffset = UNDERLINE_OFFSET_NOT_SET;
         mSkipDrawing = false;
         mHyphenWidth = -1;
         mCachedEllipsisTextRun = nullptr;
     }
 
     // If there is a user font set, check to see whether the font list or any
     // caches need updating.
-    virtual void UpdateUserFonts();
+    void UpdateUserFonts();
 
     // search for a specific userfont in the list of fonts
     bool ContainsUserFont(const gfxUserFontEntry* aUserFont);
 
     bool ShouldSkipDrawing() const {
         return mSkipDrawing;
     }
 
@@ -1192,17 +1192,17 @@ protected:
                      nsTextFrameUtils::Flags aFlags2);
 
     // Initialize the list of fonts
     void BuildFontList();
 
     // Get the font at index i within the fontlist.
     // Will initiate userfont load if not already loaded.
     // May return null if userfont not loaded or if font invalid
-    virtual gfxFont* GetFontAt(int32_t i, uint32_t aCh = 0x20);
+    gfxFont* GetFontAt(int32_t i, uint32_t aCh = 0x20);
 
     // Whether there's a font loading for a given family in the fontlist
     // for a given character
     bool FontLoadingForFamily(gfxFontFamily* aFamily, uint32_t aCh) const;
 
     // will always return a font or force a shutdown
     gfxFont* GetDefaultFont();
 
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/profiler/bug1352507-1.js
@@ -0,0 +1,3 @@
+if (helperThreadCount() === 0)
+    quit();
+evalInWorker("enableGeckoProfiling()");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/profiler/bug1352507-2.js
@@ -0,0 +1,3 @@
+if (helperThreadCount() === 0)
+    quit();
+evalInCooperativeThread("enableGeckoProfiling()");
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -3425,16 +3425,21 @@ WorkerMain(void* arg)
         if (!JS::Compile(cx, options, input->chars, input->length, &script))
             break;
         RootedValue result(cx);
         JS_ExecuteScript(cx, script, &result);
     } while (0);
 
     KillWatchdog(cx);
     JS_SetGrayGCRootsTracer(cx, nullptr, nullptr);
+
+    if (sc->geckoProfilingStack) {
+        MOZ_ALWAYS_TRUE(cx->runtime()->geckoProfiler().enable(false));
+        SetContextProfilingStack(cx, nullptr);
+    }
 }
 
 // Workers can spawn other workers, so we need a lock to access workerThreads.
 static Mutex* workerThreadsLock = nullptr;
 static Vector<js::Thread*, 0, SystemAllocPolicy> workerThreads;
 
 class MOZ_RAII AutoLockWorkerThreads : public LockGuard<Mutex>
 {
@@ -5201,73 +5206,115 @@ IsLatin1(JSContext* cx, unsigned argc, V
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     bool isLatin1 = args.get(0).isString() && args[0].toString()->hasLatin1Chars();
     args.rval().setBoolean(isLatin1);
     return true;
 }
 
 static bool
+EnsureGeckoProfilingStackInstalled(JSContext* cx, ShellContext* sc)
+{
+    if (cx->runtime()->geckoProfiler().installed()) {
+        if (!sc->geckoProfilingStack) {
+            JS_ReportErrorASCII(cx, "Profiler already installed by another context");
+            return false;
+        }
+
+        return true;
+    }
+
+    MOZ_ASSERT(!sc->geckoProfilingStack);
+    sc->geckoProfilingStack = MakeUnique<PseudoStack>();
+    if (!sc->geckoProfilingStack) {
+        JS_ReportOutOfMemory(cx);
+        return false;
+    }
+
+    SetContextProfilingStack(cx, sc->geckoProfilingStack.get());
+    return true;
+}
+
+static bool
 EnableGeckoProfiling(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     ShellContext* sc = GetShellContext(cx);
 
-    // Disable before re-enabling; see the assertion in |GeckoProfiler::setProfilingStack|.
+    if (!EnsureGeckoProfilingStackInstalled(cx, sc))
+        return false;
+
+    // Disable before re-enabling; see the assertion in
+    // |GeckoProfiler::setProfilingStack|.
     if (cx->runtime()->geckoProfiler().installed())
         MOZ_ALWAYS_TRUE(cx->runtime()->geckoProfiler().enable(false));
 
-    SetContextProfilingStack(cx, &sc->geckoProfilingStack);
     cx->runtime()->geckoProfiler().enableSlowAssertions(false);
-    if (!cx->runtime()->geckoProfiler().enable(true))
+    if (!cx->runtime()->geckoProfiler().enable(true)) {
         JS_ReportErrorASCII(cx, "Cannot ensure single threaded execution in profiler");
+        return false;
+    }
 
     args.rval().setUndefined();
     return true;
 }
 
 static bool
 EnableGeckoProfilingWithSlowAssertions(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     args.rval().setUndefined();
 
     ShellContext* sc = GetShellContext(cx);
 
+    if (!EnsureGeckoProfilingStackInstalled(cx, sc))
+        return false;
+
     if (cx->runtime()->geckoProfiler().enabled()) {
         // If profiling already enabled with slow assertions disabled,
         // this is a no-op.
         if (cx->runtime()->geckoProfiler().slowAssertionsEnabled())
             return true;
 
         // Slow assertions are off.  Disable profiling before re-enabling
         // with slow assertions on.
         MOZ_ALWAYS_TRUE(cx->runtime()->geckoProfiler().enable(false));
     }
 
     // Disable before re-enabling; see the assertion in |GeckoProfiler::setProfilingStack|.
     if (cx->runtime()->geckoProfiler().installed())
         MOZ_ALWAYS_TRUE(cx->runtime()->geckoProfiler().enable(false));
 
-    SetContextProfilingStack(cx, &sc->geckoProfilingStack);
     cx->runtime()->geckoProfiler().enableSlowAssertions(true);
-    if (!cx->runtime()->geckoProfiler().enable(true))
+    if (!cx->runtime()->geckoProfiler().enable(true)) {
         JS_ReportErrorASCII(cx, "Cannot ensure single threaded execution in profiler");
+        return false;
+    }
 
     return true;
 }
 
 static bool
 DisableGeckoProfiling(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
-    if (cx->runtime()->geckoProfiler().installed())
-        MOZ_ALWAYS_TRUE(cx->runtime()->geckoProfiler().enable(false));
     args.rval().setUndefined();
+
+    ShellContext* sc = GetShellContext(cx);
+
+    if (!cx->runtime()->geckoProfiler().installed())
+        return true;
+
+    if (!sc->geckoProfilingStack) {
+        JS_ReportErrorASCII(cx, "Profiler was not installed by this context");
+        return false;
+    }
+
+    MOZ_ALWAYS_TRUE(cx->runtime()->geckoProfiler().enable(false));
     return true;
 }
 
 // Global mailbox that is used to communicate a SharedArrayBuffer
 // value from one worker to another.
 //
 // For simplicity we store only the SharedArrayRawBuffer; retaining
 // the SAB object would require per-runtime storage, and would have no
--- a/js/src/shell/jsshell.h
+++ b/js/src/shell/jsshell.h
@@ -166,16 +166,17 @@ using MarkBitObservers = JS::WeakCache<N
 #ifdef SINGLESTEP_PROFILING
 using StackChars = Vector<char16_t, 0, SystemAllocPolicy>;
 #endif
 
 // Per-context shell state.
 struct ShellContext
 {
     explicit ShellContext(JSContext* cx);
+
     bool isWorker;
     double timeoutInterval;
     double startTime;
     mozilla::Atomic<bool> serviceInterrupt;
     mozilla::Atomic<bool> haveInterruptFunc;
     JS::PersistentRootedValue interruptFunc;
     bool lastWarningEnabled;
     JS::PersistentRootedValue lastWarning;
@@ -198,17 +199,17 @@ struct ShellContext
     bool quitting;
 
     JS::UniqueChars readLineBuf;
     size_t readLineBufPos;
 
     js::shell::RCFile** errFilePtr;
     js::shell::RCFile** outFilePtr;
 
-    PseudoStack geckoProfilingStack;
+    UniquePtr<PseudoStack> geckoProfilingStack;
 
     OffThreadState offThreadState;
 
     JS::UniqueChars moduleLoadPath;
     UniquePtr<MarkBitObservers> markObservers;
 };
 
 extern ShellContext*
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -6830,35 +6830,34 @@ nsIFrame::GetOverflowRect(nsOverflowType
   // updated (yet) to reflect any outline set on the frame or the area
   // of child frames. That's OK because any reflow that updates these
   // areas will invalidate the appropriate area, so any (mis)uses of
   // this method will be fixed up.
 
   if (mOverflow.mType == NS_FRAME_OVERFLOW_LARGE) {
     // there is an overflow rect, and it's not stored as deltas but as
     // a separately-allocated rect
-    return static_cast<nsOverflowAreas*>(const_cast<nsIFrame*>(this)->
-             GetOverflowAreasProperty())->Overflow(aType);
+    return GetOverflowAreasProperty()->Overflow(aType);
   }
 
   if (aType == eVisualOverflow &&
       mOverflow.mType != NS_FRAME_OVERFLOW_NONE) {
     return GetVisualOverflowFromDeltas();
   }
 
   return nsRect(nsPoint(0, 0), GetSize());
 }
 
 nsOverflowAreas
 nsIFrame::GetOverflowAreas() const
 {
   if (mOverflow.mType == NS_FRAME_OVERFLOW_LARGE) {
     // there is an overflow rect, and it's not stored as deltas but as
     // a separately-allocated rect
-    return *const_cast<nsIFrame*>(this)->GetOverflowAreasProperty();
+    return *GetOverflowAreasProperty();
   }
 
   return nsOverflowAreas(GetVisualOverflowFromDeltas(),
                          nsRect(nsPoint(0, 0), GetSize()));
 }
 
 nsOverflowAreas
 nsIFrame::GetOverflowAreasRelativeToSelf() const
@@ -8657,59 +8656,37 @@ nsFrame::AccessibleType()
 {
   if (IsTableCaption() && !GetRect().IsEmpty()) {
     return a11y::eHTMLCaptionType;
   }
   return a11y::eNoType;
 }
 #endif
 
-NS_DECLARE_FRAME_PROPERTY_DELETABLE(OverflowAreasProperty, nsOverflowAreas)
-
 bool
 nsIFrame::ClearOverflowRects()
 {
   if (mOverflow.mType == NS_FRAME_OVERFLOW_NONE) {
     return false;
   }
   if (mOverflow.mType == NS_FRAME_OVERFLOW_LARGE) {
     DeleteProperty(OverflowAreasProperty());
   }
   mOverflow.mType = NS_FRAME_OVERFLOW_NONE;
   return true;
 }
 
-/** Create or retrieve the previously stored overflow area, if the frame does 
- * not overflow and no creation is required return nullptr.
- * @return pointer to the overflow area rectangle 
- */
-nsOverflowAreas*
-nsIFrame::GetOverflowAreasProperty()
-{
-  nsOverflowAreas* overflow = GetProperty(OverflowAreasProperty());
-
-  if (overflow) {
-    return overflow; // the property already exists
-  }
-
-  // The property isn't set yet, so allocate a new rect, set the property,
-  // and return the newly allocated rect
-  overflow = new nsOverflowAreas;
-  AddProperty(OverflowAreasProperty(), overflow);
-  return overflow;
-}
-
 /** Set the overflowArea rect, storing it as deltas or a separate rect
  * depending on its size in relation to the primary frame rect.
  */
 bool
 nsIFrame::SetOverflowAreas(const nsOverflowAreas& aOverflowAreas)
 {
   if (mOverflow.mType == NS_FRAME_OVERFLOW_LARGE) {
-    nsOverflowAreas* overflow = GetProperty(OverflowAreasProperty());
+    nsOverflowAreas* overflow = GetOverflowAreasProperty();
     bool changed = *overflow != aOverflowAreas;
     *overflow = aOverflowAreas;
 
     // Don't bother with converting to the deltas form if we already
     // have a property.
     return changed;
   }
 
@@ -8744,19 +8721,17 @@ nsIFrame::SetOverflowAreas(const nsOverf
     // There was no scrollable overflow before, and there isn't now.
     return oldDeltas != mOverflow.mVisualDeltas;
   } else {
     bool changed = !aOverflowAreas.ScrollableOverflow().IsEqualEdges(nsRect(nsPoint(0, 0), GetSize())) ||
       !aOverflowAreas.VisualOverflow().IsEqualEdges(GetVisualOverflowFromDeltas());
 
     // it's a large overflow area that we need to store as a property
     mOverflow.mType = NS_FRAME_OVERFLOW_LARGE;
-    nsOverflowAreas* overflow = GetOverflowAreasProperty();
-    NS_ASSERTION(overflow, "should have created areas");
-    *overflow = aOverflowAreas;
+    AddProperty(OverflowAreasProperty(), new nsOverflowAreas(aOverflowAreas));
     return changed;
   }
 }
 
 /**
  * Compute the union of the border boxes of aFrame and its descendants,
  * in aFrame's coordinate space (if aApplyTransform is false) or its
  * post-transform coordinate space (if aApplyTransform is true).
--- a/layout/generic/nsIFrame.h
+++ b/layout/generic/nsIFrame.h
@@ -1151,16 +1151,18 @@ public:
   NS_DECLARE_FRAME_PROPERTY_DELETABLE(NormalPositionProperty, nsPoint)
   NS_DECLARE_FRAME_PROPERTY_DELETABLE(ComputedOffsetProperty, nsMargin)
 
   NS_DECLARE_FRAME_PROPERTY_DELETABLE(OutlineInnerRectProperty, nsRect)
   NS_DECLARE_FRAME_PROPERTY_DELETABLE(PreEffectsBBoxProperty, nsRect)
   NS_DECLARE_FRAME_PROPERTY_DELETABLE(PreTransformOverflowAreasProperty,
                                       nsOverflowAreas)
 
+  NS_DECLARE_FRAME_PROPERTY_DELETABLE(OverflowAreasProperty, nsOverflowAreas)
+
   // The initial overflow area passed to FinishAndStoreOverflow. This is only set
   // on frames that Preserve3D() or HasPerspective() or IsTransformed(), and
   // when at least one of the overflow areas differs from the frame bound rect.
   NS_DECLARE_FRAME_PROPERTY_DELETABLE(InitialOverflowProperty, nsOverflowAreas)
 
 #ifdef DEBUG
   // InitialOverflowPropertyDebug is added to the frame to indicate that either
   // the InitialOverflowProperty has been stored or the InitialOverflowProperty
@@ -4083,17 +4085,24 @@ protected:
    * @param  aPos See description in nsFrameSelection.h. The following fields are
    *              used by this method:
    *              Input: mDirection
    *              Output: mResultContent, mContentOffset
    */
   nsresult PeekOffsetParagraph(nsPeekOffsetStruct *aPos);
 
 private:
-  nsOverflowAreas* GetOverflowAreasProperty();
+  // Get a pointer to the overflow areas property attached to the frame.
+  nsOverflowAreas* GetOverflowAreasProperty() const {
+    MOZ_ASSERT(mOverflow.mType == NS_FRAME_OVERFLOW_LARGE);
+    nsOverflowAreas* overflow = GetProperty(OverflowAreasProperty());
+    MOZ_ASSERT(overflow);
+    return overflow;
+  }
+
   nsRect GetVisualOverflowFromDeltas() const {
     MOZ_ASSERT(mOverflow.mType != NS_FRAME_OVERFLOW_LARGE,
                "should not be called when overflow is in a property");
     // Calculate the rect using deltas from the frame's border rect.
     // Note that the mOverflow.mDeltas fields are unsigned, but we will often
     // need to return negative values for the left and top, so take care
     // to cast away the unsigned-ness.
     return nsRect(-(int32_t)mOverflow.mVisualDeltas.mLeft,
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1413,17 +1413,17 @@ public abstract class GeckoApp extends G
             "RuntimePermissions:Check",
             "Share:Text",
             "SystemUI:Visibility",
             "ToggleChrome:Focus",
             "ToggleChrome:Hide",
             "ToggleChrome:Show",
             null);
 
-        Tabs.getInstance().attachToContext(this, mLayerView, getAppEventDispatcher());
+        Tabs.getInstance().attachToContext(this, mLayerView);
         Tabs.registerOnTabsChangedListener(this);
 
         // Use global layout state change to kick off additional initialization
         mMainLayout.getViewTreeObserver().addOnGlobalLayoutListener(this);
 
         if (Versions.preMarshmallow) {
             mTextSelection = new ActionBarTextSelection(this, getTextSelectPresenter());
         } else {
@@ -1507,17 +1507,17 @@ public abstract class GeckoApp extends G
 
                 synchronized (GeckoApp.this) {
                     mSessionRestoreParsingFinished = true;
                     GeckoApp.this.notifyAll();
                 }
 
                 // If we are doing a restore, send the parsed session data to Gecko.
                 if (!mIsRestoringActivity) {
-                    getAppEventDispatcher().dispatch("Session:Restore", restoreMessage);
+                    EventDispatcher.getInstance().dispatch("Session:Restore", restoreMessage);
                 }
 
                 // Make sure sessionstore.old is either updated or deleted as necessary.
                 getProfile().updateSessionFile(mShouldRestore);
             }
         });
 
         // Perform background initialization.
@@ -2484,17 +2484,16 @@ public abstract class GeckoApp extends G
                     rec.close(GeckoApp.this);
                 }
             });
         }
 
         super.onDestroy();
 
         Tabs.unregisterOnTabsChangedListener(this);
-        Tabs.getInstance().detachFromContext();
 
         if (mShutdownOnDestroy) {
             GeckoApplication.shutdown(!mRestartOnShutdown ? null : new Intent(
                     Intent.ACTION_MAIN, /* uri */ null, getApplicationContext(), getClass()));
         }
     }
 
     public void showSDKVersionError() {
--- a/mobile/android/base/java/org/mozilla/gecko/GlobalHistory.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GlobalHistory.java
@@ -177,17 +177,17 @@ class GlobalHistory {
                 getInstance().update(context.getContentResolver(), db, uri, title);
             }
         });
     }
 
     /* protected */ void checkVisited(final String uri) {
         final String storedURI = ReaderModeUtils.stripAboutReaderUrl(uri);
 
-        final NotifierRunnable runnable = new NotifierRunnable(GeckoAppShell.getApplicationContext());
+        final NotifierRunnable runnable = new NotifierRunnable(GeckoAppShell.getContext());
         mHandler.post(new Runnable() {
             @Override
             public void run() {
                 // this runs on the same handler thread as the processing loop,
                 // so no synchronization needed
                 mPendingUris.add(storedURI);
                 if (mProcessing) {
                     // there's already a runnable queued up or working away, so
--- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
@@ -96,17 +96,16 @@ public class Tabs implements BundleEvent
     public static final int INVALID_TAB_ID = -1;
     // Used to indicate a new tab should be appended to the current tabs.
     public static final int NEW_LAST_INDEX = -1;
 
     private static final AtomicInteger sTabId = new AtomicInteger(0);
     private volatile boolean mInitialTabsAdded;
 
     private Context mAppContext;
-    private EventDispatcher mEventDispatcher;
     private LayerView mLayerView;
     private ContentObserver mBookmarksContentObserver;
     private PersistTabsRunnable mPersistTabsRunnable;
     private int mPrivateClearColor;
 
     public void closeAll() {
         for (final Tab tab : mOrder) {
             Tabs.getInstance().closeTab(tab, false);
@@ -161,21 +160,28 @@ public class Tabs implements BundleEvent
         EventDispatcher.getInstance().registerBackgroundThreadListener(this,
             // BrowserApp already wants this on the background thread.
             "Sanitize:ClearHistory",
             null);
 
         mPrivateClearColor = Color.RED;
     }
 
-    public synchronized void attachToContext(Context context, LayerView layerView, EventDispatcher eventDispatcher) {
+    public synchronized void attachToContext(Context context, LayerView layerView) {
         final Context appContext = context.getApplicationContext();
+        if (mAppContext == appContext) {
+            return;
+        }
+
+        if (mAppContext != null) {
+            // This should never happen.
+            Log.w(LOGTAG, "The application context has changed!");
+        }
 
         mAppContext = appContext;
-        mEventDispatcher = eventDispatcher;
         mLayerView = layerView;
         mPrivateClearColor = ContextCompat.getColor(context, R.color.tabs_tray_grey_pressed);
         mAccountManager = AccountManager.get(appContext);
 
         mAccountListener = new OnAccountsUpdateListener() {
             @Override
             public void onAccountsUpdated(Account[] accounts) {
                 queuePersistAllTabs();
@@ -187,20 +193,16 @@ public class Tabs implements BundleEvent
 
         if (mBookmarksContentObserver != null) {
             // It's safe to use the db here since we aren't doing any I/O.
             final GeckoProfile profile = GeckoProfile.get(context);
             BrowserDB.from(profile).registerBookmarkObserver(getContentResolver(), mBookmarksContentObserver);
         }
     }
 
-    public void detachFromContext() {
-        mLayerView = null;
-    }
-
     /**
      * Gets the tab count corresponding to the category and private state of the
      * selected tab.
      *
      * If the selected tab is a non-private tab, this will return the number of
      * non-private tabs; likewise, if this is a private tab, this will return
      * the number of private tabs.
      *
@@ -325,17 +327,16 @@ public class Tabs implements BundleEvent
 
         if (oldTab != null) {
             notifyListeners(oldTab, TabEvents.UNSELECTED);
         }
 
         // Pass a message to Gecko to update tab state in BrowserApp.
         final GeckoBundle data = new GeckoBundle(1);
         data.putInt("id", tab.getId());
-        mEventDispatcher.dispatch("Tab:Selected", data);
         EventDispatcher.getInstance().dispatch("Tab:Selected", data);
         return tab;
     }
 
     public synchronized boolean selectLastTab() {
         if (mOrder.isEmpty()) {
             return false;
         }
@@ -467,17 +468,17 @@ public class Tabs implements BundleEvent
         selectTab(nextTab.getId());
 
         tab.onDestroy();
 
         // Pass a message to Gecko to update tab state in BrowserApp
         final GeckoBundle data = new GeckoBundle(2);
         data.putInt("tabId", tabId);
         data.putBoolean("showUndoToast", showUndoToast);
-        mEventDispatcher.dispatch("Tab:Closed", data);
+        EventDispatcher.getInstance().dispatch("Tab:Closed", data);
     }
 
     /** Return the tab that will be selected by default after this one is closed */
     public Tab getNextTab(Tab tab) {
         Tab selectedTab = getSelectedTab();
         if (selectedTab != tab)
             return selectedTab;
 
@@ -1077,17 +1078,17 @@ public class Tabs implements BundleEvent
             }
             if (isFirstShownAfterActivityUnhidden) {
                 // We just opened Firefox so we want to show
                 // the toolbar but not animate it to avoid jank.
                 tabToSelect.setShouldShowToolbarWithoutAnimationOnFirstSelection(true);
             }
         }
 
-        mEventDispatcher.dispatch("Tab:Load", data);
+        EventDispatcher.getInstance().dispatch("Tab:Load", data);
 
         if (tabToSelect == null) {
             return null;
         }
 
         if (!delayLoad && !background) {
             selectTab(tabToSelect.getId());
             tracking(url);
@@ -1271,17 +1272,17 @@ public class Tabs implements BundleEvent
 
         notifyListeners(mOrder.get(toPosition), TabEvents.MOVED);
 
         final GeckoBundle data = new GeckoBundle();
         data.putInt("fromTabId", fromTabId);
         data.putInt("fromPosition", fromPosition);
         data.putInt("toTabId", toTabId);
         data.putInt("toPosition", toPosition);
-        mEventDispatcher.dispatch("Tab:Move", data);
+        EventDispatcher.getInstance().dispatch("Tab:Move", data);
     }
 
     /**
      * @return True if the homepage preference is not empty.
      */
     public static boolean hasHomepage(Context context) {
         return !TextUtils.isEmpty(getHomepage(context));
     }
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/ActionBarPresenter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/ActionBarPresenter.java
@@ -25,17 +25,16 @@ import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.Window;
 import android.view.WindowManager;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.TextView;
 
-import org.mozilla.gecko.GeckoView;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.SiteIdentity;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.toolbar.SecurityModeUtil;
 import org.mozilla.gecko.toolbar.SiteIdentityPopup;
 import org.mozilla.gecko.util.ColorUtil;
 
 /**
@@ -104,34 +103,35 @@ public class ActionBarPresenter {
     }
 
     /**
      * To display Url in CustomView only and immediately.
      *
      * @param url Url String to display
      */
     public void displayUrlOnly(@NonNull final String url) {
-        updateCustomView(null, url, GeckoView.ProgressListener.STATE_IS_INSECURE);
+        updateCustomView(null, null, url);
     }
 
     /**
-     * Update appearance of CustomView of ActionBar
+     * Update appearance of CustomView of ActionBar.
      *
-     * @param title          Title for current website. Could be null if don't want to show title.
-     * @param url            URL for current website. At least Custom will show this url.
-     * @param securityStatus Security status, possible values given in GeckoView.ProgressListener
+     * @param tab a Tab instance of current web page to provide information to render ActionBar.
      */
-    public void update(final String title, final String url, final int securityStatus) {
+    public void update(@NonNull final Tab tab) {
+        final String title = tab.getTitle();
+        final String url = tab.getBaseDomain();
+
         // Do not update CustomView immediately. If this method be invoked rapidly several times,
         // only apply last one.
         mHandler.removeCallbacks(mUpdateAction);
         mUpdateAction = new Runnable() {
             @Override
             public void run() {
-                updateCustomView(title, url, securityStatus);
+                updateCustomView(tab.getSiteIdentity(), title, url);
             }
         };
         mHandler.postDelayed(mUpdateAction, CUSTOM_VIEW_UPDATE_DELAY);
     }
 
     /**
      * To add a always-show-as-action button to menu, and manually create a view to insert.
      *
@@ -215,25 +215,35 @@ public class ActionBarPresenter {
     /**
      * To update appearance of CustomView of ActionBar, includes its icon, title and url text.
      *
      * @param identity SiteIdentity for current website. Could be null if don't want to show icon.
      * @param title    Title for current website. Could be null if don't want to show title.
      * @param url      URL for current website. At least Custom will show this url.
      */
     @UiThread
-    private void updateCustomView(final String title, final String url, final int securityStatus) {
-        if (securityStatus == GeckoView.ProgressListener.STATE_IS_SECURE) {
+    private void updateCustomView(@Nullable SiteIdentity identity,
+                                  @Nullable String title,
+                                  @NonNull String url) {
+        // update site-info icon
+        if (identity == null) {
+            mIconView.setVisibility(View.INVISIBLE);
+        } else {
+            final SecurityModeUtil.Mode mode = SecurityModeUtil.resolve(identity);
             mIconView.setVisibility(View.VISIBLE);
-            mIconView.setImageLevel(SecurityModeUtil.Mode.LOCK_SECURE.ordinal());
-            // Lock-Secure is special case. Keep its original green color.
-            DrawableCompat.setTintList(mIconView.getDrawable(), null);
-        } else {
-            mIconView.setVisibility(View.INVISIBLE);
-            DrawableCompat.setTint(mIconView.getDrawable(), mTextPrimaryColor);
+            mIconView.setImageLevel(mode.ordinal());
+            mIdentityPopup.setSiteIdentity(identity);
+
+            if (mode == SecurityModeUtil.Mode.LOCK_SECURE) {
+                // Lock-Secure is special case. Keep its original green color.
+                DrawableCompat.setTintList(mIconView.getDrawable(), null);
+            } else {
+                // Icon use same color as TextView.
+                DrawableCompat.setTint(mIconView.getDrawable(), mTextPrimaryColor);
+            }
         }
 
         // If no title to use, use Url as title
         if (TextUtils.isEmpty(title)) {
             mTitleView.setText(url);
             mUrlView.setText(null);
             mUrlView.setVisibility(View.GONE);
         } else {
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -15,104 +15,101 @@ import android.graphics.drawable.Drawabl
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.Browser;
 import android.support.annotation.ColorInt;
 import android.support.annotation.NonNull;
 import android.support.design.widget.Snackbar;
 import android.support.v4.util.SparseArrayCompat;
 import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatActivity;
+import android.support.v7.view.ActionMode;
 import android.support.v7.widget.Toolbar;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
+import android.widget.ProgressBar;
 
 import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoView;
-import org.mozilla.gecko.GeckoViewSettings;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.SingleTabActivity;
 import org.mozilla.gecko.SnackbarBuilder;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuInflater;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.ColorUtil;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.IntentUtils;
+import org.mozilla.gecko.widget.ActionModePresenter;
 import org.mozilla.gecko.widget.GeckoPopupMenu;
 
 import java.util.List;
 
-public class CustomTabsActivity extends AppCompatActivity
-                                implements GeckoMenu.Callback,
-                                           GeckoView.ContentListener,
-                                           GeckoView.NavigationListener,
-                                           GeckoView.ProgressListener {
+import static org.mozilla.gecko.Tabs.TabEvents;
+
+public class CustomTabsActivity extends SingleTabActivity implements Tabs.OnTabsChangedListener {
 
     private static final String LOGTAG = "CustomTabsActivity";
 
     private final SparseArrayCompat<PendingIntent> menuItemsIntent = new SparseArrayCompat<>();
     private GeckoPopupMenu popupMenu;
     private View doorhangerOverlay;
     private ActionBarPresenter actionBarPresenter;
+    private ProgressBar mProgressView;
     // A state to indicate whether this activity is finishing with customize animation
     private boolean usingCustomAnimation = false;
 
     private MenuItem menuItemControl;
 
-    private GeckoView mGeckoView;
-
-    private boolean mCanGoBack = false;
-    private boolean mCanGoForward = false;
-    private boolean mCanStop = false;
-    private String mCurrentUrl;
-    private String mCurrentTitle;
-    private int mSecurityStatus = GeckoView.ProgressListener.STATE_IS_INSECURE;
-
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        setContentView(R.layout.customtabs_activity);
-
         final SafeIntent intent = new SafeIntent(getIntent());
 
         doorhangerOverlay = findViewById(R.id.custom_tabs_doorhanger_overlay);
 
+        mProgressView = (ProgressBar) findViewById(R.id.page_progress);
         final Toolbar toolbar = (Toolbar) findViewById(R.id.actionbar);
         setSupportActionBar(toolbar);
         final ActionBar actionBar = getSupportActionBar();
         bindNavigationCallback(toolbar);
 
         actionBarPresenter = new ActionBarPresenter(actionBar, getActionBarTextColor());
         actionBarPresenter.displayUrlOnly(intent.getDataString());
         actionBarPresenter.setBackgroundColor(IntentUtil.getToolbarColor(intent), getWindow());
         actionBarPresenter.setTextLongClickListener(new UrlCopyListener());
-
-        mGeckoView = (GeckoView) findViewById(R.id.gecko_view);
+    }
 
-        mGeckoView.setNavigationListener(this);
-        mGeckoView.setProgressListener(this);
-        mGeckoView.setContentListener(this);
+    @Override
+    protected void onTabOpenFromIntent(Tab tab) {
+        super.onTabOpenFromIntent(tab);
 
-        final GeckoViewSettings settings = mGeckoView.getSettings();
-        settings.setBoolean(GeckoViewSettings.USE_MULTIPROCESS, false);
+        final String host = getReferrerHost();
+        recordCustomTabUsage(host);
+        sendTelemetry();
+    }
 
-        if (intent != null && !TextUtils.isEmpty(intent.getDataString())) {
-            mGeckoView.loadUri(intent.getDataString());
-        } else {
-            Log.w(LOGTAG, "No intend found for custom tab");
-            finish();
-        }
+    @Override
+    protected void onTabSelectFromIntent(Tab tab) {
+        super.onTabSelectFromIntent(tab);
+
+        // We already listen for SELECTED events, but if the activity has been destroyed and
+        // subsequently recreated without a different tab having been selected in Gecko in the
+        // meantime, our startup won't trigger a SELECTED event because the selected tab in Gecko
+        // doesn't actually change.
+        actionBarPresenter.update(tab);
     }
 
     private void sendTelemetry() {
         final SafeIntent startIntent = new SafeIntent(getIntent());
 
         Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "customtab");
         if (IntentUtil.hasToolbarColor(startIntent)) {
             Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "customtab-hasToolbarColor");
@@ -153,44 +150,102 @@ public class CustomTabsActivity extends 
             // Use its package name to retrieve animation resource
             return IntentUtil.getAnimationPackageName(new SafeIntent(getIntent()));
         } else {
             return super.getPackageName();
         }
     }
 
     @Override
+    public void onDone() {
+        // We're most probably running within a foreign app's task, so we have no choice what to
+        // call here if we want to allow the user to return to that task's previous activity.
+        finish();
+    }
+
+    @Override
     public void finish() {
         super.finish();
 
         final SafeIntent intent = new SafeIntent(getIntent());
         // When 3rd party app launch this Activity, it could also specify custom exit-animation.
         if (IntentUtil.hasExitAnimation(intent)) {
             usingCustomAnimation = true;
             overridePendingTransition(IntentUtil.getEnterAnimationRes(intent),
                     IntentUtil.getExitAnimationRes(intent));
             usingCustomAnimation = false;
         }
     }
 
     @Override
-    public void onBackPressed() {
-        if (mCanGoBack) {
-            mGeckoView.goBack();
-        } else {
-            finish();
+    protected int getNewTabFlags() {
+        return Tabs.LOADURL_CUSTOMTAB | super.getNewTabFlags();
+    }
+
+    @Override
+    public int getLayout() {
+        return R.layout.customtabs_activity;
+    }
+
+    @Override
+    public View getDoorhangerOverlay() {
+        return doorhangerOverlay;
+    }
+
+    @Override
+    public void onTabChanged(Tab tab, TabEvents msg, String data) {
+        super.onTabChanged(tab, msg, data);
+
+        if (!Tabs.getInstance().isSelectedTab(tab) ||
+                tab.getType() != Tab.TabType.CUSTOMTAB) {
+            return;
         }
+
+        if (msg == TabEvents.START
+                || msg == TabEvents.STOP
+                || msg == TabEvents.ADDED
+                || msg == TabEvents.LOAD_ERROR
+                || msg == TabEvents.LOADED
+                || msg == TabEvents.LOCATION_CHANGE
+                || msg == TabEvents.SELECTED) {
+
+            updateProgress((tab.getState() == Tab.STATE_LOADING),
+                    tab.getLoadProgress());
+        }
+
+        if (msg == TabEvents.LOCATION_CHANGE
+                || msg == TabEvents.SECURITY_CHANGE
+                || msg == TabEvents.TITLE
+                || msg == TabEvents.SELECTED) {
+            actionBarPresenter.update(tab);
+        }
+
+        updateMenuItemForward();
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mLayerView.getDynamicToolbarAnimator().setPinned(true, PinReason.CUSTOM_TAB);
+        actionBarPresenter.onResume();
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mLayerView.getDynamicToolbarAnimator().setPinned(false, PinReason.CUSTOM_TAB);
+        actionBarPresenter.onPause();
     }
 
     // Usually should use onCreateOptionsMenu() to initialize menu items. But GeckoApp overwrite
     // it to support custom menu(Bug 739412). Then the parameter *menu* in this.onCreateOptionsMenu()
     // and this.onPrepareOptionsMenu() are different instances - GeckoApp.onCreatePanelMenu() changed it.
     // CustomTabsActivity only use standard menu in ActionBar, so initialize menu here.
     @Override
-    public boolean onCreateOptionsMenu(final Menu menu) {
+    public boolean onCreatePanelMenu(final int id, final Menu menu) {
 
         // if 3rd-party app asks to add an action button
         SafeIntent intent = new SafeIntent(getIntent());
         if (IntentUtil.hasActionButton(intent)) {
             final Bitmap bitmap = IntentUtil.getActionButtonIcon(intent);
             final Drawable icon = new BitmapDrawable(getResources(), bitmap);
             final boolean shouldTint = IntentUtil.isActionButtonTinted(intent);
             actionBarPresenter.addActionButton(menu, icon, shouldTint)
@@ -216,26 +271,16 @@ public class CustomTabsActivity extends 
                     }
                 });
 
         updateMenuItemForward();
         return true;
     }
 
     @Override
-    public boolean onMenuItemClick(MenuItem item) {
-        return onOptionsItemSelected(item);
-    }
-
-    @Override
-    public boolean onMenuItemLongClick(MenuItem item) {
-        return false;
-    }
-
-    @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case android.R.id.home:
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "customtab-home");
                 finish();
                 return true;
             case R.id.share:
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "customtab-share");
@@ -267,29 +312,53 @@ public class CustomTabsActivity extends 
     /**
      * Called when the menu that's been clicked is added by the client
      */
     private void onCustomMenuItemClicked(PendingIntent intent) {
         Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "customtab-customized-menu");
         performPendingIntent(intent);
     }
 
+    @Override
+    protected ActionModePresenter getTextSelectPresenter() {
+        return new ActionModePresenter() {
+            private ActionMode mMode;
+
+            @Override
+            public void startActionMode(ActionMode.Callback callback) {
+                mMode = startSupportActionMode(callback);
+            }
+
+            @Override
+            public void endActionMode() {
+                if (mMode != null) {
+                    mMode.finish();
+                }
+            }
+        };
+    }
+
     private void bindNavigationCallback(@NonNull final Toolbar toolbar) {
         toolbar.setNavigationOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
-                finish();
+                onDone();
+                final Tabs tabs = Tabs.getInstance();
+                final Tab tab = tabs.getSelectedTab();
+                mSuppressActivitySwitch = true;
+                tabs.closeTab(tab);
             }
         });
     }
 
     private void performPendingIntent(@NonNull PendingIntent pendingIntent) {
         // bug 1337771: If intent-creator haven't set data url, call send() directly won't work.
         final Intent additional = new Intent();
-        additional.setData(Uri.parse(mCurrentUrl));
+        final Tab tab = Tabs.getInstance().getSelectedTab();
+        additional.setData(Uri.parse(tab.getURL()));
         try {
             pendingIntent.send(this, 0, additional);
         } catch (PendingIntent.CanceledException e) {
             Log.w(LOGTAG, "Performing a canceled pending intent", e);
         }
     }
 
     /**
@@ -361,99 +430,110 @@ public class CustomTabsActivity extends 
     private void updateMenuItemForward() {
         if ((popupMenu == null)
                 || (popupMenu.getMenu() == null)
                 || (popupMenu.getMenu().findItem(R.id.custom_tabs_menu_forward) == null)) {
             return;
         }
 
         final MenuItem forwardMenuItem = popupMenu.getMenu().findItem(R.id.custom_tabs_menu_forward);
-        forwardMenuItem.setEnabled(mCanGoForward);
+        final Tab tab = Tabs.getInstance().getSelectedTab();
+        final boolean enabled = (tab != null && tab.canDoForward());
+        forwardMenuItem.setEnabled(enabled);
     }
 
     /**
-     * Update loading status of current page
+     * Update loading progress of current page
+     *
+     * @param isLoading to indicate whether ProgressBar should be visible or not
+     * @param progress  value of loading progress in percent, should be 0 - 100.
      */
-    private void updateCanStop() {
+    private void updateProgress(final boolean isLoading, final int progress) {
+        if (isLoading) {
+            mProgressView.setVisibility(View.VISIBLE);
+            mProgressView.setProgress(progress);
+        } else {
+            mProgressView.setVisibility(View.GONE);
+        }
+
         if (menuItemControl != null) {
             Drawable icon = menuItemControl.getIcon();
-            if (mCanStop) {
-                icon.setLevel(0);
-            } else {
-                icon.setLevel(100);
-            }
+            icon.setLevel(progress);
         }
     }
 
     /**
-     * Update the state of the action bar
-     */
-    private void updateActionBar() {
-        actionBarPresenter.update(mCurrentTitle, mCurrentUrl, mSecurityStatus);
-    }
-
-    /**
      * Call this method to reload page, or stop page loading if progress not complete yet.
      */
     private void onLoadingControlClicked() {
-        if (mCanStop) {
-            // TODO: enable this after implementing GeckoView.stop()
-            //mGeckoView.stop();
-        } else {
-            mGeckoView.reload();
+        final Tab tab = Tabs.getInstance().getSelectedTab();
+        if (tab != null) {
+            if (tab.getLoadProgress() == Tab.LOAD_PROGRESS_STOP) {
+                tab.doReload(true);
+            } else {
+                tab.doStop();
+            }
         }
     }
 
     private void onForwardClicked() {
-        if (mCanGoForward) {
-            mGeckoView.goForward();
+        final Tab tab = Tabs.getInstance().getSelectedTab();
+        if ((tab != null) && tab.canDoForward()) {
+            tab.doForward();
         }
     }
 
     /**
      * Callback for Open-in menu item.
      */
     private void onOpenInClicked() {
-        final Intent intent = new Intent();
-        intent.setData(Uri.parse(mCurrentUrl));
-        intent.setAction(Intent.ACTION_VIEW);
-        startActivity(intent);
-        finish();
+        final Tab tab = Tabs.getInstance().getSelectedTab();
+        if (tab != null) {
+            // To launch default browser with url of current tab.
+            final Intent intent = new Intent();
+            intent.setData(Uri.parse(tab.getURL()));
+            intent.setAction(Intent.ACTION_VIEW);
+            startActivity(intent);
+            finish();
+        }
     }
 
     private void onActionButtonClicked() {
         Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "customtab-action-button");
         PendingIntent pendingIntent = IntentUtil.getActionButtonPendingIntent(new SafeIntent(getIntent()));
         performPendingIntent(pendingIntent);
     }
 
 
     /**
      * Callback for Share menu item.
      */
     private void onShareClicked() {
-        if (!TextUtils.isEmpty(mCurrentUrl)) {
+        final String url = Tabs.getInstance().getSelectedTab().getURL();
+
+        if (!TextUtils.isEmpty(url)) {
             Intent shareIntent = new Intent(Intent.ACTION_SEND);
             shareIntent.setType("text/plain");
-            shareIntent.putExtra(Intent.EXTRA_TEXT, mCurrentUrl);
+            shareIntent.putExtra(Intent.EXTRA_TEXT, url);
 
             Intent chooserIntent = Intent.createChooser(shareIntent, getString(R.string.share_title));
             chooserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             startActivity(chooserIntent);
         }
     }
 
     /**
      * Listener when user long-click ActionBar to copy URL.
      */
     private class UrlCopyListener implements View.OnLongClickListener {
         @Override
         public boolean onLongClick(View v) {
-            if (!TextUtils.isEmpty(mCurrentUrl)) {
-                Clipboard.setText(mCurrentUrl);
+            final String url = Tabs.getInstance().getSelectedTab().getURL();
+            if (!TextUtils.isEmpty(url)) {
+                Clipboard.setText(url);
                 SnackbarBuilder.builder(CustomTabsActivity.this)
                         .message(R.string.custom_tabs_hint_url_copy)
                         .duration(Snackbar.LENGTH_SHORT)
                         .buildAndShow();
             }
             return true;
         }
     }
@@ -469,64 +549,9 @@ public class CustomTabsActivity extends 
             return referrer.getHost();
         }
         String referrerName = intent.getStringExtra("android.intent.extra.REFERRER_NAME");
         if (referrerName != null) {
             return Uri.parse(referrerName).getHost();
         }
         return null;
     }
-
-    /* GeckoView.NavigationListener */
-    @Override
-    public void onLocationChange(GeckoView view, String url) {
-        mCurrentUrl = url;
-        updateActionBar();
-    }
-
-    @Override
-    public void onCanGoBack(GeckoView view, boolean canGoBack) {
-        mCanGoBack = canGoBack;
-    }
-
-    @Override
-    public void onCanGoForward(GeckoView view, boolean canGoForward) {
-        mCanGoForward = canGoForward;
-        updateMenuItemForward();
-    }
-
-    /* GeckoView.ProgressListener */
-    @Override
-    public void onPageStart(GeckoView view, String url) {
-        mCurrentUrl = url;
-        mCanStop = true;
-        updateActionBar();
-        updateCanStop();
-    }
-
-    @Override
-    public void onPageStop(GeckoView view, boolean success) {
-        mCanStop = false;
-        updateCanStop();
-    }
-
-    @Override
-    public void onSecurityChange(GeckoView view, int status) {
-        if ((status & STATE_IS_INSECURE) != 0) {
-            mSecurityStatus = STATE_IS_INSECURE;
-        } else if ((status & STATE_IS_BROKEN) != 0) {
-            mSecurityStatus = STATE_IS_BROKEN;
-        } else if ((status & STATE_IS_SECURE) != 0) {
-            mSecurityStatus = STATE_IS_SECURE;
-        }
-        updateActionBar();
-    }
-
-    /* GeckoView.ContentListener */
-    @Override
-    public void onTitleChange(GeckoView view, String title) {
-        mCurrentTitle = title;
-        updateActionBar();
-    }
-
-    @Override
-    public void onFullScreen(GeckoView view, boolean fullScreen) {}
 }
--- a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
@@ -2,43 +2,39 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.webapps;
 
 import java.io.File;
 import java.io.IOException;
-import java.util.List;
 
 import android.app.ActivityManager;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Bundle;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatActivity;
 import android.support.v7.view.ActionMode;
 import android.support.v7.widget.Toolbar;
+import android.support.v7.app.ActionBar;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
 import android.view.Window;
 import android.view.WindowManager;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
 import org.json.JSONObject;
 import org.json.JSONException;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoView;
-import org.mozilla.gecko.GeckoViewSettings;
 import org.mozilla.gecko.SingleTabActivity;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.icons.decoders.FaviconDecoder;
 import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
@@ -47,81 +43,188 @@ import org.mozilla.gecko.util.ColorUtil;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.FileUtils;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.widget.ActionModePresenter;
 import org.mozilla.gecko.widget.AnchoredPopup;
 
 import static org.mozilla.gecko.Tabs.TabEvents;
 
-public class WebAppActivity extends AppCompatActivity
-                            implements GeckoView.NavigationListener {
+public class WebAppActivity extends SingleTabActivity {
     private static final String LOGTAG = "WebAppActivity";
 
     public static final String MANIFEST_PATH = "MANIFEST_PATH";
     private static final String SAVED_INTENT = "savedIntent";
 
     private TextView mUrlView;
-    private GeckoView mGeckoView;
+    private View doorhangerOverlay;
 
-    private Uri mScope;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0 &&
-            savedInstanceState != null) {
+        savedInstanceState != null) {
             // Even though we're a single task activity, Android's task switcher has the
             // annoying habit of never updating its stored intent after our initial creation,
             // even if we've been subsequently started with a new intent.
 
             // This below is needed if we should ever decide to store a custom class as intent extra.
             savedInstanceState.setClassLoader(getClass().getClassLoader());
 
             Intent lastLaunchIntent = savedInstanceState.getParcelable(SAVED_INTENT);
             setIntent(lastLaunchIntent);
         }
 
         super.onCreate(savedInstanceState);
 
-        setContentView(R.layout.customtabs_activity);
-
         final Toolbar toolbar = (Toolbar) findViewById(R.id.actionbar);
         setSupportActionBar(toolbar);
 
+        final ProgressBar progressBar = (ProgressBar) findViewById(R.id.page_progress);
+        progressBar.setVisibility(View.GONE);
+
         final ActionBar actionBar = getSupportActionBar();
         actionBar.setCustomView(R.layout.webapps_action_bar_custom_view);
         actionBar.setDisplayShowCustomEnabled(true);
         actionBar.setDisplayShowTitleEnabled(false);
         actionBar.hide();
 
+        doorhangerOverlay = findViewById(R.id.custom_tabs_doorhanger_overlay);
+
         final View customView = actionBar.getCustomView();
         mUrlView = (TextView) customView.findViewById(R.id.webapps_action_bar_url);
 
-        mGeckoView = (GeckoView) findViewById(R.id.gecko_view);
+        EventDispatcher.getInstance().registerUiThreadListener(this,
+                "Website:AppEntered",
+                "Website:AppLeft",
+                null);
+    }
 
-        mGeckoView.setNavigationListener(this);
+    @Override
+    public View getDoorhangerOverlay() {
+        return doorhangerOverlay;
+    }
 
-        final GeckoViewSettings settings = mGeckoView.getSettings();
-        settings.setBoolean(GeckoViewSettings.USE_MULTIPROCESS, false);
+    @Override
+    public int getLayout() {
+        return R.layout.customtabs_activity;
+    }
 
-        final Uri u = getIntent().getData();
-        if (u != null) {
-            mGeckoView.loadUri(u.toString());
+    @Override
+    public void handleMessage(final String event, final GeckoBundle message,
+                              final EventCallback callback) {
+        super.handleMessage(event, message, callback);
+
+        if (message == null ||
+                !message.containsKey("tabId") || message.getInt("tabId") != mLastSelectedTabId) {
+            return;
         }
 
-        loadManifest(getIntent().getStringExtra(MANIFEST_PATH));
+        switch (event) {
+            case "Website:AppEntered":
+                getSupportActionBar().hide();
+                break;
+
+            case "Website:AppLeft":
+                getSupportActionBar().show();
+                break;
+        }
+    }
+
+    @Override
+    public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
+        super.onTabChanged(tab, msg, data);
+
+        if (tab == null || !Tabs.getInstance().isSelectedTab(tab) ||
+                tab.getType() != Tab.TabType.WEBAPP) {
+            return;
+        }
+
+        if (msg == TabEvents.LOCATION_CHANGE ||
+                msg == TabEvents.SELECTED) {
+            mUrlView.setText(tab.getURL());
+        }
     }
 
     @Override
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
 
         outState.putParcelable(SAVED_INTENT, getIntent());
     }
 
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        EventDispatcher.getInstance().unregisterUiThreadListener(this,
+                "Website:AppEntered",
+                "Website:AppLeft",
+                null);
+    }
+
+    @Override
+    protected int getNewTabFlags() {
+        return Tabs.LOADURL_WEBAPP | super.getNewTabFlags();
+    }
+
+    @Override
+    protected void onTabOpenFromIntent(Tab tab) {
+        super.onTabOpenFromIntent(tab);
+        Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "webapp");
+        loadManifest(tab.getManifestPath());
+    }
+
+    /**
+     * In case this activity and its tab are reused (the user has opened
+     *  > 10 current web apps), we check that app launched is still within
+     * the same host as the intent has set.
+     * If it isn't, we reload the intent URL.
+     */
+    @Override
+    protected void onTabSelectFromIntent(Tab tab) {
+        super.onTabSelectFromIntent(tab);
+
+        SafeIntent intent = new SafeIntent(getIntent());
+
+        final String launchUrl = intent.getDataString();
+        final String currentUrl = tab.getURL();
+        final boolean isSameDomain = Uri.parse(currentUrl).getHost()
+                .equals(Uri.parse(launchUrl).getHost());
+
+        final String manifestPath;
+        if (!isSameDomain) {
+            Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "webapp");
+            manifestPath = intent.getStringExtra(MANIFEST_PATH);
+            tab.setManifestUrl(manifestPath);
+            Tabs.getInstance().loadUrl(launchUrl);
+        } else {
+            manifestPath = tab.getManifestPath();
+        }
+        loadManifest(manifestPath);
+    }
+
+    @Override
+    protected ActionModePresenter getTextSelectPresenter() {
+        return new ActionModePresenter() {
+            private ActionMode mMode;
+
+            @Override
+            public void startActionMode(ActionMode.Callback callback) {
+                mMode = startSupportActionMode(callback);
+            }
+
+            @Override
+            public void endActionMode() {
+                if (mMode != null) {
+                    mMode.finish();
+                }
+            }
+        };
+    }
+
     private void loadManifest(String manifestPath) {
         if (TextUtils.isEmpty(manifestPath)) {
             Log.e(LOGTAG, "Missing manifest");
             return;
         }
         // The customisations defined in the manifest only work on Android API 21+
         if (AppConstants.Versions.preLollipop) {
             return;
@@ -129,17 +232,16 @@ public class WebAppActivity extends AppC
 
         try {
             final File manifestFile = new File(manifestPath);
             final JSONObject manifest = FileUtils.readJSONObjectFromFile(manifestFile);
             final JSONObject manifestField = manifest.getJSONObject("manifest");
             final Integer color = readColorFromManifest(manifestField);
             final String name = readNameFromManifest(manifestField);
             final Bitmap icon = readIconFromManifest(manifest);
-            mScope = readScopeFromManifest(manifest, manifestPath);
             final ActivityManager.TaskDescription taskDescription = (color == null)
                     ? new ActivityManager.TaskDescription(name, icon)
                     : new ActivityManager.TaskDescription(name, icon, color);
 
             updateStatusBarColor(color);
             setTaskDescription(taskDescription);
 
         } catch (IOException | JSONException e) {
@@ -175,81 +277,15 @@ public class WebAppActivity extends AppC
     }
 
     private Bitmap readIconFromManifest(JSONObject manifest) {
         final String iconStr = manifest.optString("cached_icon", null);
         if (iconStr == null) {
             return null;
         }
         final LoadFaviconResult loadIconResult = FaviconDecoder
-            .decodeDataURI(this, iconStr);
+            .decodeDataURI(getContext(), iconStr);
         if (loadIconResult == null) {
             return null;
         }
         return loadIconResult.getBestBitmap(GeckoAppShell.getPreferredIconSize());
     }
-
-    private Uri readScopeFromManifest(JSONObject manifest, String manifestPath) {
-        final String scopeStr = manifest.optString("scope", null);
-        if (scopeStr == null) {
-            return null;
-        }
-
-        Uri res = Uri.parse(scopeStr);
-        if (res.isRelative()) {
-            // TODO: Handle this more correctly.
-            return null;
-        }
-
-        return res;
-    }
-
-    private boolean isInScope(String url) {
-        if (mScope == null) {
-            return true;
-        }
-
-        final Uri uri = Uri.parse(url);
-
-        if (!uri.getScheme().equals(mScope.getScheme())) {
-            return false;
-        }
-
-        if (!uri.getHost().equals(mScope.getHost())) {
-            return false;
-        }
-
-        final List<String> scopeSegments = mScope.getPathSegments();
-        final List<String> urlSegments = uri.getPathSegments();
-
-        if (scopeSegments.size() > urlSegments.size()) {
-            return false;
-        }
-
-        for (int i = 0; i < scopeSegments.size(); i++) {
-            if (!scopeSegments.get(i).equals(urlSegments.get(i))) {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    /* GeckoView.NavigationListener */
-    @Override
-    public void onLocationChange(GeckoView view, String url) {
-        if (isInScope(url)) {
-            getSupportActionBar().hide();
-        } else {
-            getSupportActionBar().show();
-        }
-
-        mUrlView.setText(url);
-    }
-
-    @Override
-    public void onCanGoBack(GeckoView view, boolean canGoBack) {
-    }
-
-    @Override
-    public void onCanGoForward(GeckoView view, boolean canGoForward) {
-    }
 }
--- a/mobile/android/base/resources/layout/customtabs_activity.xml
+++ b/mobile/android/base/resources/layout/customtabs_activity.xml
@@ -7,31 +7,66 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/root_layout"
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
+    <!--
+        This layout is quite complex because GeckoApp accesses all view groups
+        in this tree. In a perfect world this should just include a GeckoView.
+    -->
+
     <android.support.v7.widget.Toolbar
         android:id="@id/actionbar"
         android:layout_width="match_parent"
         android:layout_height="?attr/actionBarSize"
         android:elevation="4dp"
         android:background="@color/text_and_tabs_tray_grey"
         app:layout_scrollFlags="scroll|enterAlways"/>
 
-    <org.mozilla.gecko.GeckoView
-        android:id="@+id/gecko_view"
-        android:layout_width="fill_parent"
+    <view class="org.mozilla.gecko.GeckoApp$MainLayout"
+        android:id="@+id/main_layout"
+        android:layout_width="match_parent"
         android:layout_below="@id/actionbar"
         android:layout_height="match_parent"
-        android:scrollbars="none"/>
+        android:background="@android:color/transparent">
+
+        <RelativeLayout android:id="@+id/gecko_layout"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_below="@+id/tablet_tab_strip"
+            android:layout_above="@+id/find_in_page">
+
+            <fragment class="org.mozilla.gecko.GeckoViewFragment"
+                android:id="@+id/layer_view"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:scrollbars="none"/>
+
+            <org.mozilla.gecko.FormAssistPopup android:id="@+id/form_assist_popup"
+                                               android:layout_width="match_parent"
+                                               android:layout_height="match_parent"
+                                               android:visibility="gone"/>
+
+        </RelativeLayout>
+
+    </view>
+
+    <ProgressBar
+        android:id="@id/page_progress"
+        style="@style/Base.Widget.AppCompat.ProgressBar.Horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="4dp"
+        android:layout_alignTop="@id/main_layout"
+        android:background="@drawable/url_bar_bg"
+        android:progressDrawable="@drawable/progressbar"
+        tools:progress="70"/>
 
     <View android:id="@+id/custom_tabs_doorhanger_overlay"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:background="@color/dark_transparent_overlay"
-        android:alpha="0"
-        android:layerType="hardware"/>
-
+          android:layout_width="match_parent"
+          android:layout_height="match_parent"
+          android:background="@color/dark_transparent_overlay"
+          android:alpha="0"
+          android:layerType="hardware"/>
 </RelativeLayout>
\ No newline at end of file
--- a/mobile/android/chrome/content/FindHelper.js
+++ b/mobile/android/chrome/content/FindHelper.js
@@ -82,17 +82,17 @@ var FindHelper = {
       throw new Error("FindHelper: " + e + "\n" +
         "JS stack: \n" + (e.stack || Components.stack.formattedStack));
     }
 
     this._finder.addResultListener(this);
     this._initialViewport = JSON.stringify(this._targetTab.getViewport());
     this._viewportChanged = false;
 
-    WindowEventDispatcher.registerListener(this, [
+    GlobalEventDispatcher.registerListener(this, [
       "Tab:Selected",
     ]);
   },
 
   /**
    * Detach from the Finder instance (so stop listening for messages) and stop
    * tracking the active viewport.
    */
@@ -104,17 +104,17 @@ var FindHelper = {
 
     this._finder.removeSelection();
     this._finder.removeResultListener(this);
     this._finder = null;
     this._targetTab = null;
     this._initialViewport = null;
     this._viewportChanged = false;
 
-    WindowEventDispatcher.unregisterListener(this, [
+    GlobalEventDispatcher.unregisterListener(this, [
       "Tab:Selected",
     ]);
   },
 
   /**
    * When the FindInPageBar closes, it's time to stop listening for its messages.
    */
   _findClosed: function() {
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -384,25 +384,21 @@ var BrowserApp = {
     Services.obs.notifyObservers(this.browser, "BrowserChrome:Ready");
 
     this.deck = document.getElementById("browsers");
 
     BrowserEventHandler.init();
 
     Services.androidBridge.browserApp = this;
 
-    WindowEventDispatcher.registerListener(this, [
-      "Session:Restore",
+    GlobalEventDispatcher.registerListener(this, [
       "Tab:Load",
       "Tab:Selected",
       "Tab:Closed",
       "Tab:Move",
-    ]);
-
-    GlobalEventDispatcher.registerListener(this, [
       "Browser:LoadManifest",
       "Browser:Quit",
       "Fonts:Reload",
       "FormHistory:Init",
       "FullScreen:Exit",
       "Locale:OS",
       "Locale:Changed",
       "Passwords:Init",
@@ -1850,20 +1846,16 @@ var BrowserApp = {
           let sh = webNav.sessionHistory;
           if (sh)
             webNav = sh.QueryInterface(Ci.nsIWebNavigation);
         } catch (e) {}
         webNav.reload(flags);
         break;
       }
 
-      case "Session:Restore":
-        GlobalEventDispatcher.dispatch("Session:Restore", data);
-        break;
-
       case "Session:Stop":
         browser.stop();
         break;
 
       case "Tab:Load": {
         let url = data.url;
         let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP
                   | Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
--- a/mobile/android/chrome/geckoview/geckoview.xul
+++ b/mobile/android/chrome/geckoview/geckoview.xul
@@ -2,15 +2,15 @@
 <!-- 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/. -->
 
 <?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
 
 <window id="main-window"
         onload="startup();"
-        windowtype="navigator:geckoview"
+        windowtype="navigator:browser"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <browser id="content" type="content" primary="true" src="about:blank" flex="1"/>
 
   <script type="application/javascript" src="chrome://geckoview/content/geckoview.js"/>
 </window>