Merge autoland to mozilla-central. a=merge
authorCosmin Sabou <csabou@mozilla.com>
Wed, 11 Jul 2018 00:50:32 +0300
changeset 425731 70f901964f9725e6dfd09750f48996e9d6670492
parent 425685 1f836d71ddc52a92a705feb9dc791413bf63bf6f (current diff)
parent 425730 51091d42db5fcbda89972416da5daa2aa15cb06e (diff)
child 425732 3edc9c3ae818490ed36b8bfc8ffdfc9e222b41db
push id105136
push usercsabou@mozilla.com
push dateTue, 10 Jul 2018 23:18:13 +0000
treeherdermozilla-inbound@3edc9c3ae818 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone63.0a1
first release with
nightly linux32
70f901964f97 / 63.0a1 / 20180710222524 / files
nightly linux64
70f901964f97 / 63.0a1 / 20180710222524 / files
nightly mac
70f901964f97 / 63.0a1 / 20180710222524 / files
nightly win32
70f901964f97 / 63.0a1 / 20180710222524 / files
nightly win64
70f901964f97 / 63.0a1 / 20180710222524 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge autoland to mozilla-central. a=merge
browser/base/content/browser.js
browser/branding/aurora/locales/browserconfig.properties
browser/branding/nightly/locales/browserconfig.properties
browser/branding/official/locales/browserconfig.properties
browser/branding/unofficial/locales/browserconfig.properties
gfx/webrender/src/query.rs
modules/libpref/init/all.js
testing/mochitest/bootstrap.js
testing/mochitest/jar.mn
toolkit/components/search/tests/xpcshell/test_location_funnelcake.js
toolkit/components/search/tests/xpcshell/test_location_partner.js
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -674,16 +674,24 @@ name = "euclid"
 version = "0.17.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
+name = "euclid"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
 name = "fallible"
 version = "0.0.1"
 dependencies = [
  "hashglobe 0.1.0",
  "smallvec 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1137,17 +1145,17 @@ dependencies = [
 ]
 
 [[package]]
 name = "malloc_size_of"
 version = "0.0.1"
 dependencies = [
  "app_units 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "cssparser 0.24.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "euclid 0.17.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "euclid 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "hashglobe 0.1.0",
  "selectors 0.19.0",
  "servo_arc 0.1.1",
  "smallbitvec 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "smallvec 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -1937,17 +1945,17 @@ dependencies = [
  "app_units 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "arrayvec 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "atomic_refcell 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "bindgen 0.37.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "cssparser 0.24.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "euclid 0.17.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "euclid 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "fallible 0.0.1",
  "fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "hashglobe 0.1.0",
  "itertools 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1992,32 +2000,32 @@ dependencies = [
 
 [[package]]
 name = "style_traits"
 version = "0.0.1"
 dependencies = [
  "app_units 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "cssparser 0.24.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "euclid 0.17.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "euclid 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "malloc_size_of 0.0.1",
  "malloc_size_of_derive 0.0.1",
  "selectors 0.19.0",
  "servo_arc 0.1.1",
 ]
 
 [[package]]
 name = "stylo_tests"
 version = "0.0.1"
 dependencies = [
  "atomic_refcell 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "cssparser 0.24.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "cstr 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "env_logger 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "euclid 0.17.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "euclid 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "geckoservo 0.0.1",
  "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "malloc_size_of 0.0.1",
  "regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "selectors 0.19.0",
  "size_of_test 0.0.1",
  "smallvec 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2586,16 +2594,17 @@ dependencies = [
 "checksum dwrote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b26e30aaa6bf31ec830db15fec14ed04f0f2ecfcc486ecfce88c55d3389b237f"
 "checksum either 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18785c1ba806c258137c937e44ada9ee7e69a37e3c72077542cd2f069d78562a"
 "checksum ena 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cabe5a5078ac8c506d3e4430763b1ba9b609b1286913e7d08e581d1c2de9b7e5"
 "checksum encoding_c 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "769ecb8b33323998e482b218c0d13cd64c267609023b4b7ec3ee740714c318ee"
 "checksum encoding_rs 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "88a1b66a0d28af4b03a8c8278c6dcb90e6e600d89c14500a9e7a02e64b9ee3ac"
 "checksum env_logger 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0561146661ae44c579e993456bc76d11ce1e0c7d745e57b2fa7146b6e49fa2ad"
 "checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
 "checksum euclid 0.17.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c95fd0d455f114291a3109286bd387bd423770058474a2d3f38b712cd661df60"
+"checksum euclid 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)" = "47d5eb6310c8dd3e79f973952ddcb180bf6a98c01d341add49126a094b5598cc"
 "checksum fixedbitset 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "85cb8fec437468d86dc7c83ca7cfc933341d561873275f22dd5eedefa63a6478"
 "checksum flate2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fac2277e84e5e858483756647a9d0aa8d9a2b7cba517fd84325a0aaa69a0909"
 "checksum fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc484842f1e2884faf56f529f960cc12ad8c71ce96cc7abba0a067c98fee344"
 "checksum foreign-types 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5ebc04f19019fff1f2d627b5581574ead502f80c48c88900575a46e0840fe5d0"
 "checksum freetype 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b659e75b7a7338fe75afd7f909fc2b71937845cffb6ebe54ba2e50f13d8e903d"
 "checksum fs2 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9ab76cfd2aaa59b7bf6688ad9ba15bbae64bff97f04ea02144cfd3443e5c2866"
 "checksum fuchsia-zircon 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f6c0581a4e363262e52b87f59ee2afe3415361c6ec35e665924eb08afe8ff159"
 "checksum fuchsia-zircon-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "43f3795b4bae048dc6123a6b972cadde2e676f9ded08aef6bb77f5f157684a82"
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -240,17 +240,17 @@ pref("browser.shell.mostRecentDateSetAsD
 pref("browser.shell.skipDefaultBrowserCheckOnFirstRun", true);
 pref("browser.shell.didSkipDefaultBrowserCheckOnFirstRun", false);
 pref("browser.shell.defaultBrowserCheckCount", 0);
 pref("browser.defaultbrowser.notificationbar", false);
 
 // 0 = blank, 1 = home (browser.startup.homepage), 2 = last visited page, 3 = resume previous browser session
 // The behavior of option 3 is detailed at: http://wiki.mozilla.org/Session_Restore
 pref("browser.startup.page",                1);
-pref("browser.startup.homepage",            "chrome://branding/locale/browserconfig.properties");
+pref("browser.startup.homepage",            "about:home");
 // Whether we should skip the homepage when opening the first-run page
 pref("browser.startup.firstrunSkipsHomepage", true);
 
 // Show an about:blank window as early as possible for quick startup feedback.
 // Held to nightly on Linux due to bug 1450626.
 // Disabled on Mac because the bouncing dock icon already provides feedback.
 #if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) && defined(NIGHTLY_BUILD)
 pref("browser.startup.blankWindow", true);
--- a/browser/base/content/browser-customization.js
+++ b/browser/base/content/browser-customization.js
@@ -41,18 +41,16 @@ var CustomizationHandler = {
     PlacesToolbarHelper.customizeStart();
   },
 
   _customizationEnding(aDetails) {
     // Update global UI elements that may have been added or removed
     if (aDetails.changed) {
       gURLBar = document.getElementById("urlbar");
 
-      gHomeButton.updateTooltip();
-
       if (AppConstants.platform != "macosx")
         updateEditUIVisibility();
 
       // Hacky: update the PopupNotifications' object's reference to the iconBox,
       // if it already exists, since it may have changed if the URL bar was
       // added/removed.
       if (!window.__lookupGetter__("PopupNotifications")) {
         PopupNotifications.iconBox =
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -25,16 +25,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
   CustomizableUI: "resource:///modules/CustomizableUI.jsm",
   Deprecated: "resource://gre/modules/Deprecated.jsm",
   DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
   E10SUtils: "resource://gre/modules/E10SUtils.jsm",
   ExtensionsUI: "resource:///modules/ExtensionsUI.jsm",
   FormValidationHandler: "resource:///modules/FormValidationHandler.jsm",
   LanguagePrompt: "resource://gre/modules/LanguagePrompt.jsm",
+  HomePage: "resource:///modules/HomePage.jsm",
   LightweightThemeConsumer: "resource://gre/modules/LightweightThemeConsumer.jsm",
   LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
   Log: "resource://gre/modules/Log.jsm",
   LoginManagerParent: "resource://gre/modules/LoginManagerParent.jsm",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
   OpenInTabsUtils: "resource:///modules/OpenInTabsUtils.jsm",
   PageActions: "resource:///modules/PageActions.jsm",
@@ -1467,21 +1468,16 @@ var gBrowserInit = {
     PanelUI.init();
 
     UpdateUrlbarSearchSplitterState();
 
     BookmarkingUI.init();
     BrowserSearch.delayedStartupInit();
     AutoShowBookmarksToolbar.init();
 
-    Services.prefs.addObserver(gHomeButton.prefDomain, gHomeButton);
-
-    var homeButton = document.getElementById("home-button");
-    gHomeButton.updateTooltip(homeButton);
-
     let safeMode = document.getElementById("helpSafeMode");
     if (Services.appinfo.inSafeMode) {
       safeMode.label = safeMode.getAttribute("stoplabel");
       safeMode.accesskey = safeMode.getAttribute("stopaccesskey");
     }
 
     // BiDi UI
     gBidiUI = isBidiEnabled();
@@ -1918,22 +1914,16 @@ var gBrowserInit = {
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-origin-blocked");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-confirmation");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete");
       window.messageManager.removeMessageListener("Browser:URIFixup", gKeywordURIFixup);
       window.messageManager.removeMessageListener("Browser:LoadURI", RedirectLoad);
 
-      try {
-        Services.prefs.removeObserver(gHomeButton.prefDomain, gHomeButton);
-      } catch (ex) {
-        Cu.reportError(ex);
-      }
-
       if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
         MenuTouchModeObserver.uninit();
       }
       BrowserOffline.uninit();
       IndexedDBPromptHelper.uninit();
       CanvasPermissionPromptHelper.uninit();
       WebAuthnPromptHelper.uninit();
       PanelUI.uninit();
@@ -2231,17 +2221,17 @@ function BrowserReloadSkipCache() {
 }
 
 var BrowserHome = BrowserGoHome;
 function BrowserGoHome(aEvent) {
   if (aEvent && "button" in aEvent &&
       aEvent.button == 2) // right-click: do nothing
     return;
 
-  var homePage = gHomeButton.getHomePage();
+  var homePage = HomePage.get();
   var where = whereToOpenLink(aEvent, false, true);
   var urls;
   var notifyObservers;
 
   // Home page should open in a new tab when current tab is an app tab
   if (where == "current" &&
       gBrowser &&
       gBrowser.selectedTab.pinned)
@@ -3308,29 +3298,23 @@ function goBackFromErrorPage() {
   }
 }
 
 /**
  * Return the default start page for the cases when the user's own homepage is
  * infected, so we can get them somewhere safe.
  */
 function getDefaultHomePage() {
-  // Get the start page from the *default* pref branch, not the user's
-  var prefs = Services.prefs.getDefaultBranch(null);
-  var url = BROWSER_NEW_TAB_URL;
+  let url = BROWSER_NEW_TAB_URL;
   if (PrivateBrowsingUtils.isWindowPrivate(window))
     return url;
-  try {
-    url = prefs.getComplexValue("browser.startup.homepage",
-                                Ci.nsIPrefLocalizedString).data;
-    // If url is a pipe-delimited set of pages, just take the first one.
-    if (url.includes("|"))
-      url = url.split("|")[0];
-  } catch (e) {
-    Cu.reportError("Couldn't get homepage pref: " + e);
+  url = HomePage.getDefault();
+  // If url is a pipe-delimited set of pages, just take the first one.
+  if (url.includes("|")) {
+    url = url.split("|")[0];
   }
   return url;
 }
 
 function BrowserFullScreen() {
   window.fullScreen = !window.fullScreen;
 }
 
@@ -3663,17 +3647,17 @@ function openHomeDialog(aURL) {
   }
 
   var pressedVal  = Services.prompt.confirmEx(window, promptTitle, promptMsg,
                           Services.prompt.STD_YES_NO_BUTTONS,
                           null, null, null, null, {value: 0});
 
   if (pressedVal == 0) {
     try {
-      Services.prefs.setStringPref("browser.startup.homepage", aURL);
+      HomePage.set(aURL);
     } catch (ex) {
       dump("Failed to set the home page.\n" + ex + "\n");
     }
   }
 }
 
 var newTabButtonObserver = {
   onDragOver(aEvent) {
@@ -4432,17 +4416,17 @@ function OpenBrowserWindow(options) {
   } else {
     // forget about the charset information.
     win = window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no" + extraFeatures, defaultArgs);
   }
 
   win.addEventListener("MozAfterPaint", () => {
     TelemetryStopwatch.finish("FX_NEW_WINDOW_MS", telemetryObj);
     if (Services.prefs.getIntPref("browser.startup.page") == 1
-        && defaultArgs == handler.startPage) {
+        && defaultArgs == HomePage.get()) {
       // A notification for when a user has triggered their homepage. This is used
       // to display a doorhanger explaining that an extension has modified the
       // homepage, if necessary.
       Services.obs.notifyObservers(win, "browser-open-homepage-start");
     }
   }, {once: true});
 
   return win;
@@ -5932,57 +5916,16 @@ var gUIDensity = {
       }
     }
 
     TabsInTitlebar.update();
     gBrowser.tabContainer.uiDensityChanged();
   },
 };
 
-var gHomeButton = {
-  prefDomain: "browser.startup.homepage",
-  observe(aSubject, aTopic, aPrefName) {
-    if (aTopic != "nsPref:changed" || aPrefName != this.prefDomain)
-      return;
-
-    this.updateTooltip();
-  },
-
-  updateTooltip(homeButton) {
-    if (!homeButton)
-      homeButton = document.getElementById("home-button");
-    if (homeButton) {
-      var homePage = this.getHomePage();
-      homePage = homePage.replace(/\|/g, ", ");
-      if (["about:home", "about:newtab"].includes(homePage.toLowerCase()))
-        homeButton.setAttribute("tooltiptext", homeButton.getAttribute("aboutHomeOverrideTooltip"));
-      else
-        homeButton.setAttribute("tooltiptext", homePage);
-    }
-  },
-
-  getHomePage() {
-    var url;
-    try {
-      url = Services.prefs.getComplexValue(this.prefDomain,
-                                  Ci.nsIPrefLocalizedString).data;
-    } catch (e) {
-    }
-
-    // use this if we can't find the pref
-    if (!url) {
-      var configBundle = Services.strings
-                                 .createBundle("chrome://branding/locale/browserconfig.properties");
-      url = configBundle.GetStringFromName(this.prefDomain);
-    }
-
-    return url;
-  },
-};
-
 const nodeToTooltipMap = {
   "bookmarks-menu-button": "bookmarksMenuButton.tooltip",
   "context-reload": "reloadButton.tooltip",
   "context-stop": "stopButton.tooltip",
   "downloads-button": "downloads.tooltip",
   "fullscreen-button": "fullscreenButton.tooltip",
   "appMenu-fullscreen-button": "fullscreenButton.tooltip",
   "new-window-button": "newWindowButton.tooltip",
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -761,17 +761,17 @@
                        label="&homeButton.label;"
                        ondragover="homeButtonObserver.onDragOver(event)"
                        ondragenter="homeButtonObserver.onDragOver(event)"
                        ondrop="homeButtonObserver.onDrop(event)"
                        ondragexit="homeButtonObserver.onDragExit(event)"
                        key="goHome"
                        onclick="BrowserGoHome(event);"
                        cui-areatype="toolbar"
-                       aboutHomeOverrideTooltip="&homeButton.defaultPage.tooltip;"/>
+                       tooltiptext="&homeButton.defaultPage.tooltip;"/>
         <toolbarspring cui-areatype="toolbar" class="chromeclass-toolbar-additional"/>
         <toolbaritem id="urlbar-container" flex="400" persist="width"
                      removable="false"
                      class="chromeclass-location" overflows="false">
             <textbox id="urlbar" flex="1"
                      placeholder="&urlbar.placeholder2;"
                      defaultPlaceholder="&urlbar.placeholder2;"
                      focused="true"
--- a/browser/base/content/test/general/browser_homeDrop.js
+++ b/browser/base/content/test/general/browser_homeDrop.js
@@ -1,18 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 add_task(async function() {
   let HOMEPAGE_PREF = "browser.startup.homepage";
 
-  let homepageStr = Cc["@mozilla.org/supports-string;1"]
-                    .createInstance(Ci.nsISupportsString);
-  homepageStr.data = "about:mozilla";
-  await pushPrefs([HOMEPAGE_PREF, homepageStr, Ci.nsISupportsString]);
+  await pushPrefs([HOMEPAGE_PREF, "about:mozilla"]);
 
   let EventUtils = {};
   Services.scriptloader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
 
   // Since synthesizeDrop triggers the srcElement, need to use another button.
   let dragSrcElement = document.getElementById("downloads-button");
   ok(dragSrcElement, "Downloads button exists");
   let homeButton = document.getElementById("home-button");
deleted file mode 100644
--- a/browser/branding/aurora/locales/browserconfig.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-# Do NOT localize or otherwise change these values
-browser.startup.homepage=about:home
--- a/browser/branding/aurora/locales/jar.mn
+++ b/browser/branding/aurora/locales/jar.mn
@@ -6,9 +6,8 @@
 [localization] @AB_CD@.jar:
   branding                                          (en-US/**/*.ftl)
 
 @AB_CD@.jar:
 % locale branding @AB_CD@ %locale/branding/
 # Aurora branding only exists in en-US
   locale/branding/brand.dtd        (en-US/brand.dtd)
   locale/branding/brand.properties (en-US/brand.properties)
-  locale/branding/browserconfig.properties
deleted file mode 100644
--- a/browser/branding/nightly/locales/browserconfig.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-# Do NOT localize or otherwise change these values
-browser.startup.homepage=about:home
--- a/browser/branding/nightly/locales/jar.mn
+++ b/browser/branding/nightly/locales/jar.mn
@@ -6,9 +6,8 @@
 [localization] @AB_CD@.jar:
   branding                                          (en-US/**/*.ftl)
 
 @AB_CD@.jar:
 % locale branding @AB_CD@ %locale/branding/
 # Nightly branding only exists in en-US
   locale/branding/brand.dtd        (en-US/brand.dtd)
   locale/branding/brand.properties (en-US/brand.properties)
-  locale/branding/browserconfig.properties
deleted file mode 100644
--- a/browser/branding/official/locales/browserconfig.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-# Do NOT localize or otherwise change these values
-browser.startup.homepage=about:home
--- a/browser/branding/official/locales/jar.mn
+++ b/browser/branding/official/locales/jar.mn
@@ -5,9 +5,8 @@
 
 [localization] @AB_CD@.jar:
   branding                                          (%*.ftl)
 
 @AB_CD@.jar:
 % locale branding @AB_CD@ %locale/branding/
   locale/branding/brand.dtd        (%brand.dtd)
   locale/branding/brand.properties (%brand.properties)
-  locale/branding/browserconfig.properties
deleted file mode 100644
--- a/browser/branding/unofficial/locales/browserconfig.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-# Do NOT localize or otherwise change these values
-browser.startup.homepage=about:home
--- a/browser/branding/unofficial/locales/jar.mn
+++ b/browser/branding/unofficial/locales/jar.mn
@@ -6,9 +6,8 @@
 [localization] @AB_CD@.jar:
   branding                                          (en-US/**/*.ftl)
 
 @AB_CD@.jar:
 % locale branding @AB_CD@ %locale/branding/
 # Unofficial branding only exists in en-US
   locale/branding/brand.dtd        (en-US/brand.dtd)
   locale/branding/brand.properties (en-US/brand.properties)
-  locale/branding/browserconfig.properties
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_set_homepage.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_set_homepage.js
@@ -1,22 +1,25 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
+ChromeUtils.defineModuleGetter(this, "HomePage",
+                               "resource:///modules/HomePage.jsm");
+
 registerCleanupFunction(function restore_pref_values() {
   // These two prefs are set as user prefs in case the "Locked"
   // option from this policy was not used. In this case, it won't
   // be tracked nor restored by the PoliciesPrefTracker.
   Services.prefs.clearUserPref("browser.startup.homepage");
   Services.prefs.clearUserPref("browser.startup.page");
 });
 
 async function check_homepage({expectedURL, expectedPageVal = 1, locked = false}) {
-  is(gHomeButton.getHomePage(),
+  is(HomePage.get(),
      expectedURL, "Homepage URL should match expected");
   is(Services.prefs.getIntPref("browser.startup.page", -1), expectedPageVal,
      "Pref page value should match expected");
   is(Services.prefs.prefIsLocked("browser.startup.homepage"), locked,
      "Lock status of browser.startup.homepage should match expected");
   is(Services.prefs.prefIsLocked("browser.startup.page"), locked,
      "Lock status of browser.startup.page should match expected");
 
--- a/browser/components/extensions/ext-browser.json
+++ b/browser/components/extensions/ext-browser.json
@@ -138,16 +138,24 @@
   "geckoProfiler": {
     "url": "chrome://browser/content/parent/ext-geckoProfiler.js",
     "schema": "chrome://browser/content/schemas/geckoProfiler.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["geckoProfiler"]
     ]
   },
+  "search": {
+    "url": "chrome://browser/content/parent/ext-search.js",
+    "schema": "chrome://browser/content/schemas/search.json",
+    "scopes": ["addon_parent"],
+    "paths": [
+      ["search"]
+    ]
+  },
   "sessions": {
     "url": "chrome://browser/content/parent/ext-sessions.js",
     "schema": "chrome://browser/content/schemas/sessions.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["sessions"]
     ]
   },
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -25,16 +25,17 @@ browser.jar:
     content/browser/parent/ext-devtools-panels.js (parent/ext-devtools-panels.js)
     content/browser/parent/ext-find.js (parent/ext-find.js)
     content/browser/parent/ext-geckoProfiler.js (parent/ext-geckoProfiler.js)
     content/browser/parent/ext-history.js (parent/ext-history.js)
     content/browser/parent/ext-menus.js (parent/ext-menus.js)
     content/browser/parent/ext-omnibox.js (parent/ext-omnibox.js)
     content/browser/parent/ext-pageAction.js (parent/ext-pageAction.js)
     content/browser/parent/ext-pkcs11.js (parent/ext-pkcs11.js)
+    content/browser/parent/ext-search.js (parent/ext-search.js)
     content/browser/parent/ext-sessions.js (parent/ext-sessions.js)
     content/browser/parent/ext-sidebarAction.js (parent/ext-sidebarAction.js)
     content/browser/parent/ext-tabs.js (parent/ext-tabs.js)
     content/browser/parent/ext-url-overrides.js (parent/ext-url-overrides.js)
     content/browser/parent/ext-windows.js (parent/ext-windows.js)
     content/browser/child/ext-browser.js (child/ext-browser.js)
     content/browser/child/ext-devtools-inspectedWindow.js (child/ext-devtools-inspectedWindow.js)
     content/browser/child/ext-devtools-network.js (child/ext-devtools-network.js)
--- a/browser/components/extensions/parent/.eslintrc.js
+++ b/browser/components/extensions/parent/.eslintrc.js
@@ -16,15 +16,16 @@ module.exports = {
     "getToolboxEvalOptions": true,
     "isContainerCookieStoreId": true,
     "isPrivateCookieStoreId": true,
     "isValidCookieStoreId": true,
     "makeWidgetId": true,
     "openOptionsPage": true,
     "pageActionFor": true,
     "replaceUrlInTab": true,
+    "searchInitialized": true,
     "sidebarActionFor": true,
     "tabGetSender": true,
     "tabTracker": true,
     "waitForTabLoaded": true,
     "windowTracker": true,
   },
 };
--- a/browser/components/extensions/parent/ext-browser.js
+++ b/browser/components/extensions/parent/ext-browser.js
@@ -218,16 +218,26 @@ global.TabContext = class extends EventE
    */
   shutdown() {
     windowTracker.removeListener("progress", this);
     windowTracker.removeListener("TabSelect", this);
     tabTracker.off("tab-adopted", this.tabAdopted);
   }
 };
 
+// This promise is used to wait for the search service to be initialized.
+// None of the code in the WebExtension modules requests that initialization.
+// It is assumed that it is started at some point. If tests start to fail
+// because this promise never resolves, that's likely the cause.
+XPCOMUtils.defineLazyGetter(global, "searchInitialized", () => {
+  if (Services.search.isInitialized) {
+    return Promise.resolve();
+  }
+  return ExtensionUtils.promiseObserved("browser-search-service", (_, data) => data == "init-complete");
+});
 
 class WindowTracker extends WindowTrackerBase {
   addProgressListener(window, listener) {
     window.gBrowser.addTabsProgressListener(listener);
   }
 
   removeProgressListener(window, listener) {
     window.gBrowser.removeTabsProgressListener(listener);
--- a/browser/components/extensions/parent/ext-chrome-settings-overrides.js
+++ b/browser/components/extensions/parent/ext-chrome-settings-overrides.js
@@ -15,37 +15,16 @@ const DEFAULT_SEARCH_STORE_TYPE = "defau
 const DEFAULT_SEARCH_SETTING_NAME = "defaultSearch";
 const ENGINE_ADDED_SETTING_NAME = "engineAdded";
 
 const HOMEPAGE_PREF = "browser.startup.homepage";
 const HOMEPAGE_CONFIRMED_TYPE = "homepageNotification";
 const HOMEPAGE_SETTING_TYPE = "prefs";
 const HOMEPAGE_SETTING_NAME = "homepage_override";
 
-// This promise is used to wait for the search service to be initialized.
-// None of the code in this module requests that initialization. It is assumed
-// that it is started at some point. If tests start to fail because this
-// promise never resolves, that's likely the cause.
-const searchInitialized = () => {
-  if (Services.search.isInitialized) {
-    return;
-  }
-  return new Promise(resolve => {
-    const SEARCH_SERVICE_TOPIC = "browser-search-service";
-    Services.obs.addObserver(function observer(subject, topic, data) {
-      if (data != "init-complete") {
-        return;
-      }
-
-      Services.obs.removeObserver(observer, SEARCH_SERVICE_TOPIC);
-      resolve();
-    }, SEARCH_SERVICE_TOPIC);
-  });
-};
-
 XPCOMUtils.defineLazyGetter(this, "homepagePopup", () => {
   return new ExtensionControlledPopup({
     confirmedType: HOMEPAGE_CONFIRMED_TYPE,
     observerTopic: "browser-open-homepage-start",
     popupnotificationId: "extension-homepage-notification",
     settingType: HOMEPAGE_SETTING_TYPE,
     settingKey: HOMEPAGE_SETTING_NAME,
     descriptionId: "extension-homepage-notification-description",
@@ -130,17 +109,17 @@ this.chrome_settings_overrides = class e
 
   static async removeEngine(id) {
     await ExtensionSettingsStore.initialize();
     let item = await ExtensionSettingsStore.getSetting(
       DEFAULT_SEARCH_STORE_TYPE, ENGINE_ADDED_SETTING_NAME, id);
     if (item) {
       ExtensionSettingsStore.removeSetting(
         id, DEFAULT_SEARCH_STORE_TYPE, ENGINE_ADDED_SETTING_NAME);
-      await searchInitialized();
+      await searchInitialized;
       let engine = Services.search.getEngineByName(item.value);
       try {
         Services.search.removeEngine(engine);
       } catch (e) {
         Cu.reportError(e);
       }
     }
   }
@@ -206,17 +185,17 @@ this.chrome_settings_overrides = class e
         close: () => {
           if (extension.shutdownReason == "ADDON_DISABLE") {
             homepagePopup.clearConfirmation(extension.id);
           }
         },
       });
     }
     if (manifest.chrome_settings_overrides.search_provider) {
-      await searchInitialized();
+      await searchInitialized;
       extension.callOnClose({
         close: () => {
           if (extension.shutdownReason == "ADDON_DISABLE") {
             chrome_settings_overrides.processDefaultSearchSetting("disable", extension.id);
             chrome_settings_overrides.removeEngine(extension.id);
           }
         },
       });
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/parent/ext-search.js
@@ -0,0 +1,96 @@
+/* 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/. */
+
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+ChromeUtils.defineModuleGetter(this, "Services",
+                               "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyPreferenceGetter(this, "searchLoadInBackground",
+                                      "browser.search.context.loadInBackground");
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["fetch", "btoa"]);
+
+var {
+  ExtensionError,
+} = ExtensionUtils;
+
+async function getDataURI(resourceURI) {
+  let response = await fetch(resourceURI);
+  let buffer = await response.arrayBuffer();
+  let contentType = response.headers.get("content-type");
+  let bytes = new Uint8Array(buffer);
+  let str = String.fromCharCode.apply(null, bytes);
+  return `data:${contentType};base64,${btoa(str)}`;
+}
+
+this.search = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      search: {
+        async get() {
+          await searchInitialized;
+          let engines = Services.search.getEngines();
+          let visibleEngines = engines.filter(engine => !engine.hidden);
+          return Promise.all(visibleEngines.map(async engine => {
+            let favicon_url = null;
+            if (engine.iconURI) {
+              if (engine.iconURI.schemeIs("resource") ||
+                  engine.iconURI.schemeIs("chrome")) {
+                // Convert internal URLs to data URLs
+                favicon_url = await getDataURI(engine.iconURI.spec);
+              } else {
+                favicon_url = engine.iconURI.spec;
+              }
+            }
+
+            return {
+              name: engine.name,
+              is_default: engine === Services.search.currentEngine,
+              alias: engine.alias,
+              favicon_url,
+            };
+          }));
+        },
+
+        async search(name, searchTerms, tabId) {
+          await searchInitialized;
+          let engine = Services.search.getEngineByName(name);
+          if (!engine) {
+            throw new ExtensionError(`${name} was not found`);
+          }
+          let submission = engine.getSubmission(searchTerms, null, "webextension");
+          let options = {
+            postData: submission.postData,
+            triggeringPrincipal: context.principal,
+          };
+          if (tabId === null) {
+            let browser = context.pendingEventBrowser || context.xulBrowser;
+            let {gBrowser} = browser.ownerGlobal;
+            if (!gBrowser || !gBrowser.addTab) {
+              // In some cases (about:addons, sidebar, maybe others), we need
+              // to go up one more level.
+              browser = browser.ownerDocument.docShell.chromeEventHandler;
+
+              ({gBrowser} = browser.ownerGlobal);
+            }
+            if (!gBrowser || !gBrowser.addTab) {
+              throw new ExtensionError("Unable to locate a browser.");
+            }
+            let nativeTab = gBrowser.addTab(submission.uri.spec, options);
+            if (!searchLoadInBackground) {
+              gBrowser.selectedTab = nativeTab;
+            }
+          } else {
+            let tab = tabTracker.getTab(tabId);
+            tab.linkedBrowser.loadURI(submission.uri.spec, options);
+          }
+        },
+      },
+    };
+  }
+};
--- a/browser/components/extensions/schemas/jar.mn
+++ b/browser/components/extensions/schemas/jar.mn
@@ -15,13 +15,14 @@ browser.jar:
     content/browser/schemas/find.json
     content/browser/schemas/geckoProfiler.json
     content/browser/schemas/history.json
     content/browser/schemas/menus.json
     content/browser/schemas/menus_internal.json
     content/browser/schemas/omnibox.json
     content/browser/schemas/page_action.json
     content/browser/schemas/pkcs11.json
+    content/browser/schemas/search.json
     content/browser/schemas/sessions.json
     content/browser/schemas/sidebar_action.json
     content/browser/schemas/tabs.json
     content/browser/schemas/url_overrides.json
     content/browser/schemas/windows.json
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/schemas/search.json
@@ -0,0 +1,64 @@
+/* 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/. */
+
+[
+  {
+    "namespace": "search",
+    "description": "Use browser.search to interact with search engines.",
+    "types": [
+      {
+        "id": "SearchEngine",
+        "type": "object",
+        "description": "An object encapsulating a search engine",
+        "properties": {
+          "name": {
+            "type": "string"
+          },
+          "is_default": {
+            "type": "boolean"
+          },
+          "alias": {
+            "type": "string",
+            "optional": true
+          },
+          "favicon_url": {
+            "type": "string",
+            "optional": true,
+            "format": "url"
+          }
+        }
+      }
+    ],
+    "functions": [
+      {
+        "name": "get",
+        "type": "function",
+        "description": "Gets a list of search engines.",
+        "async": true,
+        "parameters": []
+      },
+      {
+        "name": "search",
+        "type": "function",
+        "requireUserInput": true,
+        "description": "Perform a search.",
+        "parameters": [
+          {
+            "name": "engineName",
+            "type": "string"
+          },
+          {
+            "name": "searchTerms",
+            "type": "string"
+          },
+          {
+            "type": "integer",
+            "name": "tabId",
+            "optional": true
+          }
+        ]
+      }
+    ]
+  }
+]
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -129,16 +129,17 @@ skip-if = (verify && debug && (os == 'ma
 disabled = bug 1438663
 [browser_ext_popup_sendMessage.js]
 [browser_ext_popup_shutdown.js]
 [browser_ext_port_disconnect_on_crash.js]
 skip-if = !e10s || !crashreporter # the tab's process is killed during the test. Without e10s the parent process would die too.
 [browser_ext_port_disconnect_on_window_close.js]
 [browser_ext_runtime_openOptionsPage.js]
 [browser_ext_runtime_openOptionsPage_uninstall.js]
+[browser_ext_search.js]
 [browser_ext_runtime_setUninstallURL.js]
 [browser_ext_sessions_forgetClosedTab.js]
 [browser_ext_sessions_forgetClosedWindow.js]
 [browser_ext_sessions_getRecentlyClosed.js]
 [browser_ext_sessions_getRecentlyClosed_private.js]
 [browser_ext_sessions_getRecentlyClosed_tabs.js]
 [browser_ext_sessions_restore.js]
 [browser_ext_sessions_restoreTab.js]
--- a/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js
+++ b/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js
@@ -17,18 +17,17 @@ const HOME_URI_4 = "http://example.net/"
 
 const CONTROLLED_BY_THIS = "controlled_by_this_extension";
 const CONTROLLED_BY_OTHER = "controlled_by_other_extensions";
 const NOT_CONTROLLABLE = "not_controllable";
 
 const HOMEPAGE_URL_PREF = "browser.startup.homepage";
 
 const getHomePageURL = () => {
-  return Services.prefs.getComplexValue(
-    HOMEPAGE_URL_PREF, Ci.nsIPrefLocalizedString).data;
+  return Services.prefs.getStringPref(HOMEPAGE_URL_PREF);
 };
 
 function isConfirmed(id) {
   let item = ExtensionSettingsStore.getSetting("homepageNotification", id);
   return !!(item && item.value);
 }
 
 add_task(async function test_multiple_extensions_overriding_home_page() {
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_search.js
@@ -0,0 +1,96 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_search() {
+  const TEST_ID = "test_search@tests.mozilla.com";
+  const SEARCH_TERM = "test";
+  const SEARCH_URL = "https://localhost/?q={searchTerms}";
+
+  async function background() {
+    browser.browserAction.onClicked.addListener(tab => {
+      browser.search.search("Search Test", "test", tab.id); // Can't use SEARCH_TERM here
+    });
+    browser.tabs.onUpdated.addListener(async function(tabId, info, changedTab) {
+      if (changedTab.url == "about:blank") {
+        // Ignore events related to the initial tab open.
+        return;
+      }
+      if (info.status === "complete") {
+        await browser.tabs.remove(tabId);
+        browser.test.sendMessage("searchLoaded", changedTab.url);
+      }
+    });
+    await browser.tabs.create({url: "about:blank"});
+    let engines = await browser.search.get();
+    browser.test.sendMessage("engines", engines);
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+      name: TEST_ID,
+      "browser_action": {},
+      "chrome_settings_overrides": {
+        "search_provider": {
+          "name": "Search Test",
+          "search_url": SEARCH_URL,
+        },
+      },
+    },
+    background,
+    useAddonManager: "temporary",
+  });
+  await extension.startup();
+
+  let addonEngines = await extension.awaitMessage("engines");
+  let engines = Services.search.getEngines().filter(engine => !engine.hidden);
+  is(addonEngines.length, engines.length, "Engine lengths are the same.");
+  let defaultEngine = addonEngines.filter(engine => engine.is_default === true);
+  is(defaultEngine.length, 1, "One default engine");
+  is(defaultEngine[0].name, Services.search.currentEngine.name, "Default engine is correct");
+  await clickBrowserAction(extension);
+  let url = await extension.awaitMessage("searchLoaded");
+  is(url, SEARCH_URL.replace("{searchTerms}", SEARCH_TERM), "Loaded page matches search");
+  await extension.unload();
+});
+
+add_task(async function test_search_notab() {
+  const TEST_ID = "test_search@tests.mozilla.com";
+  const SEARCH_TERM = "test";
+  const SEARCH_URL = "https://localhost/?q={searchTerms}";
+
+  async function background() {
+    browser.browserAction.onClicked.addListener(_ => {
+      browser.tabs.onUpdated.addListener(async (tabId, info, changedTab) => {
+        if (info.status === "complete") {
+          await browser.tabs.remove(tabId);
+          browser.test.sendMessage("searchLoaded", changedTab.url);
+        }
+      });
+      browser.search.search("Search Test", "test"); // Can't use SEARCH_TERM here
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+      name: TEST_ID,
+      "browser_action": {},
+      "chrome_settings_overrides": {
+        "search_provider": {
+          "name": "Search Test",
+          "search_url": SEARCH_URL,
+        },
+      },
+    },
+    background,
+    useAddonManager: "temporary",
+  });
+  await extension.startup();
+
+  await clickBrowserAction(extension);
+  let url = await extension.awaitMessage("searchLoaded");
+  is(url, SEARCH_URL.replace("{searchTerms}", SEARCH_TERM), "Loaded page matches search");
+  await extension.unload();
+});
--- a/browser/components/extensions/test/mochitest/test_ext_all_apis.html
+++ b/browser/components/extensions/test/mochitest/test_ext_all_apis.html
@@ -11,16 +11,17 @@
 <body>
 <script>
 "use strict";
 /* exported expectedContentApisTargetSpecific, expectedBackgroundApisTargetSpecific */
 let expectedContentApisTargetSpecific = [
 ];
 
 let expectedBackgroundApisTargetSpecific = [
+  "search.get",
   "tabs.MutedInfoReason",
   "tabs.TAB_ID_NONE",
   "tabs.TabStatus",
   "tabs.UpdatePropertyName",
   "tabs.WindowType",
   "tabs.ZoomSettingsMode",
   "tabs.ZoomSettingsScope",
   "tabs.connect",
--- a/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js
@@ -1,13 +1,14 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm");
+ChromeUtils.import("resource:///modules/HomePage.jsm");
 
 const {
   createAppInfo,
   promiseShutdownManager,
   promiseStartupManager,
 } = AddonTestUtils;
 
 AddonTestUtils.init(this);
@@ -20,25 +21,20 @@ add_task(async function test_overrides_u
    * search_provider are removed between updates and therefore the
    * settings are expected to revert. */
 
   const EXTENSION_ID = "test_overrides_update@tests.mozilla.org";
   const HOMEPAGE_URI = "webext-homepage-1.html";
 
   const HOMEPAGE_URL_PREF = "browser.startup.homepage";
 
-  const getHomePageURL = () => {
-    return Services.prefs.getComplexValue(
-      HOMEPAGE_URL_PREF, Ci.nsIPrefLocalizedString).data;
-  };
-
   function promisePrefChanged(value) {
     return new Promise((resolve, reject) => {
       Services.prefs.addObserver(HOMEPAGE_URL_PREF, function observer() {
-        if (getHomePageURL().endsWith(value)) {
+        if (HomePage.get().endsWith(value)) {
           Services.prefs.removeObserver(HOMEPAGE_URL_PREF, observer);
           resolve();
         }
       });
     });
   }
 
   await promiseStartupManager();
@@ -59,25 +55,25 @@ add_task(async function test_overrides_u
           "search_url": "https://example.com/?q={searchTerms}",
           "is_default": true,
         },
       },
     },
   };
   let extension = ExtensionTestUtils.loadExtension(extensionInfo);
 
-  let defaultHomepageURL = getHomePageURL();
+  let defaultHomepageURL = HomePage.get();
   let defaultEngineName = Services.search.currentEngine.name;
 
   let prefPromise = promisePrefChanged(HOMEPAGE_URI);
   await extension.startup();
   await prefPromise;
 
   equal(extension.version, "1.0", "The installed addon has the expected version.");
-  ok(getHomePageURL().endsWith(HOMEPAGE_URI),
+  ok(HomePage.get().endsWith(HOMEPAGE_URI),
      "Home page url is overridden by the extension.");
   equal(Services.search.currentEngine.name,
         "DuckDuckGo",
         "Default engine is overridden by the extension");
 
   extensionInfo.manifest = {
     "version": "2.0",
     "applications": {
@@ -87,17 +83,17 @@ add_task(async function test_overrides_u
     },
   };
 
   prefPromise = promisePrefChanged(defaultHomepageURL);
   await extension.upgrade(extensionInfo);
   await prefPromise;
 
   equal(extension.version, "2.0", "The updated addon has the expected version.");
-  equal(getHomePageURL(),
+  equal(HomePage.get(),
         defaultHomepageURL,
         "Home page url reverted to the default after update.");
   equal(Services.search.currentEngine.name,
         defaultEngineName,
         "Default engine reverted to the default after update.");
 
   await extension.unload();
 
--- a/browser/components/nsBrowserContentHandler.js
+++ b/browser/components/nsBrowserContentHandler.js
@@ -4,16 +4,17 @@
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   HeadlessShell: "resource:///modules/HeadlessShell.jsm",
+  HomePage: "resource:///modules/HomePage.jsm",
   LaterRun: "resource:///modules/LaterRun.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   ShellService: "resource:///modules/ShellService.jsm",
   UpdatePing: "resource://gre/modules/UpdatePing.jsm"
 });
 XPCOMUtils.defineLazyServiceGetter(this, "WindowsUIUtils",
   "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils");
 
@@ -549,17 +550,17 @@ nsBrowserContentHandler.prototype = {
         overridePage = additionalPage;
       }
     }
 
     var startPage = "";
     try {
       var choice = prefb.getIntPref("browser.startup.page");
       if (choice == 1 || choice == 3)
-        startPage = this.startPage;
+        startPage = HomePage.get();
     } catch (e) {
       Cu.reportError(e);
     }
 
     if (startPage == "about:blank")
       startPage = "";
 
     let skipStartPage = override == OVERRIDE_NEW_PROFILE &&
@@ -567,27 +568,16 @@ nsBrowserContentHandler.prototype = {
     // Only show the startPage if we're not restoring an update session and are
     // not set to skip the start page on this profile
     if (overridePage && startPage && !willRestoreSession && !skipStartPage)
       return overridePage + "|" + startPage;
 
     return overridePage || startPage || "about:blank";
   },
 
-  get startPage() {
-    var uri = Services.prefs.getComplexValue("browser.startup.homepage",
-                                             Ci.nsIPrefLocalizedString).data;
-    if (!uri) {
-      Services.prefs.clearUserPref("browser.startup.homepage");
-      uri = Services.prefs.getComplexValue("browser.startup.homepage",
-                                           Ci.nsIPrefLocalizedString).data;
-    }
-    return uri;
-  },
-
   mFeatures: null,
 
   getFeatures: function bch_features(cmdLine) {
     if (this.mFeatures === null) {
       this.mFeatures = "";
 
       try {
         var width = cmdLine.handleFlagWithParam("width", false);
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -101,16 +101,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
   CustomizableUI: "resource:///modules/CustomizableUI.jsm",
   DateTimePickerParent: "resource://gre/modules/DateTimePickerParent.jsm",
   ExtensionsUI: "resource:///modules/ExtensionsUI.jsm",
   Feeds: "resource:///modules/Feeds.jsm",
   FileSource: "resource://gre/modules/L10nRegistry.jsm",
   FormValidationHandler: "resource:///modules/FormValidationHandler.jsm",
   FxAccounts: "resource://gre/modules/FxAccounts.jsm",
+  HomePage: "resource:///modules/HomePage.jsm",
   HybridContentTelemetry: "resource://gre/modules/HybridContentTelemetry.jsm",
   Integration: "resource://gre/modules/Integration.jsm",
   L10nRegistry: "resource://gre/modules/L10nRegistry.jsm",
   LanguagePrompt: "resource://gre/modules/LanguagePrompt.jsm",
   LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
   LoginHelper: "resource://gre/modules/LoginHelper.jsm",
   LoginManagerParent: "resource://gre/modules/LoginManagerParent.jsm",
   NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
@@ -397,18 +398,17 @@ BrowserGlue.prototype = {
     } else if (aboutNewTabService.newTabURL.startsWith("moz-extension://")) {
       newTabSetting = 2;
     } else if (!Services.prefs.getBoolPref("browser.newtabpage.enabled")) {
       newTabSetting = 1;
     } else {
       newTabSetting = 3;
     }
 
-    const homePageURL = Services.prefs.getComplexValue("browser.startup.homepage",
-                                                       Ci.nsIPrefLocalizedString).data;
+    const homePageURL = HomePage.get();
     if (homePageURL === "about:home") {
       homePageSetting = 0;
     } else if (homePageURL === "about:blank") {
       homePageSetting = 1;
     } else if (homePageURL.startsWith("moz-extension://")) {
       homePageSetting = 2;
     } else {
       homePageSetting = 3;
--- a/browser/components/preferences/in-content/tests/browser_extension_controlled.js
+++ b/browser/components/preferences/in-content/tests/browser_extension_controlled.js
@@ -261,18 +261,19 @@ add_task(async function testPrefLockedHo
   is(homePageInput.value, "", "The homepage is empty");
   is(homePageInput.disabled, false, "The homepage is enabled after clearing lock");
   is(homeModeEl.disabled, false, "Homepage menulist is enabled after clearing lock");
   buttonPrefs.forEach(pref => {
     is(getButton(pref).disabled, false, `The ${pref} button is enabled when unlocked`);
   });
 
   // Lock the prefs without an extension.
+  let mutationsDone = waitForAllMutations();
   lockPrefs();
-  await waitForAllMutations();
+  await mutationsDone;
 
   // Check that everything is now disabled.
   is(getHomepage(), lockedHomepage, "The reported homepage is set by the pref");
   is(homePageInput.value, lockedHomepage, "The homepage is set by the pref");
   is(homePageInput.disabled, true, "The homepage is disabed when the pref is locked");
   is(homeModeEl.disabled, true, "Homepage menulist is disabled when prefis locked");
   buttonPrefs.forEach(pref => {
     is(getButton(pref).disabled, true, `The ${pref} button is disabled when locked`);
--- a/browser/components/preferences/in-content/tests/browser_homepages_filter_aboutpreferences.js
+++ b/browser/components/preferences/in-content/tests/browser_homepages_filter_aboutpreferences.js
@@ -1,21 +1,23 @@
+ChromeUtils.import("resource:///modules/HomePage.jsm");
+
 add_task(async function testSetHomepageUseCurrent() {
   is(gBrowser.currentURI.spec, "about:blank", "Test starts with about:blank open");
   await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home");
   await openPreferencesViaOpenPreferencesAPI("paneHome", {leaveOpen: true});
   // eslint-disable-next-line mozilla/no-cpows-in-tests
   let doc = gBrowser.contentDocument;
   is(gBrowser.currentURI.spec, "about:preferences#home",
      "#home should be in the URI for about:preferences");
-  let oldHomepagePref = Services.prefs.getCharPref("browser.startup.homepage");
+  let oldHomepage = HomePage.get();
 
   let useCurrent = doc.getElementById("useCurrentBtn");
   useCurrent.click();
 
   is(gBrowser.tabs.length, 3, "Three tabs should be open");
-  is(Services.prefs.getCharPref("browser.startup.homepage"), "about:blank|about:home",
+  is(HomePage.get(), "about:blank|about:home",
      "about:blank and about:home should be the only homepages set");
 
-  Services.prefs.setCharPref("browser.startup.homepage", oldHomepagePref);
+  HomePage.set(oldHomepage);
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
--- a/browser/components/preferences/in-content/tests/browser_hometab_restore_defaults.js
+++ b/browser/components/preferences/in-content/tests/browser_hometab_restore_defaults.js
@@ -1,9 +1,9 @@
-add_task(async function testRestoreDefaultsBtn() {
+add_task(async function testRestoreDefaultsBtn_visible() {
   const before = SpecialPowers.Services.prefs.getStringPref("browser.newtabpage.activity-stream.feeds.section.topstories.options", "");
 
   await SpecialPowers.pushPrefEnv({set: [
     // Hide Pocket pref so we don't trigger network requests when we reset all preferences
     ["browser.newtabpage.activity-stream.feeds.section.topstories.options", JSON.stringify(Object.assign({}, JSON.parse(before), {hidden: true}))],
     // Set a user pref to false to force the Restore Defaults button to be visible
     ["browser.newtabpage.activity-stream.feeds.topsites", false]
   ]});
@@ -35,28 +35,32 @@ add_task(async function testRestoreDefau
 
   const topsitesPref = await SpecialPowers.Services.prefs.getBoolPref("browser.newtabpage.activity-stream.feeds.topsites");
   Assert.ok(topsitesPref, "Topsites pref should have the default value");
 
   await SpecialPowers.popPrefEnv();
   BrowserTestUtils.removeTab(tab);
 });
 
-add_task(async function testRestoreDefaultsBtn() {
+add_task(async function testRestoreDefaultsBtn_hidden() {
   const before = SpecialPowers.Services.prefs.getStringPref("browser.newtabpage.activity-stream.feeds.section.topstories.options", "");
 
   await SpecialPowers.pushPrefEnv({set: [
     // Hide Pocket pref so we don't trigger network requests when we reset all preferences
     ["browser.newtabpage.activity-stream.feeds.section.topstories.options", JSON.stringify(Object.assign({}, JSON.parse(before), {hidden: true}))],
   ]});
 
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences#home", false);
   let browser = tab.linkedBrowser;
 
   await BrowserTestUtils.waitForCondition(() => ContentTask.spawn(browser, {},
+    () => content.document.getElementById("restoreDefaultHomePageBtn") !== null),
+    "Wait for the button to be added to the page");
+
+  await BrowserTestUtils.waitForCondition(() => ContentTask.spawn(browser, {},
     () => content.document.querySelector("[data-subcategory='topsites'] checkbox") !== null),
     "Wait for the preference checkbox to load");
 
   const btnDefault = await ContentTask.spawn(browser, {}, () => content.document.getElementById("restoreDefaultHomePageBtn").style.visibility);
   Assert.equal(btnDefault, "hidden", "When no prefs are changed button should not show up");
 
   await BrowserTestUtils.waitForCondition(() => ContentTask.spawn(browser, {},
     () => content.document.querySelector("[data-subcategory='topsites'] checkbox").checked),
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -169,16 +169,17 @@ XPCOMUtils.defineLazyServiceGetters(this
 });
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.jsm",
   GlobalState: "resource:///modules/sessionstore/GlobalState.jsm",
+  HomePage: "resource:///modules/HomePage.jsm",
   PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm",
   PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
   RunState: "resource:///modules/sessionstore/RunState.jsm",
   SessionCookies: "resource:///modules/sessionstore/SessionCookies.jsm",
   SessionFile: "resource:///modules/sessionstore/SessionFile.jsm",
   SessionSaver: "resource:///modules/sessionstore/SessionSaver.jsm",
   TabAttributes: "resource:///modules/sessionstore/TabAttributes.jsm",
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
@@ -3128,17 +3129,17 @@ var SessionStoreInternal = {
     // home pages then we'll end up overwriting all of them. Otherwise we'll
     // just close the tabs that match home pages. Tabs with the about:blank
     // URI will always be overwritten.
     let homePages = ["about:blank"];
     let removableTabs = [];
     let tabbrowser = aWindow.gBrowser;
     let startupPref = this._prefBranch.getIntPref("startup.page");
     if (startupPref == 1)
-      homePages = homePages.concat(aWindow.gHomeButton.getHomePage().split("|"));
+      homePages = homePages.concat(HomePage.get().split("|"));
 
     for (let i = tabbrowser._numPinnedTabs; i < tabbrowser.tabs.length; i++) {
       let tab = tabbrowser.tabs[i];
       if (homePages.includes(tab.linkedBrowser.currentURI.spec)) {
         removableTabs.push(tab);
       }
     }
 
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/modules/ContentLinkHandler.jsm
@@ -89,28 +89,36 @@ class FaviconLoad {
     if (Services.prefs.getBoolPref("network.http.tailing.enabled", true) &&
         this.channel instanceof Ci.nsIClassOfService) {
       this.channel.addClassFlags(Ci.nsIClassOfService.Tail | Ci.nsIClassOfService.Throttleable);
     }
   }
 
   load() {
     this._deferred = PromiseUtils.defer();
+    // Clear the channel reference when we succeed or fail.
+    this._deferred.promise.then(
+      () => this.channel = null,
+      () => this.channel = null
+    );
 
     try {
       this.channel.asyncOpen2(this);
     } catch (e) {
       this._deferred.reject(e);
     }
 
     return this._deferred.promise;
   }
 
   cancel() {
-    this._deferred.reject(Components.Exception(`Favicon load from ${this.icon.iconUri.spec} was cancelled.`, Cr.NS_BINDING_ABORTED));
+    if (!this.channel) {
+      return;
+    }
+
     this.channel.cancel(Cr.NS_BINDING_ABORTED);
   }
 
   onStartRequest(request, context) {
   }
 
   onDataAvailable(request, context, inputStream, offset, count) {
     let data = NetUtil.readInputStreamToString(inputStream, count);
@@ -128,18 +136,19 @@ class FaviconLoad {
   async onStopRequest(request, context, statusCode) {
     if (request != this.channel) {
       // Indicates that a redirect has occurred. We don't care about the result
       // of the original channel.
       return;
     }
 
     if (!Components.isSuccessCode(statusCode)) {
-      // If the load was cancelled the promise will have been rejected then.
-      if (statusCode != Cr.NS_BINDING_ABORTED) {
+      if (statusCode == Cr.NS_BINDING_ABORTED) {
+        this._deferred.reject(Components.Exception(`Favicon load from ${this.icon.iconUri.spec} was cancelled.`, statusCode));
+      } else {
         this._deferred.reject(Components.Exception(`Favicon at "${this.icon.iconUri.spec}" failed to load.`, statusCode));
       }
       return;
     }
 
     if (this.channel instanceof Ci.nsIHttpChannel) {
       if (!this.channel.requestSucceeded) {
         this._deferred.reject(Components.Exception(`Favicon at "${this.icon.iconUri.spec}" failed to load: ${this.channel.responseStatusText}.`, Cr.NS_ERROR_FAILURE));
@@ -392,17 +401,16 @@ class IconLoader {
         iconURL: iconInfo.iconUri.spec,
       });
       return;
     }
 
     try {
       this._loader = new FaviconLoad(iconInfo);
       let { dataURL, expiration } = await this._loader.load();
-      this._loader = null;
 
       this.chromeGlobal.sendAsyncMessage("Link:SetIcon", {
         originalURL: iconInfo.iconUri.spec,
         canUseForTab: !iconInfo.isRichIcon,
         expiration,
         iconURL: dataURL,
       });
     } catch (e) {
@@ -410,16 +418,18 @@ class IconLoader {
         Cu.reportError(e);
 
         // Used mainly for tests currently.
         this.chromeGlobal.sendAsyncMessage("Link:SetFailedIcon", {
           originalURL: iconInfo.iconUri.spec,
           canUseForTab: !iconInfo.isRichIcon,
         });
       }
+    } finally {
+      this._loader = null;
     }
   }
 
   cancel() {
     if (!this._loader) {
       return;
     }
 
new file mode 100644
--- /dev/null
+++ b/browser/modules/HomePage.jsm
@@ -0,0 +1,65 @@
+/* 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/. */
+
+/* globals ChromeUtils, Services */
+/* exported HomePage */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["HomePage"];
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const kPrefName = "browser.startup.homepage";
+
+function getHomepagePref(useDefault) {
+  let homePage;
+  let prefs = Services.prefs;
+  if (useDefault) {
+    prefs = prefs.getDefaultBranch(null);
+  }
+  try {
+    // Historically, this was a localizable pref, but default Firefox builds
+    // don't use this.
+    // Distributions and local customizations might still use this, so let's
+    // keep it.
+    homePage = prefs.getComplexValue(kPrefName,
+                                     Ci.nsIPrefLocalizedString).data;
+  } catch (ex) {}
+
+  if (!homePage) {
+    homePage = prefs.getStringPref(kPrefName);
+  }
+
+  // Apparently at some point users ended up with blank home pages somehow.
+  // If that happens, reset the pref and read it again.
+  if (!homePage && !useDefault) {
+    Services.prefs.clearUserPref(kPrefName);
+    homePage = getHomepagePref(true);
+  }
+
+  return homePage;
+}
+
+let HomePage = {
+  get() {
+    return getHomepagePref();
+  },
+
+  getDefault() {
+    return getHomepagePref(true);
+  },
+
+  get overridden() {
+    return Services.prefs.prefHasUserValue(kPrefName);
+  },
+
+  set(value) {
+    Services.prefs.setStringPref(kPrefName, value);
+  },
+
+  reset() {
+    Services.prefs.clearUserPref(kPrefName);
+  }
+};
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -152,16 +152,17 @@ EXTRA_JS_MODULES += [
     'ContentObservers.js',
     'ContentSearch.jsm',
     'ContentWebRTC.jsm',
     'ContextMenu.jsm',
     'ExtensionsUI.jsm',
     'Feeds.jsm',
     'FormSubmitObserver.jsm',
     'FormValidationHandler.jsm',
+    'HomePage.jsm',
     'LaterRun.jsm',
     'LightweightThemeChildListener.jsm',
     'LightWeightThemeWebInstallListener.jsm',
     'NetErrorContent.jsm',
     'OpenInTabsUtils.jsm',
     'PageActions.jsm',
     'PageInfoListener.jsm',
     'PageStyleHandler.jsm',
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/unit/test_HomePage.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* globals ChromeUtils, Services, Cc, Ci, HomePage, Assert, add_task */
+"use strict";
+
+ChromeUtils.import("resource:///modules/HomePage.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+add_task(function testHomePage() {
+  Assert.ok(!HomePage.overridden, "Homepage should not be overriden by default.");
+  let newvalue = "about:blank|about:newtab";
+  HomePage.set(newvalue);
+  Assert.ok(HomePage.overridden, "Homepage should be overriden after set()");
+  Assert.equal(HomePage.get(), newvalue, "Homepage should be ${newvalue}");
+  Assert.notEqual(HomePage.getDefault(), newvalue, "Homepage should be ${newvalue}");
+  HomePage.reset();
+  Assert.ok(!HomePage.overridden, "Homepage should not be overriden by after reset.");
+  Assert.equal(HomePage.get(), HomePage.getDefault(),
+               "Homepage and default should be equal after reset.");
+});
+
+add_task(function readLocalizedHomepage() {
+  let newvalue = "data:text/plain,browser.startup.homepage%3Dabout%3Alocalized";
+  let complexvalue = Cc["@mozilla.org/pref-localizedstring;1"]
+                   .createInstance(Ci.nsIPrefLocalizedString);
+  complexvalue.data = newvalue;
+  Services.prefs.getDefaultBranch(null).setComplexValue(
+    "browser.startup.homepage",
+    Ci.nsIPrefLocalizedString,
+    complexvalue);
+    Assert.ok(!HomePage.overridden, "Complex value only works as default");
+    Assert.equal(HomePage.get(), "about:localized", "Get value from bundle");
+});
+
+add_task(function recoverEmptyHomepage() {
+  Assert.ok(!HomePage.overridden, "Homepage should not be overriden by default.");
+  Services.prefs.setStringPref("browser.startup.homepage", "");
+  Assert.ok(HomePage.overridden, "Homepage is overriden with empty string.");
+  Assert.equal(HomePage.get(), HomePage.getDefault(), "Recover is default");
+  Assert.ok(!HomePage.overridden, "Recover should have set default");
+});
--- a/browser/modules/test/unit/xpcshell.ini
+++ b/browser/modules/test/unit/xpcshell.ini
@@ -1,11 +1,12 @@
 [DEFAULT]
 head =
 firefox-appdir = browser
 skip-if = toolkit == 'android'
 
 [test_AttributionCode.js]
 skip-if = os != 'win'
 [test_E10SUtils_nested_URIs.js]
+[test_HomePage.js]
 [test_Sanitizer_interrupted.js]
 [test_SitePermissions.js]
 [test_LaterRun.js]
--- a/devtools/client/debugger/new/index.html
+++ b/devtools/client/debugger/new/index.html
@@ -10,18 +10,24 @@
   <link rel="stylesheet" type="text/css" href="chrome://devtools/content/sourceeditor/codemirror/mozilla.css" />
   <link rel="stylesheet" type="text/css" href="resource://devtools/client/debugger/new/dist/debugger.css" />
 </head>
 
 <body>
   <div id="mount"></div>
   <script type="application/javascript" src="chrome://devtools/content/shared/theme-switching.js"></script>
   <script type="text/javascript">
-    const { BrowserLoader } = ChromeUtils.import("resource://devtools/client/shared/browser-loader.js", {});
-    const { require } = BrowserLoader({
-      baseURI: "resource://devtools/client/debugger/new",
-      window,
-    });
-    Debugger = require("devtools/client/debugger/new/src/main");
+    try {
+      const { BrowserLoader } = ChromeUtils.import("resource://devtools/client/shared/browser-loader.js", {});
+      const { require } = BrowserLoader({
+        baseURI: "resource://devtools/client/debugger/new",
+        window,
+      });
+      Debugger = require("devtools/client/debugger/new/src/main");
+    } catch(e) {
+      dump("Exception happened while loading the debugger:\n");
+      dump(e + "\n");
+      dump(e.stack + "\n");
+    }
   </script>
 </body>
 
-</html>
\ No newline at end of file
+</html>
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -5,17 +5,16 @@
 "use strict";
 
 const {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils");
 const Services = require("Services");
 
 loader.lazyServiceGetter(this, "clipboardHelper",
                          "@mozilla.org/widget/clipboardhelper;1",
                          "nsIClipboardHelper");
-loader.lazyRequireGetter(this, "defer", "devtools/shared/defer");
 loader.lazyRequireGetter(this, "Debugger", "Debugger");
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup");
 loader.lazyRequireGetter(this, "PropTypes", "devtools/client/shared/vendor/react-prop-types");
 loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
 loader.lazyRequireGetter(this, "KeyCodes", "devtools/client/shared/keycodes", true);
 loader.lazyRequireGetter(this, "Editor", "devtools/client/sourceeditor/editor");
 loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
@@ -315,29 +314,26 @@ class JSTerm extends Component {
       this.inputNode.focus();
     }
   }
 
   /**
    * The JavaScript evaluation response handler.
    *
    * @private
-   * @param function [callback]
-   *        Optional function to invoke when the evaluation result is added to
-   *        the output.
    * @param object response
    *        The message received from the server.
    */
-  async _executeResultCallback(callback, response) {
+  async _executeResultCallback(response) {
     if (!this.hud) {
-      return;
+      return null;
     }
     if (response.error) {
       console.error("Evaluation error " + response.error + ": " + response.message);
-      return;
+      return null;
     }
     let errorMessage = response.exceptionMessage;
 
     // Wrap thrown strings in Error objects, so `throw "foo"` outputs "Error: foo"
     if (typeof response.exception === "string") {
       errorMessage = new Error(errorMessage).toString();
     }
     const result = response.result;
@@ -368,31 +364,32 @@ class JSTerm extends Component {
         case "copyValueToClipboard":
           clipboardHelper.copyString(helperResult.value);
           break;
         case "screenshotOutput":
           const { args, value } = helperResult;
           const results = await processScreenshot(this.hud.window, args, value);
           this.screenshotNotify(results);
           // early return as screenshot notify has dispatched all necessary messages
-          return;
+          return null;
       }
     }
 
     // Hide undefined results coming from JSTerm helper functions.
     if (!errorMessage && result && typeof result == "object" &&
       result.type == "undefined" &&
       helperResult && !helperHasRawOutput) {
-      callback && callback();
-      return;
+      return null;
     }
 
     if (this.hud.consoleOutput) {
-      this.hud.consoleOutput.dispatchMessageAdd(response, true).then(callback);
+      return this.hud.consoleOutput.dispatchMessageAdd(response, true);
     }
+
+    return null;
   }
 
   inspectObjectActor(objectActor) {
     this.hud.consoleOutput.dispatchMessageAdd({
       helperResult: {
         type: "inspectObject",
         object: objectActor
       }
@@ -403,29 +400,23 @@ class JSTerm extends Component {
   screenshotNotify(results) {
     const wrappedResults = results.map(result => ({ result }));
     this.hud.consoleOutput.dispatchMessagesAdd(wrappedResults);
   }
 
   /**
    * Execute a string. Execution happens asynchronously in the content process.
    *
-   * @param string [executeString]
+   * @param {String} executeString
    *        The string you want to execute. If this is not provided, the current
    *        user input is used - taken from |this.getInputValue()|.
-   * @param function [callback]
-   *        Optional function to invoke when the result is displayed.
-   *        This is deprecated - please use the promise return value instead.
-   * @returns Promise
+   * @returns {Promise}
    *          Resolves with the message once the result is displayed.
    */
-  async execute(executeString, callback) {
-    const deferred = defer();
-    const resultCallback = msg => deferred.resolve(msg);
-
+  async execute(executeString) {
     // attempt to execute the content of the inputNode
     executeString = executeString || this.getInputValue();
     if (!executeString) {
       return null;
     }
 
     // Append executed expression into the history list.
     this.props.appendToHistory(executeString);
@@ -436,32 +427,33 @@ class JSTerm extends Component {
 
     let selectedNodeActor = null;
     const inspectorSelection = this.hud.owner.getInspectorSelection();
     if (inspectorSelection && inspectorSelection.nodeFront) {
       selectedNodeActor = inspectorSelection.nodeFront.actorID;
     }
 
     const { ConsoleCommand } = require("devtools/client/webconsole/types");
-    const message = new ConsoleCommand({
+    const cmdMessage = new ConsoleCommand({
       messageText: executeString,
     });
-    this.hud.proxy.dispatchMessageAdd(message);
-
-    const onResult = this._executeResultCallback.bind(this, resultCallback);
+    this.hud.proxy.dispatchMessageAdd(cmdMessage);
 
     const options = {
       frame: this.SELECTED_FRAME,
-      selectedNodeActor: selectedNodeActor,
+      selectedNodeActor,
     };
 
     const mappedString = await this.hud.owner.getMappedExpression(executeString);
-    this.requestEvaluation(mappedString, options).then(onResult, onResult);
-
-    return deferred.promise;
+    // Even if requestEvaluation rejects (because of webConsoleClient.evaluateJSAsync),
+    // we still need to pass the error response to executeResultCallback.
+    const onEvaluated = this.requestEvaluation(mappedString, options)
+      .then(res => res, res => res);
+    const response = await onEvaluated;
+    return this._executeResultCallback(response);
   }
 
   /**
    * Request a JavaScript string evaluation from the server.
    *
    * @param string str
    *        String to execute.
    * @param object [options]
@@ -480,48 +472,37 @@ class JSTerm extends Component {
    *        in the Inspector, if such a selection exists. This is used by
    *        helper functions that can evaluate on the current selection.
    * @return object
    *         A promise object that is resolved when the server response is
    *         received.
    */
   requestEvaluation(str, options = {}) {
     const toolbox = gDevTools.getToolbox(this.hud.owner.target);
-    const deferred = defer();
 
-    function onResult(response) {
-      if (!response.error) {
-        deferred.resolve(response);
-      } else {
-        deferred.reject(response);
-      }
-    }
+    // Send telemetry event. If we are in the browser toolbox we send -1 as the
+    // toolbox session id.
+    this._telemetry.recordEvent("devtools.main", "execute_js", "webconsole", null, {
+      "lines": str.split(/\n/).length,
+      "session_id": toolbox ? toolbox.sessionId : -1
+    });
 
     let frameActor = null;
     if ("frame" in options) {
       frameActor = this.getFrameActor(options.frame);
     }
 
     const evalOptions = {
       bindObjectActor: options.bindObjectActor,
-      frameActor: frameActor,
+      frameActor,
       selectedNodeActor: options.selectedNodeActor,
       selectedObjectActor: options.selectedObjectActor,
     };
 
-    this.webConsoleClient.evaluateJSAsync(str, onResult, evalOptions);
-
-    // Send telemetry event. If we are in the browser toolbox we send -1 as the
-    // toolbox session id.
-    this._telemetry.recordEvent("devtools.main", "execute_js", "webconsole", null, {
-      "lines": str.split(/\n/).length,
-      "session_id": toolbox ? toolbox.sessionId : -1
-    });
-
-    return deferred.promise;
+    return this.webConsoleClient.evaluateJSAsync(str, null, evalOptions);
   }
 
   /**
    * Copy the object/variable by invoking the server
    * which invokes the `copy(variable)` command and makes it
    * available in the clipboard
    * @param evalString - string which has the evaluation string to be copied
    * @param options - object - Options for evaluation
--- a/devtools/server/actors/targets/browsing-context.js
+++ b/devtools/server/actors/targets/browsing-context.js
@@ -285,17 +285,16 @@ const browsingContextTargetPrototype = {
     if (this.exited) {
       return null;
     }
     const form = this.form();
     return this.conn._getOrCreateActor(form.consoleActor);
   },
 
   _targetScopedActorPool: null,
-  _contextPool: null,
 
   /**
    * A constant prefix that will be used to form the actor ID by the server.
    */
   typeName: "browsingContextTarget",
 
   /**
    * An object on which listen for DOMWindowCreated and pageshow events.
@@ -574,17 +573,17 @@ const browsingContextTargetPrototype = {
    * Does the actual work of attaching to a browsing context.
    */
   _attach() {
     if (this._attached) {
       return;
     }
 
     // Create a pool for context-lifetime actors.
-    this._pushContext();
+    this._createThreadActor();
 
     // on xpcshell, there is no document
     if (this.window) {
       this._progressListener = new DebuggerProgressListener(this);
 
       // Save references to the original document we attached to
       this._originalWindow = this.window;
 
@@ -845,38 +844,29 @@ const browsingContextTargetPrototype = {
 
   _notifyDocShellDestroyAll() {
     this.emit("frameUpdate", {
       destroyAll: true
     });
   },
 
   /**
-   * Creates a thread actor and a pool for context-lifetime actors. It then sets
-   * up the content window for debugging.
+   * Creates and manages the thread actor as part of the Browsing Context Target pool.
+   * This sets up the content window for being debugged
    */
-  _pushContext() {
-    assert(!this._contextPool, "Can't push multiple contexts");
-
-    this._contextPool = new ActorPool(this.conn);
-    this.conn.addActorPool(this._contextPool);
-
+  _createThreadActor() {
     this.threadActor = new ThreadActor(this, this.window);
-    this._contextPool.addActor(this.threadActor);
+    this.manage(this.threadActor);
   },
 
   /**
-   * Exits the current thread actor and removes the context-lifetime actor pool.
+   * Exits the current thread actor and removes it from the Browsing Context Target pool.
    * The content window is no longer being debugged after this call.
    */
-  _popContext() {
-    assert(!!this._contextPool, "No context to pop.");
-
-    this.conn.removeActorPool(this._contextPool);
-    this._contextPool = null;
+  _destroyThreadActor() {
     this.threadActor.exit();
     this.threadActor = null;
     this._sources = null;
   },
 
   /**
    * Does the actual work of detaching from a browsing context.
    *
@@ -900,17 +890,17 @@ const browsingContextTargetPrototype = {
 
       // Removes the observers being set in _watchDocShells
       if (this.listenForNewDocShells) {
         Services.obs.removeObserver(this, "webnavigation-create");
       }
       Services.obs.removeObserver(this, "webnavigation-destroy");
     }
 
-    this._popContext();
+    this._destroyThreadActor();
 
     // Shut down actors that belong to this target's pool.
     this._styleSheetActors.clear();
     if (this._targetScopedActorPool) {
       this.conn.removeActorPool(this._targetScopedActorPool);
       this._targetScopedActorPool = null;
     }
 
--- a/devtools/server/actors/thread.js
+++ b/devtools/server/actors/thread.js
@@ -6,17 +6,17 @@
 
 "use strict";
 
 const Services = require("Services");
 const { Cr } = require("chrome");
 const { ActorPool, GeneratedLocation } = require("devtools/server/actors/common");
 const { createValueGrip } = require("devtools/server/actors/object/utils");
 const { longStringGrip } = require("devtools/server/actors/object/long-string");
-const { ActorClassWithSpec } = require("devtools/shared/protocol");
+const { ActorClassWithSpec, Actor } = require("devtools/shared/protocol");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const flags = require("devtools/shared/flags");
 const { assert, dumpn } = DevToolsUtils;
 const { DevToolsWorker } = require("devtools/shared/worker/worker");
 const { threadSpec } = require("devtools/shared/specs/script");
 
 loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "BreakpointActor", "devtools/server/actors/breakpoint", true);
@@ -49,16 +49,17 @@ loader.lazyRequireGetter(this, "EventEmi
  *          - makeDebugger: A function that takes no arguments and instantiates
  *            a Debugger that manages its globals on its own.
  * @param aGlobal object [optional]
  *        An optional (for content debugging only) reference to the content
  *        window.
  */
 const ThreadActor = ActorClassWithSpec(threadSpec, {
   initialize: function(parent, global) {
+    Actor.prototype.initialize.call(this, parent.conn);
     this._state = "detached";
     this._frameActors = [];
     this._parent = parent;
     this._dbg = null;
     this._gripDepth = 0;
     this._threadLifetimePool = null;
     this._parentClosed = false;
     this._scripts = null;
@@ -202,16 +203,22 @@ const ThreadActor = ActorClassWithSpec(t
     this.conn.send({
       from: this.actorID,
       type: "newGlobal",
       // TODO: after bug 801084 lands see if we need to JSONify this.
       hostAnnotations: global.hostAnnotations
     });
   },
 
+  /**
+   * Clean up listeners, debuggees and clear actor pools associated with
+   * the lifetime of this actor. This does not destroy the thread actor,
+   * it resets it. This is used in methods `onReleaseMany` `onDetatch` and
+   * `exit`. The actor is truely destroyed in the `exit method`.
+   */
   destroy: function() {
     dumpn("in ThreadActor.prototype.destroy");
     if (this._state == "paused") {
       this.onResume();
     }
 
     // Blow away our source actor ID store because those IDs are only
     // valid for this connection. This is ok because we never keep
@@ -238,16 +245,24 @@ const ThreadActor = ActorClassWithSpec(t
   },
 
   /**
    * destroy the debugger and put the actor in the exited state.
    */
   exit: function() {
     this.destroy();
     this._state = "exited";
+    // This actor has a slightly different destroy behavior than other
+    // actors using Protocol.js. The thread actor may detach but still
+    // be in use, however detach calls the destroy method, even though it
+    // expects the actor to still be alive. Therefore, we are calling
+    // `Actor.prototype.destroy` in the `exit` method, after its state has
+    // been set to "exited", where we are certain that the actor is no
+    // longer in use.
+    Actor.prototype.destroy.call(this);
   },
 
   // Request handlers
   onAttach: function(request) {
     if (this.state === "exited") {
       return { type: "exited" };
     }
 
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/crashtests/1467277-1.html
@@ -0,0 +1,6 @@
+<script>
+addEventListener("DOMContentLoaded", () => {
+  document.documentElement.animate(
+    [ { "transform": "rotate3d(1e58, 2, 6, 0turn)" } ], 1000)
+})
+</script>
--- a/dom/animation/test/crashtests/crashtests.list
+++ b/dom/animation/test/crashtests/crashtests.list
@@ -36,8 +36,9 @@ pref(dom.animations-api.core.enabled,tru
 pref(dom.animations-api.core.enabled,true) load 1359658-1.html
 pref(dom.animations-api.core.enabled,true) load 1373712-1.html
 pref(dom.animations-api.core.enabled,true) load 1379606-1.html
 pref(dom.animations-api.core.enabled,true) load 1393605-1.html
 load 1400022-1.html
 pref(dom.animations-api.core.enabled,true) load 1401809.html
 pref(dom.animations-api.core.enabled,true) load 1411318-1.html
 load 1468294-1.html
+load 1467277-1.html
--- a/dom/base/DocGroup.cpp
+++ b/dom/base/DocGroup.cpp
@@ -124,18 +124,16 @@ DocGroup::ReportPerformanceInfo()
     count = mPerformanceCounter->GetDispatchCount(DispatchCategory(category));
     CategoryDispatch item = CategoryDispatch(index, count);
     if (!items.AppendElement(item, fallible)) {
       NS_ERROR("Could not complete the operation");
       return PerformanceInfo(host, pid, pwid, duration, false, isTopLevel, items);
     }
   }
 
-  // setting back all counters to zero
-  mPerformanceCounter->ResetPerformanceCounters();
   return PerformanceInfo(host, pid, pwid, duration, false, isTopLevel, items);
 }
 
 nsresult
 DocGroup::Dispatch(TaskCategory aCategory,
                    already_AddRefed<nsIRunnable>&& aRunnable)
 {
   if (mPerformanceCounter) {
--- a/dom/script/ScriptSettings.cpp
+++ b/dom/script/ScriptSettings.cpp
@@ -646,16 +646,18 @@ AutoJSAPI::IsStackTop() const
 AutoEntryScript::AutoEntryScript(nsIGlobalObject* aGlobalObject,
                                  const char* aReason,
                                  bool aIsMainThread)
   : AutoJSAPI(aGlobalObject, aIsMainThread, eEntryScript)
   , mWebIDLCallerPrincipal(nullptr)
   // This relies on us having a cx() because the AutoJSAPI constructor already
   // ran.
   , mCallerOverride(cx())
+  , mAutoProfilerLabel("AutoEntryScript", aReason, __LINE__,
+                       js::ProfilingStackFrame::Category::JS)
 {
   MOZ_ASSERT(aGlobalObject);
 
   if (aIsMainThread && gRunToCompletionListeners > 0) {
     mDocShellEntryMonitor.emplace(cx(), aReason);
   }
 }
 
--- a/dom/script/ScriptSettings.h
+++ b/dom/script/ScriptSettings.h
@@ -375,16 +375,17 @@ private:
   // is the principal of the callee function that is part of the CallArgs just a
   // bit up the stack, and which will outlive us.  So we know the principal
   // can't go away until then either.
   nsIPrincipal* MOZ_NON_OWNING_REF mWebIDLCallerPrincipal;
   friend nsIPrincipal* GetWebIDLCallerPrincipal();
 
   Maybe<DocshellEntryMonitor> mDocShellEntryMonitor;
   JS::AutoHideScriptedCaller mCallerOverride;
+  AutoProfilerLabel mAutoProfilerLabel;
 };
 
 /*
  * A class that can be used to force a particular incumbent script on the stack.
  */
 class AutoIncumbentScript : protected ScriptSettingsStackEntry {
 public:
   explicit AutoIncumbentScript(nsIGlobalObject* aGlobalObject);
--- a/dom/tests/browser/browser_test_performance_metrics.js
+++ b/dom/tests/browser/browser_test_performance_metrics.js
@@ -35,30 +35,26 @@ function jsonrpc(tab, method, params) {
   });
 }
 
 function postMessageToWorker(tab, message) {
   return jsonrpc(tab, "postMessageToWorker", [WORKER_URL, message]);
 }
 
 add_task(async function test() {
-  SpecialPowers.setBoolPref('dom.performance.enable_scheduler_timing', true);
+  SpecialPowers.setBoolPref("dom.performance.enable_scheduler_timing", true);
   waitForExplicitFinish();
 
   // Load 3 pages and wait. The 3rd one has a worker
   let page1 = await BrowserTestUtils.openNewForegroundTab({
-    gBrowser, opening: 'about:about', forceNewProcess: false
+    gBrowser, opening: "about:about", forceNewProcess: false
   });
 
   let page2 = await BrowserTestUtils.openNewForegroundTab({
-    gBrowser, opening: 'about:memory', forceNewProcess: false
-  });
-
-  let page3 = await BrowserTestUtils.openNewForegroundTab({
-    gBrowser, opening: "about:performance", forceNewProcess: true
+    gBrowser, opening: "about:memory", forceNewProcess: false
   });
 
   // load a 4th tab with a worker
   await BrowserTestUtils.withNewTab({ gBrowser, url: WORKER_URL },
     async function(browser) {
     // grab events..
     let workerDuration = 0;
     let workerTotal = 0;
@@ -103,15 +99,28 @@ add_task(async function test() {
 
     Assert.ok(workerDuration > 0, "Worker duration should be positive");
     Assert.ok(workerTotal > 0, "Worker count should be positive");
     Assert.ok(duration > 0, "Duration should be positive");
     Assert.ok(total > 0, "Should get a positive count");
     Assert.ok(parentProcessEvent, "parent process sent back some events");
     Assert.ok(isTopLevel, "example.com as a top level window");
     Assert.ok(aboutMemoryFound, "about:memory");
+
+    // Doing a second call, we shoud get bigger values
+    let previousWorkerDuration = workerDuration;
+    let previousWorkerTotal = workerTotal;
+    let previousDuration = duration;
+    let previousTotal = total;
+
+    results = await ChromeUtils.requestPerformanceMetrics();
+    exploreResults(results);
+
+    Assert.ok(workerDuration > previousWorkerDuration, "Worker duration should be positive");
+    Assert.ok(workerTotal > previousWorkerTotal, "Worker count should be positive");
+    Assert.ok(duration > previousDuration, "Duration should be positive");
+    Assert.ok(total > previousTotal, "Should get a positive count");
   });
 
   BrowserTestUtils.removeTab(page1);
   BrowserTestUtils.removeTab(page2);
-  BrowserTestUtils.removeTab(page3);
-  SpecialPowers.clearUserPref('dom.performance.enable_scheduler_timing');
+  SpecialPowers.clearUserPref("dom.performance.enable_scheduler_timing");
 });
--- a/dom/workers/WorkerDebugger.cpp
+++ b/dom/workers/WorkerDebugger.cpp
@@ -512,17 +512,16 @@ WorkerDebugger::ReportPerformanceInfo()
     count =  perf->GetTotalDispatchCount();
     duration = perf->GetExecutionDuration();
     CategoryDispatch item = CategoryDispatch(DispatchCategory::Worker.GetValue(), count);
     if (!items.AppendElement(item, fallible)) {
       NS_ERROR("Could not complete the operation");
       return PerformanceInfo(uri->GetSpecOrDefault(), pid, pwid, duration,
                             true, isTopLevel, items);
     }
-    perf->ResetPerformanceCounters();
   }
 
   return PerformanceInfo(uri->GetSpecOrDefault(), pid, pwid, duration,
                          true, isTopLevel, items);
 }
 
 } // dom namespace
 } // mozilla namespace
--- a/gfx/2d/BasePoint3D.h
+++ b/gfx/2d/BasePoint3D.h
@@ -82,43 +82,55 @@ struct BasePoint3D {
   Sub& operator*=(T aScale) {
     x *= aScale;
     y *= aScale;
     z *= aScale;
     return *static_cast<Sub*>(this);
   }
 
   Sub& operator/=(T aScale) {
-      x /= aScale;
-      y /= aScale;
-      z /= aScale;
-      return *static_cast<Sub*>(this);
+    x /= aScale;
+    y /= aScale;
+    z /= aScale;
+    return *static_cast<Sub*>(this);
   }
 
   Sub operator-() const {
     return Sub(-x, -y, -z);
   }
 
   Sub CrossProduct(const Sub& aPoint) const {
-      return Sub(y * aPoint.z - aPoint.y * z,
-                 z * aPoint.x - aPoint.z * x,
-                 x * aPoint.y - aPoint.x * y);
+    return Sub(y * aPoint.z - aPoint.y * z,
+               z * aPoint.x - aPoint.z * x,
+               x * aPoint.y - aPoint.x * y);
   }
 
   T DotProduct(const Sub& aPoint) const {
-      return x * aPoint.x + y * aPoint.y + z * aPoint.z;
+    return x * aPoint.x + y * aPoint.y + z * aPoint.z;
   }
 
   T Length() const {
-      return sqrt(x*x + y*y + z*z);
+    return sqrt(x*x + y*y + z*z);
   }
 
   // Invalid for points with distance from origin of 0.
   void Normalize() {
-      *this /= Length();
+    *this /= Length();
+  }
+
+  void RobustNormalize() {
+    // If the distance is infinite, we scale it by 1/(the maximum value of T)
+    // before doing normalization, so we can avoid getting a zero point.
+    T length = Length();
+    if (mozilla::IsInfinite(length)) {
+      *this /= std::numeric_limits<T>::max();
+      length = Length();
+    }
+
+    *this /= length;
   }
 
   friend std::ostream& operator<<(std::ostream& stream, const BasePoint3D<T, Sub>& aPoint) {
     return stream << '(' << aPoint.x << ',' << aPoint.y << ',' << aPoint.z << ')';
   }
 };
 
 } // namespace gfx
--- a/gfx/2d/Matrix.h
+++ b/gfx/2d/Matrix.h
@@ -1645,17 +1645,17 @@ public:
   // to a unit vector.
   // https://drafts.csswg.org/css-transforms-2/#Rotate3dDefined
   void SetRotateAxisAngle(double aX, double aY, double aZ, double aTheta)
   {
     Point3D vector(aX, aY, aZ);
     if (!vector.Length()) {
       return;
     }
-    vector.Normalize();
+    vector.RobustNormalize();
 
     double x = vector.x;
     double y = vector.y;
     double z = vector.z;
 
     double cosTheta = FlushToZero(cos(aTheta));
     double sinTheta = FlushToZero(sin(aTheta));
 
--- a/gfx/layers/wr/WebRenderScrollData.cpp
+++ b/gfx/layers/wr/WebRenderScrollData.cpp
@@ -45,31 +45,39 @@ WebRenderLayerScrollData::Initialize(Web
                                      const Maybe<gfx::Matrix4x4>& aAncestorTransform)
 {
   MOZ_ASSERT(aDescendantCount >= 0); // Ensure value is valid
   MOZ_ASSERT(mDescendantCount == -1); // Don't allow re-setting an already set value
   mDescendantCount = aDescendantCount;
 
   MOZ_ASSERT(aItem);
   aItem->UpdateScrollData(&aOwner, this);
-  for (const ActiveScrolledRoot* asr = aItem->GetActiveScrolledRoot();
-       asr && asr != aStopAtAsr;
-       asr = asr->mParent) {
+
+  const ActiveScrolledRoot* asr = aItem->GetActiveScrolledRoot();
+  if (ActiveScrolledRoot::IsAncestor(asr, aStopAtAsr)) {
+    // If the item's ASR is an ancestor of the stop-at ASR, then we don't need
+    // any more metrics information because we'll end up duplicating what the
+    // ancestor WebRenderLayerScrollData already has.
+    asr = nullptr;
+  }
+
+  while (asr && asr != aStopAtAsr) {
     MOZ_ASSERT(aOwner.GetManager());
     FrameMetrics::ViewID scrollId = asr->GetViewId();
     if (Maybe<size_t> index = aOwner.HasMetadataFor(scrollId)) {
       mScrollIds.AppendElement(index.ref());
     } else {
       Maybe<ScrollMetadata> metadata = asr->mScrollableFrame->ComputeScrollMetadata(
           aOwner.GetManager(), aItem->ReferenceFrame(),
           ContainerLayerParameters(), nullptr);
       MOZ_ASSERT(metadata);
       MOZ_ASSERT(metadata->GetMetrics().GetScrollId() == scrollId);
       mScrollIds.AppendElement(aOwner.AddMetadata(metadata.ref()));
     }
+    asr = asr->mParent;
   }
 
   // aAncestorTransform, if present, is the transform from an ancestor
   // nsDisplayTransform that was stored on the stacking context in order to
   // propagate it downwards in the tree. (i.e. |aItem| is a strict descendant of
   // the nsDisplayTranform which produced aAncestorTransform). We store this
   // separately from mTransform because in the case where we have multiple
   // scroll metadata on this layer item, the mAncestorTransform is associated
--- a/gfx/webrender/res/render_task.glsl
+++ b/gfx/webrender/res/render_task.glsl
@@ -80,30 +80,31 @@ PictureTask fetch_picture_task(int addre
     PictureTask task = PictureTask(
         task_data.common_data,
         task_data.data1.xy
     );
 
     return task;
 }
 
+#define CLIP_TASK_EMPTY 0x7FFF
+
 struct ClipArea {
     RenderTaskCommonData common_data;
     vec2 screen_origin;
     bool local_space;
 };
 
 ClipArea fetch_clip_area(int index) {
     ClipArea area;
 
-    if (index == 0x7FFF) { //special sentinel task index
-        area.common_data = RenderTaskCommonData(
-            RectWithSize(vec2(0.0), vec2(0.0)),
-            0.0
-        );
+    if (index >= CLIP_TASK_EMPTY) {
+        RectWithSize rect = RectWithSize(vec2(0.0), vec2(0.0));
+
+        area.common_data = RenderTaskCommonData(rect, 0.0);
         area.screen_origin = vec2(0.0);
         area.local_space = false;
     } else {
         RenderTaskData task_data = fetch_render_task_data(index);
 
         area.common_data = task_data.common_data;
         area.screen_origin = task_data.data1.xy;
         area.local_space = task_data.data1.z == 0.0;
--- a/gfx/webrender/src/device/mod.rs
+++ b/gfx/webrender/src/device/mod.rs
@@ -1,7 +1,9 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 mod gl;
+pub mod query_gl;
 
 pub use self::gl::*;
+pub use self::query_gl as query;
rename from gfx/webrender/src/query.rs
rename to gfx/webrender/src/device/query_gl.rs
--- a/gfx/webrender/src/display_list_flattener.rs
+++ b/gfx/webrender/src/display_list_flattener.rs
@@ -1,16 +1,16 @@
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{AlphaType, BorderDetails, BorderDisplayItem, BuiltDisplayListIter, ClipAndScrollInfo};
 use api::{ClipId, ColorF, ComplexClipRegion, DeviceIntPoint, DeviceIntRect, DeviceIntSize};
-use api::{DevicePixelScale, DeviceUintRect, DisplayItemRef, Epoch, ExtendMode, ExternalScrollId};
+use api::{DevicePixelScale, DeviceUintRect, DisplayItemRef, ExtendMode, ExternalScrollId};
 use api::{FilterOp, FontInstanceKey, GlyphInstance, GlyphOptions, GlyphRasterSpace, GradientStop};
 use api::{IframeDisplayItem, ImageKey, ImageRendering, ItemRange, LayoutPoint};
 use api::{LayoutPrimitiveInfo, LayoutRect, LayoutSize, LayoutTransform, LayoutVector2D};
 use api::{LineOrientation, LineStyle, LocalClip, NinePatchBorderSource, PipelineId};
 use api::{PropertyBinding, ReferenceFrame, RepeatMode, ScrollFrameDisplayItem, ScrollSensitivity};
 use api::{Shadow, SpecificDisplayItem, StackingContext, StickyFrameDisplayItem, TexelRect};
 use api::{TransformStyle, YuvColorSpace, YuvData};
 use clip::{ClipRegion, ClipSource, ClipSources, ClipStore};
@@ -145,19 +145,16 @@ pub struct DisplayListFlattener<'a> {
     scene: &'a Scene,
 
     /// The ClipScrollTree that we are currently building during flattening.
     clip_scroll_tree: &'a mut ClipScrollTree,
 
     /// The map of all font instances.
     font_instances: FontInstanceMap,
 
-    /// Used to track the latest flattened epoch for each pipeline.
-    pipeline_epochs: Vec<(PipelineId, Epoch)>,
-
     /// A set of pipelines that the caller has requested be made available as
     /// output textures.
     output_pipelines: &'a FastHashSet<PipelineId>,
 
     /// The data structure that converting between ClipId and the various index
     /// types that the ClipScrollTree uses.
     id_to_index_mapper: ClipIdToIndexMapper,
 
@@ -202,28 +199,25 @@ impl<'a> DisplayListFlattener<'a> {
         frame_builder_config: &FrameBuilderConfig,
         new_scene: &mut Scene,
         scene_id: u64,
     ) -> FrameBuilder {
         // We checked that the root pipeline is available on the render backend.
         let root_pipeline_id = scene.root_pipeline_id.unwrap();
         let root_pipeline = scene.pipelines.get(&root_pipeline_id).unwrap();
 
-        let root_epoch = scene.pipeline_epochs[&root_pipeline_id];
-
         let background_color = root_pipeline
             .background_color
             .and_then(|color| if color.a > 0.0 { Some(color) } else { None });
 
         let mut flattener = DisplayListFlattener {
             scene,
             clip_scroll_tree,
             font_instances,
             config: *frame_builder_config,
-            pipeline_epochs: Vec::new(),
             output_pipelines,
             id_to_index_mapper: ClipIdToIndexMapper::default(),
             hit_testing_runs: recycle_vec(old_builder.hit_testing_runs),
             scrollbar_prims: recycle_vec(old_builder.scrollbar_prims),
             reference_frame_stack: Vec::new(),
             picture_stack: Vec::new(),
             shadow_stack: Vec::new(),
             sc_stack: Vec::new(),
@@ -238,18 +232,17 @@ impl<'a> DisplayListFlattener<'a> {
             &root_pipeline.content_size,
         );
         flattener.setup_viewport_offset(view.inner_rect, view.accumulated_scale_factor());
         flattener.flatten_root(root_pipeline, &root_pipeline.viewport_size);
 
         debug_assert!(flattener.picture_stack.is_empty());
 
         new_scene.root_pipeline_id = Some(root_pipeline_id);
-        new_scene.pipeline_epochs.insert(root_pipeline_id, root_epoch);
-        new_scene.pipeline_epochs.extend(flattener.pipeline_epochs.drain(..));
+        new_scene.pipeline_epochs = scene.pipeline_epochs.clone();
         new_scene.pipelines = scene.pipelines.clone();
 
         FrameBuilder::with_display_list_flattener(
             view.inner_rect,
             background_color,
             view.window_size,
             scene_id,
             flattener,
@@ -524,19 +517,16 @@ impl<'a> DisplayListFlattener<'a> {
             info.clip_id,
             clip_and_scroll_ids.scroll_node_id,
             ClipRegion::create_for_clip_node_with_local_clip(
                 &LocalClip::from(*item.clip_rect()),
                 reference_frame_relative_offset
             ),
         );
 
-        let epoch = self.scene.pipeline_epochs[&iframe_pipeline_id];
-        self.pipeline_epochs.push((iframe_pipeline_id, epoch));
-
         let bounds = item.rect();
         let origin = *reference_frame_relative_offset + bounds.origin.to_vector();
         self.push_reference_frame(
             ClipId::root_reference_frame(iframe_pipeline_id),
             Some(info.clip_id),
             iframe_pipeline_id,
             None,
             None,
@@ -1887,17 +1877,16 @@ impl<'a> DisplayListFlattener<'a> {
 
         let prim = BrushPrimitive::new(
             BrushKind::Image {
                 request: ImageRequest {
                     key: image_key,
                     rendering: image_rendering,
                     tile: None,
                 },
-                current_epoch: Epoch::invalid(),
                 alpha_type,
                 stretch_size,
                 tile_spacing,
                 source: ImageSource::Default,
                 sub_rect,
                 visible_tiles: Vec::new(),
                 opacity_binding: OpacityBinding::new(),
             },
--- a/gfx/webrender/src/lib.rs
+++ b/gfx/webrender/src/lib.rs
@@ -84,17 +84,16 @@ mod gpu_glyph_renderer;
 mod gpu_types;
 mod hit_test;
 mod image;
 mod internal_types;
 mod picture;
 mod prim_store;
 mod print_tree;
 mod profiler;
-mod query;
 mod record;
 mod render_backend;
 mod render_task;
 mod renderer;
 mod resource_cache;
 mod scene;
 mod scene_builder;
 mod segment;
--- a/gfx/webrender/src/prim_store.rs
+++ b/gfx/webrender/src/prim_store.rs
@@ -1,14 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{AlphaType, BorderRadius, BoxShadowClipMode, BuiltDisplayList, ClipMode, ColorF};
-use api::{DeviceIntRect, DeviceIntSize, DevicePixelScale, Epoch, ExtendMode};
+use api::{DeviceIntRect, DeviceIntSize, DevicePixelScale, ExtendMode};
 use api::{FilterOp, GlyphInstance, GradientStop, ImageKey, ImageRendering, ItemRange, ItemTag, TileOffset};
 use api::{GlyphRasterSpace, LayoutPoint, LayoutRect, LayoutSize, LayoutToWorldTransform, LayoutVector2D};
 use api::{PipelineId, PremultipliedColorF, PropertyBinding, Shadow, YuvColorSpace, YuvFormat, DeviceIntSideOffsets};
 use api::{BorderWidths, LayoutToWorldScale, NormalBorder};
 use app_units::Au;
 use border::{BorderCacheKey, BorderRenderTaskInfo};
 use box_shadow::BLUR_SAMPLE_SCALE;
 use clip_scroll_tree::{ClipChainIndex, ClipScrollNodeIndex, CoordinateSystemId};
@@ -279,17 +279,16 @@ pub enum BrushKind {
         opacity_binding: OpacityBinding,
     },
     Clear,
     Picture {
         pic_index: PictureIndex,
     },
     Image {
         request: ImageRequest,
-        current_epoch: Epoch,
         alpha_type: AlphaType,
         stretch_size: LayoutSize,
         tile_spacing: LayoutSize,
         source: ImageSource,
         sub_rect: Option<DeviceIntRect>,
         opacity_binding: OpacityBinding,
         visible_tiles: Vec<VisibleImageTile>,
     },
@@ -1572,30 +1571,28 @@ impl PrimitiveStore {
                 let brush = &mut self.cpu_brushes[metadata.cpu_prim_index.0];
 
                 match brush.kind {
                     BrushKind::Image {
                         request,
                         sub_rect,
                         stretch_size,
                         ref mut tile_spacing,
-                        ref mut current_epoch,
                         ref mut source,
                         ref mut opacity_binding,
                         ref mut visible_tiles,
                         ..
                     } => {
                         let image_properties = frame_state
                             .resource_cache
                             .get_image_properties(request.key);
 
 
                         // Set if we need to request the source image from the cache this frame.
                         if let Some(image_properties) = image_properties {
-                            *current_epoch = image_properties.epoch;
                             is_tiled = image_properties.tiling.is_some();
 
                             // If the opacity changed, invalidate the GPU cache so that
                             // the new color for this primitive gets uploaded.
                             if opacity_binding.update(frame_context.scene_properties) {
                                 frame_state.gpu_cache.invalidate(&mut metadata.gpu_location);
                             }
 
@@ -2271,17 +2268,18 @@ impl PrimitiveStore {
                 return false;
             }
         };
 
         let mut combined_outer_rect =
             prim_screen_rect.intersection(&prim_run_context.clip_chain.combined_outer_screen_rect);
         let clip_chain = prim_run_context.clip_chain.nodes.clone();
         if cfg!(debug_assertions) && Some(prim_index) == self.chase_id {
-            println!("\tbase combined outer rect {:?}", combined_outer_rect);
+            println!("\tbase screen {:?}, combined clip chain {:?}",
+                prim_screen_rect, prim_run_context.clip_chain.combined_outer_screen_rect);
         }
 
         let prim_coordinate_system_id = prim_run_context.scroll_node.coordinate_system_id;
         let transform = &prim_run_context.scroll_node.world_content_transform;
         let extra_clip =  {
             let metadata = &self.cpu_metadata[prim_index.0];
             metadata.clip_sources.as_ref().map(|clip_sources| {
                 let prim_clips = frame_state.clip_store.get_mut(clip_sources);
@@ -2690,16 +2688,30 @@ impl PrimitiveStore {
                         .nodes[original_reference_frame_index.0]
                         .world_content_transform
                         .inverse()
                 })
                 .map(|inv_parent| {
                     inv_parent.pre_mul(&scroll_node.world_content_transform)
                 });
 
+            if run.is_chasing(self.chase_id) {
+                println!("\teffective clip chain from {:?} {}",
+                    scroll_node.coordinate_system_id,
+                    if pic_context.apply_local_clip_rect { "(applied)" } else { "" },
+                );
+                let iter = ClipChainNodeIter { current: clip_chain.nodes.clone() };
+                for node in iter {
+                    println!("\t\t{:?} {:?}",
+                        node.work_item.coordinate_system_id,
+                        node.local_clip_rect,
+                    );
+                }
+            }
+
             let clip_chain_rect = if pic_context.apply_local_clip_rect {
                 get_local_clip_rect_for_nodes(scroll_node, clip_chain)
             } else {
                 None
             };
 
             let local_clip_chain_rect = match clip_chain_rect {
                 Some(rect) if rect.is_empty() => continue,
--- a/gfx/webrender/src/profiler.rs
+++ b/gfx/webrender/src/profiler.rs
@@ -1,24 +1,24 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::ColorF;
-use query::{GpuTimer, NamedTag};
+use device::query::{GpuTimer, NamedTag};
 use std::collections::vec_deque::VecDeque;
 use std::f32;
 use time::precise_time_ns;
 
 cfg_if! {
     if #[cfg(feature = "debug_renderer")] {
         use api::ColorU;
         use debug_render::DebugRenderer;
         use euclid::{Point2D, Rect, Size2D, vec2};
-        use query::GpuSampler;
+        use device::query::GpuSampler;
         use internal_types::FastHashMap;
         use renderer::MAX_VERTEX_TEXTURE_WIDTH;
         use std::mem;
     }
 }
 
 cfg_if! {
     if #[cfg(feature = "debug_renderer")] {
--- a/gfx/webrender/src/renderer.rs
+++ b/gfx/webrender/src/renderer.rs
@@ -34,17 +34,17 @@ use gpu_cache::{GpuBlockData, GpuCacheUp
 use gpu_glyph_renderer::GpuGlyphRenderer;
 use internal_types::{SourceTexture, ORTHO_FAR_PLANE, ORTHO_NEAR_PLANE, ResourceCacheError};
 use internal_types::{CacheTextureId, DebugOutput, FastHashMap, RenderedDocument, ResultMsg};
 use internal_types::{TextureUpdateList, TextureUpdateOp, TextureUpdateSource};
 use internal_types::{RenderTargetInfo, SavedTargetIndex};
 use prim_store::DeferredResolve;
 use profiler::{BackendProfileCounters, FrameProfileCounters,
                GpuProfileTag, RendererProfileCounters, RendererProfileTimers};
-use query::GpuProfiler;
+use device::query::GpuProfiler;
 use rayon::{ThreadPool, ThreadPoolBuilder};
 use record::ApiRecordingReceiver;
 use render_backend::RenderBackend;
 use scene_builder::SceneBuilder;
 use shade::Shaders;
 use render_task::{RenderTask, RenderTaskKind, RenderTaskTree};
 use resource_cache::ResourceCache;
 
@@ -78,17 +78,17 @@ cfg_if! {
     }
 }
 
 cfg_if! {
     if #[cfg(feature = "debug_renderer")] {
         use api::ColorU;
         use debug_render::DebugRenderer;
         use profiler::{Profiler, ChangeIndicator};
-        use query::GpuTimer;
+        use device::query::GpuTimer;
     }
 }
 
 pub const MAX_VERTEX_TEXTURE_WIDTH: usize = 1024;
 /// Enabling this toggle would force the GPU cache scattered texture to
 /// be resized every frame, which enables GPU debuggers to see if this
 /// is performed correctly.
 const GPU_CACHE_RESIZE_TEST: bool = false;
--- a/gfx/webrender/src/resource_cache.rs
+++ b/gfx/webrender/src/resource_cache.rs
@@ -1,16 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{AddFont, BlobImageResources, ResourceUpdate};
 use api::{BlobImageDescriptor, BlobImageError, BlobImageRenderer, BlobImageRequest};
 use api::{ClearCache, ColorF, DevicePoint, DeviceUintPoint, DeviceUintRect, DeviceUintSize};
-use api::{Epoch, FontInstanceKey, FontKey, FontTemplate, GlyphIndex};
+use api::{FontInstanceKey, FontKey, FontTemplate, GlyphIndex};
 use api::{ExternalImageData, ExternalImageType};
 use api::{FontInstanceOptions, FontInstancePlatformOptions, FontVariation};
 use api::{GlyphDimensions, IdNamespace};
 use api::{ImageData, ImageDescriptor, ImageKey, ImageRendering};
 use api::{TileOffset, TileSize};
 use app_units::Au;
 #[cfg(feature = "capture")]
 use capture::ExternalCaptureImage;
@@ -27,17 +27,18 @@ use glyph_rasterizer::{FontInstance, Gly
 use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use gpu_types::UvRectKind;
 use internal_types::{FastHashMap, FastHashSet, SourceTexture, TextureUpdateList};
 use profiler::{ResourceProfileCounters, TextureCacheProfileCounters};
 use render_backend::FrameId;
 use render_task::{RenderTaskCache, RenderTaskCacheKey, RenderTaskId};
 use render_task::{RenderTaskCacheEntry, RenderTaskCacheEntryHandle, RenderTaskTree};
 use std::collections::hash_map::Entry::{self, Occupied, Vacant};
-use std::cmp;
+use std::collections::hash_map::ValuesMut;
+use std::{cmp, mem};
 use std::fmt::Debug;
 use std::hash::Hash;
 #[cfg(any(feature = "capture", feature = "replay"))]
 use std::path::PathBuf;
 use std::sync::{Arc, RwLock};
 use texture_cache::{TextureCache, TextureCacheHandle};
 use tiling::SpecialRenderPasses;
 
@@ -80,33 +81,30 @@ impl CacheItem {
 
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ImageProperties {
     pub descriptor: ImageDescriptor,
     pub external_image: Option<ExternalImageData>,
     pub tiling: Option<TileSize>,
-    pub epoch: Epoch,
 }
 
 #[derive(Debug, Copy, Clone, PartialEq)]
 enum State {
     Idle,
     AddResources,
     QueryResources,
 }
 
 #[derive(Debug)]
 struct ImageResource {
     data: ImageData,
     descriptor: ImageDescriptor,
-    epoch: Epoch,
     tiling: Option<TileSize>,
-    dirty_rect: Option<DeviceUintRect>,
 }
 
 #[derive(Clone, Debug)]
 pub struct ImageTiling {
     pub image_size: DeviceUintSize,
     pub tile_size: TileSize,
 }
 
@@ -132,17 +130,17 @@ impl ImageTemplates {
         self.images.get_mut(&key)
     }
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct CachedImageInfo {
     texture_cache_handle: TextureCacheHandle,
-    epoch: Epoch,
+    dirty_rect: Option<DeviceUintRect>,
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ResourceClassCache<K: Hash + Eq, V, U: Default> {
     resources: FastHashMap<K, V>,
     pub user_data: U,
 }
@@ -163,16 +161,34 @@ pub fn intersect_for_tile(
     )).map(|mut r| {
         // we can't translate by a negative size so do it manually
         r.origin.x -= tile_offset.x as u32 * tile_size as u32;
         r.origin.y -= tile_offset.y as u32 * tile_size as u32;
         r
     })
 }
 
+fn merge_dirty_rect(
+    prev_dirty_rect: &Option<DeviceUintRect>,
+    dirty_rect: &Option<DeviceUintRect>,
+    descriptor: &ImageDescriptor,
+) -> Option<DeviceUintRect> {
+    // It is important to never assume an empty dirty rect implies a full reupload here,
+    // although we are able to do so elsewhere. We store the descriptor's full rect instead
+    // There are update sequences which could cause us to forget the correct dirty regions
+    // regions if we cleared the dirty rect when we received None, e.g.:
+    //      1) Update with no dirty rect. We want to reupload everything.
+    //      2) Update with dirty rect B. We still want to reupload everything, not just B.
+    //      3) Perform the upload some time later.
+    match (dirty_rect, prev_dirty_rect) {
+        (&Some(ref rect), &Some(ref prev_rect)) => Some(rect.union(&prev_rect)),
+        (&Some(ref rect), &None) => Some(*rect),
+        (&None, _) => Some(descriptor.full_rect()),
+    }
+}
 
 impl<K, V, U> ResourceClassCache<K, V, U>
 where
     K: Clone + Hash + Eq + Debug,
     U: Default,
 {
     pub fn new() -> Self {
         ResourceClassCache {
@@ -185,25 +201,37 @@ where
         self.resources.get(key)
             .expect("Didn't find a cached resource with that ID!")
     }
 
     pub fn insert(&mut self, key: K, value: V) {
         self.resources.insert(key, value);
     }
 
+    pub fn remove(&mut self, key: &K) {
+        self.resources.remove(key);
+    }
+
     pub fn get_mut(&mut self, key: &K) -> &mut V {
         self.resources.get_mut(key)
             .expect("Didn't find a cached resource with that ID!")
     }
 
+    pub fn try_get_mut(&mut self, key: &K) -> Option<&mut V> {
+        self.resources.get_mut(key)
+    }
+
     pub fn entry(&mut self, key: K) -> Entry<K, V> {
         self.resources.entry(key)
     }
 
+    pub fn values_mut(&mut self) -> ValuesMut<K, V> {
+        self.resources.values_mut()
+    }
+
     pub fn clear(&mut self) {
         self.resources.clear();
     }
 
     fn clear_keys<F>(&mut self, key_fun: F)
     where
         for<'r> F: Fn(&'r &K) -> bool,
     {
@@ -220,16 +248,23 @@ where
     pub fn retain<F>(&mut self, f: F)
     where
         F: FnMut(&K, &mut V) -> bool,
     {
         self.resources.retain(f);
     }
 }
 
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct CachedImageKey {
+    pub rendering: ImageRendering,
+    pub tile: Option<TileOffset>,
+}
 
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ImageRequest {
     pub key: ImageKey,
     pub rendering: ImageRendering,
     pub tile: Option<TileOffset>,
@@ -238,35 +273,56 @@ pub struct ImageRequest {
 impl ImageRequest {
     pub fn with_tile(&self, offset: TileOffset) -> Self {
         ImageRequest {
             key: self.key,
             rendering: self.rendering,
             tile: Some(offset),
         }
     }
+
+    pub fn is_untiled_auto(&self) -> bool {
+        self.tile.is_none() && self.rendering == ImageRendering::Auto
+    }
 }
 
 impl Into<BlobImageRequest> for ImageRequest {
     fn into(self) -> BlobImageRequest {
         BlobImageRequest {
             key: self.key,
             tile: self.tile,
         }
     }
 }
 
+impl Into<CachedImageKey> for ImageRequest {
+    fn into(self) -> CachedImageKey {
+        CachedImageKey {
+            rendering: self.rendering,
+            tile: self.tile,
+        }
+    }
+}
+
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Clone, Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum ImageCacheError {
     OverLimitSize,
 }
 
-type ImageCache = ResourceClassCache<ImageRequest, Result<CachedImageInfo, ImageCacheError>, ()>;
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+enum ImageResult {
+    UntiledAuto(CachedImageInfo),
+    Multi(ResourceClassCache<CachedImageKey, CachedImageInfo, ()>),
+    Err(ImageCacheError),
+}
+
+type ImageCache = ResourceClassCache<ImageKey, ImageResult, ()>;
 pub type FontInstanceMap = Arc<RwLock<FastHashMap<FontInstanceKey, FontInstance>>>;
 
 #[derive(Default)]
 struct Resources {
     font_templates: FastHashMap<FontKey, FontTemplate>,
     font_instances: FontInstanceMap,
     image_templates: ImageTemplates,
 }
@@ -505,24 +561,21 @@ impl ResourceCache {
 
         if let ImageData::Blob(ref blob) = data {
             self.blob_image_renderer.as_mut().unwrap().add(
                 image_key,
                 Arc::clone(&blob),
                 tiling,
             );
         }
-        let dirty_rect = Some(descriptor.full_rect());
 
         let resource = ImageResource {
             descriptor,
             data,
-            epoch: Epoch(0),
             tiling,
-            dirty_rect,
         };
 
         self.resources.image_templates.insert(image_key, resource);
     }
 
     pub fn update_image_template(
         &mut self,
         image_key: ImageKey,
@@ -543,34 +596,41 @@ impl ResourceCache {
 
         if let ImageData::Blob(ref blob) = data {
             self.blob_image_renderer
                 .as_mut()
                 .unwrap()
                 .update(image_key, Arc::clone(blob), dirty_rect);
         }
 
+        // Each cache entry stores its own copy of the image's dirty rect. This allows them to be
+        // updated independently.
+        match self.cached_images.try_get_mut(&image_key) {
+            Some(&mut ImageResult::UntiledAuto(ref mut entry)) => {
+                entry.dirty_rect = merge_dirty_rect(&entry.dirty_rect, &dirty_rect, &descriptor);
+            }
+            Some(&mut ImageResult::Multi(ref mut entries)) => {
+                for entry in entries.values_mut() {
+                    entry.dirty_rect = merge_dirty_rect(&entry.dirty_rect, &dirty_rect, &descriptor);
+                }
+            }
+            _ => {}
+        }
+
         *image = ImageResource {
             descriptor,
             data,
-            epoch: Epoch(image.epoch.0 + 1),
             tiling,
-            dirty_rect: match (dirty_rect, image.dirty_rect) {
-                (Some(rect), Some(prev_rect)) => Some(rect.union(&prev_rect)),
-                (Some(rect), None) => Some(rect),
-                (None, _) => None,
-            },
         };
     }
 
     pub fn delete_image_template(&mut self, image_key: ImageKey) {
         let value = self.resources.image_templates.remove(image_key);
 
-        self.cached_images
-            .clear_keys(|request| request.key == image_key);
+        self.cached_images.remove(&image_key);
 
         match value {
             Some(image) => if image.data.is_blob() {
                 self.blob_image_renderer.as_mut().unwrap().delete(image_key);
             },
             None => {
                 warn!("Delete the non-exist key");
                 debug!("key={:?}", image_key);
@@ -601,51 +661,86 @@ impl ResourceCache {
 
         let side_size =
             template.tiling.map_or(cmp::max(template.descriptor.size.width, template.descriptor.size.height),
                                    |tile_size| tile_size as u32);
         if side_size > self.texture_cache.max_texture_size() {
             // The image or tiling size is too big for hardware texture size.
             warn!("Dropping image, image:(w:{},h:{}, tile:{}) is too big for hardware!",
                   template.descriptor.size.width, template.descriptor.size.height, template.tiling.unwrap_or(0));
-            self.cached_images.insert(request, Err(ImageCacheError::OverLimitSize));
+            self.cached_images.insert(request.key, ImageResult::Err(ImageCacheError::OverLimitSize));
             return;
         }
 
-        // If this image exists in the texture cache, *and* the epoch
-        // in the cache matches that of the template, then it is
-        // valid to use as-is.
-        let (entry, needs_update) = match self.cached_images.entry(request) {
-            Occupied(entry) => {
-                let info = entry.into_mut();
-                let needs_update = info.as_mut().unwrap().epoch != template.epoch;
-                info.as_mut().unwrap().epoch = template.epoch;
-                (info, needs_update)
+        let storage = match self.cached_images.entry(request.key) {
+            Occupied(e) => {
+                // We might have an existing untiled entry, and need to insert
+                // a second entry. In such cases we need to move the old entry
+                // out first, replacing it with a dummy entry, and then creating
+                // the tiled/multi-entry variant.
+                let entry = e.into_mut();
+                if !request.is_untiled_auto() {
+                    let untiled_entry = match entry {
+                        &mut ImageResult::UntiledAuto(ref mut entry) => {
+                            Some(mem::replace(entry, CachedImageInfo {
+                                texture_cache_handle: TextureCacheHandle::new(),
+                                dirty_rect: None,
+                            }))
+                        }
+                        _ => None
+                    };
+
+                    if let Some(untiled_entry) = untiled_entry {
+                        let mut entries = ResourceClassCache::new();
+                        let untiled_key = CachedImageKey {
+                            rendering: ImageRendering::Auto,
+                            tile: None,
+                        };
+                        entries.insert(untiled_key, untiled_entry);
+                        *entry = ImageResult::Multi(entries);
+                    }
+                }
+                entry
             }
-            Vacant(entry) => (
-                entry.insert(Ok(
-                    CachedImageInfo {
-                        epoch: template.epoch,
+            Vacant(entry) => {
+                entry.insert(if request.is_untiled_auto() {
+                    ImageResult::UntiledAuto(CachedImageInfo {
                         texture_cache_handle: TextureCacheHandle::new(),
-                    }
-                )),
-                true,
-            ),
+                        dirty_rect: Some(template.descriptor.full_rect()),
+                    })
+                } else {
+                    ImageResult::Multi(ResourceClassCache::new())
+                })
+            }
+        };
+
+        // If this image exists in the texture cache, *and* the dirty rect
+        // in the cache is empty, then it is valid to use as-is.
+        let entry = match *storage {
+            ImageResult::UntiledAuto(ref mut entry) => entry,
+            ImageResult::Multi(ref mut entries) => {
+                entries.entry(request.into())
+                    .or_insert(CachedImageInfo {
+                        texture_cache_handle: TextureCacheHandle::new(),
+                        dirty_rect: Some(template.descriptor.full_rect()),
+                    })
+            },
+            ImageResult::Err(_) => panic!("Errors should already have been handled"),
         };
 
         let needs_upload = self.texture_cache
-            .request(&entry.as_ref().unwrap().texture_cache_handle, gpu_cache);
+            .request(&entry.texture_cache_handle, gpu_cache);
 
         let dirty_rect = if needs_upload {
             // the texture cache entry has been evicted, treat it as all dirty
-            Some(template.descriptor.full_rect())
-        } else if needs_update {
-            template.dirty_rect
+            None
+        } else if entry.dirty_rect.is_none() {
+            return
         } else {
-            return
+            entry.dirty_rect
         };
 
         if !self.pending_image_requests.insert(request) {
             return
         }
 
         // If we are tiling, then we need to confirm the dirty rect intersects
         // the tile before leaving the request in the pending queue.
@@ -661,16 +756,17 @@ impl ResourceCache {
                         &template.descriptor,
                         tile_size,
                         tile_offset,
                     );
 
                     if let Some(dirty) = dirty_rect {
                         if intersect_for_tile(dirty, actual_size, tile_size, tile_offset).is_none() {
                             // don't bother requesting unchanged tiles
+                            entry.dirty_rect = None;
                             self.pending_image_requests.remove(&request);
                             return
                         }
                     }
 
                     let offset = DevicePoint::new(
                         tile_offset.x as f32 * tile_size as f32,
                         tile_offset.y as f32 * tile_size as f32,
@@ -844,21 +940,25 @@ impl ResourceCache {
     pub fn get_cached_image(
         &self,
         request: ImageRequest,
     ) -> Result<CacheItem, ()> {
         debug_assert_eq!(self.state, State::QueryResources);
 
         // TODO(Jerry): add a debug option to visualize the corresponding area for
         // the Err() case of CacheItem.
-        match *self.cached_images.get(&request) {
-            Ok(ref image_info) => {
+        match *self.cached_images.get(&request.key) {
+            ImageResult::UntiledAuto(ref image_info) => {
                 Ok(self.texture_cache.get(&image_info.texture_cache_handle))
             }
-            Err(_) => {
+            ImageResult::Multi(ref entries) => {
+                let image_info = entries.get(&request.into());
+                Ok(self.texture_cache.get(&image_info.texture_cache_handle))
+            }
+            ImageResult::Err(_) => {
                 Err(())
             }
         }
     }
 
     pub fn get_cached_render_task(
         &self,
         handle: &RenderTaskCacheEntryHandle,
@@ -883,17 +983,16 @@ impl ResourceCache {
                 // raw and blob image are all using resource_cache.
                 ImageData::Raw(..) | ImageData::Blob(..) => None,
             };
 
             ImageProperties {
                 descriptor: image_template.descriptor,
                 external_image,
                 tiling: image_template.tiling,
-                epoch: image_template.epoch,
             }
         })
     }
 
     pub fn begin_frame(&mut self, frame_id: FrameId) {
         debug_assert_eq!(self.state, State::Idle);
         self.state = State::AddResources;
         self.texture_cache.begin_frame(frame_id);
@@ -929,18 +1028,16 @@ impl ResourceCache {
             gpu_cache,
             &mut self.texture_cache,
             render_tasks,
         );
         self.texture_cache.end_frame(texture_cache_profile);
     }
 
     fn update_texture_cache(&mut self, gpu_cache: &mut GpuCache) {
-        let mut keys_to_clear_dirty_rect = FastHashSet::default();
-
         for request in self.pending_image_requests.drain() {
             let image_template = self.resources.image_templates.get_mut(request.key).unwrap();
             debug_assert!(image_template.data.uses_texture_cache());
 
             let image_data = match image_template.data {
                 ImageData::Raw(..) | ImageData::External(..) => {
                     // Safe to clone here since the Raw image data is an
                     // Arc, and the external image data is small.
@@ -969,30 +1066,32 @@ impl ResourceCache {
                         }
                         Err(BlobImageError::Other(msg)) => {
                             panic!("Vector image error {}", msg);
                         }
                     }
                 }
             };
 
-            let entry = self.cached_images.get_mut(&request).as_mut().unwrap();
+            let entry = match *self.cached_images.get_mut(&request.key) {
+                ImageResult::UntiledAuto(ref mut entry) => entry,
+                ImageResult::Multi(ref mut entries) => entries.get_mut(&request.into()),
+                ImageResult::Err(_) => panic!("Update requested for invalid entry")
+            };
             let mut descriptor = image_template.descriptor.clone();
             let local_dirty_rect;
 
             if let Some(tile) = request.tile {
                 let tile_size = image_template.tiling.unwrap();
                 let clipped_tile_size = compute_tile_size(&descriptor, tile_size, tile);
 
-                local_dirty_rect = if let Some(ref rect) = image_template.dirty_rect {
-                    keys_to_clear_dirty_rect.insert(request.key.clone());
-
+                local_dirty_rect = if let Some(rect) = entry.dirty_rect.take() {
                     // We should either have a dirty rect, or we are re-uploading where the dirty
                     // rect is ignored anyway.
-                    let intersection = intersect_for_tile(*rect, clipped_tile_size, tile_size, tile);
+                    let intersection = intersect_for_tile(rect, clipped_tile_size, tile_size, tile);
                     debug_assert!(intersection.is_some() ||
                                   self.texture_cache.needs_upload(&entry.texture_cache_handle));
                     intersection
                 } else {
                     None
                 };
 
                 // The tiled image could be stored on the CPU as one large image or be
@@ -1005,17 +1104,17 @@ impl ResourceCache {
                     descriptor.stride = Some(stride);
                     descriptor.offset +=
                         tile.y as u32 * tile_size as u32 * stride +
                         tile.x as u32 * tile_size as u32 * bpp;
                 }
 
                 descriptor.size = clipped_tile_size;
             } else {
-                local_dirty_rect = image_template.dirty_rect.take();
+                local_dirty_rect = entry.dirty_rect.take();
             }
 
             let filter = match request.rendering {
                 ImageRendering::Pixelated => {
                     TextureFilter::Nearest
                 }
                 ImageRendering::Auto | ImageRendering::CrispEdges => {
                     // If the texture uses linear filtering, enable mipmaps and
@@ -1047,21 +1146,16 @@ impl ResourceCache {
                 Some(image_data),
                 [0.0; 3],
                 local_dirty_rect,
                 gpu_cache,
                 None,
                 UvRectKind::Rect,
             );
         }
-
-        for key in keys_to_clear_dirty_rect.drain() {
-            let image_template = self.resources.image_templates.get_mut(key).unwrap();
-            image_template.dirty_rect.take();
-        }
     }
 
     pub fn end_frame(&mut self) {
         debug_assert_eq!(self.state, State::QueryResources);
         self.state = State::Idle;
     }
 
     pub fn clear(&mut self, what: ClearCache) {
@@ -1083,17 +1177,17 @@ impl ResourceCache {
     }
 
     pub fn clear_namespace(&mut self, namespace: IdNamespace) {
         self.resources
             .image_templates
             .images
             .retain(|key, _| key.0 != namespace);
         self.cached_images
-            .clear_keys(|request| request.key.0 == namespace);
+            .clear_keys(|key| key.0 == namespace);
 
         self.resources.font_instances
             .write()
             .unwrap()
             .retain(|key, _| key.0 != namespace);
         for &key in self.resources.font_templates.keys().filter(|key| key.0 == namespace) {
             self.glyph_rasterizer.delete_font(key);
         }
@@ -1146,17 +1240,16 @@ enum PlainFontTemplate {
 }
 
 #[cfg(any(feature = "capture", feature = "replay"))]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct PlainImageTemplate {
     data: String,
     descriptor: ImageDescriptor,
-    epoch: Epoch,
     tiling: Option<TileSize>,
 }
 
 #[cfg(any(feature = "capture", feature = "replay"))]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct PlainResources {
     font_templates: FastHashMap<FontKey, PlainFontTemplate>,
@@ -1345,17 +1438,16 @@ impl ResourceCache {
                 .map(|(key, template)| {
                     (*key, PlainImageTemplate {
                         data: match template.data {
                             ImageData::Raw(ref arc) => image_paths[&arc.as_ptr()].clone(),
                             _ => other_paths[key].clone(),
                         },
                         descriptor: template.descriptor.clone(),
                         tiling: template.tiling,
-                        epoch: template.epoch,
                     })
                 })
                 .collect(),
         };
 
         (resources, external_images)
     }
 
@@ -1470,16 +1562,14 @@ impl ResourceCache {
                     ImageData::Raw(arc)
                 }
             };
 
             res.image_templates.images.insert(key, ImageResource {
                 data,
                 descriptor: template.descriptor,
                 tiling: template.tiling,
-                epoch: template.epoch,
-                dirty_rect: None,
             });
         }
 
         external_images
     }
 }
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-34a498f7e46c385a189299e7369e204e1cb2060c
+0e9563688e575cd662570f54bc9d6f849040dbf8
--- a/gfx/wrench/src/blob.rs
+++ b/gfx/wrench/src/blob.rs
@@ -50,17 +50,17 @@ fn render_blob(
     // Generate a per-tile pattern to see it in the demo. For a real use case it would not
     // make sense for the rendered content to depend on its tile.
     let tile_checker = match tile {
         Some((_, tile)) => (tile.x % 2 == 0) != (tile.y % 2 == 0),
         None => true,
     };
 
     let mut dirty_rect = dirty_rect.unwrap_or(DeviceUintRect::new(
-        DeviceUintPoint::origin(),
+        descriptor.offset.to_u32(),
         descriptor.size,
     ));
 
     if let Some((tile_size, tile)) = tile {
         dirty_rect = intersect_for_tile(dirty_rect, size2(tile_size as u32, tile_size as u32),
                                         tile_size, tile)
             .expect("empty rects should be culled by webrender");
     }
--- a/js/public/SliceBudget.h
+++ b/js/public/SliceBudget.h
@@ -32,17 +32,22 @@ struct JS_PUBLIC_API(WorkBudget)
 /*
  * This class records how much work has been done in a given collection slice,
  * so that we can return before pausing for too long. Some slices are allowed
  * to run for unlimited time, and others are bounded. To reduce the number of
  * gettimeofday calls, we only check the time every 1000 operations.
  */
 class JS_PUBLIC_API(SliceBudget)
 {
-    static const mozilla::TimeStamp unlimitedDeadline;
+    const mozilla::TimeStamp &UnlimitedDeadline() const {
+        static const mozilla::TimeStamp unlimitedDeadline =
+            mozilla::TimeStamp::Now() + mozilla::TimeDuration::Forever();
+        return unlimitedDeadline;
+    }
+
     static const intptr_t unlimitedStartCounter = INTPTR_MAX;
 
     bool checkOverBudget();
 
     SliceBudget();
 
   public:
     // Memory of the originally requested budget. If isUnlimited, neither of
@@ -64,32 +69,32 @@ class JS_PUBLIC_API(SliceBudget)
 
     /* Instantiate as SliceBudget(TimeBudget(n)). */
     explicit SliceBudget(TimeBudget time);
 
     /* Instantiate as SliceBudget(WorkBudget(n)). */
     explicit SliceBudget(WorkBudget work);
 
     void makeUnlimited() {
-        deadline = unlimitedDeadline;
+        deadline = UnlimitedDeadline();
         counter = unlimitedStartCounter;
     }
 
     void step(intptr_t amt = 1) {
         counter -= amt;
     }
 
     bool isOverBudget() {
         if (counter > 0)
             return false;
         return checkOverBudget();
     }
 
     bool isWorkBudget() const { return deadline.IsNull(); }
     bool isTimeBudget() const { return !deadline.IsNull() && !isUnlimited(); }
-    bool isUnlimited() const { return deadline == unlimitedDeadline; }
+    bool isUnlimited() const { return deadline == UnlimitedDeadline(); }
 
     int describe(char* buffer, size_t maxlen) const;
 };
 
 } // namespace js
 
 #endif /* js_SliceBudget_h */
--- a/js/src/gc/GC.cpp
+++ b/js/src/gc/GC.cpp
@@ -300,17 +300,17 @@ namespace TuningDefaults {
 
     /* no parameter */
     static const size_t ZoneAllocDelayBytes = 1024 * 1024;
 
     /* JSGC_DYNAMIC_HEAP_GROWTH */
     static const bool DynamicHeapGrowthEnabled = false;
 
     /* JSGC_HIGH_FREQUENCY_TIME_LIMIT */
-    static const auto HighFrequencyThreshold = mozilla::TimeDuration::FromSeconds(1);
+    static const auto HighFrequencyThreshold = 1; // in seconds
 
     /* JSGC_HIGH_FREQUENCY_LOW_LIMIT */
     static const uint64_t HighFrequencyLowLimitBytes = 100 * 1024 * 1024;
 
     /* JSGC_HIGH_FREQUENCY_HIGH_LIMIT */
     static const uint64_t HighFrequencyHighLimitBytes = 500 * 1024 * 1024;
 
     /* JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MAX */
@@ -341,18 +341,16 @@ namespace TuningDefaults {
     static const bool CompactingEnabled = true;
 
     /* JSGC_NURSERY_FREE_THRESHOLD_FOR_IDLE_COLLECTION */
     static const uint32_t NurseryFreeThresholdForIdleCollection =
         Nursery::NurseryChunkUsableSize / 4;
 
 }}} // namespace js::gc::TuningDefaults
 
-static const auto ONE_SECOND = mozilla::TimeDuration::FromSeconds(1);
-
 /*
  * We start to incremental collection for a zone when a proportion of its
  * threshold is reached. This is configured by the
  * JSGC_ALLOCATION_THRESHOLD_FACTOR and
  * JSGC_ALLOCATION_THRESHOLD_FACTOR_AVOID_INTERRUPT parameters.
  */
 static const double MinAllocationThresholdFactor = 0.9;
 
@@ -1567,17 +1565,17 @@ GCSchedulingTunables::GCSchedulingTunabl
   : gcMaxBytes_(0),
     maxMallocBytes_(TuningDefaults::MaxMallocBytes),
     gcMaxNurseryBytes_(0),
     gcZoneAllocThresholdBase_(TuningDefaults::GCZoneAllocThresholdBase),
     allocThresholdFactor_(TuningDefaults::AllocThresholdFactor),
     allocThresholdFactorAvoidInterrupt_(TuningDefaults::AllocThresholdFactorAvoidInterrupt),
     zoneAllocDelayBytes_(TuningDefaults::ZoneAllocDelayBytes),
     dynamicHeapGrowthEnabled_(TuningDefaults::DynamicHeapGrowthEnabled),
-    highFrequencyThreshold_(TuningDefaults::HighFrequencyThreshold),
+    highFrequencyThreshold_(mozilla::TimeDuration::FromSeconds(TuningDefaults::HighFrequencyThreshold)),
     highFrequencyLowLimitBytes_(TuningDefaults::HighFrequencyLowLimitBytes),
     highFrequencyHighLimitBytes_(TuningDefaults::HighFrequencyHighLimitBytes),
     highFrequencyHeapGrowthMax_(TuningDefaults::HighFrequencyHeapGrowthMax),
     highFrequencyHeapGrowthMin_(TuningDefaults::HighFrequencyHeapGrowthMin),
     lowFrequencyHeapGrowth_(TuningDefaults::LowFrequencyHeapGrowth),
     dynamicMarkSliceEnabled_(TuningDefaults::DynamicMarkSliceEnabled),
     minEmptyChunkCount_(TuningDefaults::MinEmptyChunkCount),
     maxEmptyChunkCount_(TuningDefaults::MaxEmptyChunkCount),
@@ -1619,17 +1617,17 @@ GCSchedulingTunables::resetParameter(JSG
       case JSGC_MAX_BYTES:
         gcMaxBytes_ = 0xffffffff;
         break;
       case JSGC_MAX_NURSERY_BYTES:
         gcMaxNurseryBytes_ = JS::DefaultNurseryBytes;
         break;
       case JSGC_HIGH_FREQUENCY_TIME_LIMIT:
         highFrequencyThreshold_ =
-            TuningDefaults::HighFrequencyThreshold;
+            mozilla::TimeDuration::FromSeconds(TuningDefaults::HighFrequencyThreshold);
         break;
       case JSGC_HIGH_FREQUENCY_LOW_LIMIT:
         setHighFrequencyLowLimit(TuningDefaults::HighFrequencyLowLimitBytes);
         break;
       case JSGC_HIGH_FREQUENCY_HIGH_LIMIT:
         setHighFrequencyHighLimit(TuningDefaults::HighFrequencyHighLimitBytes);
         break;
       case JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MAX:
@@ -2120,27 +2118,31 @@ GCMarker::delayMarkingChildren(const voi
 
 bool
 GCRuntime::shouldCompact()
 {
     // Compact on shrinking GC if enabled.  Skip compacting in incremental GCs
     // if we are currently animating, unless the user is inactive or we're
     // responding to memory pressure.
 
+    static const auto oneSecond = mozilla::TimeDuration::FromSeconds(1);
+
     if (invocationKind != GC_SHRINK || !isCompactingGCEnabled())
         return false;
 
     if (initialReason == JS::gcreason::USER_INACTIVE ||
         initialReason == JS::gcreason::MEM_PRESSURE)
     {
         return true;
     }
 
     const auto &lastAnimationTime = rt->lastAnimationTime.ref();
-    return !isIncremental || lastAnimationTime.IsNull() || lastAnimationTime + ONE_SECOND < mozilla::TimeStamp::Now();
+    return !isIncremental
+        || lastAnimationTime.IsNull()
+        || lastAnimationTime + oneSecond < mozilla::TimeStamp::Now();
 }
 
 bool
 GCRuntime::isCompactingGCEnabled() const
 {
     return compactingEnabled && rt->mainContextFromOwnThread()->compactingDisabledCount == 0;
 }
 
@@ -3224,18 +3226,16 @@ void
 ArenaLists::queueForegroundThingsForSweep()
 {
     gcShapeArenasToUpdate = arenaListsToSweep(AllocKind::SHAPE);
     gcAccessorShapeArenasToUpdate = arenaListsToSweep(AllocKind::ACCESSOR_SHAPE);
     gcObjectGroupArenasToUpdate = arenaListsToSweep(AllocKind::OBJECT_GROUP);
     gcScriptArenasToUpdate = arenaListsToSweep(AllocKind::SCRIPT);
 }
 
-const mozilla::TimeStamp SliceBudget::unlimitedDeadline = mozilla::TimeStamp::Now() + mozilla::TimeDuration::Forever();
-
 SliceBudget::SliceBudget()
   : timeBudget(UnlimitedTimeBudget), workBudget(UnlimitedWorkBudget)
 {
     makeUnlimited();
 }
 
 SliceBudget::SliceBudget(TimeBudget time)
   : timeBudget(time), workBudget(UnlimitedWorkBudget)
@@ -4041,29 +4041,30 @@ GCRuntime::purgeRuntime()
     MOZ_ASSERT(unmarkGrayStack.empty());
     unmarkGrayStack.clearAndFree();
 }
 
 bool
 GCRuntime::shouldPreserveJITCode(Realm* realm, const mozilla::TimeStamp &currentTime,
                                  JS::gcreason::Reason reason, bool canAllocateMoreCode)
 {
+    static const auto oneSecond = mozilla::TimeDuration::FromSeconds(1);
 
     if (cleanUpEverything)
         return false;
     if (!canAllocateMoreCode)
         return false;
 
     if (alwaysPreserveCode)
         return true;
     if (realm->preserveJitCode())
         return true;
 
     const auto &lastAnimationTime = realm->lastAnimationTime.ref();
-    if (!lastAnimationTime.IsNull() && lastAnimationTime + ONE_SECOND >= currentTime)
+    if (!lastAnimationTime.IsNull() && lastAnimationTime + oneSecond >= currentTime)
         return true;
 
     if (reason == JS::gcreason::DEBUG_GC)
         return true;
 
     return false;
 }
 
--- a/layout/painting/FrameLayerBuilder.cpp
+++ b/layout/painting/FrameLayerBuilder.cpp
@@ -221,17 +221,16 @@ DisplayItemData::DisplayItemData(LayerMa
   : mRefCnt(0)
   , mParent(aParent)
   , mLayer(aLayer)
   , mDisplayItemKey(aKey)
   , mItem(nullptr)
   , mUsed(true)
   , mIsInvalid(false)
   , mReusedItem(false)
-  , mDisconnected(false)
 {
   MOZ_COUNT_CTOR(DisplayItemData);
 
   if (!sAliveDisplayItemDatas) {
     sAliveDisplayItemDatas = new nsTHashtable<nsPtrHashKey<DisplayItemData>>();
   }
   MOZ_RELEASE_ASSERT(!sAliveDisplayItemDatas->Contains(this));
   sAliveDisplayItemDatas->PutEntry(this);
@@ -264,17 +263,16 @@ DisplayItemData::RemoveFrame(nsIFrame* a
   SmallPointerArray<DisplayItemData>& array = aFrame->DisplayItemData();
   array.RemoveElement(this);
 }
 
 void
 DisplayItemData::EndUpdate()
 {
   MOZ_RELEASE_ASSERT(mLayer);
-  mItem = nullptr;
   mIsInvalid = false;
   mUsed = false;
   mReusedItem = false;
 }
 
 void
 DisplayItemData::EndUpdate(nsAutoPtr<nsDisplayItemGeometry> aGeometry)
 {
@@ -283,17 +281,16 @@ DisplayItemData::EndUpdate(nsAutoPtr<nsD
   MOZ_ASSERT(mGeometry || aGeometry);
 
   if (aGeometry) {
     mGeometry = aGeometry;
   }
   mClip = mItem->GetClip();
   mChangedFrameInvalidations.SetEmpty();
 
-  mItem = nullptr;
   EndUpdate();
 }
 
 void
 DisplayItemData::BeginUpdate(Layer* aLayer, LayerState aState,
                              bool aFirstUpdate,
                              nsDisplayItem* aItem /* = nullptr */)
 {
@@ -311,17 +308,21 @@ DisplayItemData::BeginUpdate(Layer* aLay
   MOZ_RELEASE_ASSERT(aLayer);
   mLayer = aLayer;
   mOptLayer = nullptr;
   mInactiveManager = nullptr;
   mLayerState = aState;
   mUsed = true;
 
   if (aLayer->AsPaintedLayer()) {
-    mItem = aItem;
+    if (aItem != mItem) {
+      aItem->SetDisplayItemData(this, aLayer->Manager());
+    } else {
+      MOZ_ASSERT(aItem->GetDisplayItemData() == this);
+    }
     mReusedItem = aIsReused;
   }
 
   if (!aItem) {
     return;
   }
 
   if (!aIsMerged && mFrameList.Length() == 1) {
@@ -354,53 +355,46 @@ DisplayItemData::BeginUpdate(Layer* aLay
                                   copy[i]->GetVisualOverflowRect());
   }
 }
 
 static const nsIFrame* sDestroyedFrame = nullptr;
 DisplayItemData::~DisplayItemData()
 {
   MOZ_COUNT_DTOR(DisplayItemData);
-  Disconnect();
+
+  if (mItem) {
+    MOZ_ASSERT(mItem->GetDisplayItemData() == this);
+    mItem->SetDisplayItemData(nullptr, nullptr);
+  }
+
+  for (uint32_t i = 0; i < mFrameList.Length(); i++) {
+    nsIFrame* frame = mFrameList[i];
+    if (frame == sDestroyedFrame) {
+      continue;
+    }
+
+    SmallPointerArray<DisplayItemData>& array = frame->DisplayItemData();
+    array.RemoveElement(this);
+  }
 
   MOZ_RELEASE_ASSERT(sAliveDisplayItemDatas);
   nsPtrHashKey<mozilla::DisplayItemData>* entry
     = sAliveDisplayItemDatas->GetEntry(this);
   MOZ_RELEASE_ASSERT(entry);
 
   sAliveDisplayItemDatas->RemoveEntry(entry);
 
   if (sAliveDisplayItemDatas->Count() == 0) {
     delete sAliveDisplayItemDatas;
     sAliveDisplayItemDatas = nullptr;
   }
 }
 
 void
-DisplayItemData::Disconnect()
-{
-  if (mDisconnected) {
-    return;
-  }
-  mDisconnected = true;
-
-  for (uint32_t i = 0; i < mFrameList.Length(); i++) {
-    nsIFrame* frame = mFrameList[i];
-    if (frame == sDestroyedFrame) {
-      continue;
-    }
-    SmallPointerArray<DisplayItemData>& array = frame->DisplayItemData();
-    array.RemoveElement(this);
-  }
-
-  mLayer = nullptr;
-  mOptLayer = nullptr;
-}
-
-void
 DisplayItemData::ClearAnimationCompositorState()
 {
   if (mDisplayItemKey != static_cast<uint32_t>(DisplayItemType::TYPE_TRANSFORM) &&
       mDisplayItemKey != static_cast<uint32_t>(DisplayItemType::TYPE_OPACITY)) {
     return;
   }
 
   for (nsIFrame* frame : mFrameList) {
@@ -436,20 +430,16 @@ public:
     , mParent(nullptr)
 #endif
     , mInvalidateAllLayers(false)
   {
     MOZ_COUNT_CTOR(LayerManagerData);
   }
   ~LayerManagerData() {
     MOZ_COUNT_DTOR(LayerManagerData);
-
-    for (auto& item : mDisplayItems) {
-      item->Disconnect();
-    }
   }
 
 #ifdef DEBUG_DISPLAY_ITEM_DATA
   void Dump(const char *aPrefix = "") {
     printf_stderr("%sLayerManagerData %p\n", aPrefix, this);
 
     for (auto& data : mDisplayItems) {
       nsAutoCString prefix;
@@ -2143,17 +2133,16 @@ FrameLayerBuilder::RemoveFrameFromLayerM
         nsRegion old = data->mGeometry->ComputeInvalidationRegion();
         nsIntRegion rgn = old.ScaleToOutsidePixels(paintedData->mXScale, paintedData->mYScale, paintedData->mAppUnitsPerDevPixel);
         rgn.MoveBy(-GetTranslationForPaintedLayer(t));
         paintedData->mRegionToInvalidate.Or(paintedData->mRegionToInvalidate, rgn);
         paintedData->mRegionToInvalidate.SimplifyOutward(8);
       }
     }
 
-    data->Disconnect();
     auto it = std::find(data->mParent->mDisplayItems.begin(),
                         data->mParent->mDisplayItems.end(),
                         data);
     MOZ_ASSERT(it != data->mParent->mDisplayItems.end());
     std::iter_swap(it, data->mParent->mDisplayItems.end() - 1);
     data->mParent->mDisplayItems.pop_back();
   }
 
@@ -2208,17 +2197,16 @@ FrameLayerBuilder::WillEndTransaction()
 #endif
         InvalidatePostTransformRegion(t,
                                       did->mGeometry->ComputeInvalidationRegion(),
                                       did->mClip,
                                       GetLastPaintOffset(t));
       }
 
       did->ClearAnimationCompositorState();
-      did->Disconnect();
 
       // Remove this item. Swapping it with the last element first is
       // quicker than erasing from the middle.
       if (iter != data->mDisplayItems.end() - 1) {
         std::iter_swap(iter, data->mDisplayItems.end() - 1);
         data->mDisplayItems.pop_back();
       } else {
         data->mDisplayItems.pop_back();
@@ -2264,33 +2252,38 @@ FrameLayerBuilder::HasRetainedDataFor(ns
   }
   if (RefPtr<WebRenderUserData> data = GetWebRenderUserData<WebRenderFallbackData>(aFrame, aDisplayItemKey)) {
     return true;
   }
   return false;
 }
 
 DisplayItemData*
-FrameLayerBuilder::GetOldLayerForFrame(nsIFrame* aFrame, uint32_t aDisplayItemKey, DisplayItemData* aOldData /* = nullptr */)
+FrameLayerBuilder::GetOldLayerForFrame(nsIFrame* aFrame,
+                                       uint32_t aDisplayItemKey,
+                                       DisplayItemData* aOldData, /* = nullptr */
+                                       LayerManager* aOldLayerManager /* = nullptr */)
 {
   // If we need to build a new layer tree, then just refuse to recycle
   // anything.
   if (!mRetainingManager || mInvalidateAllLayers)
     return nullptr;
 
+  MOZ_ASSERT(!aOldData || aOldLayerManager,
+             "You must provide aOldLayerManager to check aOldData's validity.");
+  MOZ_ASSERT_IF(aOldData, aOldLayerManager == aOldData->mLayer->Manager());
+
   DisplayItemData* data = aOldData;
-  if (!data || data->Disconnected() || data->mLayer->Manager() != mRetainingManager) {
+  if (!data || aOldLayerManager != mRetainingManager) {
     data = GetDisplayItemData(aFrame, aDisplayItemKey);
   }
+
   MOZ_ASSERT(data == GetDisplayItemData(aFrame, aDisplayItemKey));
 
-  if (data && data->mLayer->Manager() == mRetainingManager) {
-    return data;
-  }
-  return nullptr;
+  return data;
 }
 
 Layer*
 FrameLayerBuilder::GetOldLayerFor(nsDisplayItem* aItem,
                                   nsDisplayItemGeometry** aOldGeometry,
                                   DisplayItemClip** aOldClip)
 {
   uint32_t key = aItem->GetPerFrameKey();
@@ -3685,17 +3678,18 @@ PaintedLayerData::Accumulate(ContainerSt
   bool clipMatches = (oldClip == mItemClip) || (oldClip && *oldClip == *mItemClip);
 
   DisplayItemData* currentData =
     aItem->HasMergedFrames() ? nullptr : aItem->GetDisplayItemData();
 
   DisplayItemData* oldData =
     aState->mLayerBuilder->GetOldLayerForFrame(aItem->Frame(),
                                                aItem->GetPerFrameKey(),
-                                               currentData);
+                                               currentData,
+                                               aItem->GetDisplayItemDataLayerManager());
 
   mAssignedDisplayItems.emplace_back(
     aItem, aLayerState, oldData, aContentRect, aType, hasOpacity);
 
   if (aType == DisplayItemEntryType::PUSH_OPACITY) {
     aOpacityIndices.AppendElement(mAssignedDisplayItems.size() - 1);
   }
 
@@ -5063,26 +5057,25 @@ FrameLayerBuilder::AddPaintedDisplayItem
   if (layer->Manager() == mRetainingManager) {
     DisplayItemData *data = aItem.mDisplayItemData;
     if (data && !data->mUsed) {
       data->BeginUpdate(layer, aItem.mLayerState, aItem.mItem, aItem.mReused, aItem.mMerged);
     } else {
       if (data && data->mUsed) {
         // If the DID has already been used (by a previously merged frame,
         // which is not merged this paint) we must create a new DID for the item.
-        aItem.mItem->SetDisplayItemData(nullptr);
+        aItem.mItem->SetDisplayItemData(nullptr, nullptr);
       }
       data = StoreDataForFrame(aItem.mItem, layer, aItem.mLayerState, nullptr);
     }
     data->mInactiveManager = tempManager;
     // We optimized this PaintedLayer into a ColorLayer/ImageLayer. Store the optimized
     // layer here.
     if (aLayer != layer) {
       data->mOptLayer = aLayer;
-      data->mItem = nullptr;
     }
   }
 
   if (tempManager) {
     FLB_LOG_PAINTED_LAYER_DECISION(aLayerData, "Creating nested FLB for item %p\n", aItem.mItem);
     FrameLayerBuilder* layerBuilder = new FrameLayerBuilder();
     layerBuilder->Init(mDisplayListBuilder, tempManager, aLayerData, true,
                        &aItem.mItem->GetClip());
@@ -5187,20 +5180,16 @@ FrameLayerBuilder::StoreDataForFrame(nsD
   }
 
   LayerManagerData* lmd = static_cast<LayerManagerData*>
     (mRetainingManager->GetUserData(&gLayerManagerUserData));
 
   RefPtr<DisplayItemData> data =
     new (aItem->Frame()->PresContext()) DisplayItemData(lmd, aItem->GetPerFrameKey(), aLayer);
 
-  if (!data->HasMergedFrames()) {
-    aItem->SetDisplayItemData(data);
-  }
-
   data->BeginUpdate(aLayer, aState, true, aItem);
 
   lmd->mDisplayItems.push_back(data);
   return data;
 }
 
 void
 FrameLayerBuilder::StoreDataForFrame(nsIFrame* aFrame,
--- a/layout/painting/FrameLayerBuilder.h
+++ b/layout/painting/FrameLayerBuilder.h
@@ -74,16 +74,18 @@ public:
   friend class ContainerState;
 
   uint32_t GetDisplayItemKey() { return mDisplayItemKey; }
   layers::Layer* GetLayer() const { return mLayer; }
   nsDisplayItemGeometry* GetGeometry() const { return mGeometry.get(); }
   const DisplayItemClip& GetClip() const { return mClip; }
   void Invalidate() { mIsInvalid = true; }
   void ClearAnimationCompositorState();
+  void SetItem(nsDisplayItem* aItem) { mItem = aItem; }
+  nsDisplayItem* GetItem() const { return mItem; }
 
   bool HasMergedFrames() const { return mFrameList.Length() > 1; }
 
   static DisplayItemData* AssertDisplayItemData(DisplayItemData* aData);
 
   void* operator new(size_t sz, nsPresContext* aPresContext)
   {
     // Check the recycle list first.
@@ -110,20 +112,16 @@ public:
     NS_LOG_RELEASE(this, mRefCnt, "ComputedStyle");
     if (mRefCnt == 0) {
       Destroy();
       return 0;
     }
     return mRefCnt;
   }
 
-  void Disconnect();
-
-  bool Disconnected() { return mDisconnected; }
-
 private:
   DisplayItemData(LayerManagerData* aParent,
                   uint32_t aKey,
                   layers::Layer* aLayer,
                   nsIFrame* aFrame = nullptr);
 
   /**
     * Removes any references to this object from frames
@@ -199,17 +197,16 @@ private:
 
   /**
     * Used to track if data currently stored in mFramesWithLayers (from an existing
     * paint) has been updated in the current paint.
     */
   bool            mUsed;
   bool            mIsInvalid;
   bool            mReusedItem;
-  bool            mDisconnected;
 };
 
 class RefCountedRegion {
 private:
   ~RefCountedRegion() {}
 public:
   NS_INLINE_DECL_REFCOUNTING(RefCountedRegion)
 
@@ -608,17 +605,20 @@ public:
 
   /**
    * Given a frame and a display item key that uniquely identifies a
    * display item for the frame, find the layer that was last used to
    * render that display item. Returns null if there is no such layer.
    * This could be a dedicated layer for the display item, or a PaintedLayer
    * that renders many display items.
    */
-  DisplayItemData* GetOldLayerForFrame(nsIFrame* aFrame, uint32_t aDisplayItemKey, DisplayItemData* aOldData = nullptr);
+  DisplayItemData* GetOldLayerForFrame(nsIFrame* aFrame,
+                                       uint32_t aDisplayItemKey,
+                                       DisplayItemData* aOldData = nullptr,
+                                       LayerManager* aOldLayerManager = nullptr);
 
   /**
    * Stores DisplayItemData associated with aFrame, stores the data in
    * mNewDisplayItemData.
    */
   DisplayItemData* StoreDataForFrame(nsDisplayItem* aItem, Layer* aLayer,
                                      LayerState aState, DisplayItemData* aData);
   void StoreDataForFrame(nsIFrame* aFrame,
--- a/layout/painting/crashtests/crashtests.list
+++ b/layout/painting/crashtests/crashtests.list
@@ -3,14 +3,14 @@ skip-if(Android) load 1407470-1.html
 load 1413073-1.html
 load 1413073-2.html
 load 1405881-1.html
 load 1418177-1.html
 load 1418722-1.html
 load 1419917.html
 load 1425271-1.html
 load 1428906-1.html
-skip-if(webrender) load 1430589-1.html # bug 1421825 for webrender
+load 1430589-1.html
 load 1454105-1.html
 load 1455944-1.html
 load 1465305-1.html
 load 1468124-1.html
 load 1469472.html
--- a/layout/painting/nsDisplayList.h
+++ b/layout/painting/nsDisplayList.h
@@ -2005,20 +2005,22 @@ MakeDisplayItem(nsDisplayListBuilder* aB
 {
   T* item = new (aBuilder) T(aBuilder, std::forward<Args>(aArgs)...);
 
   const mozilla::SmallPointerArray<mozilla::DisplayItemData>& array =
     item->Frame()->DisplayItemData();
   for (uint32_t i = 0; i < array.Length(); i++) {
     mozilla::DisplayItemData* did = array.ElementAt(i);
     if (did->GetDisplayItemKey() == item->GetPerFrameKey()) {
-      if (!did->HasMergedFrames()) {
-        item->SetDisplayItemData(did);
+      if (did->GetLayer()->AsPaintedLayer()) {
+        if (!did->HasMergedFrames()) {
+          item->SetDisplayItemData(did, did->GetLayer()->Manager());
+        }
+        break;
       }
-      break;
     }
   }
 
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
   if (aBuilder->IsRetainingDisplayList() &&
       !aBuilder->IsInPageSequence() &&
       aBuilder->IsBuilding()) {
     AssertUniqueItem(item);
@@ -2090,16 +2092,17 @@ public:
 #endif
   {
     MOZ_COUNT_CTOR(nsDisplayItem);
   }
 
 protected:
   virtual ~nsDisplayItem() {
     MOZ_COUNT_DTOR(nsDisplayItem);
+    SetDisplayItemData(nullptr, nullptr);
     if (mFrame) {
       mFrame->RemoveDisplayItem(this);
     }
   }
 public:
 
   virtual void Destroy(nsDisplayListBuilder* aBuilder)
   {
@@ -2115,17 +2118,17 @@ public:
     mDisableSubpixelAA = false;
   }
 
   virtual void RemoveFrame(nsIFrame* aFrame)
   {
     if (mFrame && aFrame == mFrame) {
       MOZ_ASSERT(!mFrame->HasDisplayItem(this));
       mFrame = nullptr;
-      mDisplayItemData = nullptr;
+      SetDisplayItemData(nullptr, nullptr);
     }
   }
 
   /**
    * Downcasts this item to nsDisplayWrapList, if possible.
    */
   virtual const nsDisplayWrapList* AsDisplayWrapList() const { return nullptr; }
   virtual nsDisplayWrapList* AsDisplayWrapList() { return nullptr; }
@@ -2823,21 +2826,33 @@ public:
   {
     return nullptr;
   }
 
   virtual mozilla::Maybe<nsRect> GetClipWithRespectToASR(
       nsDisplayListBuilder* aBuilder,
       const ActiveScrolledRoot* aASR) const;
 
-  void SetDisplayItemData(mozilla::DisplayItemData* aDID) {
+  void SetDisplayItemData(mozilla::DisplayItemData* aDID, mozilla::layers::LayerManager* aLayerManager) {
+    if (mDisplayItemData) {
+      MOZ_ASSERT(!mDisplayItemData->GetItem() || mDisplayItemData->GetItem() == this);
+      mDisplayItemData->SetItem(nullptr);
+    }
+    if (aDID) {
+      if (aDID->GetItem()) {
+        aDID->GetItem()->SetDisplayItemData(nullptr, nullptr);
+      }
+      aDID->SetItem(this);
+    }
     mDisplayItemData = aDID;
+    mDisplayItemDataLayerManager = aLayerManager;
   }
 
   mozilla::DisplayItemData* GetDisplayItemData() { return mDisplayItemData; }
+  mozilla::layers::LayerManager* GetDisplayItemDataLayerManager() { return mDisplayItemDataLayerManager; }
 
   // Set the nsDisplayList that this item belongs to, and what
   // index it is within that list. Temporary state for merging
   // used by RetainedDisplayListBuilder.
   void SetOldListIndex(nsDisplayList* aList, OldListIndex aIndex, uint32_t aListKey, uint32_t aNestingDepth)
   {
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
     mOldListKey = aListKey;
@@ -2873,17 +2888,18 @@ protected:
   RefPtr<const DisplayItemClipChain> mClipChain;
   const DisplayItemClip* mClip;
   RefPtr<const ActiveScrolledRoot> mActiveScrolledRoot;
   // Result of FindReferenceFrameFor(mFrame), if mFrame is non-null
   const nsIFrame* mReferenceFrame;
   RefPtr<struct AnimatedGeometryRoot> mAnimatedGeometryRoot;
   // Result of ToReferenceFrame(mFrame), if mFrame is non-null
   nsPoint   mToReferenceFrame;
-  RefPtr<mozilla::DisplayItemData> mDisplayItemData;
+  mozilla::DisplayItemData* mDisplayItemData = nullptr;
+  mozilla::layers::LayerManager* mDisplayItemDataLayerManager = nullptr;
 
 private:
   // This is the rectangle that nsDisplayListBuilder was using as the visible
   // rect to decide which items to construct.
   nsRect    mBuildingRect;
 
   // nsDisplayList::ComputeVisibility sets this to the visible region
   // of the item by intersecting the visible region with the bounds
new file mode 100644
--- /dev/null
+++ b/layout/reftests/transform-3d/1467277-1.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+</head>
+<body>
+  <div style="transform: rotate3d(1e50, 0, 0, 45deg); width: 100px; height: 100px;">
+    Test Text
+  </div>
+</body>
+</html>
--- a/layout/reftests/transform-3d/reftest.list
+++ b/layout/reftests/transform-3d/reftest.list
@@ -27,16 +27,17 @@ fuzzy-if(winWidget,143,689) fuzzy-if(OSX
 fuzzy-if(winWidget,143,689) fuzzy-if(OSX,224,924) fuzzy-if(winWidget,154,644) random-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) == scale3d-all-separate.html scale3d-1-ref.html # subpixel AA
 == scale3d-xz.html scale3d-1-ref.html
 == translatez-1a.html translatez-1-ref.html
 != translatez-1b.html translatez-1-ref.html
 == translate3d-1a.html translate3d-1-ref.html
 fuzzy-if(skiaContent,1,4) == matrix3d-1a.html matrix3d-1-ref.html
 == matrix3d-2a.html matrix3d-2-ref.html
 == rotate3d-1a.html rotatex-1-ref.html
+== 1467277-1.html rotatex-1-ref.html
 fuzzy-if(webrender,0-1,0-6) == rotate3d-2a.html rotatey-1-ref.html
 != backface-visibility-1a.html about:blank
 == backface-visibility-1b.html about:blank
 == backface-visibility-1c.html about:blank
 fuzzy-if(winWidget&&!layersGPUAccelerated,1,251) == backface-visibility-2.html backface-visibility-2-ref.html
 == backface-visibility-3.html backface-visibility-3-ref.html
 == perspective-clipping-1.html perspective-clipping-1-ref.html
 == perspective-clipping-2.html perspective-clipping-2-ref.html
--- a/media/webrtc/signaling/gtest/sdp_unittests.cpp
+++ b/media/webrtc/signaling/gtest/sdp_unittests.cpp
@@ -3269,16 +3269,28 @@ TEST_P(NewSdpTest, CheckSctpmap) {
   // Need to know name of type
   CheckSctpmap("5000",
               "webrtc-datachannel",
               16,
               appsec.GetFormats()[0],
               sctpmap);
 }
 
+TEST_P(NewSdpTest, CheckMaxPtime) {
+  ParseSdp(kBasicAudioVideoOffer);
+  ASSERT_TRUE(!!mSdp) << "Parse failed: " << GetParseErrors();
+  ASSERT_EQ(3U, mSdp->GetMediaSectionCount())
+    << "Wrong number of media sections";
+
+  ASSERT_TRUE(mSdp->GetMediaSection(0)
+                          .GetAttributeList()
+                          .HasAttribute(SdpAttribute::kMaxptimeAttribute));
+  ASSERT_EQ(mSdp->GetMediaSection(0).GetAttributeList().GetMaxptime(), 20U);
+}
+
 const std::string kNewSctpportOfferDraft21 =
 "v=0" CRLF
 "o=Mozilla-SIPUA-35.0a1 27987 0 IN IP4 0.0.0.0" CRLF
 "s=SIP Call" CRLF
 "t=0 0" CRLF
 "a=ice-ufrag:8a39d2ae" CRLF
 "a=ice-pwd:601d53aba51a318351b3ecf5ee00048f" CRLF
 "a=fingerprint:sha-256 30:FF:8E:2B:AC:9D:ED:70:18:10:67:C8:AE:9E:68:F3:86:53:51:B0:AC:31:B7:BE:6D:CF:A4:2E:D3:6E:B4:28" CRLF
--- a/media/webrtc/signaling/src/sdp/RsdparsaSdpAttributeList.cpp
+++ b/media/webrtc/signaling/src/sdp/RsdparsaSdpAttributeList.cpp
@@ -475,20 +475,22 @@ RsdparsaSdpAttributeList::LoadAttribute(
         LoadSctpPort(attributeList);
         return ;
       case SdpAttribute::kExtmapAttribute:
         LoadExtmap(attributeList);
         return;
       case SdpAttribute::kSimulcastAttribute:
         LoadSimulcast(attributeList);
         return;
+      case SdpAttribute::kMaxptimeAttribute:
+        LoadMaxPtime(attributeList);
+        return;
 
       case SdpAttribute::kDtlsMessageAttribute:
       case SdpAttribute::kLabelAttribute:
-      case SdpAttribute::kMaxptimeAttribute:
       case SdpAttribute::kSsrcGroupAttribute:
       case SdpAttribute::kRtcpRsizeAttribute:
       case SdpAttribute::kCandidateAttribute:
       case SdpAttribute::kConnectionAttribute:
       case SdpAttribute::kIceMismatchAttribute:
         // TODO: Not implemented, or not applicable.
         // Sort this out in Bug 1437165.
         return;
@@ -1167,16 +1169,26 @@ RsdparsaSdpAttributeList::LoadExtmap(Rus
     std::string extensionAttributes;
     extensionAttributes = convertStringView(rustExtmap.extensionAttributes);
     extmaps->PushEntry((uint16_t) rustExtmap.id, direction,
                        directionSpecified, name, extensionAttributes);
   }
   SetAttribute(extmaps.release());
 }
 
+void
+RsdparsaSdpAttributeList::LoadMaxPtime(RustAttributeList* attributeList)
+{
+  uint64_t maxPtime = 0;
+  nsresult nr = sdp_get_maxptime(attributeList, &maxPtime);
+  if (NS_SUCCEEDED(nr)) {
+    SetAttribute(new SdpNumberAttribute(SdpAttribute::kMaxptimeAttribute,
+                                        maxPtime));
+  }
+}
 
 bool
 RsdparsaSdpAttributeList::IsAllowedHere(SdpAttribute::AttributeType type)
 {
   if (AtSessionLevel() && !SdpAttribute::IsAllowedAtSessionLevel(type)) {
     return false;
   }
 
--- a/media/webrtc/signaling/src/sdp/RsdparsaSdpAttributeList.h
+++ b/media/webrtc/signaling/src/sdp/RsdparsaSdpAttributeList.h
@@ -139,16 +139,17 @@ private:
   void LoadSctpPort(RustAttributeList* attributeList);
   void LoadSimulcast(RustAttributeList* attributeList);
   void LoadImageattr(RustAttributeList* attributeList);
   void LoadSctpmaps(RustAttributeList* attributeList);
   void LoadDirection(RustAttributeList* attributeList);
   void LoadRemoteCandidates(RustAttributeList* attributeList);
   void LoadRids(RustAttributeList* attributeList);
   void LoadExtmap(RustAttributeList* attributeList);
+  void LoadMaxPtime(RustAttributeList* attributeList);
 
   void WarnAboutMisplacedAttribute(SdpAttribute::AttributeType type,
                                    uint32_t lineNumber,
                                    SdpErrorHolder& errorHolder);
 
 
   SdpAttribute* mAttributes[kNumAttributeTypes];
 
--- a/media/webrtc/signaling/src/sdp/RsdparsaSdpInc.h
+++ b/media/webrtc/signaling/src/sdp/RsdparsaSdpInc.h
@@ -251,17 +251,17 @@ nsresult u32_vec_get(const U32Vec* vec, 
 size_t u16_vec_len(const U16Vec* vec);
 nsresult u16_vec_get(const U16Vec* vec, size_t index, uint16_t* ret);
 
 size_t u8_vec_len(const U8Vec* vec);
 nsresult u8_vec_get(const U8Vec* vec, size_t index, uint8_t* ret);
 
 void sdp_free_string(char* string);
 
-nsresult parse_sdp(const char* sdp, uint32_t length, bool fail_on_warning,
+nsresult parse_sdp(StringView sdp, bool fail_on_warning,
                    RustSdpSession** ret, RustSdpError** err);
 RustSdpSession* sdp_new_reference(RustSdpSession* aSess);
 void sdp_free_session(RustSdpSession* ret);
 size_t sdp_get_error_line_num(const RustSdpError* err);
 StringView sdp_get_error_message(const RustSdpError* err);
 void sdp_free_error(RustSdpError* err);
 
 RustSdpOrigin sdp_get_origin(const RustSdpSession* aSess);
@@ -330,16 +330,17 @@ void sdp_get_rtpmaps(const RustAttribute
 
 size_t sdp_get_fmtp_count(const RustAttributeList* aList);
 size_t sdp_get_fmtp(const RustAttributeList* aList, size_t listSize,
                     RustSdpAttributeFmtp* ret);
 
 int64_t sdp_get_ptime(const RustAttributeList* aList);
 int64_t sdp_get_max_msg_size(const RustAttributeList* aList);
 int64_t sdp_get_sctp_port(const RustAttributeList* aList);
+nsresult sdp_get_maxptime(const RustAttributeList* aList, uint64_t* aMaxPtime);
 
 RustSdpAttributeFlags sdp_get_attribute_flags(const RustAttributeList* aList);
 
 nsresult sdp_get_mid(const RustAttributeList* aList, StringView* ret);
 
 size_t sdp_get_msid_count(const RustAttributeList* aList);
 void sdp_get_msids(const RustAttributeList* aList, size_t listSize,
                    RustSdpAttributeMsid* ret);
--- a/media/webrtc/signaling/src/sdp/RsdparsaSdpParser.cpp
+++ b/media/webrtc/signaling/src/sdp/RsdparsaSdpParser.cpp
@@ -17,32 +17,25 @@
 
 namespace mozilla
 {
 
 UniquePtr<Sdp>
 RsdparsaSdpParser::Parse(const std::string &sdpText)
 {
   ClearParseErrors();
-  const char* rawString = sdpText.c_str();
   RustSdpSession* result;
   RustSdpError* err;
-  nsresult rv = parse_sdp(rawString, sdpText.length() + 1, false,
-                          &result, &err);
+  StringView sdpTextView{sdpText.c_str(), sdpText.length()};
+  nsresult rv = parse_sdp(sdpTextView, false, &result, &err);
   if (rv != NS_OK) {
-    // TODO: err should eventually never be null if rv != NS_OK
-    // see Bug 1433529
-    if (err != nullptr) {
-      size_t line = sdp_get_error_line_num(err);
-      std::string errMsg = convertStringView(sdp_get_error_message(err));
-      sdp_free_error(err);
-      AddParseError(line, errMsg);
-    } else {
-      AddParseError(0, "Unhandled Rsdparsa parsing error");
-    }
+    size_t line = sdp_get_error_line_num(err);
+    std::string errMsg = convertStringView(sdp_get_error_message(err));
+    sdp_free_error(err);
+    AddParseError(line, errMsg);
     return nullptr;
   }
 
   if(err) {
     size_t line = sdp_get_error_line_num(err);
     std::string warningMsg = convertStringView(sdp_get_error_message(err));
     sdp_free_error(err);
     AddParseWarnings(line, warningMsg);
--- a/media/webrtc/signaling/src/sdp/rsdparsa_capi/src/attribute.rs
+++ b/media/webrtc/signaling/src/sdp/rsdparsa_capi/src/attribute.rs
@@ -1,10 +1,10 @@
 use std::slice;
-use libc::{size_t, uint8_t, uint16_t, uint32_t, int64_t};
+use libc::{size_t, uint8_t, uint16_t, uint32_t, int64_t, uint64_t};
 
 use rsdparsa::SdpSession;
 use rsdparsa::attribute_type::*;
 use nserror::{nsresult, NS_OK, NS_ERROR_INVALID_ARG};
 
 use types::StringView;
 use network::RustIpAddr;
 
@@ -177,16 +177,26 @@ pub unsafe extern "C" fn sdp_get_iceopti
     let attr = get_attribute((*attributes).as_slice(), RustSdpAttributeType::IceOptions);
     if let Some(&SdpAttribute::IceOptions(ref options)) = attr {
         *ret = options;
         return NS_OK;
     }
     NS_ERROR_INVALID_ARG
 }
 
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_maxptime(attributes: *const Vec<SdpAttribute>, ret: *mut uint64_t) -> nsresult {
+    let attr = get_attribute((*attributes).as_slice(), RustSdpAttributeType::MaxPtime);
+    if let Some(&SdpAttribute::MaxPtime(ref max_ptime)) = attr {
+        *ret = *max_ptime;
+        return NS_OK;
+    }
+    NS_ERROR_INVALID_ARG
+}
+
 #[repr(C)]
 #[derive(Clone, Copy)]
 pub struct RustSdpAttributeFingerprint {
     hash_algorithm: StringView,
     fingerprint: StringView
 }
 
 impl<'a> From<&'a SdpAttributeFingerprint> for RustSdpAttributeFingerprint {
--- a/media/webrtc/signaling/src/sdp/rsdparsa_capi/src/lib.rs
+++ b/media/webrtc/signaling/src/sdp/rsdparsa_capi/src/lib.rs
@@ -1,15 +1,14 @@
 extern crate rsdparsa;
 extern crate libc;
 #[macro_use] extern crate log;
 extern crate nserror;
 
-use std::ffi::CStr;
-use std::{str, slice, ptr};
+use std::ptr;
 use std::os::raw::c_char;
 use std::error::Error;
 
 use libc::size_t;
 
 use std::rc::Rc;
 
 use nserror::{nsresult, NS_OK, NS_ERROR_INVALID_ARG};
@@ -23,44 +22,33 @@ pub mod network;
 pub mod attribute;
 pub mod media_section;
 
 pub use types::{StringView, NULL_STRING};
 use network::{RustSdpOrigin, origin_view_helper, RustSdpConnection,
               get_bandwidth};
 
 #[no_mangle]
-pub unsafe extern "C" fn parse_sdp(sdp: *const u8, length: u32,
+pub unsafe extern "C" fn parse_sdp(sdp: StringView,
                                    fail_on_warning: bool,
                                    session: *mut *const SdpSession,
                                    error: *mut *const SdpParserError) -> nsresult {
-    // Bug 1433529 tracks fixing the TODOs in this function.
-    // TODO: Do I need to add explicit lifetime here?
-    // https://gankro.github.io/blah/only-in-rust/#honorable-mention-variance
-    let sdp_slice: &[u8] = slice::from_raw_parts(sdp, length as usize);
-    let sdp_c_str = match CStr::from_bytes_with_nul(sdp_slice) {
+    let sdp_str = match sdp.into() {
         Ok(string) => string,
-        Err(_) => {
+        Err(boxed_error) => {
             *session = ptr::null();
-            *error = ptr::null(); // TODO: Give more useful return value here
-            debug!("Error converting string");
+            *error = Box::into_raw(Box::new(SdpParserError::Sequence {
+                message: (*boxed_error).description().to_string(),
+                line_number: 0,
+            }));
             return NS_ERROR_INVALID_ARG;
         }
     };
-    let sdp_buf: &[u8] = sdp_c_str.to_bytes();
-    let sdp_str_slice: &str = match str::from_utf8(sdp_buf) {
-        Ok(string) => string,
-        Err(_) => {
-            *session = ptr::null();
-            *error = ptr::null(); // TODO: Give more useful return value here
-            debug!("Error converting string to utf8");
-            return NS_ERROR_INVALID_ARG;
-        }
-    };
-    let parser_result = rsdparsa::parse_sdp(sdp_str_slice, fail_on_warning);
+
+    let parser_result = rsdparsa::parse_sdp(&sdp_str, fail_on_warning);
     match parser_result {
         Ok(parsed) => {
             *error = match parsed.warnings.len(){
                 0 => ptr::null(),
                 _ => Box::into_raw(Box::new(parsed.warnings[0].clone())),
             };
             *session = Rc::into_raw(Rc::new(parsed));
             NS_OK
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -140,16 +140,17 @@ android {
                 } else {
                     exclude 'org/mozilla/gecko/adjust/AdjustHelper.java'
                 }
 
                 if (mozconfig.substs.MOZ_ANDROID_MMA) {
                     exclude 'org/mozilla/gecko/mma/MmaStubImp.java'
                 } else {
                     exclude 'org/mozilla/gecko/mma/MmaLeanplumImp.java'
+                    exclude 'org/mozilla/gecko/mma/LeanplumVariables.java'
                 }
 
                 if (!mozconfig.substs.MOZ_ANDROID_GCM) {
                     exclude 'org/mozilla/gecko/gcm/**/*.java'
                     exclude 'org/mozilla/gecko/push/**/*.java'
                 }
             }
 
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -36,17 +36,16 @@ import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.StrictMode;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.StringRes;
-import android.support.annotation.UiThread;
 import android.support.design.widget.Snackbar;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.NotificationCompat;
 import android.support.v4.content.res.ResourcesCompat;
 import android.support.v4.view.MenuItemCompat;
 import android.text.TextUtils;
 import android.util.AttributeSet;
@@ -89,17 +88,17 @@ import org.mozilla.gecko.db.SuggestedSit
 import org.mozilla.gecko.delegates.BookmarkStateChangeDelegate;
 import org.mozilla.gecko.delegates.BrowserAppDelegate;
 import org.mozilla.gecko.delegates.OfflineTabStatusDelegate;
 import org.mozilla.gecko.delegates.ScreenshotDelegate;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.dlc.DownloadContentService;
 import org.mozilla.gecko.extensions.ExtensionPermissionsHelper;
-import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
+import org.mozilla.gecko.firstrun.OnboardingHelper;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
 import org.mozilla.gecko.home.BrowserSearch;
 import org.mozilla.gecko.home.HomeBanner;
 import org.mozilla.gecko.home.HomeConfig;
 import org.mozilla.gecko.home.HomeConfig.PanelType;
 import org.mozilla.gecko.home.HomeConfigPrefsBackend;
 import org.mozilla.gecko.home.HomeFragment;
@@ -157,16 +156,17 @@ import org.mozilla.gecko.util.ActivityUt
 import org.mozilla.gecko.util.ContextUtils;
 import org.mozilla.gecko.util.DrawableUtil;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.util.MenuUtils;
+import org.mozilla.gecko.util.NetworkUtils;
 import org.mozilla.gecko.util.PrefUtils;
 import org.mozilla.gecko.util.ShortcutUtils;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.WindowUtil;
 import org.mozilla.gecko.widget.ActionModePresenter;
 import org.mozilla.gecko.widget.AnchoredPopup;
 import org.mozilla.gecko.widget.AnimatedProgressBar;
@@ -181,17 +181,16 @@ import java.lang.reflect.Method;
 import java.net.URLEncoder;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
-import java.util.UUID;
 import java.util.regex.Pattern;
 
 import static org.mozilla.gecko.mma.MmaDelegate.NEW_TAB;
 
 public class BrowserApp extends GeckoApp
                         implements ActionModePresenter,
                                    AnchoredPopup.OnVisibilityChangeListener,
                                    BookmarkEditFragment.Callbacks,
@@ -199,17 +198,18 @@ public class BrowserApp extends GeckoApp
                                    BrowserSearch.OnSearchListener,
                                    DynamicToolbarAnimator.ToolbarChromeProxy,
                                    LayoutInflater.Factory,
                                    LightweightTheme.OnChangeListener,
                                    OnUrlOpenListener,
                                    OnUrlOpenInBackgroundListener,
                                    PropertyAnimator.PropertyAnimationListener,
                                    TabsPanel.TabsLayoutChangeListener,
-                                   View.OnKeyListener {
+                                   View.OnKeyListener,
+                                   OnboardingHelper.OnboardingListener {
     private static final String LOGTAG = "GeckoBrowserApp";
 
     private static final int TABS_ANIMATION_DURATION = 450;
 
     // Intent String extras used to specify custom Switchboard configurations.
     private static final String INTENT_KEY_SWITCHBOARD_SERVER = "switchboard-server";
 
     // TODO: Replace with kinto endpoint.
@@ -228,38 +228,30 @@ public class BrowserApp extends GeckoApp
     public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS = 3002;
     public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE = 3003;
     public static final int ACTIVITY_REQUEST_TRIPLE_READERVIEW = 4001;
     public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK = 4002;
     public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE = 4003;
 
     public static final String ACTION_VIEW_MULTIPLE = AppConstants.ANDROID_PACKAGE_NAME + ".action.VIEW_MULTIPLE";
 
-    @RobocopTarget
-    public static final String EXTRA_SKIP_STARTPANE = "skipstartpane";
     private static final String EOL_NOTIFIED = "eol_notified";
 
-    /**
-     * Be aware of {@link org.mozilla.gecko.fxa.EnvironmentUtils.GECKO_PREFS_FIRSTRUN_UUID}.
-     */
-    private static final String FIRSTRUN_UUID = "firstrun_uuid";
-
     private BrowserSearch mBrowserSearch;
     private View mBrowserSearchContainer;
 
     public ViewGroup mBrowserChrome;
     public ViewFlipper mActionBarFlipper;
     public ActionModeCompatView mActionBar;
     private VideoPlayer mVideoPlayer;
     private BrowserToolbar mBrowserToolbar;
     private View doorhangerOverlay;
     // We can't name the TabStrip class because it's not included on API 9.
     private TabStripInterface mTabStrip;
     private AnimatedProgressBar mProgressView;
-    private FirstrunAnimationContainer mFirstrunAnimationContainer;
     private HomeScreen mHomeScreen;
     private TabsPanel mTabsPanel;
 
     private boolean showSplashScreen = false;
     private SplashScreen splashScreen;
     /**
      * Container for the home screen implementation. This will be populated with any valid
      * home screen implementation (currently that is just the HomePager, but that will be extended
@@ -428,16 +420,17 @@ public class BrowserApp extends GeckoApp
             new PostUpdateHandler(),
             mTelemetryCorePingDelegate,
             new OfflineTabStatusDelegate(),
             new AdjustBrowserAppDelegate(mTelemetryCorePingDelegate)
     ));
 
     @NonNull
     private SearchEngineManager mSearchEngineManager; // Contains reference to Context - DO NOT LEAK!
+    private OnboardingHelper mOnboardingHelper;       // Contains reference to Context - DO NOT LEAK!
 
     private boolean mHasResumed;
 
     @Override
     public View onCreateView(final View parent, final String name, final Context context, final AttributeSet attrs) {
         final View view;
         if (BrowserToolbar.class.getName().equals(name)) {
             view = BrowserToolbar.create(context, attrs);
@@ -744,16 +737,17 @@ public class BrowserApp extends GeckoApp
         app.prepareLightweightTheme();
 
         super.onCreate(savedInstanceState);
 
         if (mIsAbortingAppLaunch) {
           return;
         }
 
+        mOnboardingHelper = new OnboardingHelper(this, safeStartingIntent);
         initSwitchboardAndMma(this, safeStartingIntent, isInAutomation);
         initTelemetryUploader(isInAutomation);
 
         mBrowserChrome = (ViewGroup) findViewById(R.id.browser_chrome);
         mActionBarFlipper = (ViewFlipper) findViewById(R.id.browser_actionbar);
         mActionBar = (ActionModeCompatView) findViewById(R.id.actionbar);
 
         mVideoPlayer = (VideoPlayer) findViewById(R.id.video_player);
@@ -1009,24 +1003,26 @@ public class BrowserApp extends GeckoApp
             return;
         } else if (!AppConstants.MOZ_SWITCHBOARD) {
             Log.d(LOGTAG, "Switchboard compile-time disabled");
             return;
         }
 
         final String serverExtra = intent.getStringExtra(INTENT_KEY_SWITCHBOARD_SERVER);
         final String serverUrl = TextUtils.isEmpty(serverExtra) ? SWITCHBOARD_SERVER : serverExtra;
-        new AsyncConfigLoader(context, serverUrl) {
+        final SwitchBoard.ConfigStatusListener configStatuslistener = mOnboardingHelper;
+        final MmaDelegate.MmaVariablesChangedListener variablesChangedListener = mOnboardingHelper;
+        new AsyncConfigLoader(context, serverUrl, configStatuslistener) {
             @Override
             protected Void doInBackground(Void... params) {
                 super.doInBackground(params);
-                SwitchBoard.loadConfig(context, serverUrl);
+                SwitchBoard.loadConfig(context, serverUrl, configStatuslistener);
                 if (GeckoPreferences.isMmaAvailableAndEnabled(context)) {
                     // Do LeanPlum start/init here
-                    MmaDelegate.init(BrowserApp.this);
+                    MmaDelegate.init(BrowserApp.this, variablesChangedListener);
                 }
                 return null;
             }
         }.execute();
     }
 
     private static void initTelemetryUploader(final boolean isInAutomation) {
         TelemetryUploadService.setDisabled(isInAutomation);
@@ -1084,116 +1080,16 @@ public class BrowserApp extends GeckoApp
                                 .putBoolean(EOL_NOTIFIED, true)
                                 .apply();
             }
         } finally {
             StrictMode.setThreadPolicy(savedPolicy);
         }
     }
 
-    /**
-     * Code to actually show the first run pager, separated
-     * for distribution purposes.
-     */
-    @UiThread
-    private void checkFirstrunInternal() {
-        showFirstrunPager();
-
-        if (HardwareUtils.isTablet()) {
-            mTabStrip.setOnTabChangedListener(new TabStripInterface.OnTabAddedOrRemovedListener() {
-                @Override
-                public void onTabChanged() {
-                    hideFirstrunPager(TelemetryContract.Method.BUTTON);
-                    mTabStrip.setOnTabChangedListener(null);
-                }
-            });
-        }
-    }
-
-    /**
-     * Check and show the firstrun pane if the browser has never been launched and
-     * is not opening an external link from another application.
-     *
-     * @param context Context of application; used to show firstrun pane if appropriate
-     * @param intent Intent that launched this activity
-     */
-    private void checkFirstrun(Context context, SafeIntent intent) {
-        if (getProfile().inGuestMode()) {
-            // We do not want to show any first run tour for guest profiles.
-            return;
-        }
-
-        if (intent.getBooleanExtra(EXTRA_SKIP_STARTPANE, false)) {
-            // Note that we don't set the pref, so subsequent launches can result
-            // in the firstrun pane being shown.
-            return;
-        }
-        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
-
-        try {
-            final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
-
-            if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED_OLD, true) &&
-                prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) {
-                showSplashScreen = false;
-                if (!Intent.ACTION_VIEW.equals(intent.getAction())) {
-                    // Check to see if a distribution has turned off the first run pager.
-                    final Distribution distribution = Distribution.getInstance(BrowserApp.this);
-                    if (!distribution.shouldWaitForSystemDistribution()) {
-                        checkFirstrunInternal();
-                    } else {
-                        distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
-                            @Override
-                            public void distributionNotFound() {
-                                ThreadUtils.postToUiThread(new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        checkFirstrunInternal();
-                                    }
-                                });
-                            }
-
-                            @Override
-                            public void distributionFound(final Distribution distribution) {
-                                // Check preference again in case distribution turned it off.
-                                if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) {
-                                    ThreadUtils.postToUiThread(new Runnable() {
-                                        @Override
-                                        public void run() {
-                                            checkFirstrunInternal();
-                                        }
-                                    });
-                                }
-                            }
-
-                            @Override
-                            public void distributionArrivedLate(final Distribution distribution) {
-                            }
-                        });
-                    }
-                }
-
-                prefs.edit()
-                        // Don't bother trying again to show the v1 minimal first run.
-                        .putBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, false)
-                        // Generate a unique identify for the current first run.
-                        // See Bug 1429735 for why we care to do this.
-                        .putString(FIRSTRUN_UUID, UUID.randomUUID().toString())
-                        .apply();
-
-                // We have no intention of stopping this session. The FIRSTRUN session
-                // ends when the browsing session/activity has ended. All events
-                // during firstrun will be tagged as FIRSTRUN.
-                Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN);
-            }
-        } finally {
-            StrictMode.setThreadPolicy(savedPolicy);
-        }
-    }
-
     private Class<?> getMediaPlayerManager() {
         if (AppConstants.MOZ_MEDIA_PLAYER) {
             try {
                 return Class.forName("org.mozilla.gecko.MediaPlayerManager");
             } catch (Exception ex) {
                 // Ignore failures
                 Log.e(LOGTAG, "No native casting support", ex);
             }
@@ -1241,17 +1137,17 @@ public class BrowserApp extends GeckoApp
     }
 
     @Override
     public void onAttachedToWindow() {
         final SafeIntent intent = new SafeIntent(getIntent());
 
         if (!IntentUtils.getIsInAutomationFromEnvironment(intent)) {
             // We can't show the first run experience until Gecko has finished initialization (bug 1077583).
-            checkFirstrun(this, intent);
+            mOnboardingHelper.checkFirstRun();
         }
     }
 
     @Override
     protected void processTabQueue() {
         if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized) {
             ThreadUtils.postToBackgroundThread(new Runnable() {
                 @Override
@@ -2655,19 +2551,20 @@ public class BrowserApp extends GeckoApp
         mBrowserToolbar.cancelEdit();
     }
 
     private boolean isHomePagerVisible() {
         return (mHomeScreen != null && mHomeScreen.isVisible()
                 && mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE);
     }
 
-    private boolean isFirstrunVisible() {
-        return (mFirstrunAnimationContainer != null && mFirstrunAnimationContainer.isVisible()
-                && mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE);
+    private SplashScreen getSplashScreen() {
+        final ViewGroup main = (ViewGroup) findViewById(R.id.gecko_layout);
+        final View splashLayout = LayoutInflater.from(this).inflate(R.layout.splash_screen, main);
+        return (SplashScreen) splashLayout.findViewById(R.id.splash_root);
     }
 
     /**
      * Enters editing mode with the current tab's URL. There might be no
      * tabs loaded by the time the user enters editing mode e.g. just after
      * the app starts. In this case, we simply fallback to an empty URL.
      */
     private void enterEditingMode() {
@@ -2910,18 +2807,21 @@ public class BrowserApp extends GeckoApp
             return;
         }
 
         // History will only store that we were visiting about:home, however the specific panel
         // isn't stored. (We are able to navigate directly to homepanels using an about:home?panel=...
         // URL, but the reverse doesn't apply: manually switching panels doesn't update the URL.)
         // Hence we need to restore the panel, in addition to panel state, here.
         if (isAboutHome(tab)) {
-            // For some reason(e.g. from SearchWidget) we are showing the splash schreen. We should hide it now.
-            if (splashScreen != null && splashScreen.getVisibility() == View.VISIBLE) {
+            // For some reason(e.g. from SearchWidget) we are showing the splash schreen.
+            // If we are not waiting for the onboarding screens we should hide it now.
+            if (!mOnboardingHelper.isPreparing() &&
+                    splashScreen != null &&
+                    splashScreen.getVisibility() == View.VISIBLE) {
                 // Below line will be run when LOCATION_CHANGE. Which means the page load is almost completed.
                 splashScreen.hide();
             }
 
             String panelId = AboutPages.getPanelIdFromAboutHomeUrl(tab.getURL());
             Bundle panelRestoreData = null;
             if (panelId == null) {
                 // No panel was specified in the URL. Try loading the most recent
@@ -2950,20 +2850,19 @@ public class BrowserApp extends GeckoApp
             }
             showSplashScreen = false;
         } else {
             // The tab going to load is not about page. It's a web page.
             // If showSplashScreen is true, it means the app is first launched. We want to show the SlashScreen
             // But if GeckoThread.isRunning, the will be 0 sec for web rendering.
             // In that case, we don't want to show the SlashScreen/
             if (showSplashScreen && !GeckoThread.isRunning()) {
-
-                final ViewGroup main = (ViewGroup) findViewById(R.id.gecko_layout);
-                final View splashLayout = LayoutInflater.from(this).inflate(R.layout.splash_screen, main);
-                splashScreen = (SplashScreen) splashLayout.findViewById(R.id.splash_root);
+                if (splashScreen == null) {
+                    splashScreen = getSplashScreen();
+                }
 
                 showSplashScreen = false;
             } else if (splashScreen != null) {
                 // Below line will be run when LOCATION_CHANGE. Which means the page load is almost completed.
                 splashScreen.hide();
             }
             hideHomePager();
         }
@@ -3018,36 +2917,16 @@ public class BrowserApp extends GeckoApp
                 for (final BrowserAppDelegate delegate : delegates) {
                     delegate.onActivityResult(this, requestCode, resultCode, data);
                 }
 
                 super.onActivityResult(requestCode, resultCode, data);
         }
     }
 
-    private void showFirstrunPager() {
-
-        if (mFirstrunAnimationContainer == null) {
-            final ViewStub firstrunPagerStub = (ViewStub) findViewById(R.id.firstrun_pager_stub);
-            mFirstrunAnimationContainer = (FirstrunAnimationContainer) firstrunPagerStub.inflate();
-            mFirstrunAnimationContainer.load(getApplicationContext(), getSupportFragmentManager());
-            mFirstrunAnimationContainer.registerOnFinishListener(new FirstrunAnimationContainer.OnFinishListener() {
-                @Override
-                public void onFinish() {
-                    if (mFirstrunAnimationContainer.showBrowserHint() &&
-                        !Tabs.hasHomepage(BrowserApp.this)) {
-                        enterEditingMode();
-                    }
-                }
-            });
-        }
-
-        mHomeScreenContainer.setVisibility(View.VISIBLE);
-    }
-
     private void showHomePager(String panelId, Bundle panelRestoreData) {
         showHomePagerWithAnimator(panelId, panelRestoreData, null);
     }
 
     private void showHomePagerWithAnimator(String panelId, Bundle panelRestoreData, PropertyAnimator animator) {
         if (isHomePagerVisible()) {
             // Home pager already visible, make sure it shows the correct panel.
             mHomeScreen.showPanel(panelId, panelRestoreData);
@@ -3169,25 +3048,22 @@ public class BrowserApp extends GeckoApp
     }
 
     /**
      * Hide the Onboarding pager on user action, and don't show any onFinish hints.
      * @param method TelemetryContract method by which action was taken
      * @return boolean of whether pager was visible
      */
     private boolean hideFirstrunPager(TelemetryContract.Method method) {
-        if (!isFirstrunVisible()) {
+        if (!mOnboardingHelper.hideOnboarding()) {
             return false;
         }
 
         Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, method, "firstrun-pane");
 
-        // Don't show any onFinish actions when hiding from this Activity.
-        mFirstrunAnimationContainer.registerOnFinishListener(null);
-        mFirstrunAnimationContainer.hide();
         return true;
     }
 
     /**
      * Hides the HomePager, using the url of the currently selected tab as the url to be
      * loaded.
      */
     private void hideHomePager() {
@@ -4629,9 +4505,40 @@ public class BrowserApp extends GeckoApp
     public void onLightweightThemeReset() {
         refreshStatusBarColor();
     }
 
     private void refreshStatusBarColor() {
         final boolean isPrivate = mBrowserToolbar.isPrivateMode();
         WindowUtil.setStatusBarColor(BrowserApp.this, isPrivate);
     }
+
+    @Override
+    public void onOnboardingProcessStarted() {
+        if (splashScreen == null) {
+            splashScreen = getSplashScreen();
+        }
+
+        splashScreen.show(OnboardingHelper.DELAY_SHOW_DEFAULT_ONBOARDING);
+    }
+
+    @Override
+    public void onOnboardingScreensVisible() {
+        mHomeScreenContainer.setVisibility(View.VISIBLE);
+
+        if (HardwareUtils.isTablet()) {
+            mTabStrip.setOnTabChangedListener(new BrowserApp.TabStripInterface.OnTabAddedOrRemovedListener() {
+                @Override
+                public void onTabChanged() {
+                    hideFirstrunPager(TelemetryContract.Method.BUTTON);
+                    mTabStrip.setOnTabChangedListener(null);
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onFinishedOnboarding(final boolean showBrowserHint) {
+        if (showBrowserHint && !Tabs.hasHomepage(this)) {
+            enterEditingMode();
+        }
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -141,17 +141,17 @@ public abstract class GeckoApp extends G
     public static final String PREFS_CLEANUP_TEMP_FILES    = "cleanupTempFiles";
 
     /**
      * Used with SharedPreferences, per profile, to determine if this is the first run of
      * the application. When accessing SharedPreferences, the default value of true should be used.
 
      * Originally, this was only used for the telemetry core ping logic. To avoid
      * having to write custom migration logic, we just keep the original pref key.
-     * Be aware of {@link org.mozilla.gecko.fxa.EnvironmentUtils.GECKO_PREFS_IS_FIRST_RUN}.
+     * Be aware of {@link org.mozilla.gecko.fxa.EnvironmentUtils#GECKO_PREFS_IS_FIRST_RUN}.
      */
     public static final String PREFS_IS_FIRST_RUN = "telemetry-isFirstRun";
 
     public static final String SAVED_STATE_IN_BACKGROUND   = "inBackground";
     public static final String SAVED_STATE_PRIVATE_SESSION = "privateSession";
 
     // Delay before running one-time "cleanup" tasks that may be needed
     // after a version upgrade.
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstRunPanelConfigProviderStrategy.java
@@ -0,0 +1,13 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.firstrun;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+public interface FirstRunPanelConfigProviderStrategy {
+    PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE panelConfigType, final boolean useLocalValues);
+}
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java
@@ -10,56 +10,59 @@ import android.support.v4.app.FragmentMa
 import android.util.AttributeSet;
 
 import android.view.View;
 import android.widget.LinearLayout;
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.Experiments;
 import org.mozilla.gecko.mma.MmaDelegate;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 
 /**
  * A container for the pager and the entire first run experience.
  * This is used for animation purposes.
  */
 public class FirstrunAnimationContainer extends LinearLayout {
     // See bug 1330714. Need NON_PREF_PREFIX to set from distribution.
     public static final String PREF_FIRSTRUN_ENABLED_OLD = "startpane_enabled";
     // After 57, the pref name will be changed. Thus all user since 57 will check this new pref.
     public static final String PREF_FIRSTRUN_ENABLED = GeckoPreferences.NON_PREF_PREFIX + "startpane_enabled_after_57";
 
-    public static interface OnFinishListener {
-        public void onFinish();
+    public interface OnFinishListener {
+        void onFinish();
     }
 
     private FirstrunPager pager;
     private boolean visible;
     private OnFinishListener onFinishListener;
 
     public FirstrunAnimationContainer(Context context) {
         this(context, null);
     }
     public FirstrunAnimationContainer(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
-    public void load(Context appContext, FragmentManager fm) {
+    public void load(Context appContext, FragmentManager fm, final boolean useLocalValues) {
         visible = true;
-        pager = (FirstrunPager) findViewById(R.id.firstrun_pager);
-        pager.load(appContext, fm, new OnFinishListener() {
+        pager = findViewById(R.id.firstrun_pager);
+        pager.load(appContext, fm, useLocalValues, new OnFinishListener() {
             @Override
             public void onFinish() {
                 hide();
             }
         });
+
+        if (useLocalValues) {
+            MmaDelegate.track(MmaDelegate.ONBOARDING_DEFAULT_VALUES);
+        } else {
+            MmaDelegate.track(MmaDelegate.ONBOARDING_REMOTE_VALUES);
+        }
     }
 
     public boolean isVisible() {
         return visible;
     }
 
     public void hide() {
 
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java
@@ -58,25 +58,26 @@ public class FirstrunPager extends RtlVi
                     setCurrentItem(index, true);
                 }
             });
         }
 
         super.addView(child, index, params);
     }
 
-    public void load(Context appContext, FragmentManager fm, final FirstrunAnimationContainer.OnFinishListener onFinishListener) {
+    public void load(Context appContext, FragmentManager fm, final boolean useLocalValues,
+                     final FirstrunAnimationContainer.OnFinishListener onFinishListener) {
         final List<FirstrunPagerConfig.FirstrunPanelConfig> panels;
 
-        if (Restrictions.isRestrictedProfile(context)) {
-            panels = FirstrunPagerConfig.getRestricted();
-        } else if (FirefoxAccounts.firefoxAccountsExist(context)) {
-            panels = FirstrunPagerConfig.forFxAUser(appContext);
+        if (Restrictions.isRestrictedProfile(appContext)) {
+            panels = FirstrunPagerConfig.getRestricted(appContext);
+        } else if (FirefoxAccounts.firefoxAccountsExist(appContext)) {
+            panels = FirstrunPagerConfig.forFxAUser(appContext, useLocalValues);
         } else {
-            panels = FirstrunPagerConfig.getDefault(appContext);
+            panels = FirstrunPagerConfig.getDefault(appContext, useLocalValues);
         }
 
         setAdapter(new ViewPagerAdapter(fm, panels));
         this.pagerNavigation = new FirstrunPanel.PagerNavigation() {
             @Override
             public void next() {
                 final int currentPage = FirstrunPager.this.getCurrentItem();
                 if (currentPage < FirstrunPager.this.getAdapter().getCount() - 1) {
@@ -139,17 +140,17 @@ public class FirstrunPager extends RtlVi
         private final List<FirstrunPagerConfig.FirstrunPanelConfig> panels;
         private final Fragment[] fragments;
 
         public ViewPagerAdapter(FragmentManager fm, List<FirstrunPagerConfig.FirstrunPanelConfig> panels) {
             super(fm);
             this.panels = panels;
             this.fragments = new Fragment[panels.size()];
             for (FirstrunPagerConfig.FirstrunPanelConfig panel : panels) {
-                mDecor.onAddPagerView(context.getString(panel.getTitleRes()));
+                mDecor.onAddPagerView(panel.getTitle());
             }
 
             if (panels.size() > 0) {
                 mDecor.onPageSelected(0);
             }
         }
 
         @Override
@@ -167,12 +168,12 @@ public class FirstrunPager extends RtlVi
         @Override
         public int getCount() {
             return panels.size();
         }
 
         @Override
         public CharSequence getPageTitle(int i) {
             // Unused now that we use TabMenuStrip.
-            return context.getString(panels.get(i).getTitleRes()).toUpperCase();
+            return panels.get(i).getTitle().toUpperCase();
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java
@@ -1,100 +1,99 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.firstrun;
 
 import android.content.Context;
+import android.graphics.Bitmap;
 import android.os.Bundle;
-import android.util.Log;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.Experiments;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.mma.MmaDelegate;
 
 import java.util.LinkedList;
 import java.util.List;
 
-public class FirstrunPagerConfig {
-    public static final String LOGTAG = "FirstrunPagerConfig";
+class FirstrunPagerConfig {
+    static final String LOGTAG = "FirstrunPagerConfig";
 
-    public static final String KEY_IMAGE = "imageRes";
-    public static final String KEY_TEXT = "textRes";
-    public static final String KEY_SUBTEXT = "subtextRes";
+    static final String KEY_IMAGE = "panelImage";
+    static final String KEY_MESSAGE = "panelMessage";
+    static final String KEY_SUBTEXT = "panelDescription";
 
-   public static List<FirstrunPanelConfig> getDefault(Context context) {
+    static List<FirstrunPanelConfig> getDefault(Context context, final boolean useLocalValues) {
         final List<FirstrunPanelConfig> panels = new LinkedList<>();
-       panels.add(SimplePanelConfigs.welcomePanelConfig);
-       panels.add(SimplePanelConfigs.privatePanelConfig);
-       panels.add(SimplePanelConfigs.customizePanelConfig);
-       panels.add(SimplePanelConfigs.syncPanelConfig);
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.WELCOME, useLocalValues));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.PRIVACY, useLocalValues));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.CUSTOMIZE, useLocalValues));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.SYNC, useLocalValues));
 
         return panels;
     }
 
-    public static List<FirstrunPanelConfig> forFxAUser(Context context) {
+    static List<FirstrunPanelConfig> forFxAUser(Context context, final boolean useLocalValues) {
         final List<FirstrunPanelConfig> panels = new LinkedList<>();
-        panels.add(SimplePanelConfigs.welcomePanelConfig);
-        panels.add(SimplePanelConfigs.privatePanelConfig);
-        panels.add(SimplePanelConfigs.customizeLastPanelConfig);
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.WELCOME, useLocalValues));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.PRIVACY, useLocalValues));
+        panels.add(FirstrunPanelConfig.getConfiguredPanel(context, PanelConfig.TYPE.LAST_CUSTOMIZE, useLocalValues));
 
         return panels;
     }
 
-    public static List<FirstrunPanelConfig> getRestricted() {
+    static List<FirstrunPanelConfig> getRestricted(Context context) {
         final List<FirstrunPanelConfig> panels = new LinkedList<>();
-        panels.add(new FirstrunPanelConfig(RestrictedWelcomePanel.class.getName(), RestrictedWelcomePanel.TITLE_RES));
+        panels.add(new FirstrunPanelConfig(RestrictedWelcomePanel.class.getName(),
+                context.getString(RestrictedWelcomePanel.TITLE_RES)));
         return panels;
     }
 
-    public static class FirstrunPanelConfig {
-
+    static class FirstrunPanelConfig {
         private String classname;
-        private int titleRes;
+        private String title;
         private Bundle args;
 
-        public FirstrunPanelConfig(String resource, int titleRes) {
-            this(resource, titleRes, -1, -1, -1, true);
+        FirstrunPanelConfig(String resource, String title) {
+            this(resource, title, null, null, null, true);
         }
 
-        public FirstrunPanelConfig(String classname, int titleRes, int imageRes, int textRes, int subtextRes) {
-            this(classname, titleRes, imageRes, textRes, subtextRes, false);
-        }
-
-        private FirstrunPanelConfig(String classname, int titleRes, int imageRes, int textRes, int subtextRes, boolean isCustom) {
+        private FirstrunPanelConfig(String classname, String title, Bitmap image, String message,
+                                    String subtext, boolean isCustom) {
             this.classname = classname;
-            this.titleRes = titleRes;
+            this.title = title;
 
             if (!isCustom) {
-                this.args = new Bundle();
-                this.args.putInt(KEY_IMAGE, imageRes);
-                this.args.putInt(KEY_TEXT, textRes);
-                this.args.putInt(KEY_SUBTEXT, subtextRes);
+                args = new Bundle();
+                args.putParcelable(KEY_IMAGE, image);
+                args.putString(KEY_MESSAGE, message);
+                args.putString(KEY_SUBTEXT, subtext);
             }
         }
 
-        public String getClassname() {
-            return this.classname;
+        static FirstrunPanelConfig getConfiguredPanel(@NonNull Context context,
+                                                      PanelConfig.TYPE wantedPanelConfig,
+                                                      final boolean useLocalValues) {
+            PanelConfig panelConfig;
+            if (useLocalValues) {
+                panelConfig = new LocalFirstRunPanelProvider().getPanelConfig(context, wantedPanelConfig, useLocalValues);
+            } else {
+                panelConfig = new RemoteFirstRunPanelConfig().getPanelConfig(context, wantedPanelConfig, useLocalValues);
+            }
+            return new FirstrunPanelConfig(panelConfig.getClassName(), panelConfig.getTitle(),
+                    panelConfig.getImage(), panelConfig.getMessage(), panelConfig.getText(), false);
         }
 
-        public int getTitleRes() {
-            return this.titleRes;
+
+        String getClassname() {
+            return classname;
         }
 
-        public Bundle getArgs() {
+        String getTitle() {
+            return title;
+        }
+
+        Bundle getArgs() {
             return args;
         }
     }
-
-    private static class SimplePanelConfigs {
-        public static final FirstrunPanelConfig welcomePanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_panel_title_welcome, R.drawable.firstrun_welcome, R.string.firstrun_urlbar_message, R.string.firstrun_urlbar_subtext);
-        public static final FirstrunPanelConfig privatePanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_panel_title_privacy, R.drawable.firstrun_private, R.string.firstrun_privacy_message, R.string.firstrun_privacy_subtext);
-        public static final FirstrunPanelConfig customizePanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_panel_title_customize, R.drawable.firstrun_data, R.string.firstrun_customize_message, R.string.firstrun_customize_subtext);
-        public static final FirstrunPanelConfig customizeLastPanelConfig = new FirstrunPanelConfig(LastPanel.class.getName(), R.string.firstrun_panel_title_customize, R.drawable.firstrun_data, R.string.firstrun_customize_message, R.string.firstrun_customize_subtext);
-
-        public static final FirstrunPanelConfig syncPanelConfig = new FirstrunPanelConfig(SyncPanel.class.getName(), R.string.firstrun_sync_title, R.drawable.firstrun_sync, R.string.firstrun_sync_message, R.string.firstrun_sync_subtext);
-
-    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java
@@ -1,49 +1,50 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.firstrun;
 
+import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.TextView;
+
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 
 /**
  * Base class for our first run pages. We call these FirstrunPanel for consistency
  * with HomePager/HomePanel.
  *
  * @see FirstrunPager for the containing pager.
  */
 public class FirstrunPanel extends Fragment {
 
-    public static final int TITLE_RES = -1;
     protected boolean showBrowserHint = true;
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
         final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_basepanel_checkable_fragment, container, false);
         final Bundle args = getArguments();
         if (args != null) {
-            final int imageRes = args.getInt(FirstrunPagerConfig.KEY_IMAGE);
-            final int textRes = args.getInt(FirstrunPagerConfig.KEY_TEXT);
-            final int subtextRes = args.getInt(FirstrunPagerConfig.KEY_SUBTEXT);
+            final Bitmap image = args.getParcelable(FirstrunPagerConfig.KEY_IMAGE);
+            final String message = args.getString(FirstrunPagerConfig.KEY_MESSAGE);
+            final String subtext = args.getString(FirstrunPagerConfig.KEY_SUBTEXT);
 
-            ((ImageView) root.findViewById(R.id.firstrun_image)).setImageResource(imageRes);
-            ((TextView) root.findViewById(R.id.firstrun_text)).setText(textRes);
-            ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtextRes);
+            ((ImageView) root.findViewById(R.id.firstrun_image)).setImageBitmap(image);
+            ((TextView) root.findViewById(R.id.firstrun_text)).setText(message);
+            ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtext);
         }
 
         root.findViewById(R.id.firstrun_link).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-next");
                 pagerNavigation.next();
             }
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/LastPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/LastPanel.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.firstrun;
 
+import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.TextView;
 
 import org.mozilla.gecko.R;
@@ -17,31 +18,29 @@ import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 
 public class LastPanel extends FirstrunPanel {
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
         final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_basepanel_checkable_fragment, container, false);
         final Bundle args = getArguments();
         if (args != null) {
-            final int imageRes = args.getInt(FirstrunPagerConfig.KEY_IMAGE);
-            final int textRes = args.getInt(FirstrunPagerConfig.KEY_TEXT);
-            final int subtextRes = args.getInt(FirstrunPagerConfig.KEY_SUBTEXT);
+            final Bitmap image = args.getParcelable(FirstrunPagerConfig.KEY_IMAGE);
+            final String message = args.getString(FirstrunPagerConfig.KEY_MESSAGE);
+            final String subtext = args.getString(FirstrunPagerConfig.KEY_SUBTEXT);
 
-            ((ImageView) root.findViewById(R.id.firstrun_image)).setImageResource(imageRes);
-            ((TextView) root.findViewById(R.id.firstrun_text)).setText(textRes);
-            ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtextRes);
+            ((ImageView) root.findViewById(R.id.firstrun_image)).setImageBitmap(image);
+            ((TextView) root.findViewById(R.id.firstrun_text)).setText(message);
+            ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtext);
             ((TextView) root.findViewById(R.id.firstrun_link)).setText(R.string.firstrun_welcome_button_browser);
-
         }
 
         root.findViewById(R.id.firstrun_link).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-next");
                 close();
             }
         });
 
-
         return root;
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/LocalFirstRunPanelProvider.java
@@ -0,0 +1,47 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.firstrun;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.BitmapFactory;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.R;
+
+public class LocalFirstRunPanelProvider implements FirstRunPanelConfigProviderStrategy {
+    public PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE type, final boolean useLocalValues) {
+        final Resources resources = context.getResources();
+        switch (type) {
+            case WELCOME:
+                return new PanelConfig(type, useLocalValues, resources.getString(R.string.firstrun_panel_title_welcome),
+                        resources.getString(R.string.firstrun_urlbar_message),
+                        resources.getString(R.string.firstrun_urlbar_subtext),
+                        BitmapFactory.decodeResource(resources, R.drawable.firstrun_welcome));
+            case PRIVACY:
+                return new PanelConfig(type, useLocalValues, resources.getString(R.string.firstrun_panel_title_privacy),
+                        resources.getString(R.string.firstrun_privacy_message),
+                        resources.getString(R.string.firstrun_privacy_subtext),
+                        BitmapFactory.decodeResource(resources, R.drawable.firstrun_private));
+            case CUSTOMIZE:
+            case LAST_CUSTOMIZE:
+                return new PanelConfig(type, useLocalValues, resources.getString(R.string.firstrun_panel_title_customize),
+                        resources.getString(R.string.firstrun_customize_message),
+                        resources.getString(R.string.firstrun_customize_subtext),
+                        BitmapFactory.decodeResource(resources, R.drawable.firstrun_data));
+            case SYNC:
+                return new PanelConfig(type, useLocalValues, resources.getString(R.string.firstrun_sync_title),
+                        resources.getString(R.string.firstrun_sync_message),
+                        resources.getString(R.string.firstrun_sync_subtext),
+                        BitmapFactory.decodeResource(resources, R.drawable.firstrun_sync));
+            default:    // This will also be the case for "WELCOME"
+                return new PanelConfig(type, useLocalValues, resources.getString(R.string.firstrun_panel_title_welcome),
+                        resources.getString(R.string.firstrun_urlbar_message),
+                        resources.getString(R.string.firstrun_urlbar_subtext),
+                        BitmapFactory.decodeResource(resources, R.drawable.firstrun_welcome));
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/OnboardingHelper.java
@@ -0,0 +1,354 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.firstrun;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.StrictMode;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.UiThread;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.ViewStub;
+
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.distribution.Distribution;
+import org.mozilla.gecko.mma.MmaDelegate;
+import org.mozilla.gecko.mozglue.SafeIntent;
+import org.mozilla.gecko.switchboard.SwitchBoard;
+import org.mozilla.gecko.util.NetworkUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.UUID;
+
+/**
+ * Helper class of an an {@link AppCompatActivity} for managing showing the Onboarding screens.
+ * <br>The user class will have to implement {@link OnboardingListener}.
+ */
+public class OnboardingHelper implements MmaDelegate.MmaVariablesChangedListener,
+                                         SwitchBoard.ConfigStatusListener {
+    private static final String LOGTAG = "OnboardingHelper";
+    private static final boolean DEBUG = false;
+
+    @RobocopTarget
+    public static final String EXTRA_SKIP_STARTPANE = "skipstartpane";
+
+    /** Be aware of {@link org.mozilla.gecko.fxa.EnvironmentUtils#GECKO_PREFS_FIRSTRUN_UUID}. */
+    private static final String FIRSTRUN_UUID = "firstrun_uuid";
+
+    // Speculative timeout for showing the Onboarding screens with the default local values.
+    public static final int DELAY_SHOW_DEFAULT_ONBOARDING = 3 * 1000;
+
+    private WeakReference<AppCompatActivity> activityRef;
+    private OnboardingListener listener;
+    private SafeIntent activityStartingIntent;
+    private FirstrunAnimationContainer firstrunAnimationContainer;
+    private Runnable showOnboarding;
+    private boolean onboardingIsPreparing;
+    private boolean abortOnboarding;
+    private long startTimeForCheckingOnlineVariables;
+
+    public OnboardingHelper(
+            @NonNull final AppCompatActivity activity,
+            @NonNull final SafeIntent activityStartingIntent)
+            throws IllegalArgumentException {
+
+        if (!(activity instanceof OnboardingListener)) {
+            final String activityClass = activity.getClass().getSimpleName();
+            final String listenerInterface = OnboardingListener.class.getSimpleName();
+            throw new IllegalArgumentException(
+                    String.format("%s does not implement %s", activityClass, listenerInterface));
+        }
+
+        this.activityRef = new WeakReference<>(activity);
+        this.listener = (OnboardingListener) activity;
+        this.activityStartingIntent = activityStartingIntent;
+    }
+
+    /**
+     * Check and show the firstrun pane if the browser has never been launched and
+     * is not opening an external link from another application.
+     */
+    public void checkFirstRun() {
+        if (GeckoThread.getActiveProfile().inGuestMode()) {
+            // We do not want to show any first run tour for guest profiles.
+            return;
+        }
+
+        if (activityStartingIntent.getBooleanExtra(EXTRA_SKIP_STARTPANE, false)) {
+            // Note that we don't set the pref, so subsequent launches can result
+            // in the firstrun pane being shown.
+            return;
+        }
+
+        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+
+        try {
+            AppCompatActivity activity = activityRef.get();
+            if (activity == null) {
+                return;
+            }
+            final SharedPreferences prefs = GeckoSharedPrefs.forProfile(activity);
+
+            if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED_OLD, true) &&
+                    prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) {
+
+                onboardingIsPreparing = true;
+                listener.onOnboardingProcessStarted();
+
+                // Allow the activity to be gc'ed while waiting for the distribution
+                activity = null;
+
+                if (!Intent.ACTION_VIEW.equals(activityStartingIntent.getAction())) {
+                    // Check to see if a distribution has turned off the first run pager.
+                    final Distribution distribution = Distribution.getInstance(activityRef.get());
+                    if (!distribution.shouldWaitForSystemDistribution()) {
+                        checkFirstrunInternal();
+                    } else {
+                        distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
+                            @Override
+                            public void distributionNotFound() {
+                                ThreadUtils.postToUiThread(new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        checkFirstrunInternal();
+                                    }
+                                });
+                            }
+
+                            @Override
+                            public void distributionFound(final Distribution distribution) {
+                                // Check preference again in case distribution turned it off.
+                                if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) {
+                                    ThreadUtils.postToUiThread(new Runnable() {
+                                        @Override
+                                        public void run() {
+                                            checkFirstrunInternal();
+                                        }
+                                    });
+                                }
+                            }
+
+                            @Override
+                            public void distributionArrivedLate(final Distribution distribution) {
+                            }
+                        });
+                    }
+                }
+
+                // We have no intention of stopping this session. The FIRSTRUN session
+                // ends when the browsing session/activity has ended. All events
+                // during firstrun will be tagged as FIRSTRUN.
+                Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN);
+            }
+        } finally {
+            StrictMode.setThreadPolicy(savedPolicy);
+        }
+    }
+
+    /**
+     * Call this to prevent or finish displaying of the Onboarding process.<br>
+     * If it has not yet been shown to the user and now it has been prevented to,
+     * showing the Onboarding screens will be retried at the next app start.
+     *
+     * @return whether Onboarding was prevented / finished early or not.
+     */
+    public boolean hideOnboarding() {
+        abortOnboarding = true;
+
+        if (DEBUG) {
+            Log.d(LOGTAG, "hideOnboarding");
+        }
+
+        if (isPreparing()) {
+            onboardingIsPreparing = false;
+            // Cancel showing Onboarding. Will retry automatically at the next app startup.
+            ThreadUtils.removeCallbacksFromUiThread(showOnboarding);
+            return true;
+        }
+
+        if (isOnboardingVisible()) {
+            onboardingIsPreparing = false;
+            firstrunAnimationContainer.registerOnFinishListener(null);
+            firstrunAnimationContainer.hide();
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean isOnboardingVisible() {
+        return firstrunAnimationContainer != null && firstrunAnimationContainer.isVisible();
+    }
+
+    /**
+     * Get if we are in the process of preparing the Onboarding screens.<br>
+     * If the Onboarding screens should be shown to the user, they will be so after a small delay -
+     * up to {@link #DELAY_SHOW_DEFAULT_ONBOARDING} necessary for downloading the data
+     * needed to populate the screens.
+     *
+     * @return <code>true</code> - we are preparing for showing Onboarding but haven't yet done
+     *         <code>false</code> - Onboarding has been displayed
+     */
+    public boolean isPreparing() {
+        return onboardingIsPreparing;
+    }
+
+    /**
+     * Code to actually show the first run pager, separated for distribution purposes.<br>
+     * If network is available it will first try to use server values for populating the
+     * onboarding screens. If that isn't possible the default local values will be used.
+     */
+    @UiThread
+    private void checkFirstrunInternal() {
+        final AppCompatActivity activity = activityRef.get();
+        if (activity == null) {
+            return;
+        }
+
+        if (abortOnboarding) {
+            return;
+        }
+
+        if (NetworkUtils.isConnected(activity)) {
+            showOnboarding = new Runnable() {
+                @Override
+                public void run() {
+                    showFirstrunPager(true);
+                }
+            };
+
+            if (DEBUG) {
+                startTimeForCheckingOnlineVariables = System.currentTimeMillis();
+            }
+
+            ThreadUtils.postDelayedToUiThread(showOnboarding, DELAY_SHOW_DEFAULT_ONBOARDING);
+        } else {
+            showFirstrunPager(true);
+        }
+    }
+
+    private void showFirstrunPager(final boolean useLocalValues) {
+        final AppCompatActivity activity = activityRef.get();
+        if (activity == null) {
+            return;
+        }
+
+        onboardingIsPreparing = false;
+
+        if (firstrunAnimationContainer == null) {
+            final ViewStub firstrunPagerStub = (ViewStub) activity.findViewById(R.id.firstrun_pager_stub);
+            firstrunAnimationContainer = (FirstrunAnimationContainer) firstrunPagerStub.inflate();
+        }
+
+        if (DEBUG) {
+            final StringBuilder logMessage =
+                    new StringBuilder("Will show Onboarding using ")
+                            .append((useLocalValues ? "local" : "server"))
+                            .append(" values");
+            Log.d(LOGTAG, logMessage.toString());
+        }
+
+        firstrunAnimationContainer.load
+                (activity.getApplicationContext(), activity.getSupportFragmentManager(), useLocalValues);
+        firstrunAnimationContainer.registerOnFinishListener(new FirstrunAnimationContainer.OnFinishListener() {
+            @Override
+            public void onFinish() {
+                listener.onFinishedOnboarding(firstrunAnimationContainer.showBrowserHint());
+            }
+        });
+
+        listener.onOnboardingScreensVisible();
+        saveOnboardingShownStatus();
+    }
+
+    // The Onboarding screens should only be shown one time.
+    private void saveOnboardingShownStatus() {
+        // The method is called serially from showFirstrunPager()
+        // which stores a hard reference to the activity so it's safe to use it directly
+        final SharedPreferences prefs = GeckoSharedPrefs.forProfile(activityRef.get());
+
+        prefs.edit()
+                // Don't bother trying again to show the v1 minimal first run.
+                .putBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, false)
+                // Generate a unique identifier for the current first run.
+                // See Bug 1429735 for why we care to do this.
+                .putString(FIRSTRUN_UUID, UUID.randomUUID().toString())
+                .apply();
+    }
+
+    /**
+     * Try showing the Onboarding screens even before #DELAY_SHOW_DEFAULT_ONBOARDING.<br>
+     * If they have already been shown calling this method has no effect.
+     */
+    private void tryShowOnboarding(final boolean shouldUseLocalValues) {
+        final AppCompatActivity activity = activityRef.get();
+        if (activity == null) {
+            return;
+        }
+
+        if (isPreparing()) {
+            ThreadUtils.removeCallbacksFromUiThread(showOnboarding);
+            showFirstrunPager(shouldUseLocalValues);
+        }
+    }
+
+    @Override
+    @MainThread
+    public void onRemoteVariablesChanged() {
+        if (DEBUG) {
+            final long timeElapsed = System.currentTimeMillis() - startTimeForCheckingOnlineVariables;
+            Log.d(LOGTAG, String.format("Got online variables after: %d millis", timeElapsed));
+        }
+
+        tryShowOnboarding(false);
+    }
+
+    @Override
+    @MainThread
+    public void onRemoteVariablesUnavailable() {
+        tryShowOnboarding(true);
+    }
+
+    @Override
+    @MainThread
+    public void onExperimentsConfigLoaded() {
+        final AppCompatActivity activity = activityRef.get();
+        if (activity == null) {
+            return;
+        }
+
+        // Only if the Mma experiment is available we should continue to wait for server values.
+        if (!MmaDelegate.isMmaExperimentEnabled(activity)) {
+            tryShowOnboarding(true);
+        }
+    }
+
+    @Override
+    @MainThread
+    public void onExperimentsConfigLoadFailed() {
+        tryShowOnboarding(true);
+    }
+
+    /**
+     * Informs about the status of the onboarding process.
+     */
+    public interface OnboardingListener {
+
+        void onOnboardingProcessStarted();
+
+        void onOnboardingScreensVisible();
+
+        void onFinishedOnboarding(final boolean showBrowserHint);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/PanelConfig.java
@@ -0,0 +1,72 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.firstrun;
+
+import android.graphics.Bitmap;
+
+/**
+ * Onboarding screens configuration object.
+ */
+public class PanelConfig {
+    public enum TYPE {
+        WELCOME, PRIVACY, CUSTOMIZE, LAST_CUSTOMIZE, SYNC
+    }
+
+    private final TYPE type;
+    private final boolean useLocalValues;
+    private final String title;
+    private final String message;
+    private final String text;
+    private final Bitmap image;
+
+    public PanelConfig(TYPE type, boolean useLocalValues, String title, String message, String text, Bitmap image) {
+        this.type = type;
+        this.useLocalValues = useLocalValues;
+        this.title = title;
+        this.message = message;
+        this.text = text;
+        this.image = image;
+    }
+
+    public String getClassName() {
+        switch (type) {
+            case WELCOME:
+            case PRIVACY:
+            case CUSTOMIZE:
+                return FirstrunPanel.class.getName();
+            case LAST_CUSTOMIZE:
+                return LastPanel.class.getName();
+            case SYNC:
+                return SyncPanel.class.getName();
+            default:    // Return the default Panel, same as for "WELCOME"
+                return FirstrunPanel.class.getName();
+        }
+    }
+
+    public TYPE getType() {
+        return type;
+    }
+
+    public boolean isUsingLocalValues() {
+        return useLocalValues;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public String getText() {
+        return text;
+    }
+
+    public Bitmap getImage() {
+        return image;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/RemoteFirstRunPanelConfig.java
@@ -0,0 +1,18 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.firstrun;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.mma.MmaDelegate;
+
+public class RemoteFirstRunPanelConfig implements FirstRunPanelConfigProviderStrategy {
+    @Override
+    public PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE type, final boolean useLocalValues) {
+        return MmaDelegate.getPanelConfig(context, type, useLocalValues);
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java
@@ -1,16 +1,17 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.firstrun;
 
 import android.content.Intent;
+import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.TextView;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
@@ -19,23 +20,23 @@ import org.mozilla.gecko.fxa.FxAccountCo
 import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
 
 public class SyncPanel extends FirstrunPanel {
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
         final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_sync_fragment, container, false);
         final Bundle args = getArguments();
         if (args != null) {
-            final int imageRes = args.getInt(FirstrunPagerConfig.KEY_IMAGE);
-            final int textRes = args.getInt(FirstrunPagerConfig.KEY_TEXT);
-            final int subtextRes = args.getInt(FirstrunPagerConfig.KEY_SUBTEXT);
+            final Bitmap image = args.getParcelable(FirstrunPagerConfig.KEY_IMAGE);
+            final String message = args.getString(FirstrunPagerConfig.KEY_MESSAGE);
+            final String subtext = args.getString(FirstrunPagerConfig.KEY_SUBTEXT);
 
-            ((ImageView) root.findViewById(R.id.firstrun_image)).setImageResource(imageRes);
-            ((TextView) root.findViewById(R.id.firstrun_text)).setText(textRes);
-            ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtextRes);
+            ((ImageView) root.findViewById(R.id.firstrun_image)).setImageBitmap(image);
+            ((TextView) root.findViewById(R.id.firstrun_text)).setText(message);
+            ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtext);
         }
 
         root.findViewById(R.id.welcome_account).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-sync");
                 showBrowserHint = false;
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/LeanplumVariables.java
@@ -0,0 +1,122 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.mma;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+
+import com.leanplum.annotations.Variable;
+
+import org.mozilla.gecko.R;
+
+import java.lang.reflect.Field;
+
+/**
+ * Unified repo for all LeanPlum variables.<br>
+ * <ul>To make them appear in the LP dashboard and get new values from the server
+ *      <li>they must be annotated with {@link com.leanplum.annotations.Variable}.</li>
+ *      <li>they need to be parsed with {@link com.leanplum.annotations.Parser} after {@link com.leanplum.Leanplum#setApplicationContext(Context)}</li>
+ * </ul>
+ * Although some fields are public (LP SDK limitation) they are not to be written into.
+ *
+ * @see <a href="https://docs.leanplum.com/reference#defining-variables">Official LP variables documentation</a>
+ */
+public class LeanplumVariables {
+    private static LeanplumVariables INSTANCE;
+    private static Resources appResources;
+
+    private static final String FIRSTRUN_WELCOME_PANEL_GROUP_NAME = "FirstRun Welcome Panel";
+    private static final String FIRSTRUN_PRIVACY_PANEL_GROUP_NAME = "FirstRun Privacy Panel";
+    private static final String FIRSTRUN_CUSTOMIZE_PANEL_GROUP_NAME = "FirstRun Customize Panel";
+    private static final String FIRSTRUN_SYNC_PANEL_GROUP_NAME = "FirstRun Sync Panel";
+
+    @Variable(group = FIRSTRUN_WELCOME_PANEL_GROUP_NAME) public static String welcomePanelTitle;
+    @Variable(group = FIRSTRUN_WELCOME_PANEL_GROUP_NAME) public static String welcomePanelMessage;
+    @Variable(group = FIRSTRUN_WELCOME_PANEL_GROUP_NAME) public static String welcomePanelSubtext;
+    @DrawableRes private static int welcomeDrawableId;
+
+    @Variable(group = FIRSTRUN_PRIVACY_PANEL_GROUP_NAME) public static String privacyPanelTitle;
+    @Variable(group = FIRSTRUN_PRIVACY_PANEL_GROUP_NAME) public static String privacyPanelMessage;
+    @Variable(group = FIRSTRUN_PRIVACY_PANEL_GROUP_NAME) public static String privacyPanelSubtext;
+    @DrawableRes private static int privacyDrawableId;
+
+    @Variable(group = FIRSTRUN_CUSTOMIZE_PANEL_GROUP_NAME) public static String customizePanelTitle;
+    @Variable(group = FIRSTRUN_CUSTOMIZE_PANEL_GROUP_NAME) public static String customizePanelMessage;
+    @Variable(group = FIRSTRUN_CUSTOMIZE_PANEL_GROUP_NAME) public static String customizePanelSubtext;
+    @DrawableRes private static int customizingDrawableId;
+
+    @Variable(group = FIRSTRUN_SYNC_PANEL_GROUP_NAME) public static String syncPanelTitle;
+    @Variable(group = FIRSTRUN_SYNC_PANEL_GROUP_NAME) public static String syncPanelMessage;
+    @Variable(group = FIRSTRUN_SYNC_PANEL_GROUP_NAME) public static String syncPanelSubtext;
+    @DrawableRes private static int syncDrawableId;
+
+    /**
+     * Allows constructing and/or returning an already constructed instance of this class
+     * which has all it's fields populated with values from Resources.<br><br>
+     *
+     * An instance of this class needs exist to allow overwriting it's fields with downloaded values from LeanPlum
+     * @see com.leanplum.annotations.Parser#defineFileVariable(Object, String, String, Field)
+     */
+    public static LeanplumVariables getInstance(Context appContext) {
+        // Simple Singleton as we don't expect concurrency problems.
+        if (INSTANCE == null) {
+            INSTANCE = new LeanplumVariables(appContext);
+        }
+
+        return INSTANCE;
+    }
+
+    /**
+     * Allows setting default values for instance variables.
+     * @param context used to access application resources
+     */
+    private LeanplumVariables(@NonNull Context context) {
+        appResources = context.getResources();
+        welcomePanelTitle = appResources.getString(R.string.firstrun_panel_title_welcome);
+        welcomePanelMessage = appResources.getString(R.string.firstrun_urlbar_message);
+        welcomePanelSubtext = appResources.getString(R.string.firstrun_urlbar_subtext);
+        welcomeDrawableId = R.drawable.firstrun_welcome;
+
+        privacyPanelTitle = appResources.getString(R.string.firstrun_panel_title_privacy);
+        privacyPanelMessage = appResources.getString(R.string.firstrun_privacy_message);
+        privacyPanelSubtext = appResources.getString(R.string.firstrun_privacy_subtext);
+        privacyDrawableId = R.drawable.firstrun_private;
+
+        customizePanelTitle = appResources.getString(R.string.firstrun_panel_title_customize);
+        customizePanelMessage = appResources.getString(R.string.firstrun_customize_message);
+        customizePanelSubtext = appResources.getString(R.string.firstrun_customize_subtext);
+        customizingDrawableId = R.drawable.firstrun_data;
+
+        syncPanelTitle = appResources.getString(R.string.firstrun_sync_title);
+        syncPanelMessage = appResources.getString(R.string.firstrun_sync_message);
+        syncPanelSubtext = appResources.getString(R.string.firstrun_sync_subtext);
+        syncDrawableId = R.drawable.firstrun_sync;
+    }
+
+    public static Bitmap getWelcomeImage() {
+        return getBitmapFromMmaVar(welcomeDrawableId);
+    }
+
+    public static Bitmap getPrivacyImage() {
+        return getBitmapFromMmaVar(privacyDrawableId);
+    }
+
+    public static Bitmap getCustomizingImage() {
+        return getBitmapFromMmaVar(customizingDrawableId);
+    }
+
+    public static Bitmap getSyncImage() {
+        return getBitmapFromMmaVar(syncDrawableId);
+    }
+
+    private static Bitmap getBitmapFromMmaVar(@DrawableRes final int drawableRes) {
+        return BitmapFactory.decodeResource(appResources, drawableRes);
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
@@ -19,20 +19,22 @@ import android.text.TextUtils;
 
 import org.mozilla.gecko.Experiments;
 import org.mozilla.gecko.MmaConstants;
 import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.activitystream.homepanel.ActivityStreamConfiguration;
+import org.mozilla.gecko.firstrun.PanelConfig;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.switchboard.SwitchBoard;
 import org.mozilla.gecko.util.ContextUtils;
+import org.mozilla.gecko.util.ThreadUtils;
 
 import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
 
 
 public class MmaDelegate {
 
@@ -42,16 +44,18 @@ public class MmaDelegate {
     public static final String SAVED_BOOKMARK = "E_Saved_Bookmark";
     public static final String OPENED_BOOKMARK = "E_Opened_Bookmark";
     public static final String INTERACT_WITH_SEARCH_URL_AREA = "E_Interact_With_Search_URL_Area";
     public static final String SCREENSHOT = "E_Screenshot";
     public static final String SAVED_LOGIN_AND_PASSWORD = "E_Saved_Login_And_Password";
     public static final String RESUMED_FROM_BACKGROUND = "E_Resumed_From_Background";
     public static final String NEW_TAB = "E_Opened_New_Tab";
     public static final String DISMISS_ONBOARDING = "E_Dismiss_Onboarding";
+    public static final String ONBOARDING_DEFAULT_VALUES = "E_Onboarding_With_Default_Values";
+    public static final String ONBOARDING_REMOTE_VALUES = "E_Onboarding_With_Remote_Values";
 
     private static final String LAUNCH_BUT_NOT_DEFAULT_BROWSER = "E_Launch_But_Not_Default_Browser";
     private static final String LAUNCH_BROWSER = "E_Launch_Browser";
     private static final String CHANGED_DEFAULT_TO_FENNEC = "E_Changed_Default_To_Fennec";
     private static final String INSTALLED_FOCUS = "E_Just_Installed_Focus";
     private static final String INSTALLED_KLAR = "E_Just_Installed_Klar";
 
     private static final String USER_ATT_FOCUS_INSTALLED = "Focus Installed";
@@ -70,33 +74,40 @@ public class MmaDelegate {
     public static final String KEY_ANDROID_PREF_STRING_LEANPLUM_DEVICE_ID = "android.not_a_preference.leanplum.device_id";
     private static final String KEY_ANDROID_PREF_BOOLEAN_FENNEC_IS_DEFAULT = "android.not_a_preference.fennec.default.browser.status";
 
     private static final String DEBUG_LEANPLUM_DEVICE_ID = "8effda84-99df-11e7-abc4-cec278b6b50a";
 
     private static final MmaInterface mmaHelper = MmaConstants.getMma();
     private static Context applicationContext;
 
-    public static void init(Activity activity) {
+    public static void init(final Activity activity,
+                            final MmaVariablesChangedListener remoteVariablesListener) {
         applicationContext = activity.getApplicationContext();
         // Since user attributes are gathered in Fennec, not in MMA implementation,
         // we gather the information here then pass to mmaHelper.init()
         // Note that generateUserAttribute always return a non null HashMap.
         final Map<String, Object> attributes = gatherUserAttributes(activity);
         final String deviceId = getDeviceId(activity);
         mmaHelper.setDeviceId(deviceId);
         PrefsHelper.setPref(GeckoPreferences.PREFS_MMA_DEVICE_ID, deviceId);
         // above two config setup required to be invoked before mmaHelper.init.
         mmaHelper.init(activity, attributes);
 
         if (!isDefaultBrowser(activity)) {
             mmaHelper.event(MmaDelegate.LAUNCH_BUT_NOT_DEFAULT_BROWSER);
         }
         mmaHelper.event(MmaDelegate.LAUNCH_BROWSER);
 
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mmaHelper.listenOnceForVariableChanges(remoteVariablesListener);
+            }
+        });
     }
 
     public static void stop() {
         mmaHelper.stop();
     }
 
     /* This method must be called at background thread to avoid performance issues in some API level */
     @NonNull
@@ -206,22 +217,32 @@ public class MmaDelegate {
         if (isMmaAllowed(context)) {
             mmaHelper.setCustomIcon(R.drawable.ic_status_logo);
             return mmaHelper.handleGcmMessage(context, from, bundle);
         } else {
             return false;
         }
     }
 
+    public static PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE panelConfigType, final boolean useLocalValues) {
+        return mmaHelper.getPanelConfig(context, panelConfigType, useLocalValues);
+    }
+
     private static String getDeviceId(Activity activity) {
         if (SwitchBoard.isInExperiment(activity, Experiments.LEANPLUM_DEBUG)) {
             return DEBUG_LEANPLUM_DEVICE_ID;
         }
 
         final SharedPreferences sharedPreferences = activity.getPreferences(Context.MODE_PRIVATE);
         String deviceId = sharedPreferences.getString(KEY_ANDROID_PREF_STRING_LEANPLUM_DEVICE_ID, null);
         if (deviceId == null) {
             deviceId = UUID.randomUUID().toString();
             sharedPreferences.edit().putString(KEY_ANDROID_PREF_STRING_LEANPLUM_DEVICE_ID, deviceId).apply();
         }
         return deviceId;
     }
+
+    public interface MmaVariablesChangedListener {
+        void onRemoteVariablesChanged();
+
+        void onRemoteVariablesUnavailable();
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaInterface.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaInterface.java
@@ -8,16 +8,18 @@ package org.mozilla.gecko.mma;
 
 import android.app.Activity;
 import android.content.Context;
 import android.os.Bundle;
 import android.support.annotation.CheckResult;
 import android.support.annotation.DrawableRes;
 import android.support.annotation.NonNull;
 
+import org.mozilla.gecko.firstrun.PanelConfig;
+
 import java.util.Map;
 
 
 public interface MmaInterface {
 
     void init(Activity Activity, Map<String, ?> attributes);
 
     void setCustomIcon(@DrawableRes int iconResId);
@@ -28,9 +30,13 @@ public interface MmaInterface {
 
     void event(String mmaEvent, double value);
 
     void stop();
 
     @CheckResult boolean handleGcmMessage(Context context, String from, Bundle bundle);
 
     void setDeviceId(@NonNull String deviceId);
+
+    PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE panelConfigType, final boolean useLocalValues);
+
+    void listenOnceForVariableChanges(@NonNull final MmaDelegate.MmaVariablesChangedListener listener);
 }
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaLeanplumImp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaLeanplumImp.java
@@ -4,35 +4,36 @@
  * 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.mma;
 
 import android.app.Activity;
 import android.app.Notification;
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.support.annotation.DrawableRes;
 import android.support.annotation.NonNull;
 import android.support.v4.app.NotificationCompat;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 import com.leanplum.LeanplumPushNotificationCustomizer;
 import com.leanplum.LeanplumPushService;
+import com.leanplum.annotations.Parser;
+import com.leanplum.callbacks.VariablesChangedCallback;
 import com.leanplum.internal.Constants;
 import com.leanplum.internal.LeanplumInternal;
-import com.leanplum.internal.VarCache;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.MmaConstants;
+import org.mozilla.gecko.firstrun.PanelConfig;
 
+import java.lang.ref.WeakReference;
 import java.util.Map;
-import java.util.UUID;
 
 
 public class MmaLeanplumImp implements MmaInterface {
 
 
     @Override
     public void init(final Activity activity, Map<String, ?> attributes) {
         if (activity == null) {
@@ -42,16 +43,18 @@ public class MmaLeanplumImp implements M
         // Need to call this in the eventuality that in this app session stop() has been called.
         // It will allow LeanPlum to communicate again with the servers.
         Leanplum.setIsTestModeEnabled(false);
 
         Leanplum.setApplicationContext(activity.getApplicationContext());
 
         LeanplumActivityHelper.enableLifecycleCallbacks(activity.getApplication());
 
+        Parser.parseVariables(LeanplumVariables.getInstance(activity.getApplicationContext()));
+
         if (AppConstants.MOZILLA_OFFICIAL) {
             Leanplum.setAppIdForProductionMode(MmaConstants.MOZ_LEANPLUM_SDK_CLIENTID, MmaConstants.MOZ_LEANPLUM_SDK_KEY);
         } else {
             Leanplum.setAppIdForDevelopmentMode(MmaConstants.MOZ_LEANPLUM_SDK_CLIENTID, MmaConstants.MOZ_LEANPLUM_SDK_KEY);
         }
 
         LeanplumPushService.setGcmSenderId(AppConstants.MOZ_ANDROID_GCM_SENDERIDS);
 
@@ -140,9 +143,50 @@ public class MmaLeanplumImp implements M
         return false;
     }
 
     @Override
     public void setDeviceId(@NonNull String deviceId) {
         Leanplum.setDeviceId(deviceId);
     }
 
+    @Override
+    public PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE type, final boolean useLocalValues) {
+        if (useLocalValues) {
+            throw new UnsupportedOperationException("Cannot build remote panel config with local values");
+        }
+
+        switch (type) {
+            case WELCOME:
+                return new PanelConfig(type, useLocalValues, LeanplumVariables.welcomePanelTitle, LeanplumVariables.welcomePanelMessage,
+                        LeanplumVariables.welcomePanelSubtext, LeanplumVariables.getWelcomeImage());
+            case PRIVACY:
+                return new PanelConfig(type, useLocalValues, LeanplumVariables.privacyPanelTitle, LeanplumVariables.privacyPanelMessage,
+                        LeanplumVariables.privacyPanelSubtext, LeanplumVariables.getPrivacyImage());
+            case CUSTOMIZE:
+            case LAST_CUSTOMIZE:
+                return new PanelConfig(type, useLocalValues, LeanplumVariables.customizePanelTitle, LeanplumVariables.customizePanelMessage,
+                        LeanplumVariables.customizePanelSubtext, LeanplumVariables.getCustomizingImage());
+            case SYNC:
+                return new PanelConfig(type, useLocalValues, LeanplumVariables.syncPanelTitle, LeanplumVariables.syncPanelMessage,
+                        LeanplumVariables.syncPanelSubtext, LeanplumVariables.getSyncImage());
+            default:    // This will also be the case for "WELCOME"
+                return new PanelConfig(type, useLocalValues, LeanplumVariables.welcomePanelTitle, LeanplumVariables.welcomePanelMessage,
+                        LeanplumVariables.welcomePanelSubtext, LeanplumVariables.getWelcomeImage());
+        }
+    }
+
+    @Override
+    public void listenOnceForVariableChanges(@NonNull final MmaDelegate.MmaVariablesChangedListener listener) {
+        final WeakReference<MmaDelegate.MmaVariablesChangedListener> listenerRef = new WeakReference<>(listener);
+
+        Leanplum.addVariablesChangedHandler(new VariablesChangedCallback() {
+            @Override
+            public void variablesChanged() {
+                Leanplum.removeVariablesChangedHandler(this);
+                MmaDelegate.MmaVariablesChangedListener variablesChangesListener = listenerRef.get();
+                if (variablesChangesListener != null) {
+                    variablesChangesListener.onRemoteVariablesChanged();
+                }
+            }
+        });
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaStubImp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaStubImp.java
@@ -7,16 +7,18 @@
 package org.mozilla.gecko.mma;
 
 import android.app.Activity;
 import android.content.Context;
 import android.os.Bundle;
 import android.support.annotation.DrawableRes;
 import android.support.annotation.NonNull;
 
+import org.mozilla.gecko.firstrun.PanelConfig;
+
 import java.util.Map;
 
 
 public class MmaStubImp implements MmaInterface {
     @Override
     public void init(Activity activity, Map<String, ?> attributes) {
 
     }
@@ -51,9 +53,18 @@ public class MmaStubImp implements MmaIn
         return false;
     }
 
     @Override
     public void setDeviceId(@NonNull String deviceId) {
 
     }
 
+    @Override
+    public PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE panelConfigType, boolean useLocalValues) {
+        return null;
+    }
+
+    @Override
+    public void listenOnceForVariableChanges(@NonNull MmaDelegate.MmaVariablesChangedListener listener) {
+        listener.onRemoteVariablesUnavailable();
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java
@@ -1,32 +1,33 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.prompts;
 
-import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 
 import android.app.AlertDialog;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnCancelListener;
 import android.content.DialogInterface.OnClickListener;
 import android.content.res.Resources;
 import android.text.TextUtils;
 import android.util.Log;
+import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.LinearLayout;
 import android.widget.ListView;
 import android.widget.ScrollView;
 
 import java.util.ArrayList;
 
@@ -361,17 +362,25 @@ public class Prompt implements OnClickLi
     /* Wraps an input in a linearlayout. We do this so that we can set padding that appears outside the background
      * drawable for the view.
      */
     private View wrapInput(final PromptInput input) {
         final LinearLayout linearLayout = new LinearLayout(mContext);
         linearLayout.setOrientation(LinearLayout.VERTICAL);
         applyInputStyle(linearLayout, input);
 
-        linearLayout.addView(input.getView(mContext));
+        View widget = input.getView(mContext);
+
+        // Make sure the widget will not be chopped on smaller screens
+        LinearLayout.LayoutParams parentParams = new LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+        parentParams.gravity = Gravity.CENTER;
+        widget.setLayoutParams(parentParams);
+
+        linearLayout.addView(widget);
 
         return linearLayout;
     }
 
     /* Add the requested input elements to the dialog.
      *
      * @param builder
      *        the alert builder currently building this dialog.
--- a/mobile/android/base/java/org/mozilla/gecko/switchboard/AsyncConfigLoader.java
+++ b/mobile/android/base/java/org/mozilla/gecko/switchboard/AsyncConfigLoader.java
@@ -28,27 +28,30 @@ import android.os.AsyncTask;
  *
  * @author Philipp Berner
  *
  */
 public class AsyncConfigLoader extends AsyncTask<Void, Void, Void> {
 
     private Context context;
     private String defaultServerUrl;
+    private SwitchBoard.ConfigStatusListener listener;
 
     /**
      * Sets the params for async loading either SwitchBoard.updateConfigServerUrl()
      * or SwitchBoard.loadConfig.
      * Loads config with a custom UUID
      * @param c Application context
      * @param defaultServerUrl Default URL endpoint for Switchboard config.
      */
-    public AsyncConfigLoader(Context c, String defaultServerUrl) {
+    public AsyncConfigLoader(Context c, String defaultServerUrl,
+                             SwitchBoard.ConfigStatusListener listener) {
         this.context = c;
         this.defaultServerUrl = defaultServerUrl;
+        this.listener = listener;
     }
 
     @Override
     protected Void doInBackground(Void... params) {
-        SwitchBoard.loadConfig(context, defaultServerUrl);
+        SwitchBoard.loadConfig(context, defaultServerUrl, listener);
         return null;
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/switchboard/SwitchBoard.java
+++ b/mobile/android/base/java/org/mozilla/gecko/switchboard/SwitchBoard.java
@@ -33,16 +33,17 @@ import org.json.JSONException;
 import org.json.JSONObject;
 import org.json.JSONArray;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.search.SearchEngineManager;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.IOUtils;
 import org.mozilla.gecko.util.ProxySelector;
+import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.Context;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.os.Build;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import android.util.Log;
@@ -94,33 +95,52 @@ public class SwitchBoard {
 
     /**
      * Loads a new config for a user. This method does network I/O, so it
      * should not be called on the main thread.
      *
      * @param c ApplicationContext
      * @param serverUrl Server URL endpoint.
      */
-    public static void loadConfig(Context c, @NonNull String serverUrl) {
+    public static void loadConfig(Context c, @NonNull String serverUrl,
+                                  @NonNull final ConfigStatusListener listener) {
         final URL url;
         try {
             url = new URL(serverUrl);
         } catch (MalformedURLException e) {
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    listener.onExperimentsConfigLoadFailed();
+                }
+            });
             Log.e(TAG, "Exception creating server URL", e);
             return;
         }
 
         final String result = readFromUrlGET(url);
         if (DEBUG) Log.d(TAG, "Result: " + result);
         if (result == null) {
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    listener.onExperimentsConfigLoadFailed();
+                }
+            });
             return;
         }
 
         // Cache result locally in shared preferences.
         Preferences.setDynamicConfigJson(c, result);
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                listener.onExperimentsConfigLoaded();
+            }
+        });
     }
 
     public static boolean isInBucket(Context c, int low, int high) {
         final int userBucket = getUserBucket(c);
         return (userBucket >= low) && (userBucket < high);
     }
 
     /**
@@ -448,9 +468,15 @@ public class SwitchBoard {
         final DeviceUuidFactory df = new DeviceUuidFactory(c);
         final String uuid = df.getDeviceUuid().toString();
 
         CRC32 crc = new CRC32();
         crc.update(uuid.getBytes());
         long checksum = crc.getValue();
         return (int)(checksum % 100L);
     }
+
+    public interface ConfigStatusListener {
+        void onExperimentsConfigLoaded();
+
+        void onExperimentsConfigLoadFailed();
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/widget/SplashScreen.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/SplashScreen.java
@@ -34,16 +34,20 @@ public class SplashScreen extends Relati
         if (hasReachedThreshold) {
             vanish();
         } else {
             // if the threshold not reached, mark
             shouldHideAsap = true;
         }
     }
 
+    public void show(final int duration) {
+        atLeast(duration);
+    }
+
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         // Splash Screen will at least wait for this long before disappear.
         atLeast(MIN_DISPLAY_TIME);
     }
 
     // the minimum time the splash screen will stay on the screen
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/www/loremIpsum.html
@@ -0,0 +1,13 @@
+<html>
+    <head><title>Lorem ipsum</title></head>
+    <body>
+        <p>
+            <a href="#">Lorem ipsum</a> dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
+            incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
+            exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
+            dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+            Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
+            mollit anim id est laborum.
+        </p>
+    </body>
+</html>
\ No newline at end of file
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
@@ -27,16 +27,17 @@ open class BaseSessionTest(noErrorCollec
     companion object {
         const val CLICK_TO_RELOAD_HTML_PATH = "/assets/www/clickToReload.html"
         const val CONTENT_CRASH_URL = "about:crashcontent"
         const val DOWNLOAD_HTML_PATH = "/assets/www/download.html"
         const val HELLO_HTML_PATH = "/assets/www/hello.html"
         const val HELLO2_HTML_PATH = "/assets/www/hello2.html"
         const val INPUTS_PATH = "/assets/www/inputs.html"
         const val INVALID_URI = "http://www.test.invalid/"
+        const val LOREM_IPSUM_HTML_PATH = "/assets/www/loremIpsum.html"
         const val NEW_SESSION_HTML_PATH = "/assets/www/newSession.html"
         const val NEW_SESSION_CHILD_HTML_PATH = "/assets/www/newSession_child.html"
         const val SAVE_STATE_PATH = "/assets/www/saveState.html"
         const val TITLE_CHANGE_HTML_PATH = "/assets/www/titleChange.html"
         const val TRACKERS_PATH = "/assets/www/trackers.html"
     }
 
     @get:Rule val sessionRule = GeckoSessionTestRule()
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/FinderTest.kt
@@ -0,0 +1,170 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.geckoview.test
+
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ReuseSession
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI
+
+import android.support.test.filters.MediumTest
+import android.support.test.runner.AndroidJUnit4
+import org.hamcrest.Matchers.*
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class FinderTest : BaseSessionTest() {
+
+    @ReuseSession(false) // "wrapped" in the first result depends on a fresh session.
+    @Test fun find() {
+        mainSession.loadTestPath(LOREM_IPSUM_HTML_PATH)
+        mainSession.waitForPageStop()
+
+        // Initial search.
+        var result = sessionRule.waitForResult(mainSession.finder.find("dolore", 0))
+
+        assertThat("Should be found", result.found, equalTo(true))
+        // "wrapped" is true for the first found result of a new session.
+        assertThat("Should have wrapped", result.wrapped, equalTo(true))
+        assertThat("Current count should be correct", result.current, equalTo(1))
+        assertThat("Total count should be correct", result.total, equalTo(2))
+        assertThat("Search string should be correct",
+                   result.searchString, equalTo("dolore"))
+        assertThat("Flags should be correct", result.flags, equalTo(0))
+
+        // Search again using new flags.
+        result = sessionRule.waitForResult(mainSession.finder.find(
+                null, GeckoSession.FINDER_FIND_BACKWARDS
+                                        or GeckoSession.FINDER_FIND_MATCH_CASE
+                                        or GeckoSession.FINDER_FIND_WHOLE_WORD))
+
+        assertThat("Should be found", result.found, equalTo(true))
+        assertThat("Should have wrapped", result.wrapped, equalTo(true))
+        assertThat("Current count should be correct", result.current, equalTo(2))
+        assertThat("Total count should be correct", result.total, equalTo(2))
+        assertThat("Search string should be correct",
+                   result.searchString, equalTo("dolore"))
+        assertThat("Flags should be correct", result.flags,
+                   equalTo(GeckoSession.FINDER_FIND_BACKWARDS
+                                            or GeckoSession.FINDER_FIND_MATCH_CASE
+                                            or GeckoSession.FINDER_FIND_WHOLE_WORD))
+
+        // And again using same flags.
+        result = sessionRule.waitForResult(mainSession.finder.find(
+                null, GeckoSession.FINDER_FIND_BACKWARDS
+                                        or GeckoSession.FINDER_FIND_MATCH_CASE
+                                        or GeckoSession.FINDER_FIND_WHOLE_WORD))
+
+        assertThat("Should be found", result.found, equalTo(true))
+        assertThat("Should not have wrapped", result.wrapped, equalTo(false))
+        assertThat("Current count should be correct", result.current, equalTo(1))
+        assertThat("Total count should be correct", result.total, equalTo(2))
+        assertThat("Search string should be correct",
+                   result.searchString, equalTo("dolore"))
+        assertThat("Flags should be correct", result.flags,
+                   equalTo(GeckoSession.FINDER_FIND_BACKWARDS
+                                            or GeckoSession.FINDER_FIND_MATCH_CASE
+                                            or GeckoSession.FINDER_FIND_WHOLE_WORD))
+
+        // And again but go forward.
+        result = sessionRule.waitForResult(mainSession.finder.find(
+                null, GeckoSession.FINDER_FIND_MATCH_CASE
+                                        or GeckoSession.FINDER_FIND_WHOLE_WORD))
+
+        assertThat("Should be found", result.found, equalTo(true))
+        assertThat("Should not have wrapped", result.wrapped, equalTo(false))
+        assertThat("Current count should be correct", result.current, equalTo(2))
+        assertThat("Total count should be correct", result.total, equalTo(2))
+        assertThat("Search string should be correct",
+                   result.searchString, equalTo("dolore"))
+        assertThat("Flags should be correct", result.flags,
+                   equalTo(GeckoSession.FINDER_FIND_MATCH_CASE
+                                            or GeckoSession.FINDER_FIND_WHOLE_WORD))
+    }
+
+    @Test fun find_notFound() {
+        mainSession.loadTestPath(LOREM_IPSUM_HTML_PATH)
+        mainSession.waitForPageStop()
+
+        var result = sessionRule.waitForResult(mainSession.finder.find("foo", 0))
+
+        assertThat("Should not be found", result.found, equalTo(false))
+        assertThat("Should have wrapped", result.wrapped, equalTo(true))
+        assertThat("Current count should be correct", result.current, equalTo(0))
+        assertThat("Total count should be correct", result.total, equalTo(0))
+        assertThat("Search string should be correct",
+                   result.searchString, equalTo("foo"))
+        assertThat("Flags should be correct", result.flags, equalTo(0))
+
+        result = sessionRule.waitForResult(mainSession.finder.find("lore", 0))
+
+        assertThat("Should be found", result.found, equalTo(true))
+    }
+
+    @Test fun find_matchCase() {
+        mainSession.loadTestPath(LOREM_IPSUM_HTML_PATH)
+        mainSession.waitForPageStop()
+
+        var result = sessionRule.waitForResult(mainSession.finder.find("lore", 0))
+
+        assertThat("Total count should be correct", result.total, equalTo(3))
+
+        result = sessionRule.waitForResult(mainSession.finder.find(
+                null, GeckoSession.FINDER_FIND_MATCH_CASE))
+
+        assertThat("Total count should be correct", result.total, equalTo(2))
+        assertThat("Flags should be correct",
+                   result.flags, equalTo(GeckoSession.FINDER_FIND_MATCH_CASE))
+    }
+
+    @Test fun find_wholeWord() {
+        mainSession.loadTestPath(LOREM_IPSUM_HTML_PATH)
+        mainSession.waitForPageStop()
+
+        var result = sessionRule.waitForResult(mainSession.finder.find("dolor", 0))
+
+        assertThat("Total count should be correct", result.total, equalTo(4))
+
+        result = sessionRule.waitForResult(mainSession.finder.find(
+                null, GeckoSession.FINDER_FIND_WHOLE_WORD))
+
+        assertThat("Total count should be correct", result.total, equalTo(2))
+        assertThat("Flags should be correct",
+                   result.flags, equalTo(GeckoSession.FINDER_FIND_WHOLE_WORD))
+    }
+
+    @Test fun find_linksOnly() {
+        mainSession.loadTestPath(LOREM_IPSUM_HTML_PATH)
+        mainSession.waitForPageStop()
+
+        val result = sessionRule.waitForResult(mainSession.finder.find(
+                "lore", GeckoSession.FINDER_FIND_LINKS_ONLY))
+
+        assertThat("Total count should be correct", result.total, equalTo(1))
+        assertThat("Flags should be correct",
+                   result.flags, equalTo(GeckoSession.FINDER_FIND_LINKS_ONLY))
+    }
+
+    @WithDevToolsAPI
+    @Test fun clear() {
+        mainSession.loadTestPath(LOREM_IPSUM_HTML_PATH)
+        mainSession.waitForPageStop()
+
+        val result = sessionRule.waitForResult(mainSession.finder.find("lore", 0))
+
+        assertThat("Match should be found", result.found, equalTo(true))
+
+        assertThat("Match should be selected",
+                   mainSession.evaluateJS("window.getSelection().toString()") as String,
+                   equalTo("Lore"))
+
+        mainSession.finder.clear()
+
+        assertThat("Match should be cleared",
+                   mainSession.evaluateJS("window.getSelection().isCollapsed") as Boolean,
+                   equalTo(true))
+    }
+}
\ No newline at end of file
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -11,17 +11,16 @@ import java.lang.annotation.RetentionPol
 import java.net.URLConnection;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.UUID;
 
 import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.gfx.LayerSession;
-import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEditableChild;
 import org.mozilla.gecko.GeckoThread;
 import org.mozilla.gecko.IGeckoEditableParent;
 import org.mozilla.gecko.mozglue.JNIObject;
 import org.mozilla.gecko.NativeQueue;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
@@ -29,17 +28,16 @@ import org.mozilla.gecko.util.ThreadUtil
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.graphics.RectF;
 import android.net.Uri;
 import android.os.Binder;
-import android.os.Bundle;
 import android.os.IBinder;
 import android.os.IInterface;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.SystemClock;
 import android.support.annotation.IntDef;
 import android.support.annotation.Nullable;
 import android.support.annotation.NonNull;
@@ -89,26 +87,24 @@ public class GeckoSession extends LayerS
 
     private final NativeQueue mNativeQueue =
         new NativeQueue(State.INITIAL, State.READY);
 
     private final EventDispatcher mEventDispatcher =
         new EventDispatcher(mNativeQueue);
 
     private final SessionTextInput mTextInput = new SessionTextInput(this, mNativeQueue);
-
-    private SessionAccessibility mSessionAccessibility;
+    private SessionAccessibility mAccessibility;
+    private SessionFinder mFinder;
 
     private String mId = UUID.randomUUID().toString().replace("-", "");
     /* package */ String getId() { return mId; }
 
-    private static abstract class CallbackResult<T> extends GeckoResult<T> implements EventCallback {
-        @Override
-        public abstract void sendSuccess(Object response);
-
+    /* package */ static abstract class CallbackResult<T> extends GeckoResult<T>
+                                                          implements EventCallback {
         @Override
         public void sendError(Object response) {
             completeExceptionally(response != null ?
                     new Exception(response.toString()) :
                     new UnknownError());
         }
     }
 
@@ -866,20 +862,20 @@ public class GeckoSession extends LayerS
     }
 
     /**
       * Get the SessionAccessibility instance for this session.
       *
       * @return SessionAccessibility instance.
       */
     public @NonNull SessionAccessibility getAccessibility() {
-        if (mSessionAccessibility == null) {
-            mSessionAccessibility = new SessionAccessibility(this);
+        if (mAccessibility == null) {
+            mAccessibility = new SessionAccessibility(this);
         }
-        return mSessionAccessibility;
+        return mAccessibility;
     }
 
     @IntDef(flag = true,
             value = { LOAD_FLAGS_NONE, LOAD_FLAGS_BYPASS_CACHE, LOAD_FLAGS_BYPASS_PROXY,
                       LOAD_FLAGS_EXTERNAL, LOAD_FLAGS_ALLOW_POPUPS })
     public @interface LoadFlags {}
 
     // These flags follow similarly named ones in Gecko's nsIWebNavigation.idl
@@ -1086,16 +1082,99 @@ public class GeckoSession extends LayerS
 
     /**
     * Go forward in history.
     */
     public void goForward() {
         mEventDispatcher.dispatch("GeckoView:GoForward", null);
     }
 
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true,
+            value = {FINDER_FIND_BACKWARDS, FINDER_FIND_LINKS_ONLY,
+                    FINDER_FIND_MATCH_CASE, FINDER_FIND_WHOLE_WORD})
+    /* package */ @interface FinderFindFlags {}
+
+    /** Go backwards when finding the next match. */
+    public static final int FINDER_FIND_BACKWARDS = 1;
+    /** Perform case-sensitive match; default is to perform a case-insensitive match. */
+    public static final int FINDER_FIND_MATCH_CASE = 1 << 1;
+    /** Must match entire words; default is to allow matching partial words. */
+    public static final int FINDER_FIND_WHOLE_WORD = 1 << 2;
+    /** Limit matches to links on the page. */
+    public static final int FINDER_FIND_LINKS_ONLY = 1 << 3;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true,
+            value = {FINDER_DISPLAY_HIGHLIGHT_ALL, FINDER_DISPLAY_DIM_PAGE,
+                    FINDER_DISPLAY_DRAW_LINK_OUTLINE})
+    /* package */ @interface FinderDisplayFlags {}
+
+    /** Highlight all find-in-page matches. */
+    public static final int FINDER_DISPLAY_HIGHLIGHT_ALL = 1;
+    /** Dim the rest of the page when showing a find-in-page match. */
+    public static final int FINDER_DISPLAY_DIM_PAGE = 1 << 1;
+    /** Draw outlines around matching links. */
+    public static final int FINDER_DISPLAY_DRAW_LINK_OUTLINE = 1 << 2;
+
+    /**
+     * Represent the result of a find-in-page operation.
+     */
+    public static final class FinderResult {
+        /** Whether a match was found. */
+        public final boolean found;
+        /** Whether the search wrapped around the top or bottom of the page. */
+        public final boolean wrapped;
+        /** Ordinal number of the current match starting from 1, or 0 if no match. */
+        public final int current;
+        /** Total number of matches found so far, or -1 if unknown. */
+        public final int total;
+        /** Search string. */
+        @NonNull public final String searchString;
+        /** Flags used for the search; either 0 or a combination of {@link #FINDER_FIND_BACKWARDS
+         * FINDER_FIND_*} flags. */
+        @FinderFindFlags public final int flags;
+        /** URI of the link, if the current match is a link, or null otherwise. */
+        @Nullable public final String linkUri;
+        /** Bounds of the current match in client coordinates, or null if unknown. */
+        @Nullable public final RectF clientRect;
+
+        /* package */ FinderResult(@NonNull final GeckoBundle bundle) {
+            found = bundle.getBoolean("found");
+            wrapped = bundle.getBoolean("wrapped");
+            current = bundle.getInt("current", 0);
+            total = bundle.getInt("total", -1);
+            searchString = bundle.getString("searchString");
+            flags = SessionFinder.getFlagsFromBundle(bundle.getBundle("flags"));
+            linkUri = bundle.getString("linkURL");
+
+            final GeckoBundle rectBundle = bundle.getBundle("clientRect");
+            if (rectBundle == null) {
+                clientRect = null;
+            } else {
+                clientRect = new RectF((float) rectBundle.getDouble("left"),
+                                       (float) rectBundle.getDouble("top"),
+                                       (float) rectBundle.getDouble("right"),
+                                       (float) rectBundle.getDouble("bottom"));
+            }
+        }
+    }
+
+    /**
+     * Get the SessionFinder instance for this session, to perform find-in-page operations.
+     *
+     * @return SessionFinder instance.
+     */
+    public SessionFinder getFinder() {
+        if (mFinder == null) {
+            mFinder = new SessionFinder(getEventDispatcher());
+        }
+        return mFinder;
+    }
+
     /**
     * Set this GeckoSession as active or inactive. Setting a GeckoSession to inactive will
     * significantly reduce its memory footprint, but should only be done if the
     * GeckoSession is not currently visible.
     * @param active A boolean determining whether the GeckoSession is active
     */
     public void setActive(boolean active) {
         final GeckoBundle msg = new GeckoBundle();
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionFinder.java
@@ -0,0 +1,139 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.geckoview;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.GeckoBundle;
+import org.mozilla.geckoview.GeckoSession.FinderFindFlags;
+import org.mozilla.geckoview.GeckoSession.FinderDisplayFlags;
+import org.mozilla.geckoview.GeckoSession.FinderResult;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Pair;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * {@code SessionFinder} instances returned by {@link GeckoSession#getFinder()} performs
+ * find-in-page operations.
+ */
+public final class SessionFinder {
+    private static final String LOGTAG = "GeckoSessionFinder";
+
+    private static final List<Pair<Integer, String>> sFlagNames = Arrays.asList(
+            new Pair<>(GeckoSession.FINDER_FIND_BACKWARDS, "backwards"),
+            new Pair<>(GeckoSession.FINDER_FIND_LINKS_ONLY, "linksOnly"),
+            new Pair<>(GeckoSession.FINDER_FIND_MATCH_CASE, "matchCase"),
+            new Pair<>(GeckoSession.FINDER_FIND_WHOLE_WORD, "wholeWord")
+    );
+
+    private static void addFlagsToBundle(@FinderFindFlags final int flags,
+                                         @NonNull final GeckoBundle bundle) {
+        for (final Pair<Integer, String> name : sFlagNames) {
+            if ((flags & name.first) != 0) {
+                bundle.putBoolean(name.second, true);
+            }
+        }
+    }
+
+    /* package */ static int getFlagsFromBundle(@Nullable final GeckoBundle bundle) {
+        if (bundle == null) {
+            return 0;
+        }
+
+        int flags = 0;
+        for (final Pair<Integer, String> name : sFlagNames) {
+            if (bundle.getBoolean(name.second)) {
+                flags |= name.first;
+            }
+        }
+        return flags;
+    }
+
+    private final EventDispatcher mDispatcher;
+    @FinderDisplayFlags private int mDisplayFlags;
+
+    /* package */ SessionFinder(@NonNull final EventDispatcher dispatcher) {
+        mDispatcher = dispatcher;
+        setDisplayFlags(0);
+    }
+
+    /**
+     * Find and select a string on the current page, starting from the current selection or the
+     * start of the page if there is no selection. Optionally return results related to the
+     * search in a {@link FinderResult} object. If {@code searchString} is null, search
+     * is performed using the previous search string.
+     *
+     * @param searchString String to search, or null to find again using the previous string.
+     * @param flags Flags for performing the search; either 0 or a combination of {@link
+     *              GeckoSession#FINDER_FIND_BACKWARDS FINDER_FIND_*} constants.
+     * @return Result of the search operation as a {@link GeckoResult} object.
+     * @see #clear
+     * @see #setDisplayFlags
+     */
+    @NonNull
+    public GeckoResult<FinderResult> find(@Nullable final String searchString,
+                                          @FinderFindFlags final int flags) {
+        final GeckoBundle bundle = new GeckoBundle(sFlagNames.size() + 1);
+        bundle.putString("searchString", searchString);
+        addFlagsToBundle(flags, bundle);
+
+        final GeckoSession.CallbackResult<FinderResult> result =
+                new GeckoSession.CallbackResult<FinderResult>() {
+            @Override
+            public void sendSuccess(Object response) {
+                complete(new FinderResult((GeckoBundle) response));
+            }
+        };
+        mDispatcher.dispatch("GeckoView:FindInPage", bundle, result);
+        return result;
+    }
+
+    /**
+     * Clear any highlighted find-in-page matches.
+     *
+     * @see #find
+     * @see #setDisplayFlags
+     */
+    public void clear() {
+        mDispatcher.dispatch("GeckoView:ClearMatches", null);
+    }
+
+    /**
+     * Return flags for displaying find-in-page matches.
+     *
+     * @return Display flags as a combination of {@link GeckoSession#FINDER_DISPLAY_HIGHLIGHT_ALL
+     *         FINDER_DISPLAY_*} constants.
+     * @see #setDisplayFlags
+     * @see #find
+     */
+    @FinderDisplayFlags public int getDisplayFlags() {
+        return mDisplayFlags;
+    }
+
+    /**
+     * Set flags for displaying find-in-page matches.
+     *
+     * @param flags Display flags as a combination of {@link
+     *              GeckoSession#FINDER_DISPLAY_HIGHLIGHT_ALL FINDER_DISPLAY_*} constants.
+     * @see #getDisplayFlags
+     * @see #find
+     */
+    public void setDisplayFlags(@FinderDisplayFlags final int flags) {
+        mDisplayFlags = flags;
+
+        final GeckoBundle bundle = new GeckoBundle(3);
+        bundle.putBoolean("highlightAll",
+                          (flags & GeckoSession.FINDER_DISPLAY_HIGHLIGHT_ALL) != 0);
+        bundle.putBoolean("dimPage",
+                          (flags & GeckoSession.FINDER_DISPLAY_DIM_PAGE) != 0);
+        bundle.putBoolean("drawOutline",
+                          (flags & GeckoSession.FINDER_DISPLAY_DRAW_LINK_OUTLINE) != 0);
+        mDispatcher.dispatch("GeckoView:DisplayMatches", bundle);
+    }
+}
--- a/mobile/android/modules/geckoview/GeckoViewContent.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewContent.jsm
@@ -12,16 +12,19 @@ ChromeUtils.import("resource://gre/modul
 XPCOMUtils.defineLazyModuleGetters(this, {
   Services: "resource://gre/modules/Services.jsm",
 });
 
 class GeckoViewContent extends GeckoViewModule {
   onInit() {
     this.registerListener([
         "GeckoViewContent:ExitFullScreen",
+        "GeckoView:ClearMatches",
+        "GeckoView:DisplayMatches",
+        "GeckoView:FindInPage",
         "GeckoView:RestoreState",
         "GeckoView:SaveState",
         "GeckoView:SetActive",
         "GeckoView:ZoomToInput",
     ]);
 
     this.messageManager.addMessageListener("GeckoView:SaveStateFinish", this);
   }
@@ -53,16 +56,28 @@ class GeckoViewContent extends GeckoView
   // Bundle event handler.
   onEvent(aEvent, aData, aCallback) {
     debug `onEvent: event=${aEvent}, data=${aData}`;
 
     switch (aEvent) {
       case "GeckoViewContent:ExitFullScreen":
         this.messageManager.sendAsyncMessage("GeckoView:DOMFullscreenExited");
         break;
+      case "GeckoView:ClearMatches": {
+        this._clearMatches();
+        break;
+      }
+      case "GeckoView:DisplayMatches": {
+        this._displayMatches(aData);
+        break;
+      }
+      case "GeckoView:FindInPage": {
+        this._findInPage(aData, aCallback);
+        break;
+      }
       case "GeckoView:ZoomToInput":
         this.messageManager.sendAsyncMessage(aEvent);
         break;
       case "GeckoView:SetActive":
         if (aData.active) {
           this.browser.setAttribute("primary", "true");
           this.browser.focus();
           this.browser.docShellIsActive = true;
@@ -143,9 +158,160 @@ class GeckoViewContent extends GeckoView
 
         this.eventDispatcher.sendRequest({
           type: "GeckoView:ContentCrash"
         });
       }
       break;
     }
   }
+
+  _findInPage(aData, aCallback) {
+    debug `findInPage: data=${aData} callback=${aCallback && "non-null"}`;
+
+    let finder;
+    try {
+      finder = this.browser.finder;
+    } catch (e) {
+      if (aCallback) {
+        aCallback.onError(`No finder: ${e}`);
+      }
+      return;
+    }
+
+    if (this._finderListener) {
+      finder.removeResultListener(this._finderListener);
+    }
+
+    this._finderListener = {
+      response: {
+        found: false,
+        wrapped: false,
+        current: 0,
+        total: -1,
+        searchString: aData.searchString || finder.searchString,
+        linkURL: null,
+        clientRect: null,
+        flags: {
+          backwards: !!aData.backwards,
+          linksOnly: !!aData.linksOnly,
+          matchCase: !!aData.matchCase,
+          wholeWord: !!aData.wholeWord,
+        },
+      },
+
+      onFindResult(aOptions) {
+        if (!aCallback || aOptions.searchString !== aData.searchString) {
+          // Result from a previous search.
+          return;
+        }
+
+        Object.assign(this.response, {
+          found: aOptions.result !== Ci.nsITypeAheadFind.FIND_NOTFOUND,
+          wrapped: aOptions.result !== Ci.nsITypeAheadFind.FIND_FOUND,
+          linkURL: aOptions.linkURL,
+          clientRect: aOptions.rect && {
+            left: aOptions.rect.left,
+            top: aOptions.rect.top,
+            right: aOptions.rect.right,
+            bottom: aOptions.rect.bottom,
+          },
+          flags: {
+            backwards: aOptions.findBackwards,
+            linksOnly: aOptions.linksOnly,
+            matchCase: this.response.flags.matchCase,
+            wholeWord: this.response.flags.wholeWord,
+          },
+        });
+
+        if (!this.response.found) {
+          this.response.current = 0;
+          this.response.total = 0;
+        }
+
+        // Only send response if we have a count.
+        if (!this.response.found || this.response.current !== 0) {
+          debug `onFindResult: ${this.response}`;
+          aCallback.onSuccess(this.response);
+          aCallback = undefined;
+        }
+      },
+
+      onMatchesCountResult(aResult) {
+        if (!aCallback || finder.searchString !== aData.searchString) {
+          // Result from a previous search.
+          return;
+        }
+
+        Object.assign(this.response, {
+          current: aResult.current,
+          total: aResult.total,
+        });
+
+        // Only send response if we have a result. `found` and `wrapped` are
+        // both false only when we haven't received a result yet.
+        if (this.response.found || this.response.wrapped) {
+          debug `onMatchesCountResult: ${this.response}`;
+          aCallback.onSuccess(this.response);
+          aCallback = undefined;
+        }
+      },
+    };
+
+    finder.caseSensitive = !!aData.matchCase;
+    finder.entireWord = !!aData.wholeWord;
+
+    if (aCallback) {
+      finder.addResultListener(this._finderListener);
+    }
+
+    const drawOutline = this._matchDisplayOptions &&
+                        !!this._matchDisplayOptions.drawOutline;
+
+    if (!aData.searchString || aData.searchString === finder.searchString) {
+      // Search again.
+      aData.searchString = finder.searchString;
+      finder.findAgain(!!aData.backwards,
+                       !!aData.linksOnly,
+                       drawOutline);
+    } else {
+      finder.fastFind(aData.searchString,
+                      !!aData.linksOnly,
+                      drawOutline);
+    }
+  }
+
+  _clearMatches() {
+    try {
+      this.browser.finder.removeSelection();
+    } catch (e) {
+    }
+  }
+
+  _displayMatches(aData) {
+    debug `displayMatches: data=${aData}`;
+
+    let finder;
+    try {
+      finder = this.browser.finder;
+    } catch (e) {
+      return;
+    }
+
+    this._matchDisplayOptions = aData;
+    finder.onHighlightAllChange(!!aData.highlightAll);
+    finder.onModalHighlightChange(!!aData.dimPage);
+
+    if (!finder.searchString) {
+      return;
+    }
+    if (!aData.highlightAll && !aData.dimPage && !aData.drawOutline) {
+      finder.highlighter.highlight(false);
+      return;
+    }
+    const linksOnly = this._finderListener &&
+                      this._finderListener.response.linksOnly;
+    finder.highlighter.highlight(true,
+                                 finder.searchString,
+                                 linksOnly,
+                                 !!aData.drawOutline);
+  }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/EnvironmentUtils.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/EnvironmentUtils.java
@@ -8,21 +8,21 @@ import android.content.Context;
 import android.content.SharedPreferences;
 import android.support.annotation.Nullable;
 
 /**
  * Provides information about the current environment.
  */
 public class EnvironmentUtils {
     /**
-     * Must be kept in-sync with {@link org.mozilla.gecko.GeckoApp.PREFS_IS_FIRST_RUN}.
+     * Must be kept in-sync with {@link org.mozilla.gecko.GeckoApp#PREFS_IS_FIRST_RUN}.
      */
     private static final String GECKO_PREFS_IS_FIRST_RUN = "telemetry-isFirstRun";
     /**
-     * Must be kept in-sync with {@link org.mozilla.gecko.BrowserApp.FIRSTRUN_UUID}.
+     * Must be kept in-sync with {@link org.mozilla.gecko.firstrun.OnboardingHelper#FIRSTRUN_UUID}.
      */
     private static final String GECKO_PREFS_FIRSTRUN_UUID = "firstrun_uuid";
 
     public static boolean isFirstRun(final Context context) {
         return getDefaultGeckoSharedPreferences(context).getBoolean(GECKO_PREFS_IS_FIRST_RUN, true);
     }
 
     @Nullable
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java
@@ -12,25 +12,24 @@ import android.test.ActivityInstrumentat
 import android.text.TextUtils;
 import android.util.Log;
 
 import com.robotium.solo.Solo;
 
 import org.mozilla.gecko.Actions;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.Assert;
-import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.Driver;
 import org.mozilla.gecko.FennecInstrumentationTestRunner;
 import org.mozilla.gecko.FennecMochitestAssert;
 import org.mozilla.gecko.FennecNativeActions;
 import org.mozilla.gecko.FennecNativeDriver;
 import org.mozilla.gecko.FennecTalosAssert;
-import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.firstrun.OnboardingHelper;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.util.Map;
 
 @SuppressWarnings("unchecked")
 public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<Activity> {
@@ -121,17 +120,17 @@ public abstract class BaseRobocopTest ex
         return BaseRobocopTest.createActivityIntent(mConfig);
     }
 
     // Static function to allow re-use.
     public static Intent createActivityIntent(Map<String, String> config) {
         final Intent intent = new Intent(Intent.ACTION_MAIN);
         intent.putExtra("args", "-no-remote -profile " + config.get("profile"));
         // Don't show the first run experience.
-        intent.putExtra(BrowserApp.EXTRA_SKIP_STARTPANE, true);
+        intent.putExtra(OnboardingHelper.EXTRA_SKIP_STARTPANE, true);
 
         final String envString = config.get("envvars");
         if (!TextUtils.isEmpty(envString)) {
             final String[] envStrings = envString.split(",");
 
             for (int iter = 0; iter < envStrings.length; iter++) {
                 intent.putExtra("env" + iter, envStrings[iter]);
             }
--- a/mozglue/build/WindowsDllBlocklist.cpp
+++ b/mozglue/build/WindowsDllBlocklist.cpp
@@ -369,16 +369,55 @@ static wchar_t* lastslash(wchar_t* s, in
   for (wchar_t* c = s + len - 1; c >= s; --c) {
     if (*c == L'\\' || *c == L'/') {
       return c;
     }
   }
   return nullptr;
 }
 
+
+#ifdef ENABLE_TESTS
+DllLoadHookType gDllLoadHook = nullptr;
+
+void
+DllBlocklist_SetDllLoadHook(DllLoadHookType aHook)
+{
+  gDllLoadHook = aHook;
+}
+
+void
+CallDllLoadHook(bool aDllLoaded, NTSTATUS aStatus, HANDLE aDllBase, PUNICODE_STRING aDllName)
+{
+  if (gDllLoadHook) {
+    gDllLoadHook(aDllLoaded, aStatus, aDllBase, aDllName);
+  }
+}
+
+CreateThreadHookType gCreateThreadHook = nullptr;
+
+void
+DllBlocklist_SetCreateThreadHook(CreateThreadHookType aHook)
+{
+  gCreateThreadHook = aHook;
+}
+
+void
+CallCreateThreadHook(bool aWasAllowed, void* aStartAddress)
+{
+  if (gCreateThreadHook) {
+    gCreateThreadHook(aWasAllowed, aStartAddress);
+  }
+}
+
+#else // ENABLE_TESTS
+#define CallDllLoadHook(...)
+#define CallCreateThreadHook(...)
+#endif // ENABLE_TESTS
+
 static NTSTATUS NTAPI
 patched_LdrLoadDll (PWCHAR filePath, PULONG flags, PUNICODE_STRING moduleFileName, PHANDLE handle)
 {
   // We have UCS2 (UTF16?), we want ASCII, but we also just want the filename portion
 #define DLLNAME_MAX 128
   char dllName[DLLNAME_MAX+1];
   wchar_t *dll_part;
   char *dot;
@@ -448,26 +487,28 @@ patched_LdrLoadDll (PWCHAR filePath, PUL
   if (!(sInitFlags & eDllBlocklistInitFlagWasBootstrapped)) {
     // Block a suspicious binary that uses various 12-digit hex strings
     // e.g. MovieMode.48CA2AEFA22D.dll (bug 973138)
     dot = strchr(dllName, '.');
     if (dot && (strchr(dot+1, '.') == dot+13)) {
       char * end = nullptr;
       _strtoui64(dot+1, &end, 16);
       if (end == dot+13) {
+        CallDllLoadHook(false, STATUS_DLL_NOT_FOUND, 0, moduleFileName);
         return STATUS_DLL_NOT_FOUND;
       }
     }
     // Block binaries where the filename is at least 16 hex digits
     if (dot && ((dot - dllName) >= 16)) {
       char * current = dllName;
       while (current < dot && isxdigit(*current)) {
         current++;
       }
       if (current == dot) {
+        CallDllLoadHook(false, STATUS_DLL_NOT_FOUND, 0, moduleFileName);
         return STATUS_DLL_NOT_FOUND;
       }
     }
 
     // then compare to everything on the blocklist
     DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY(info);
     while (info->name) {
       if (strcmp(info->name, dllName) == 0)
@@ -505,16 +546,17 @@ patched_LdrLoadDll (PWCHAR filePath, PUL
         if (sentinel.BailOut()) {
           goto continue_loading;
         }
 
         full_fname = getFullPath(filePath, fname);
         if (!full_fname) {
           // uh, we couldn't find the DLL at all, so...
           printf_stderr("LdrLoadDll: Blocking load of '%s' (SearchPathW didn't find it?)\n", dllName);
+          CallDllLoadHook(false, STATUS_DLL_NOT_FOUND, 0, moduleFileName);
           return STATUS_DLL_NOT_FOUND;
         }
 
         if (info->flags & DllBlockInfo::USE_TIMESTAMP) {
           fVersion = GetTimestamp(full_fname.get());
           if (fVersion > info->maxVersion) {
             load_ok = true;
           }
@@ -543,16 +585,17 @@ patched_LdrLoadDll (PWCHAR filePath, PUL
             }
           }
         }
       }
 
       if (!load_ok) {
         printf_stderr("LdrLoadDll: Blocking load of '%s' -- see http://www.mozilla.com/en-US/blocklist/\n", dllName);
         DllBlockSet::Add(info->name, fVersion);
+        CallDllLoadHook(false, STATUS_DLL_NOT_FOUND, 0, moduleFileName);
         return STATUS_DLL_NOT_FOUND;
       }
     }
   }
 
 continue_loading:
 #ifdef DEBUG_very_verbose
     printf_stderr("LdrLoadDll: continuing load... ('%S')\n", moduleFileName->Buffer);
@@ -565,17 +608,19 @@ continue_loading:
                           __LINE__);
 
 #ifdef _M_AMD64
   // Prevent the stack walker from suspending this thread when LdrLoadDll
   // holds the RtlLookupFunctionEntry lock.
   AutoSuppressStackWalking suppress;
 #endif
 
-  return stub_LdrLoadDll(filePath, flags, moduleFileName, handle);
+  NTSTATUS ret = stub_LdrLoadDll(filePath, flags, moduleFileName, handle);
+  CallDllLoadHook(true, ret, handle ? *handle : 0, moduleFileName);
+  return ret;
 }
 
 #if defined(NIGHTLY_BUILD)
 // Map of specific thread proc addresses we should block. In particular,
 // LoadLibrary* APIs which indicate DLL injection
 static mozilla::Vector<void*, 4>* gStartAddressesToBlock;
 #endif
 
@@ -611,17 +656,20 @@ NopThreadProc(void* /* aThreadParam */)
   return 0;
 }
 
 static MOZ_NORETURN void __fastcall
 patched_BaseThreadInitThunk(BOOL aIsInitialThread, void* aStartAddress,
                             void* aThreadParam)
 {
   if (ShouldBlockThread(aStartAddress)) {
+    CallCreateThreadHook(false, aStartAddress);
     aStartAddress = (void*)NopThreadProc;
+  } else {
+    CallCreateThreadHook(true, aStartAddress);
   }
 
   stub_BaseThreadInitThunk(aIsInitialThread, aStartAddress, aThreadParam);
 }
 
 
 static WindowsDllInterceptor NtDllIntercept;
 static WindowsDllInterceptor Kernel32Intercept;
--- a/mozglue/build/WindowsDllBlocklist.h
+++ b/mozglue/build/WindowsDllBlocklist.h
@@ -4,32 +4,43 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_windowsdllblocklist_h
 #define mozilla_windowsdllblocklist_h
 
 #if (defined(_MSC_VER) || defined(__MINGW32__))  && (defined(_M_IX86) || defined(_M_X64))
 
 #include <windows.h>
+#ifdef ENABLE_TESTS
+#include <winternl.h>
+#endif // ENABLE_TESTS
 #include "mozilla/Attributes.h"
 #include "mozilla/Types.h"
 
 #define HAS_DLL_BLOCKLIST
 
 enum DllBlocklistInitFlags
 {
   eDllBlocklistInitFlagDefault = 0,
   eDllBlocklistInitFlagIsChildProcess = 1,
   eDllBlocklistInitFlagWasBootstrapped = 2
 };
 
 MFBT_API void DllBlocklist_Initialize(uint32_t aInitFlags = eDllBlocklistInitFlagDefault);
 MFBT_API void DllBlocklist_WriteNotes(HANDLE file);
 MFBT_API bool DllBlocklist_CheckStatus();
 
+#ifdef ENABLE_TESTS
+typedef void (*DllLoadHookType)(bool aDllLoaded, NTSTATUS aNtStatus,
+                                HANDLE aDllBase, PUNICODE_STRING aDllName);
+MFBT_API void DllBlocklist_SetDllLoadHook(DllLoadHookType aHook);
+typedef void (*CreateThreadHookType)(bool aWasAllowed, void *aStartAddress);
+MFBT_API void DllBlocklist_SetCreateThreadHook(CreateThreadHookType aHook);
+#endif // ENABLE_TESTS
+
 // Forward declaration
 namespace mozilla {
 namespace glue {
 namespace detail {
 class DllServicesBase;
 } // namespace detail
 } // namespace glue
 } // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/mozglue/tests/gtest/Injector/Injector.cpp
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <Windows.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+int
+main(int argc, char** argv)
+{
+  if (argc < 4) {
+    fprintf(stderr,
+            "Not enough command line arguments.\n"
+            "Command line syntax:\n"
+            "Injector.exe [pid] [startAddr] [threadParam]\n");
+    return 1;
+  }
+
+  DWORD pid = strtoul(argv[1], 0, 0);
+#ifdef HAVE_64BIT_BUILD
+  void* startAddr = (void*)strtoull(argv[2], 0, 0);
+  void* threadParam = (void*)strtoull(argv[3], 0, 0);
+#else
+  void* startAddr = (void*)strtoul(argv[2], 0, 0);
+  void* threadParam = (void*)strtoul(argv[3], 0, 0);
+#endif
+  HANDLE targetProc = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
+                                  PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
+                                  FALSE,
+                                  pid);
+  if (targetProc == nullptr) {
+    fprintf(stderr, "Error %lu opening target process, PID %lu \n", GetLastError(), pid);
+    return 1;
+  }
+
+  HANDLE hThread = CreateRemoteThread(targetProc, nullptr, 0,
+                                      (LPTHREAD_START_ROUTINE)startAddr,
+                                      threadParam, 0, nullptr);
+  if (hThread == nullptr) {
+    fprintf(stderr, "Error %lu in CreateRemoteThread\n", GetLastError());
+    CloseHandle(targetProc);
+    return 1;
+  }
+
+  CloseHandle(hThread);
+  CloseHandle(targetProc);
+
+  return 0;
+}
new file mode 100644
--- /dev/null
+++ b/mozglue/tests/gtest/Injector/moz.build
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIST_INSTALL = False
+
+SimplePrograms(['Injector'])
+
+TEST_HARNESS_FILES.gtest += ['!Injector.exe']
new file mode 100644
--- /dev/null
+++ b/mozglue/tests/gtest/InjectorDLL/InjectorDLL.cpp
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <Windows.h>
+
+BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID)
+{
+  return TRUE;
+}
+
new file mode 100644
--- /dev/null
+++ b/mozglue/tests/gtest/InjectorDLL/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+DIST_INSTALL = False
+
+SharedLibrary('InjectorDLL')
+
+UNIFIED_SOURCES = [
+    'InjectorDLL.cpp',
+]
+
+TEST_HARNESS_FILES.gtest += ['!InjectorDLL.dll']
new file mode 100644
--- /dev/null
+++ b/mozglue/tests/gtest/TestDLLEject.cpp
@@ -0,0 +1,277 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <Windows.h>
+#include <winternl.h>
+#include "gtest/gtest.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsUnicharUtils.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WindowsDllBlocklist.h"
+
+static HANDLE sThreadWasBlocked = 0;
+static HANDLE sThreadWasAllowed = 0;
+static HANDLE sDllWasLoaded = 0;
+static uintptr_t sStartAddress = 0;
+
+static const int sTimeoutMS = 10000;
+
+#define DLL_LEAF_NAME (u"InjectorDLL.dll")
+
+static nsString
+makeString(PUNICODE_STRING aOther)
+{
+  size_t numChars = aOther->Length / sizeof(WCHAR);
+  return nsString((const char16_t *)aOther->Buffer, numChars);
+}
+
+static void
+DllLoadHook(bool aDllLoaded, NTSTATUS aStatus, HANDLE aDllBase,
+            PUNICODE_STRING aDllName)
+{
+  nsString str = makeString(aDllName);
+
+  nsString dllName = nsString(DLL_LEAF_NAME);
+  if (StringEndsWith(str, dllName, nsCaseInsensitiveStringComparator())) {
+    if (aDllLoaded) {
+      SetEvent(sDllWasLoaded);
+    }
+  }
+}
+
+static void
+CreateThreadHook(bool aWasAllowed, void* aStartAddress)
+{
+  if (sStartAddress == (uintptr_t)aStartAddress) {
+    if (!aWasAllowed) {
+      SetEvent(sThreadWasBlocked);
+    } else {
+      SetEvent(sThreadWasAllowed);
+    }
+  }
+}
+
+/**
+ * This function tests that we correctly block DLLs injected into this process
+ * via an injection technique which calls CreateRemoteThread with LoadLibrary*()
+ * as the thread start address, and the path to the DLL as the thread param.
+ *
+ * We prevent this technique by blocking threads with a start address in any
+ * LoadLibrary*() APIs.
+ *
+ * This function launches Injector.exe which simulates a 3rd-party application
+ * executing this technique.
+ *
+ * @param aGetArgsProc  A callable procedure that specifies the thread start
+ *                      address and thread param passed as arguments to
+ *                      Injector.exe, which are in turn passed as arguments to
+ *                      CreateRemoteThread. This procedure is defined as such:
+ *
+ *                      void (*aGetArgsProc)(const nsString& aDllPath,
+ *                                           const nsCString& aDllPathC,
+ *                                           uintptr_t& startAddress,
+ *                                           uintptr_t& threadParam);
+ *
+ *                      aDllPath is a WCHAR-friendly path to InjectorDLL.dll.
+ *                      Its memory will persist during the injection attempt.
+ *
+ *                      aDllPathC is the equivalent char-friendly path.
+ *
+ *                      startAddress and threadParam are passed into
+ *                      CreateRemoteThread as arguments.
+ */
+template<typename TgetArgsProc>
+static void
+DoTest_CreateRemoteThread_LoadLibrary(TgetArgsProc aGetArgsProc)
+{
+  sThreadWasBlocked = CreateEvent(NULL, FALSE, FALSE, NULL);
+  if (!sThreadWasBlocked) {
+    EXPECT_TRUE(!"Unable to create sThreadWasBlocked event");
+    ASSERT_EQ(GetLastError(), ERROR_SUCCESS);
+  }
+
+  sThreadWasAllowed = CreateEvent(NULL, FALSE, FALSE, NULL);
+  if (!sThreadWasAllowed) {
+    EXPECT_TRUE(!"Unable to create sThreadWasAllowed event");
+    ASSERT_EQ(GetLastError(), ERROR_SUCCESS);
+  }
+
+  sDllWasLoaded = CreateEvent(NULL, FALSE, FALSE, NULL);
+  if (!sDllWasLoaded) {
+    EXPECT_TRUE(!"Unable to create sDllWasLoaded event");
+    ASSERT_EQ(GetLastError(), ERROR_SUCCESS);
+  }
+
+  auto closeEvents = mozilla::MakeScopeExit([&](){
+    CloseHandle(sThreadWasAllowed);
+    CloseHandle(sThreadWasBlocked);
+    CloseHandle(sDllWasLoaded);
+  });
+
+  // Hook into our DLL and thread blocking routines during this test.
+  DllBlocklist_SetDllLoadHook(DllLoadHook);
+  DllBlocklist_SetCreateThreadHook(CreateThreadHook);
+  auto undoHooks = mozilla::MakeScopeExit([&](){
+    DllBlocklist_SetDllLoadHook(nullptr);
+    DllBlocklist_SetCreateThreadHook(nullptr);
+  });
+
+  // Launch Injector.exe.
+  STARTUPINFOW si = { 0 };
+  si.cb = sizeof(si);
+  ::GetStartupInfoW(&si);
+  PROCESS_INFORMATION pi = { 0 };
+
+  nsString path(u"Injector.exe");
+  nsString dllPath(DLL_LEAF_NAME);
+  nsCString dllPathC = NS_ConvertUTF16toUTF8(dllPath);
+
+  uintptr_t threadParam;
+  aGetArgsProc(dllPath, dllPathC, sStartAddress, threadParam);
+
+  path.AppendPrintf(" %lu 0x%p 0x%p", GetCurrentProcessId(), sStartAddress,
+                    threadParam);
+  if (::CreateProcessW(NULL, path.get(), 0, 0, FALSE, 0, NULL, NULL,
+    &si, &pi) == FALSE) {
+    EXPECT_TRUE(!"Error in CreateProcessW() launching Injector.exe");
+    ASSERT_EQ(GetLastError(), ERROR_SUCCESS);
+    return;
+  }
+
+  // Ensure Injector.exe doesn't stay running after this test finishes.
+  auto cleanup = mozilla::MakeScopeExit([&](){
+    CloseHandle(pi.hThread);
+    EXPECT_TRUE("Shutting down.");
+    WaitForSingleObject(pi.hProcess, INFINITE);
+    CloseHandle(pi.hProcess);
+  });
+
+  // Wait for information to come in and complete the test.
+  HANDLE handles[] = {
+    sThreadWasBlocked,
+    sThreadWasAllowed,
+    sDllWasLoaded,
+    pi.hProcess
+  };
+  int handleCount = mozilla::ArrayLength(handles);
+  bool keepGoing = true; // Set to false to signal that the test is over.
+
+  while(keepGoing) {
+    switch(WaitForMultipleObjectsEx(handleCount, handles,
+                                    FALSE, sTimeoutMS, FALSE)) {
+      case WAIT_OBJECT_0: { // sThreadWasBlocked
+        EXPECT_TRUE("Thread was blocked successfully.");
+        // No need to continue testing; blocking was successful.
+        keepGoing = false;
+        break;
+      }
+      case WAIT_OBJECT_0 + 1: { // sThreadWasAllowed
+        EXPECT_TRUE(!"Thread was allowed but should have been blocked.");
+        // No need to continue testing; blocking failed.
+        keepGoing = false;
+        break;
+      }
+      case WAIT_OBJECT_0 + 2: { // sDllWasLoaded
+        EXPECT_TRUE(!"DLL was loaded.");
+        // No need to continue testing; blocking failed and the DLL was
+        // consequently loaded. In theory we should never see this fire, because
+        // the thread being allowed should already trigger a test failure.
+        keepGoing = false;
+        break;
+      }
+      case WAIT_OBJECT_0 + 3: { // pi.hProcess
+        // Check to see if we got an error code from Injector.exe, in which case
+        // fail the test and exit.
+        DWORD exitCode;
+        if (!GetExitCodeProcess(pi.hProcess, &exitCode)) {
+          EXPECT_TRUE(!"Injector.exe exited but we were unable to get the exit code.");
+          keepGoing = false;
+          break;
+        }
+        EXPECT_EQ(exitCode, 0);
+        if (exitCode != 0) {
+          EXPECT_TRUE(!"Injector.exe returned non-zero exit code");
+          keepGoing = false;
+          break;
+        }
+        // Process exited successfully. This can be ignored; we expect to get an
+        // event whether the DLL was loaded or blocked.
+        EXPECT_TRUE("Process exited as expected.");
+        handleCount--;
+        break;
+      }
+      case WAIT_TIMEOUT:
+      default: {
+        EXPECT_TRUE(!"An error or timeout occurred while waiting for activity "
+                    "from Injector.exe");
+        keepGoing = false;
+        break;
+      }
+    }
+  }
+
+  // Double-check that injectordll is not loaded.
+  auto hExisting = GetModuleHandleW(dllPath.get());
+  EXPECT_TRUE(!hExisting);
+
+  // If the DLL was erroneously loaded, attempt to unload it before exiting.
+  if (hExisting) {
+    FreeLibrary(hExisting);
+  }
+
+  return;
+}
+
+TEST(TestInjectEject, CreateRemoteThread_LoadLibraryA)
+{
+  DoTest_CreateRemoteThread_LoadLibrary([](const nsString& dllPath,
+                                           const nsCString& dllPathC,
+                                           uintptr_t& aStartAddress,
+                                           uintptr_t& aThreadParam){
+    HMODULE hKernel32 = GetModuleHandleW(L"Kernel32");
+    aStartAddress = (uintptr_t)GetProcAddress(hKernel32, "LoadLibraryA");
+    aThreadParam = (uintptr_t)dllPathC.get();
+  });
+}
+
+TEST(TestInjectEject, CreateRemoteThread_LoadLibraryW)
+{
+  DoTest_CreateRemoteThread_LoadLibrary([](const nsString& dllPath,
+                                           const nsCString& dllPathC,
+                                           uintptr_t& aStartAddress,
+                                           uintptr_t& aThreadParam){
+    HMODULE hKernel32 = GetModuleHandleW(L"Kernel32");
+    aStartAddress = (uintptr_t)GetProcAddress(hKernel32, "LoadLibraryW");
+    aThreadParam = (uintptr_t)dllPath.get();
+  });
+}
+
+TEST(TestInjectEject, CreateRemoteThread_LoadLibraryExW)
+{
+  DoTest_CreateRemoteThread_LoadLibrary([](const nsString& dllPath,
+                                           const nsCString& dllPathC,
+                                           uintptr_t& aStartAddress,
+                                           uintptr_t& aThreadParam){
+    HMODULE hKernel32 = GetModuleHandleW(L"Kernel32");
+    // LoadLibraryEx requires three arguments so this injection method may not
+    // be viable. It's certainly not an allowable thread start in any case.
+    aStartAddress = (uintptr_t)GetProcAddress(hKernel32, "LoadLibraryExW");
+    aThreadParam = (uintptr_t)dllPath.get();
+  });
+}
+
+TEST(TestInjectEject, CreateRemoteThread_LoadLibraryExA)
+{
+  DoTest_CreateRemoteThread_LoadLibrary([](const nsString& dllPath,
+                                           const nsCString& dllPathC,
+                                           uintptr_t& aStartAddress,
+                                           uintptr_t& aThreadParam){
+    HMODULE hKernel32 = GetModuleHandleW(L"Kernel32");
+    aStartAddress = (uintptr_t)GetProcAddress(hKernel32, "LoadLibraryExA");
+    aThreadParam = (uintptr_t)dllPathC.get();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/mozglue/tests/gtest/moz.build
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+    'TestDLLEject.cpp'
+]
+
+FINAL_LIBRARY = 'xul-gtest'
+
+TEST_DIRS += [
+  'Injector',
+  'InjectorDLL',
+]
--- a/mozglue/tests/moz.build
+++ b/mozglue/tests/moz.build
@@ -12,9 +12,10 @@ GeckoCppUnitTests([
 
 CppUnitTests([
     'TestPrintf',
 ])
 
 if CONFIG['OS_ARCH'] == 'WINNT':
     TEST_DIRS += [
         'interceptor',
+        'gtest',
     ]
--- a/python/mozbuild/mozbuild/codecoverage/lcov_rewriter.py
+++ b/python/mozbuild/mozbuild/codecoverage/lcov_rewriter.py
@@ -6,16 +6,18 @@ from argparse import ArgumentParser
 import json
 import os
 
 try:
     import urlparse
 except ImportError:
     import urllib.parse as urlparse
 
+from six import viewitems
+
 from mozpack.chrome.manifest import parse_manifest
 import mozpack.path as mozpath
 from manifest_handler import ChromeManifestHandler
 
 class LcovRecord(object):
     __slots__ = ("test_name",
                  "source_file",
                  "functions",
@@ -37,37 +39,37 @@ class LcovRecord(object):
     def __iadd__(self, other):
 
         # These shouldn't differ.
         self.source_file = other.source_file
         if hasattr(other, 'test_name'):
             self.test_name = other.test_name
         self.functions.update(other.functions)
 
-        for name, count in other.function_exec_counts.iteritems():
+        for name, count in viewitems(other.function_exec_counts):
             self.function_exec_counts[name] = count + self.function_exec_counts.get(name, 0)
 
-        for key, taken in other.branches.iteritems():
+        for key, taken in viewitems(other.branches):
             self.branches[key] = taken + self.branches.get(key, 0)
 
-        for line, (exec_count, checksum) in other.lines.iteritems():
+        for line, (exec_count, checksum) in viewitems(other.lines):
             new_exec_count = exec_count
             if line in self.lines:
                 old_exec_count, _ = self.lines[line]
                 new_exec_count += old_exec_count
             self.lines[line] = new_exec_count, checksum
 
         self.resummarize()
         return self
 
     def resummarize(self):
         # Re-calculate summaries after generating or splitting a record.
         self.function_count = len(self.functions.keys())
         # Function records may have moved between files, so filter here.
-        self.function_exec_counts = {fn_name: count for fn_name, count in self.function_exec_counts.iteritems()
+        self.function_exec_counts = {fn_name: count for fn_name, count in viewitems(self.function_exec_counts)
                                      if fn_name in self.functions.values()}
         self.covered_function_count = len([c for c in self.function_exec_counts.values() if c])
         self.line_count = len(self.lines)
         self.covered_line_count = len([c for c, _ in self.lines.values() if c])
         self.branch_count = len(self.branches)
         self.covered_branch_count = len([c for c in self.branches.values() if c])
 
 class RecordRewriter(object):
@@ -96,17 +98,17 @@ class RecordRewriter(object):
         else:
             gen_rec = LcovRecord()
             gen_rec.source_file = inc_source
             self._additions[inc_source] = gen_rec
         return gen_rec
 
     def _rewrite_lines(self, record):
         rewritten_lines = {}
-        for ln, line_info in record.lines.iteritems():
+        for ln, line_info in viewitems(record.lines):
             r = self._get_range(ln)
             if r is None:
                 rewritten_lines[ln] = line_info
                 continue
             new_ln = self._get_mapped_line(ln, r)
             inc_source, _ = self._current_pp_info[r]
 
             if inc_source != record.source_file:
@@ -121,17 +123,17 @@ class RecordRewriter(object):
 
     def _rewrite_functions(self, record):
         rewritten_fns = {}
 
         # Sometimes we get multiple entries for a named function ("top-level", for
         # instance). It's not clear the records that result are well-formed, but
         # we act as though if a function has multiple FN's, the corresponding
         # FNDA's are all the same.
-        for ln, fn_name in record.functions.iteritems():
+        for ln, fn_name in viewitems(record.functions):
             r = self._get_range(ln)
             if r is None:
                 rewritten_fns[ln] = fn_name
                 continue
             new_ln = self._get_mapped_line(ln, r)
             inc_source, _ = self._current_pp_info[r]
             if inc_source != record.source_file:
                 gen_rec = self._get_record(inc_source)
@@ -139,17 +141,17 @@ class RecordRewriter(object):
                 if fn_name in record.function_exec_counts:
                     gen_rec.function_exec_counts[fn_name] = record.function_exec_counts[fn_name]
                 continue
             rewritten_fns[new_ln] = fn_name
         record.functions = rewritten_fns
 
     def _rewrite_branches(self, record):
         rewritten_branches = {}
-        for (ln, block_number, branch_number), taken in record.branches.iteritems():
+        for (ln, block_number, branch_number), taken in viewitems(record.branches):
             r = self._get_range(ln)
             if r is None:
                 rewritten_branches[ln, block_number, branch_number] = taken
                 continue
             new_ln = self._get_mapped_line(ln, r)
             inc_source, _ = self._current_pp_info[r]
             if inc_source != record.source_file:
                 gen_rec = self._get_record(inc_source)
@@ -309,23 +311,23 @@ class LcovFile(object):
 
     def format_source_file(self, record):
         return "SF:%s" % record.source_file
 
     def format_functions(self, record):
         # Sorting results gives deterministic output (and is a lot faster than
         # using OrderedDict).
         fns = []
-        for start_lineno, fn_name in sorted(record.functions.iteritems()):
+        for start_lineno, fn_name in sorted(viewitems(record.functions)):
             fns.append('FN:%s,%s' % (start_lineno, fn_name))
         return '\n'.join(fns)
 
     def format_function_exec_counts(self, record):
         fndas = []
-        for name, exec_count in sorted(record.function_exec_counts.iteritems()):
+        for name, exec_count in sorted(viewitems(record.function_exec_counts)):
             fndas.append('FNDA:%s,%s' % (exec_count, name))
         return '\n'.join(fndas)
 
     def format_function_count(self, record):
         return 'FNF:%s' % record.function_count
 
     def format_covered_function_count(self, record):
         return 'FNH:%s' % record.covered_function_count
@@ -342,17 +344,17 @@ class LcovFile(object):
     def format_branch_count(self, record):
         return 'BRF:%s' % record.branch_count
 
     def format_covered_branch_count(self, record):
         return 'BRH:%s' % record.covered_branch_count
 
     def format_lines(self, record):
         das = []
-        for line_no, (exec_count, checksum) in sorted(record.lines.iteritems()):
+        for line_no, (exec_count, checksum) in sorted(viewitems(record.lines)):
             s = 'DA:%s,%s' % (line_no, exec_count)
             if checksum:
                 s += ',%s' % checksum
             das.append(s)
         return '\n'.join(das)
 
     def format_line_count(self, record):
         return 'LF:%s' % record.line_count
@@ -511,17 +513,17 @@ class UrlFinder(object):
         term = url
         if term in self._url_overrides:
             term = self._url_overrides[term]
 
         if os.path.isabs(term) and term.startswith(self.topobjdir):
             source_path, pp_info = self._abs_objdir_install_info(term)
             return source_path, pp_info
 
-        for prefix, dests in self._url_prefixes.iteritems():
+        for prefix, dests in viewitems(self._url_prefixes):
             if term.startswith(prefix):
                 for dest in dests:
                     if not dest.endswith('/'):
                         dest += '/'
                     objdir_path = term.replace(prefix, dest)
 
                     while objdir_path.startswith('//'):
                         # The mochitest harness produces some wonky file:// uris
--- a/servo/components/malloc_size_of/Cargo.toml
+++ b/servo/components/malloc_size_of/Cargo.toml
@@ -20,17 +20,17 @@ servo = [
     "url",
     "webrender_api",
     "xml5ever",
 ]
 
 [dependencies]
 app_units = "0.6"
 cssparser = "0.24.0"
-euclid = "0.17"
+euclid = "0.18"
 hashglobe = { path = "../hashglobe" }
 hyper = { version = "0.10", optional = true }
 hyper_serde = { version = "0.8", optional = true }
 mozjs = { version = "0.7.1", features = ["promises"], optional = true }
 selectors = { path = "../selectors" }
 serde = { version = "1.0.27", optional = true }
 serde_bytes = { version = "0.10", optional = true }
 servo_arc = { path = "../servo_arc" }
--- a/servo/components/style/Cargo.toml
+++ b/servo/components/style/Cargo.toml
@@ -29,17 +29,17 @@ app_units = "0.6"
 arrayvec = "0.4.6"
 atomic_refcell = "0.1"
 bitflags = "1.0"
 byteorder = "1.0"
 cfg-if = "0.1.0"
 cssparser = "0.24.0"
 new_debug_unreachable = "1.0"
 encoding_rs = {version = "0.7", optional = true}
-euclid = "0.17"
+euclid = "0.18"
 fallible = { path = "../fallible" }
 fnv = "1.0"
 hashglobe = { path = "../hashglobe" }
 html5ever = {version = "0.22", optional = true}
 itertools = "0.7.6"
 itoa = "0.3"
 lazy_static = "1"
 log = "0.4"
--- a/servo/components/style/values/generics/transform.rs
+++ b/servo/components/style/values/generics/transform.rs
@@ -431,17 +431,16 @@ where
             },
             RotateZ(theta) | Rotate(theta) => {
                 let theta = euclid::Angle::radians(TWO_PI - theta.as_ref().radians64());
                 Transform3D::create_rotation(0., 0., 1., theta)
             },
             Perspective(ref d) => {
                 let m = create_perspective_matrix(d.to_pixel_length(None)?);
                 m.cast()
-                    .expect("Casting from f32 to f64 should be successful")
             },
             Scale3D(sx, sy, sz) => Transform3D::create_scale(sx.into(), sy.into(), sz.into()),
             Scale(sx, sy) => Transform3D::create_scale(sx.into(), sy.unwrap_or(sx).into(), 1.),
             ScaleX(s) => Transform3D::create_scale(s.into(), 1., 1.),
             ScaleY(s) => Transform3D::create_scale(1., s.into(), 1.),
             ScaleZ(s) => Transform3D::create_scale(1., 1., s.into()),
             Translate3D(ref tx, ref ty, ref tz) => {
                 let tx = tx.to_pixel_length(reference_width)? as f64;
@@ -565,17 +564,17 @@ pub fn get_normalized_vector_and_angle<T
     use values::computed::transform::DirectionVector;
     let vector = DirectionVector::new(x, y, z);
     if vector.square_length().approx_eq(&f32::zero()) {
         // https://www.w3.org/TR/css-transforms-1/#funcdef-rotate3d
         // A direction vector that cannot be normalized, such as [0, 0, 0], will cause the
         // rotation to not be applied, so we use identity matrix (i.e. rotate3d(0, 0, 1, 0)).
         (0., 0., 1., T::zero())
     } else {
-        let vector = vector.normalize();
+        let vector = vector.robust_normalize();
         (vector.x, vector.y, vector.z, angle)
     }
 }
 
 #[derive(Clone, ComputeSquaredDistance, Copy, Debug, MallocSizeOf, PartialEq,
          SpecifiedValueInfo, ToAnimatedZero, ToComputedValue, ToCss)]
 /// A value of the `Rotate` property
 ///
--- a/servo/components/style_traits/Cargo.toml
+++ b/servo/components/style_traits/Cargo.toml
@@ -12,17 +12,17 @@ path = "lib.rs"
 [features]
 servo = ["serde", "servo_atoms", "cssparser/serde", "webrender_api", "servo_url"]
 gecko = []
 
 [dependencies]
 app_units = "0.6"
 cssparser = "0.24.0"
 bitflags = "1.0"
-euclid = "0.17"
+euclid = "0.18"
 malloc_size_of = { path = "../malloc_size_of" }
 malloc_size_of_derive = { path = "../malloc_size_of_derive" }
 selectors = { path = "../selectors" }
 serde = {version = "1.0", optional = true}
 webrender_api = {git = "https://github.com/servo/webrender", optional = true}
 servo_atoms = {path = "../atoms", optional = true}
 servo_arc = { path = "../servo_arc" }
 servo_url = { path = "../url", optional = true }
--- a/servo/ports/geckolib/tests/Cargo.toml
+++ b/servo/ports/geckolib/tests/Cargo.toml
@@ -11,17 +11,17 @@ name = "stylo_tests"
 path = "lib.rs"
 doctest = false
 
 [dependencies]
 atomic_refcell = "0.1"
 cssparser = "0.24.0"
 cstr = "0.1.2"
 env_logger = { version = "0.5", default-features = false }
-euclid = "0.17"
+euclid = "0.18"
 geckoservo = {path = "../../../ports/geckolib"}
 libc = "0.2"
 log = {version = "0.4", features = ["release_max_level_info"]}
 malloc_size_of = {path = "../../../components/malloc_size_of"}
 selectors = {path = "../../../components/selectors"}
 size_of_test = {path = "../../../components/size_of_test"}
 smallvec = "0.6"
 style_traits = {path = "../../../components/style_traits"}
--- a/servo/tests/unit/style/Cargo.toml
+++ b/servo/tests/unit/style/Cargo.toml
@@ -8,17 +8,17 @@ license = "MPL-2.0"
 name = "style_tests"
 path = "lib.rs"
 doctest = false
 
 [dependencies]
 byteorder = "1.0"
 app_units = "0.6"
 cssparser = "0.24.0"
-euclid = "0.17"
+euclid = "0.18"
 html5ever = "0.22"
 parking_lot = "0.5"
 rayon = "1"
 serde_json = "1.0"
 selectors = {path = "../../../components/selectors"}
 servo_arc = {path = "../../../components/servo_arc"}
 servo_atoms = {path = "../../../components/atoms"}
 servo_config = {path = "../../../components/config"}
--- a/taskcluster/ci/build/android-stuff.yml
+++ b/taskcluster/ci/build/android-stuff.yml
@@ -50,16 +50,17 @@ android-test-ccov/opt:
     index:
         product: mobile
         job-name: android-test-ccov
     treeherder:
         platform: android-4-0-armv7-api16/opt
         kind: build
         tier: 1
         symbol: A(test-ccov)
+    run-on-projects: [mozilla-central]
     worker-type: aws-provisioner-v1/gecko-{level}-b-android
     worker:
         docker-image: {in-tree: android-build}
         env:
             GRADLE_USER_HOME: "/builds/worker/workspace/build/src/mobile/android/gradle/dotgradle-offline"
             PERFHERDER_EXTRA_OPTIONS: android-test-ccov
         artifacts:
             - name: public/code-coverage-grcov.zip
--- a/taskcluster/ci/fetch/toolchains.yml
+++ b/taskcluster/ci/fetch/toolchains.yml
@@ -209,17 +209,17 @@ grcov-osx-x86_64:
   treeherder:
     symbol: grcov-osx-x86_64
   run:
     using: fetch-url
     url: https://github.com/mozilla/grcov/releases/download/v0.2.3/grcov-osx-x86_64.tar.bz2
     sha256: 3db4ef6c0dfaf35d39c9cacf19c60c482dae9413b5fe3bf343fa26667451c893
     size: 919638
 
-grcov-win-i686:
+grcov-win-x86_64:
   description: grcov binary release
   treeherder:
-    symbol: grcov-win-i686
+    symbol: grcov-win-x86_64
   run:
     using: fetch-url
-    url: https://github.com/mozilla/grcov/releases/download/v0.2.3/grcov-win-i686.tar.bz2
-    sha256: 87c3c22cd1bb99d9c41b6a6f9e20b6c655a580021776f1a532297f5e8315ad9b
-    size: 919079
+    url: https://github.com/mozilla/grcov/releases/download/v0.2.3/grcov-win-x86_64.tar.bz2
+    sha256: baede397959ec6f5e0c9630c8e9e1e59ebe723ec95acea9bb204511a886e0f2f
+    size: 1009309
--- a/taskcluster/taskgraph/transforms/tests.py
+++ b/taskcluster/taskgraph/transforms/tests.py
@@ -734,17 +734,17 @@ def enable_code_coverage(config, tests):
             test['optimization'] = None
 
             # Add a fetch task for the grcov binary.
             if 'linux' in test['build-platform']:
                 test['fetches'] = ['grcov-linux-x86_64']
             elif 'osx' in test['build-platform']:
                 test['fetches'] = ['grcov-osx-x86_64']
             elif 'win' in test['build-platform']:
-                test['fetches'] = ['grcov-win-i686']
+                test['fetches'] = ['grcov-win-x86_64']
 
             if 'talos' in test['test-name']:
                 test['max-run-time'] = 7200
                 if 'linux' in test['build-platform']:
                     test['docker-image'] = {"in-tree": "desktop1604-test"}
                 test['mozharness']['extra-options'].append('--add-option')
                 test['mozharness']['extra-options'].append('--cycles,1')
                 test['mozharness']['extra-options'].append('--add-option')
--- a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/window.py
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/window.py
@@ -30,17 +30,16 @@ class BrowserWindow(BaseWindow):
         'chrome://branding/locale/brand.dtd',
         'chrome://browser/locale/aboutPrivateBrowsing.dtd',
         'chrome://browser/locale/browser.dtd',
         'chrome://browser/locale/netError.dtd',
     ]
 
     properties = [
         'chrome://branding/locale/brand.properties',
-        'chrome://branding/locale/browserconfig.properties',
         'chrome://browser/locale/browser.properties',
         'chrome://browser/locale/preferences/preferences.properties',
         'chrome://global/locale/browser.properties',
     ]
 
     def __init__(self, *args, **kwargs):
         super(BrowserWindow, self).__init__(*args, **kwargs)
 
@@ -48,18 +47,17 @@ class BrowserWindow(BaseWindow):
         self._tabbar = None
 
     @property
     def default_homepage(self):
         """The default homepage as used by the current locale.
 
         :returns: The default homepage for the current locale.
         """
-        return self.marionette.get_pref('browser.startup.homepage',
-                                        value_type='nsIPrefLocalizedString')
+        return self.marionette.get_pref('browser.startup.homepage')
 
     @property
     def is_private(self):
         """Returns True if this is a Private Browsing window."""
         self.switch_to()
 
         with self.marionette.using_context('chrome'):
             return self.marionette.execute_script("""
rename from testing/mochitest/bootstrap.js
rename to testing/mochitest/api.js
--- a/testing/mochitest/bootstrap.js
+++ b/testing/mochitest/api.js
@@ -1,12 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+/* globals ExtensionAPI */
+
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 function loadChromeScripts(win) {
   Services.scriptloader.loadSubScript("chrome://mochikit/content/chrome-harness.js", win);
   Services.scriptloader.loadSubScript("chrome://mochikit/content/mochitest-e10s-utils.js", win);
@@ -33,20 +35,30 @@ const windowTracker = {
       if (documentURI !== "chrome://browser/content/browser.xul") {
         return;
       }
       loadChromeScripts(window);
     }
   },
 };
 
-function androidStartup(data, reason) {
+function androidStartup() {
   // Only browser chrome tests need help starting.
   let testRoot = Services.prefs.getStringPref("mochitest.testRoot", "");
   if (testRoot.endsWith("/chrome")) {
+    // The initial window is created from browser startup, which races
+    // against extension initialization.  If it has already been created,
+    // inject the test scripts now, otherwise wait for the browser window
+    // to show up.
+    let win = Services.wm.getMostRecentWindow("navigator:browser");
+    if (win) {
+      loadChromeScripts(win);
+      return;
+    }
+
     windowTracker.init();
   }
 }
 
 // ///// Desktop ///////
 
 var WindowListener = {
   // browser-test.js is only loaded into the first window. Setup that
@@ -89,34 +101,43 @@ function loadMochitest(e) {
   }
 
   WindowListener.setupWindow(win);
   Services.wm.addListener(WindowListener);
 
   loadChromeScripts(win);
 }
 
-function startup(data, reason) {
-  if (AppConstants.platform == "android") {
-    androidStartup(data, reason);
-  } else {
-    let win = Services.wm.getMostRecentWindow("navigator:browser");
-    // wait for event fired from start_desktop.js containing the
-    // suite and url to load
-    win.addEventListener("mochitest-load", loadMochitest);
+this.mochikit = class extends ExtensionAPI {
+  onStartup() {
+    let aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"]
+                                 .getService(Ci.amIAddonManagerStartup);
+    const manifestURI = Services.io.newURI("manifest.json", null, this.extension.rootURI);
+    const targetURL = this.extension.rootURI.resolve("content/");
+    this.chromeHandle = aomStartup.registerChrome(manifestURI, [
+      ["content", "mochikit", targetURL],
+    ]);
+
+    if (AppConstants.platform == "android") {
+      androidStartup();
+    } else {
+      let win = Services.wm.getMostRecentWindow("navigator:browser");
+      // wait for event fired from start_desktop.js containing the
+      // suite and url to load
+      win.addEventListener("mochitest-load", loadMochitest);
+    }
   }
-}
 
-function shutdown(data, reason) {
-  if (AppConstants.platform != "android") {
-    let windows = Services.wm.getEnumerator("navigator:browser");
-    while (windows.hasMoreElements()) {
-      let win = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
-      WindowListener.tearDownWindow(win);
+  onShutdown() {
+    if (AppConstants.platform != "android") {
+      let windows = Services.wm.getEnumerator("navigator:browser");
+      while (windows.hasMoreElements()) {
+        let win = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
+        WindowListener.tearDownWindow(win);
+      }
+
+      Services.wm.removeListener(WindowListener);
     }
 
-    Services.wm.removeListener(WindowListener);
+    this.chromeHandle.destruct();
+    this.chromeHandle = null;
   }
-}
-
-function install(data, reason) {}
-function uninstall(data, reason) {}
-
+};
deleted file mode 100644
--- a/testing/mochitest/jar.mn
+++ /dev/null
@@ -1,42 +0,0 @@
-mochikit.jar:
-% content mochikit %content/
-  content/browser-harness.xul (browser-harness.xul)
-  content/browser-test.js (browser-test.js)
-  content/chrome-harness.js (chrome-harness.js)
-  content/mochitest-e10s-utils.js (mochitest-e10s-utils.js)
-  content/shutdown-leaks-collector.js (shutdown-leaks-collector.js)
-  content/ShutdownLeaksCollector.jsm (ShutdownLeaksCollector.jsm)
-  content/harness.xul (harness.xul)
-  content/redirect.html (redirect.html)
-  content/server.js (server.js)
-  content/chunkifyTests.js (chunkifyTests.js)
-  content/manifestLibrary.js (manifestLibrary.js)
-  content/nested_setup.js (nested_setup.js)
-  content/dynamic/getMyDirectory.sjs (dynamic/getMyDirectory.sjs)
-  content/static/harness.css (static/harness.css)
-  content/tests/SimpleTest/ChromePowers.js (tests/SimpleTest/ChromePowers.js)
-  content/tests/SimpleTest/EventUtils.js (tests/SimpleTest/EventUtils.js)
-  content/tests/SimpleTest/ExtensionTestUtils.js (tests/SimpleTest/ExtensionTestUtils.js)
-  content/tests/SimpleTest/AddTask.js (tests/SimpleTest/AddTask.js)
-  content/tests/SimpleTest/AsyncUtilsContent.js (tests/SimpleTest/AsyncUtilsContent.js)
-  content/tests/SimpleTest/LogController.js (tests/SimpleTest/LogController.js)
-  content/tests/SimpleTest/MemoryStats.js (tests/SimpleTest/MemoryStats.js)
-  content/tests/SimpleTest/MozillaLogger.js (../specialpowers/content/MozillaLogger.js)
-  content/tests/SimpleTest/specialpowers.js (../specialpowers/content/specialpowers.js)
-  content/tests/SimpleTest/SpecialPowersObserverAPI.js (../specialpowers/content/SpecialPowersObserverAPI.js)
-  content/tests/SimpleTest/specialpowersAPI.js (../specialpowers/content/specialpowersAPI.js)
-  content/tests/SimpleTest/setup.js (tests/SimpleTest/setup.js)
-  content/tests/SimpleTest/SimpleTest.js (tests/SimpleTest/SimpleTest.js)
-  content/tests/SimpleTest/StructuredLog.jsm (../modules/StructuredLog.jsm)
-  content/tests/SimpleTest/test.css (tests/SimpleTest/test.css)
-  content/tests/SimpleTest/TestRunner.js (tests/SimpleTest/TestRunner.js)
-  content/tests/SimpleTest/iframe-between-tests.html (tests/SimpleTest/iframe-between-tests.html)
-  content/tests/SimpleTest/WindowSnapshot.js (tests/SimpleTest/WindowSnapshot.js)
-  content/tests/SimpleTest/MockObjects.js (tests/SimpleTest/MockObjects.js)
-  content/tests/SimpleTest/NativeKeyCodes.js (tests/SimpleTest/NativeKeyCodes.js)
-  content/tests/SimpleTest/paint_listener.js (tests/SimpleTest/paint_listener.js)
-  content/tests/SimpleTest/docshell_helpers.js (../../docshell/test/chrome/docshell_helpers.js)
-  content/tests/BrowserTestUtils/content-task.js (BrowserTestUtils/content/content-task.js)
-  content/tests/BrowserTestUtils/content-about-page-utils.js (BrowserTestUtils/content/content-about-page-utils.js)
-  content/tests/BrowserTestUtils/content-utils.js (BrowserTestUtils/content/content-utils.js)
-
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/manifest.json
@@ -0,0 +1,22 @@
+{
+  "manifest_version": 2,
+  "name": "Mochitest",
+  "version": "2.0",
+
+  "applications": {
+    "gecko": {
+      "id": "mochikit@mozilla.org"
+    }
+  },
+
+  "experiment_apis": {
+    "mochikit": {
+      "schema": "schema.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "api.js",
+        "events": ["startup"]
+      }
+    }
+  }
+}
--- a/testing/mochitest/moz.build
+++ b/testing/mochitest/moz.build
@@ -8,23 +8,77 @@ DIRS += [
     'manifests',
     'tests',
     'ssltunnel',
     'BrowserTestUtils',
 ]
 
 XPI_NAME = 'mochijar'
 
-JAR_MANIFESTS += ['jar.mn']
-
 USE_EXTENSION_MANIFEST = True
 
-FINAL_TARGET_PP_FILES += ['install.rdf']
+FINAL_TARGET_FILES += [
+    'api.js',
+    'manifest.json',
+    'schema.json',
+]
+
+FINAL_TARGET_FILES.content += [
+    'browser-harness.xul',
+    'browser-test.js',
+    'chrome-harness.js',
+    'chunkifyTests.js',
+    'harness.xul',
+    'manifestLibrary.js',
+    'mochitest-e10s-utils.js',
+    'nested_setup.js',
+    'redirect.html',
+    'server.js',
+    'shutdown-leaks-collector.js',
+    'ShutdownLeaksCollector.jsm',
+]
+
+FINAL_TARGET_FILES.content.dynamic += [
+    'dynamic/getMyDirectory.sjs',
+]
+
+FINAL_TARGET_FILES.content.static += [
+    'static/harness.css',
+]
 
-FINAL_TARGET_FILES += ['bootstrap.js']
+FINAL_TARGET_FILES.content.tests.SimpleTest += [
+    '../../docshell/test/chrome/docshell_helpers.js',
+    '../modules/StructuredLog.jsm',
+    '../specialpowers/content/MozillaLogger.js',
+    '../specialpowers/content/specialpowers.js',
+    '../specialpowers/content/specialpowersAPI.js',
+    '../specialpowers/content/SpecialPowersObserverAPI.js',
+    'tests/SimpleTest/AddTask.js',
+    'tests/SimpleTest/AsyncUtilsContent.js',
+    'tests/SimpleTest/ChromePowers.js',
+    'tests/SimpleTest/EventUtils.js',
+    'tests/SimpleTest/ExtensionTestUtils.js',
+    'tests/SimpleTest/iframe-between-tests.html',
+    'tests/SimpleTest/LogController.js',
+    'tests/SimpleTest/MemoryStats.js',
+    'tests/SimpleTest/MockObjects.js',
+    'tests/SimpleTest/NativeKeyCodes.js',
+    'tests/SimpleTest/paint_listener.js',
+    'tests/SimpleTest/setup.js',
+    'tests/SimpleTest/SimpleTest.js',
+    'tests/SimpleTest/test.css',
+    'tests/SimpleTest/TestRunner.js',
+    'tests/SimpleTest/WindowSnapshot.js',
+]
+
+FINAL_TARGET_FILES.content.tests.BrowserTestUtils += [
+    'BrowserTestUtils/content/content-about-page-utils.js',
+    'BrowserTestUtils/content/content-task.js',
+    'BrowserTestUtils/content/content-utils.js',
+]
 
 MOCHITEST_MANIFESTS += [
     'baselinecoverage/plain/mochitest.ini',
     'tests/MochiKit-1.4.2/tests/mochitest.ini',
 ]
 
 MOCHITEST_CHROME_MANIFESTS += [
     'baselinecoverage/chrome/chrome.ini',
--- a/testing/mochitest/redirect.html
+++ b/testing/mochitest/redirect.html
@@ -20,18 +20,19 @@
       redirect("chrome://mochikit/content/harness.xul");
     }
 
     function onLoad() {
       // Wait for MozAfterPaint, since the listener in browser-test.js is not
       // added until then.
       window.addEventListener("MozAfterPaint", function() {
         setTimeout(redirectToHarness, 0);
-        // In case the listener was not ready, try again after a few seconds.
-        setTimeout(redirectToHarness, 5000);
+
+        // In case the listener is not ready, re-try periodically
+        setInterval(redirectToHarness, 5000);
       }, {once: true});
 
     }
   </script>
 </head>
 
 <body onload="onLoad();">
 redirecting...
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/schema.json
@@ -0,0 +1,1 @@
+[]
--- a/testing/talos/talos/ffsetup.py
+++ b/testing/talos/talos/ffsetup.py
@@ -196,17 +196,18 @@ class FFSetup(object):
     def _init_gecko_profile(self):
         upload_dir = os.getenv('MOZ_UPLOAD_DIR')
         if self.test_config.get('gecko_profile') and not upload_dir:
             LOG.critical("Profiling ignored because MOZ_UPLOAD_DIR was not"
                          " set")
         if upload_dir and self.test_config.get('gecko_profile'):
             self.gecko_profile = GeckoProfile(upload_dir,
                                               self.browser_config,
-                                              self.test_config)
+                                              self.test_config,
+                                              str(os.getenv('MOZ_WEBRENDER')) == '1')
             self.gecko_profile.update_env(self.env)
 
     def clean(self):
         try:
             mozfile.remove(self._tmp_dir)
         except Exception as e:
             LOG.info("Exception while removing profile directory: %s" % self._tmp_dir)
             LOG.info(e)
--- a/testing/talos/talos/gecko_profile.py
+++ b/testing/talos/talos/gecko_profile.py
@@ -20,29 +20,31 @@ LOG = get_proxy_logger()
 
 
 class GeckoProfile(object):
     """
     Handle Gecko profilling.
 
     This allow to collect Gecko profiling data and to zip results in one file.
     """
-    def __init__(self, upload_dir, browser_config, test_config):
+    def __init__(self, upload_dir, browser_config, test_config, webrender_enabled):
         self.upload_dir = upload_dir
         self.browser_config, self.test_config = browser_config, test_config
         self.cleanup = True
 
         # Create a temporary directory into which the tests can put
         # their profiles. These files will be assembled into one big
         # zip file later on, which is put into the MOZ_UPLOAD_DIR.
         gecko_profile_dir = tempfile.mkdtemp()
 
         gecko_profile_interval = test_config.get('gecko_profile_interval', 1)
         gecko_profile_entries = test_config.get('gecko_profile_entries', 1000000)
         gecko_profile_threads = 'GeckoMain,Compositor'
+        if webrender_enabled:
+            gecko_profile_threads += ',WR,Renderer'
 
         # Make sure no archive already exists in the location where
         # we plan to output our profiler archive
         self.profile_arcname = os.path.join(
             self.upload_dir,
             "profile_{0}.zip".format(test_config['name'])
         )
         LOG.info("Clearing archive {0}".format(self.profile_arcname))
@@ -77,17 +79,18 @@ class GeckoProfile(object):
         if not self.test_config.get('gecko_profile_startup'):
             return
         # Set environment variables which will cause profiling to
         # start as early as possible. These are consumed by Gecko
         # itself, not by Talos JS code.
         env.update({
             'MOZ_PROFILER_STARTUP': '1',
             'MOZ_PROFILER_STARTUP_INTERVAL': str(self.option('interval')),
-            'MOZ_PROFILER_STARTUP_ENTRIES': str(self.option('entries'))
+            'MOZ_PROFILER_STARTUP_ENTRIES': str(self.option('entries')),
+            'MOZ_PROFILER_STARTUP_FILTERS': str(self.option('threads'))
         })
 
     def _save_gecko_profile(self, cycle, symbolicator, missing_symbols_zip,
                             profile_path):
         try:
             with open(profile_path, 'r') as profile_file:
                 profile = json.load(profile_file)
             symbolicator.dump_and_integrate_missing_symbols(
copy from third_party/rust/euclid/.cargo-checksum.json
copy to third_party/rust/euclid-0.17.3/.cargo-checksum.json
new file mode 100644
--- /dev/null
+++ b/third_party/rust/euclid-0.17.3/.travis.yml
@@ -0,0 +1,25 @@
+language: rust
+
+notifications:
+  webhooks: http://build.servo.org:54856/travis
+
+rust:
+  - 1.23.0
+  - stable
+  - beta
+  - nightly
+
+env:
+  - FEATURES=""
+  - FEATURES="--features serde"
+
+matrix:
+  include:
+    - rust: nightly
+      env: FEATURES="--features unstable"
+    - rust: nightly
+      env: FEATURES="--features unstable,serde"
+
+script:
+  - cargo build $FEATURES
+  - cargo test --verbose $FEATURES
new file mode 100644
--- /dev/null
+++ b/third_party/rust/euclid-0.17.3/COPYRIGHT
@@ -0,0 +1,5 @@
+Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+<LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+option. All files in the project carrying such notice may not be
+copied, modified, or distributed except according to those terms.
copy from third_party/rust/euclid/Cargo.toml
copy to third_party/rust/euclid-0.17.3/Cargo.toml
new file mode 100644
--- /dev/null
+++ b/third_party/rust/euclid-0.17.3/LICENSE-APACHE
@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+   To apply the Apache License to your work, attach the following
+   boilerplate notice, with the fields enclosed by brackets "[]"
+   replaced with your own identifying information. (Don't include
+   the brackets!)  The text should be enclosed in the appropriate
+   comment syntax for the file format. We also recommend that a
+   file or class name and description of purpose be included on the
+   same "printed page" as the copyright notice for easier
+   identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
new file mode 100644
--- /dev/null
+++ b/third_party/rust/euclid-0.17.3/LICENSE-MIT
@@ -0,0 +1,25 @@
+Copyright (c) 2012-2013 Mozilla Foundation
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
new file mode 100644
--- /dev/null
+++ b/third_party/rust/euclid-0.17.3/README.md
@@ -0,0 +1,8 @@
+# euclid
+
+This is a small library for geometric types with a focus on 2d graphics and
+layout.
+
+* [Documentation](https://docs.rs/euclid/)
+* [Release notes](https://github.com/servo/euclid/releases)
+* [crates.io](https://crates.io/crates/euclid)
new file mode 100644
--- /dev/null
+++ b/third_party/rust/euclid-0.17.3/src/approxeq.rs
@@ -0,0 +1,35 @@
+// Copyright 2013 The Servo Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+/// Trait for testing approximate equality
+pub trait ApproxEq<Eps> {
+    fn approx_epsilon() -> Eps;
+    fn approx_eq(&self, other: &Self) -> bool;
+    fn approx_eq_eps(&self, other: &Self, approx_epsilon: &Eps) -> bool;
+}
+
+macro_rules! approx_eq {
+    ($ty:ty, $eps:expr) => (
+        impl ApproxEq<$ty> for $ty {
+            #[inline]
+            fn approx_epsilon() -> $ty { $eps }
+            #[inline]
+            fn approx_eq(&self, other: &$ty) -> bool {
+                self.approx_eq_eps(other, &$eps)
+            }
+            #[inline]
+            fn approx_eq_eps(&self, other: &$ty, approx_epsilon: &$ty) -> bool {
+                (*self - *other).abs() < *approx_epsilon
+            }
+        }
+    )
+}
+
+approx_eq!(f32, 1.0e-6);
+approx_eq!(f64, 1.0e-6);
copy from third_party/rust/euclid/src/homogen.rs
copy to third_party/rust/euclid-0.17.3/src/homogen.rs
copy from third_party/rust/euclid/src/length.rs
copy to third_party/rust/euclid-0.17.3/src/length.rs
copy from third_party/rust/euclid/src/lib.rs
copy to third_party/rust/euclid-0.17.3/src/lib.rs
copy from third_party/rust/euclid/src/macros.rs
copy to third_party/rust/euclid-0.17.3/src/macros.rs
new file mode 100644
--- /dev/null
+++ b/third_party/rust/euclid-0.17.3/src/num.rs
@@ -0,0 +1,85 @@
+// Copyright 2014 The Servo Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+//! A one-dimensional length, tagged with its units.
+
+use num_traits;
+
+pub trait Zero {
+    fn zero() -> Self;
+}
+
+impl<T: num_traits::Zero> Zero for T {
+    fn zero() -> T {
+        num_traits::Zero::zero()
+    }
+}
+
+pub trait One {
+    fn one() -> Self;
+}
+
+impl<T: num_traits::One> One for T {
+    fn one() -> T {
+        num_traits::One::one()
+    }
+}
+
+pub trait Round: Copy {
+    fn round(self) -> Self;
+}
+pub trait Floor: Copy {
+    fn floor(self) -> Self;
+}
+pub trait Ceil: Copy {
+    fn ceil(self) -> Self;
+}
+
+macro_rules! num_int {
+    ($ty:ty) => (
+        impl Round for $ty {
+            #[inline]
+            fn round(self) -> $ty { self }
+        }
+        impl Floor for $ty {
+            #[inline]
+            fn floor(self) -> $ty { self }
+        }
+        impl Ceil for $ty {
+            #[inline]
+            fn ceil(self) -> $ty { self }
+        }
+    )
+}
+macro_rules! num_float {
+    ($ty:ty) => (
+        impl Round for $ty {
+            #[inline]
+            fn round(self) -> $ty { self.round() }
+        }
+        impl Floor for $ty {
+            #[inline]
+            fn floor(self) -> $ty { self.floor() }
+        }
+        impl Ceil for $ty {
+            #[inline]
+            fn ceil(self) -> $ty { self.ceil() }
+        }
+    )
+}
+
+num_int!(i16);
+num_int!(u16);
+num_int!(i32);
+num_int!(u32);
+num_int!(i64);
+num_int!(u64);
+num_int!(isize);
+num_int!(usize);
+num_float!(f32);
+num_float!(f64);
copy from third_party/rust/euclid/src/point.rs
copy to third_party/rust/euclid-0.17.3/src/point.rs
copy from third_party/rust/euclid/src/rect.rs
copy to third_party/rust/euclid-0.17.3/src/rect.rs
copy from third_party/rust/euclid/src/rotation.rs
copy to third_party/rust/euclid-0.17.3/src/rotation.rs
copy from third_party/rust/euclid/src/scale.rs
copy to third_party/rust/euclid-0.17.3/src/scale.rs
copy from third_party/rust/euclid/src/side_offsets.rs
copy to third_party/rust/euclid-0.17.3/src/side_offsets.rs
copy from third_party/rust/euclid/src/size.rs
copy to third_party/rust/euclid-0.17.3/src/size.rs
copy from third_party/rust/euclid/src/transform2d.rs
copy to third_party/rust/euclid-0.17.3/src/transform2d.rs
copy from third_party/rust/euclid/src/transform3d.rs
copy to third_party/rust/euclid-0.17.3/src/transform3d.rs
copy from third_party/rust/euclid/src/trig.rs
copy to third_party/rust/euclid-0.17.3/src/trig.rs
copy from third_party/rust/euclid/src/vector.rs
copy to third_party/rust/euclid-0.17.3/src/vector.rs
--- a/third_party/rust/euclid/.cargo-checksum.json
+++ b/third_party/rust/euclid/.cargo-checksum.json
@@ -1,1 +1,1 @@
-{"files":{".travis.yml":"301590735ff27f124c03cef8598aa5397c88c59aba3d058edf0bde532965c346","COPYRIGHT":"ec82b96487e9e778ee610c7ab245162464782cfa1f555c2299333f8dbe5c036a","Cargo.toml":"2d7ade90b1883e9ec1e718b52a7c1785adf7d8f482a3d2a2813079ad15de906b","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"62065228e42caebca7e7d7db1204cbb867033de5982ca4009928915e4095f3a3","README.md":"625bec69c76ce5423fdd05cfe46922b2680ec517f97c5854ce34798d1d8a9541","src/approxeq.rs":"6594377e8f6c20f88f628520d8de9b9a59c5892a0ee9a6ccd13c8400c1499911","src/homogen.rs":"df6bdb87aee7422c19cf8ce633c70656ebea789b96f6fcc396baecc4c3ef6ab3","src/length.rs":"5c0784bccb1840f1bc86f45c80094584ca2f60b6a644797a5e760341c15c6e50","src/lib.rs":"ef31060a582a8a133750aeaa7244cc6bbb709a0aec7d964a76b54643bb9f7134","src/macros.rs":"ccb9aeb942f571ec4207334b87c87f59a9a4d666039d143d7673372679c42347","src/num.rs":"4439479fad5729073e0bfe0b96b547672a237430d48e564519759b9550baa033","src/point.rs":"50ccf38962b2aee2b0b2e7c516f24e54908286953cb7cf97b5a6b9fb7bdfc91b","src/rect.rs":"b96f267123972d7d924d08d8b93bea9333d71654febe20063c532a11f7c7ae30","src/rotation.rs":"2686d8624671f48e9c657a98c9ac3345f3c4028e65ee3ef588d407ffd020fb86","src/scale.rs":"80c96c99cc916cac155fc898cd34a771a64ab46a60340cb7de876d224f0c7cb1","src/side_offsets.rs":"604e104616777515e0e0e68262110c55fe9c0ce4deeb6d022e5b4984df11e29f","src/size.rs":"ee722964a6e6654eacd8f321f5c3f62452237316d9d2dac8a98753f6227c4fce","src/transform2d.rs":"edf9b82411a25d8f6b2a867a5b579c15316b3fd112eb463f6589012039670be3","src/transform3d.rs":"797c445c99edace0a6e51e166cdbeb667620c6fd98cbc0249742bbd09588dc7f","src/trig.rs":"78b8fb26d2fded11c4b8aa47935e80c474696aee1999c688642534b667e005d9","src/vector.rs":"37215522068612107acca427c83159f4bca79ae41a2f54d9c7d0feb4b28b2348"},"package":"c95fd0d455f114291a3109286bd387bd423770058474a2d3f38b712cd661df60"}
\ No newline at end of file
+{"files":{".travis.yml":"301590735ff27f124c03cef8598aa5397c88c59aba3d058edf0bde532965c346","COPYRIGHT":"ec82b96487e9e778ee610c7ab245162464782cfa1f555c2299333f8dbe5c036a","Cargo.toml":"d67b137d287c8debd8811e4deb3c2973eb9ee1ea11b31fcdd8217591482021f6","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"62065228e42caebca7e7d7db1204cbb867033de5982ca4009928915e4095f3a3","README.md":"625bec69c76ce5423fdd05cfe46922b2680ec517f97c5854ce34798d1d8a9541","src/approxeq.rs":"6594377e8f6c20f88f628520d8de9b9a59c5892a0ee9a6ccd13c8400c1499911","src/homogen.rs":"cb26346ad1ea2797bdc1cac7f532872becabf28a1f9c60792f86ad4a655582f9","src/length.rs":"3171315822707728b1bfbdd04a4190ffb7206b4bfc59e9dd072bb2caa05ff292","src/lib.rs":"b3c2303ab06ce972784c2ec4faa09ecdaa8e0706316f427c8a2009445a52f6e9","src/macros.rs":"877b4bd62b63ea120e568803281e7169f33fb811fe1c1515e56bfc44a74c34a2","src/num.rs":"4439479fad5729073e0bfe0b96b547672a237430d48e564519759b9550baa033","src/point.rs":"d18046853e19012e649a01991d45fdb1ba8f51eb55e52273e68f567cd7df932a","src/rect.rs":"1a4fbcf482e447218894c6a31753cb9b5a6c3e5377447ba7b5bceae7941a0772","src/rotation.rs":"982aaca640215bacc5d2dc60a8949bb2510d5b6d492975b8b6946a7c8f69b496","src/scale.rs":"fc07bcf47f3a1215023c830059f0d270e570cbd37fe8c367ef4a47b191f4ae3e","src/side_offsets.rs":"f114cb881256bbeff2ee2aa305d363e2dea65aa8535140f104f6fa9364bd02f5","src/size.rs":"f6a4f12fc50cc54220af089339cb7fde37f22c6dfcc4c2c676d24caab07b1790","src/transform2d.rs":"137344a16162f5cd1dc4a2ae87b8ea3fdde7597874835582378945f55e45513e","src/transform3d.rs":"efd971ba35e8a9ab59b0c4062b2625532147af0e57bf96b8cd09117524cf23ed","src/trig.rs":"97a263c4f178b0332501659ca8143f9f637a0755aca189dd31ac551bcd4cb73c","src/vector.rs":"d84103384907174d2b2206acd60d6b3261edb3ac971ec5e121ae22ce6bcca5d9"},"package":"47d5eb6310c8dd3e79f973952ddcb180bf6a98c01d341add49126a094b5598cc"}
\ No newline at end of file
--- a/third_party/rust/euclid/Cargo.toml
+++ b/third_party/rust/euclid/Cargo.toml
@@ -7,17 +7,17 @@
 #
 # If you believe there's an error in this file please file an
 # issue against the rust-lang/cargo repository. If you're
 # editing this file be aware that the upstream Cargo.toml
 # will likely look very different (and much more reasonable)
 
 [package]
 name = "euclid"
-version = "0.17.3"
+version = "0.18.1"
 authors = ["The Servo Project Developers"]
 description = "Geometry primitives"
 documentation = "https://docs.rs/euclid/"
 keywords = ["matrix", "vector", "linear-algebra", "geometry"]
 categories = ["science"]
 license = "MIT / Apache-2.0"
 repository = "https://github.com/servo/euclid"
 [dependencies.num-traits]
--- a/third_party/rust/euclid/src/homogen.rs
+++ b/third_party/rust/euclid/src/homogen.rs
@@ -7,19 +7,19 @@
 // option. This file may not be copied, modified, or distributed
 // except according to those terms.
 
 use point::{TypedPoint2D, TypedPoint3D};
 use vector::{TypedVector2D, TypedVector3D};
 
 use num::{One, Zero};
 
-use std::fmt;
-use std::marker::PhantomData;
-use std::ops::Div;
+use core::fmt;
+use core::marker::PhantomData;
+use core::ops::Div;
 
 
 define_matrix! {
     /// Homogeneous vector in 3D space.
     pub struct HomogeneousVector<T, U> {
         pub x: T,
         pub y: T,
         pub z: T,
@@ -32,31 +32,39 @@ impl<T, U> HomogeneousVector<T, U> {
     /// Constructor taking scalar values directly.
     #[inline]
     pub fn new(x: T, y: T, z: T, w: T) -> Self {
         HomogeneousVector { x, y, z, w, _unit: PhantomData }
     }
 }
 
 
-impl<T: Copy + Div<T, Output=T>, U> HomogeneousVector<T, U> {
+impl<T: Copy + Div<T, Output=T> + Zero + PartialOrd, U> HomogeneousVector<T, U> {
     /// Convert into Cartesian 2D point.
     ///
-    /// Note: possible division by zero.
+    /// Returns None if the point is on or behind the W=0 hemisphere.
     #[inline]
-    pub fn to_point2d(&self) -> TypedPoint2D<T, U> {
-        TypedPoint2D::new(self.x / self.w, self.y / self.w)
+    pub fn to_point2d(&self) -> Option<TypedPoint2D<T, U>> {
+        if self.w > T::zero() {
+            Some(TypedPoint2D::new(self.x / self.w, self.y / self.w))
+        } else {
+            None
+        }
     }
 
     /// Convert into Cartesian 3D point.
     ///
-    /// Note: possible division by zero.
+    /// Returns None if the point is on or behind the W=0 hemisphere.
     #[inline]
-    pub fn to_point3d(&self) -> TypedPoint3D<T, U> {
-        TypedPoint3D::new(self.x / self.w, self.y / self.w, self.z / self.w)
+    pub fn to_point3d(&self) -> Option<TypedPoint3D<T, U>> {
+        if self.w > T::zero() {
+            Some(TypedPoint3D::new(self.x / self.w, self.y / self.w, self.z / self.w))
+        } else {
+            None
+        }
     }
 }
 
 impl<T: Zero, U> From<TypedVector2D<T, U>> for HomogeneousVector<T, U> {
     #[inline]
     fn from(v: TypedVector2D<T, U>) -> Self {
         HomogeneousVector::new(v.x, v.y, T::zero(), T::zero())
     }
@@ -98,12 +106,18 @@ impl<T: fmt::Display, U> fmt::Display fo
 
 #[cfg(test)]
 mod homogeneous {
     use super::HomogeneousVector;
     use point::{Point2D, Point3D};
 
     #[test]
     fn roundtrip() {
-        assert_eq!(Point2D::new(1.0, 2.0), HomogeneousVector::from(Point2D::new(1.0, 2.0)).to_point2d());
-        assert_eq!(Point3D::new(1.0, -2.0, 0.1), HomogeneousVector::from(Point3D::new(1.0, -2.0, 0.1)).to_point3d());
+        assert_eq!(Some(Point2D::new(1.0, 2.0)), HomogeneousVector::from(Point2D::new(1.0, 2.0)).to_point2d());
+        assert_eq!(Some(Point3D::new(1.0, -2.0, 0.1)), HomogeneousVector::from(Point3D::new(1.0, -2.0, 0.1)).to_point3d());
+    }
+
+    #[test]
+    fn negative() {
+        assert_eq!(None, HomogeneousVector::<f32, ()>::new(1.0, 2.0, 3.0, 0.0).to_point2d());
+        assert_eq!(None, HomogeneousVector::<f32, ()>::new(1.0, -2.0, -3.0, -2.0).to_point3d());
     }
 }
--- a/third_party/rust/euclid/src/length.rs
+++ b/third_party/rust/euclid/src/length.rs
@@ -10,37 +10,37 @@
 
 use scale::TypedScale;
 use num::Zero;
 
 use num_traits::{NumCast, Saturating};
 use num::One;
 #[cfg(feature = "serde")]
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
-use std::cmp::Ordering;
-use std::ops::{Add, Div, Mul, Neg, Sub};
-use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign};
-use std::marker::PhantomData;
-use std::fmt;
+use core::cmp::Ordering;
+use core::ops::{Add, Div, Mul, Neg, Sub};
+use core::ops::{AddAssign, DivAssign, MulAssign, SubAssign};
+use core::marker::PhantomData;
+use core::fmt;
 
 /// A one-dimensional distance, with value represented by `T` and unit of measurement `Unit`.
 ///
 /// `T` can be any numeric type, for example a primitive type like `u64` or `f32`.
 ///
 /// `Unit` is not used in the representation of a `Length` value. It is used only at compile time
 /// to ensure that a `Length` stored with one unit is converted explicitly before being used in an
 /// expression that requires a different unit.  It may be a type without values, such as an empty
 /// enum.
 ///
 /// You can multiply a `Length` by a `scale::TypedScale` to convert it from one unit to
 /// another. See the [`TypedScale`] docs for an example.
 ///
 /// [`TypedScale`]: struct.TypedScale.html
 #[repr(C)]
-pub struct Length<T, Unit>(pub T, PhantomData<Unit>);
+pub struct Length<T, Unit>(pub T, #[doc(hidden)] pub PhantomData<Unit>);
 
 impl<T: Clone, Unit> Clone for Length<T, Unit> {
     fn clone(&self) -> Self {
         Length(self.0.clone(), PhantomData)
     }
 }
 
 impl<T: Copy, Unit> Copy for Length<T, Unit> {}
@@ -206,17 +206,22 @@ impl<U, T: Clone + Neg<Output = T>> Neg 
     #[inline]
     fn neg(self) -> Length<T, U> {
         Length::new(-self.get())
     }
 }
 
 impl<Unit, T0: NumCast + Clone> Length<T0, Unit> {
     /// Cast from one numeric representation to another, preserving the units.
-    pub fn cast<T1: NumCast + Clone>(&self) -> Option<Length<T1, Unit>> {
+    pub fn cast<T1: NumCast + Clone>(&self) -> Length<T1, Unit> {
+        self.try_cast().unwrap()
+    }
+
+    /// Fallible cast from one numeric representation to another, preserving the units.
+    pub fn try_cast<T1: NumCast + Clone>(&self) -> Option<Length<T1, Unit>> {
         NumCast::from(self.get()).map(Length::new)
     }
 }
 
 impl<Unit, T: Clone + PartialEq> PartialEq for Length<T, Unit> {
     fn eq(&self, other: &Self) -> bool {
         self.get().eq(&other.get())
     }
@@ -258,17 +263,17 @@ where
 
 #[cfg(test)]
 mod tests {
     use super::Length;
     use num::Zero;
 
     use num_traits::Saturating;
     use scale::TypedScale;
-    use std::f32::INFINITY;
+    use core::f32::INFINITY;
 
     enum Inch {}
     enum Mm {}
     enum Cm {}
     enum Second {}
 
     #[cfg(feature = "serde")]
     mod serde {
@@ -456,17 +461,17 @@ mod tests {
         let expected: Length<f32, Cm> = Length::new(-5.0);
         assert_eq!(result, expected);
     }
 
     #[test]
     fn test_cast() {
         let length_as_i32: Length<i32, Cm> = Length::new(5);
 
-        let result: Length<f32, Cm> = length_as_i32.cast().unwrap();
+        let result: Length<f32, Cm> = length_as_i32.cast();
 
         let length_as_f32: Length<f32, Cm> = Length::new(5.0);
         assert_eq!(result, length_as_f32);
     }
 
     #[test]
     fn test_equality() {
         let length_5_point_0: Length<f32, Cm> = Length::new(5.0);
--- a/third_party/rust/euclid/src/lib.rs
+++ b/third_party/rust/euclid/src/lib.rs
@@ -3,16 +3,17 @@
 //
 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
 // option. This file may not be copied, modified, or distributed
 // except according to those terms.
 
 #![cfg_attr(feature = "unstable", feature(fn_must_use))]
+#![cfg_attr(not(test), no_std)]
 
 //! A collection of strongly typed math tools for computer graphics with an inclination
 //! towards 2d graphics and layout.
 //!
 //! All types are generic over the scalar type of their component (`f32`, `i32`, etc.),
 //! and tagged with a generic Unit parameter which is useful to prevent mixing
 //! values from different spaces. For example it should not be legal to translate
 //! a screen-space position by a world-space vector and this can be expressed using
@@ -58,16 +59,19 @@
 #[cfg(feature = "serde")]
 #[macro_use]
 extern crate serde;
 
 extern crate num_traits;
 #[cfg(test)]
 extern crate rand;
 
+#[cfg(test)]
+use std as core;
+
 pub use length::Length;
 pub use scale::TypedScale;
 pub use transform2d::{Transform2D, TypedTransform2D};
 pub use transform3d::{Transform3D, TypedTransform3D};
 pub use point::{Point2D, Point3D, TypedPoint2D, TypedPoint3D, point2, point3};
 pub use vector::{TypedVector2D, TypedVector3D, Vector2D, Vector3D, vec2, vec3};
 pub use vector::{BoolVector2D, BoolVector3D, bvec2, bvec3};
 pub use homogen::HomogeneousVector;
@@ -118,9 +122,8 @@ pub type TypedMatrix4D<T, Src, Dst> = Ty
 
 /// Temporary alias to facilitate the transition to the new naming scheme
 #[deprecated]
 pub type ScaleFactor<T, Src, Dst> = TypedScale<T, Src, Dst>;
 
 /// Temporary alias to facilitate the transition to the new naming scheme
 #[deprecated]
 pub use Angle as Radians;
-
--- a/third_party/rust/euclid/src/macros.rs
+++ b/third_party/rust/euclid/src/macros.rs
@@ -13,17 +13,22 @@ macro_rules! define_matrix {
         pub struct $name:ident<T, $($phantom:ident),+> {
             $(pub $field:ident: T,)+
         }
     ) => (
         #[repr(C)]
         $(#[$attr])*
         pub struct $name<T, $($phantom),+> {
             $(pub $field: T,)+
-            _unit: PhantomData<($($phantom),+)>
+
+            // Keep this (secretly) public for the few cases where we would like to
+            // create static constants which currently can't be initialized with a
+            // function.
+            #[doc(hidden)]
+            pub _unit: PhantomData<($($phantom),+)>
         }
 
         impl<T: Clone, $($phantom),+> Clone for $name<T, $($phantom),+> {
             fn clone(&self) -> Self {
                 $name {
                     $($field: self.$field.clone(),)+
                     _unit: PhantomData,
                 }
@@ -54,28 +59,28 @@ macro_rules! define_matrix {
         {
             fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
                 where S: ::serde::Serializer
             {
                 ($(&self.$field,)+).serialize(serializer)
             }
         }
 
-        impl<T, $($phantom),+> ::std::cmp::Eq for $name<T, $($phantom),+>
-            where T: ::std::cmp::Eq {}
+        impl<T, $($phantom),+> ::core::cmp::Eq for $name<T, $($phantom),+>
+            where T: ::core::cmp::Eq {}
 
-        impl<T, $($phantom),+> ::std::cmp::PartialEq for $name<T, $($phantom),+>
-            where T: ::std::cmp::PartialEq
+        impl<T, $($phantom),+> ::core::cmp::PartialEq for $name<T, $($phantom),+>
+            where T: ::core::cmp::PartialEq
         {
             fn eq(&self, other: &Self) -> bool {
                 true $(&& self.$field == other.$field)+
             }
         }
 
-        impl<T, $($phantom),+> ::std::hash::Hash for $name<T, $($phantom),+>
-            where T: ::std::hash::Hash
+        impl<T, $($phantom),+> ::core::hash::Hash for $name<T, $($phantom),+>
+            where T: ::core::hash::Hash
         {
-            fn hash<H: ::std::hash::Hasher>(&self, h: &mut H) {
+            fn hash<H: ::core::hash::Hasher>(&self, h: &mut H) {
                 $(self.$field.hash(h);)+
             }
         }
     )
 }
--- a/third_party/rust/euclid/src/point.rs
+++ b/third_party/rust/euclid/src/point.rs
@@ -10,19 +10,19 @@
 use super::UnknownUnit;
 use approxeq::ApproxEq;
 use length::Length;
 use scale::TypedScale;
 use size::TypedSize2D;
 use num::*;
 use num_traits::{Float, NumCast};
 use vector::{TypedVector2D, TypedVector3D, vec2, vec3};
-use std::fmt;
-use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
-use std::marker::PhantomData;
+use core::fmt;
+use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
+use core::marker::PhantomData;
 
 define_matrix! {
     /// A 2d Point tagged with a unit.
     pub struct TypedPoint2D<T, U> {
         pub x: T,
         pub y: T,
     }
 }
@@ -63,18 +63,18 @@ impl<T: fmt::Display, U> fmt::Display fo
     }
 }
 
 impl<T, U> TypedPoint2D<T, U> {
     /// Constructor taking scalar values directly.
     #[inline]
     pub fn new(x: T, y: T) -> Self {
         TypedPoint2D {
-            x: x,
-            y: y,
+            x,
+            y,
             _unit: PhantomData,
         }
     }
 }
 
 impl<T: Copy, U> TypedPoint2D<T, U> {
     /// Constructor taking properly typed Lengths instead of scalar values.
     #[inline]
@@ -281,75 +281,84 @@ impl<T: Floor, U> TypedPoint2D<T, U> {
 
 impl<T: NumCast + Copy, U> TypedPoint2D<T, U> {
     /// Cast from one numeric representation to another, preserving the units.
     ///
     /// When casting from floating point to integer coordinates, the decimals are truncated
     /// as one would expect from a simple cast, but this behavior does not always make sense
     /// geometrically. Consider using `round()`, `ceil()` or `floor()` before casting.
     #[inline]
-    pub fn cast<NewT: NumCast + Copy>(&self) -> Option<TypedPoint2D<NewT, U>> {
+    pub fn cast<NewT: NumCast + Copy>(&self) -> TypedPoint2D<NewT, U> {
+        self.try_cast().unwrap()
+    }
+
+    /// Fallible cast from one numeric representation to another, preserving the units.
+    ///
+    /// When casting from floating point to integer coordinates, the decimals are truncated
+    /// as one would expect from a simple cast, but this behavior does not always make sense
+    /// geometrically. Consider using `round()`, `ceil()` or `floor()` before casting.
+    pub fn try_cast<NewT: NumCast + Copy>(&self) -> Option<TypedPoint2D<NewT, U>> {
         match (NumCast::from(self.x), NumCast::from(self.y)) {
             (Some(x), Some(y)) => Some(point2(x, y)),
             _ => None,
         }
     }
 
     // Convenience functions for common casts
 
     /// Cast into an `f32` point.
     #[inline]
     pub fn to_f32(&self) -> TypedPoint2D<f32, U> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an `f64` point.
     #[inline]
     pub fn to_f64(&self) -> TypedPoint2D<f64, U> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an `usize` point, truncating decimals if any.
     ///
     /// When casting from floating point points, it is worth considering whether
     /// to `round()`, `ceil()` or `floor()` before the cast in order to obtain
     /// the desired conversion behavior.
     #[inline]
     pub fn to_usize(&self) -> TypedPoint2D<usize, U> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an `u32` point, truncating decimals if any.
     ///
     /// When casting from floating point points, it is worth considering whether
     /// to `round()`, `ceil()` or `floor()` before the cast in order to obtain
     /// the desired conversion behavior.
     #[inline]
     pub fn to_u32(&self) -> TypedPoint2D<u32, U> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an i32 point, truncating decimals if any.
     ///
     /// When casting from floating point points, it is worth considering whether
     /// to `round()`, `ceil()` or `floor()` before the cast in order to obtain
     /// the desired conversion behavior.
     #[inline]
     pub fn to_i32(&self) -> TypedPoint2D<i32, U> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an i64 point, truncating decimals if any.
     ///
     /// When casting from floating point points, it is worth considering whether
     /// to `round()`, `ceil()` or `floor()` before the cast in order to obtain
     /// the desired conversion behavior.
     #[inline]
     pub fn to_i64(&self) -> TypedPoint2D<i64, U> {
-        self.cast().unwrap()
+        self.cast()
     }
 }
 
 impl<T, U> TypedPoint2D<T, U>
 where
     T: Copy + One + Add<Output = T> + Sub<Output = T> + Mul<Output = T>,
 {
     /// Linearly interpolate between this point and another point.
@@ -450,19 +459,19 @@ impl<T: fmt::Display, U> fmt::Display fo
     }
 }
 
 impl<T: Copy, U> TypedPoint3D<T, U> {
     /// Constructor taking scalar values directly.
     #[inline]
     pub fn new(x: T, y: T, z: T) -> Self {
         TypedPoint3D {
-            x: x,
-            y: y,
-            z: z,
+            x,
+            y,
+            z,
             _unit: PhantomData,
         }
     }
 
     /// Constructor taking properly typed Lengths instead of scalar values.
     #[inline]
     pub fn from_lengths(x: Length<T, U>, y: Length<T, U>, z: Length<T, U>) -> Self {
         point3(x.0, y.0, z.0)
@@ -645,79 +654,89 @@ impl<T: Floor, U> TypedPoint3D<T, U> {
 
 impl<T: NumCast + Copy, U> TypedPoint3D<T, U> {
     /// Cast from one numeric representation to another, preserving the units.
     ///
     /// When casting from floating point to integer coordinates, the decimals are truncated
     /// as one would expect from a simple cast, but this behavior does not always make sense
     /// geometrically. Consider using `round()`, `ceil()` or `floor()` before casting.
     #[inline]
-    pub fn cast<NewT: NumCast + Copy>(&self) -> Option<TypedPoint3D<NewT, U>> {
+    pub fn cast<NewT: NumCast + Copy>(&self) -> TypedPoint3D<NewT, U> {
+        self.try_cast().unwrap()
+    }
+
+    /// Fallible cast from one numeric representation to another, preserving the units.
+    ///
+    /// When casting from floating point to integer coordinates, the decimals are truncated
+    /// as one would expect from a simple cast, but this behavior does not always make sense
+    /// geometrically. Consider using `round()`, `ceil()` or `floor()` before casting.
+    #[inline]
+    pub fn try_cast<NewT: NumCast + Copy>(&self) -> Option<TypedPoint3D<NewT, U>> {
         match (
             NumCast::from(self.x),
             NumCast::from(self.y),
             NumCast::from(self.z),
         ) {
             (Some(x), Some(y), Some(z)) => Some(point3(x, y, z)),
             _ => None,
         }
     }
 
     // Convenience functions for common casts
 
     /// Cast into an `f32` point.
     #[inline]
     pub fn to_f32(&self) -> TypedPoint3D<f32, U> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an `f64` point.
     #[inline]
     pub fn to_f64(&self) -> TypedPoint3D<f64, U> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an `usize` point, truncating decimals if any.
     ///
     /// When casting from floating point points, it is worth considering whether
     /// to `round()`, `ceil()` or `floor()` before the cast in order to obtain
     /// the desired conversion behavior.
     #[inline]
     pub fn to_usize(&self) -> TypedPoint3D<usize, U> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an `u32` point, truncating decimals if any.
     ///
     /// When casting from floating point points, it is worth considering whether
     /// to `round()`, `ceil()` or `floor()` before the cast in order to obtain
     /// the desired conversion behavior.
     #[inline]
     pub fn to_u32(&self) -> TypedPoint3D<u32, U> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an `i32` point, truncating decimals if any.
     ///
     /// When casting from floating point points, it is worth considering whether
     /// to `round()`, `ceil()` or `floor()` before the cast in order to obtain
     /// the desired conversion behavior.
     #[inline]
     pub fn to_i32(&self) -> TypedPoint3D<i32, U> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an `i64` point, truncating decimals if any.
     ///
     /// When casting from floating point points, it is worth considering whether
     /// to `round()`, `ceil()` or `floor()` before the cast in order to obtain
     /// the desired conversion behavior.
     #[inline]
     pub fn to_i64(&self) -> TypedPoint3D<i64, U> {
-        self.cast().unwrap()
+        self.cast()
     }
 }
 
 impl<T: Copy + ApproxEq<T>, U> ApproxEq<TypedPoint3D<T, U>> for TypedPoint3D<T, U> {
     #[inline]
     fn approx_epsilon() -> Self {
         point3(
             T::approx_epsilon(),
--- a/third_party/rust/euclid/src/rect.rs
+++ b/third_party/rust/euclid/src/rect.rs
@@ -15,21 +15,21 @@ use point::TypedPoint2D;
 use vector::TypedVector2D;
 use side_offsets::TypedSideOffsets2D;
 use size::TypedSize2D;
 
 use num_traits::NumCast;
 #[cfg(feature = "serde")]
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 
-use std::borrow::Borrow;
-use std::cmp::PartialOrd;
-use std::fmt;
-use std::hash::{Hash, Hasher};
-use std::ops::{Add, Div, Mul, Sub};
+use core::borrow::Borrow;
+use core::cmp::PartialOrd;
+use core::fmt;
+use core::hash::{Hash, Hasher};
+use core::ops::{Add, Div, Mul, Sub};
 
 
 /// A 2d Rectangle optionally tagged with a unit.
 #[repr(C)]
 pub struct TypedRect<T, U = UnknownUnit> {
     pub origin: TypedPoint2D<T, U>,
     pub size: TypedSize2D<T, U>,
 }
@@ -92,18 +92,31 @@ impl<T: fmt::Display, U> fmt::Display fo
         write!(formatter, "Rect({} at {})", self.size, self.origin)
     }
 }
 
 impl<T, U> TypedRect<T, U> {
     /// Constructor.
     pub fn new(origin: TypedPoint2D<T, U>, size: TypedSize2D<T, U>) -> Self {
         TypedRect {
-            origin: origin,
-            size: size,
+            origin,
+            size,
+        }
+    }
+}
+
+impl<T, U> TypedRect<T, U>
+where
+    T: Copy + Zero
+{
+    /// Creates a rect of the given size, at offset zero.
+    pub fn from_size(size: TypedSize2D<T, U>) -> Self {
+        TypedRect {
+            origin: TypedPoint2D::zero(),
+            size,
         }
     }
 }
 
 impl<T, U> TypedRect<T, U>
 where
     T: Copy + Clone + Zero + PartialOrd + PartialEq + Add<T, Output = T> + Sub<T, Output = T>,
 {
@@ -452,18 +465,30 @@ impl<T: Copy, Unit> TypedRect<T, Unit> {
 }
 
 impl<T0: NumCast + Copy, Unit> TypedRect<T0, Unit> {
     /// Cast from one numeric representation to another, preserving the units.
     ///
     /// When casting from floating point to integer coordinates, the decimals are truncated
     /// as one would expect from a simple cast, but this behavior does not always make sense
     /// geometrically. Consider using round(), round_in or round_out() before casting.
-    pub fn cast<T1: NumCast + Copy>(&self) -> Option<TypedRect<T1, Unit>> {
-        match (self.origin.cast(), self.size.cast()) {
+    pub fn cast<T1: NumCast + Copy>(&self) -> TypedRect<T1, Unit> {
+        TypedRect::new(
+            self.origin.cast(),
+            self.size.cast(),
+        )
+    }
+
+    /// Fallible cast from one numeric representation to another, preserving the units.
+    ///
+    /// When casting from floating point to integer coordinates, the decimals are truncated
+    /// as one would expect from a simple cast, but this behavior does not always make sense
+    /// geometrically. Consider using round(), round_in or round_out() before casting.
+    pub fn try_cast<T1: NumCast + Copy>(&self) -> Option<TypedRect<T1, Unit>> {
+        match (self.origin.try_cast(), self.size.try_cast()) {
             (Some(origin), Some(size)) => Some(TypedRect::new(origin, size)),
             _ => None,
         }
     }
 }
 
 impl<T: Floor + Ceil + Round + Add<T, Output = T> + Sub<T, Output = T>, U> TypedRect<T, U> {
     /// Return a rectangle with edges rounded to integer coordinates, such that
@@ -500,58 +525,66 @@ impl<T: Floor + Ceil + Round + Add<T, Ou
         TypedRect::new(origin, TypedSize2D::new(size.x, size.y))
     }
 }
 
 // Convenience functions for common casts
 impl<T: NumCast + Copy, Unit> TypedRect<T, Unit> {
     /// Cast into an `f32` rectangle.
     pub fn to_f32(&self) -> TypedRect<f32, Unit> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an `f64` rectangle.
     pub fn to_f64(&self) -> TypedRect<f64, Unit> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an `usize` rectangle, truncating decimals if any.
     ///
     /// When casting from floating point rectangles, it is worth considering whether
     /// to `round()`, `round_in()` or `round_out()` before the cast in order to
     /// obtain the desired conversion behavior.
     pub fn to_usize(&self) -> TypedRect<usize, Unit> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an `u32` rectangle, truncating decimals if any.
     ///
     /// When casting from floating point rectangles, it is worth considering whether
     /// to `round()`, `round_in()` or `round_out()` before the cast in order to
     /// obtain the desired conversion behavior.
     pub fn to_u32(&self) -> TypedRect<u32, Unit> {
-        self.cast().unwrap()
+        self.cast()
     }
 
     /// Cast into an `i32` rectangle, truncating decimals if any.
     ///
     /// When casting from floating point rectangles, it is worth considering whether
     /// to `round()`, `round_in()` or `round_out()` before the cast in order to
     /// obtain the desired conversion behavior.
     pub fn to_i32(&self) -> TypedRect<i32, Unit> {
-        self.cast().unwrap()