Backed out 5 changesets (bug 1077652) for a11y bustage
authorPhil Ringnalda <philringnalda@gmail.com>
Thu, 04 Dec 2014 21:12:35 -0800
changeset 218381 b5d29f43e52a7e22fba5047eb74855da9cc49814
parent 218380 cb8849c52508954b43f1e4fd57990e6b1904e717
child 218382 32853a227d727b8cd13543d5cb381364d8e993c5
child 218495 18188c19a3c3ec9cd4474de5bd1f5558ec750a36
push id10266
push userphilringnalda@gmail.com
push dateFri, 05 Dec 2014 05:12:54 +0000
treeherderfx-team@b5d29f43e52a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1077652
milestone37.0a1
backs outeda93688e4b51393270c27e59adf802c89432018
00bdefd9c4fa4d4ff5b1c77e2b2d876b91a00528
31c163560ed49a0e8129d18afc70f3aedb5f4b68
afd5df8ab5afb1379a89d0a2b5090385c0dfefb8
884ce04c007f82475d4074ee6bb76c1d7373bf45
Backed out 5 changesets (bug 1077652) for a11y bustage CLOSED TREE Backed out changeset eda93688e4b5 (bug 1077652) Backed out changeset 00bdefd9c4fa (bug 1077652) Backed out changeset 31c163560ed4 (bug 1077652) Backed out changeset afd5df8ab5af (bug 1077652) Backed out changeset 884ce04c007f (bug 1077652)
accessible/tests/mochitest/events/test_docload.html
accessible/tests/mochitest/tree/test_dochierarchy.html
accessible/tests/mochitest/tree/test_tabbrowser.xul
browser/base/content/browser.js
browser/base/content/tabbrowser.xml
browser/base/content/test/newtab/browser_newtab_background_captures.js
browser/base/content/test/newtab/head.js
browser/components/nsBrowserGlue.js
browser/modules/BrowserNewTabPreloader.jsm
browser/modules/moz.build
testing/mochitest/browser-test.js
--- a/accessible/tests/mochitest/events/test_docload.html
+++ b/accessible/tests/mochitest/events/test_docload.html
@@ -10,16 +10,35 @@
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
 
   <script type="application/javascript"
           src="../common.js"></script>
   <script type="application/javascript"
           src="../role.js"></script>
   <script type="application/javascript"
           src="../states.js"></script>
+
+  <script type="application/javascript">
+    // Front end stuff sometimes likes to stuff things in the hidden window(s)
+    // in which case there's accessibles for that content.
+    Components.utils.import("resource://gre/modules/Services.jsm");
+
+    // Force the creation of an accessible for the hidden window's document.
+    var doc = Services.appShell.hiddenDOMWindow.document;
+    gAccRetrieval.getAccessibleFor(doc);
+
+    // The private hidden window will be lazily created that's why we need to do
+    // it here *before* loading '../events.js' or else we'll have a duplicate
+    // reorder event.
+    var privateDoc = Services.appShell.hiddenPrivateDOMWindow.document;
+
+    // Force the creation of an accessible for the private hidden window's doc.
+    gAccRetrieval.getAccessibleFor(privateDoc);
+  </script>
+
   <script type="application/javascript"
           src="../events.js"></script>
 
   <script type="application/javascript">
     ////////////////////////////////////////////////////////////////////////////
     // Invokers
 
     function changeIframeSrc(aIdentifier, aURL)
@@ -174,16 +193,22 @@
         var accTree = {
           role: ROLE_APP_ROOT,
           children: [
             {
               role: ROLE_CHROME_WINDOW
             },
             {
               role: ROLE_CHROME_WINDOW
+            },
+            {
+              role: ROLE_CHROME_WINDOW
+            },
+            {
+              role: ROLE_CHROME_WINDOW
             }
           ]
         };
 
         testAccessibleTree(this.mRootAcc, accTree);
 
         var dlgDoc = this.mDialog.document;
         ok(isAccessibleInCache(dlgDoc),
--- a/accessible/tests/mochitest/tree/test_dochierarchy.html
+++ b/accessible/tests/mochitest/tree/test_dochierarchy.html
@@ -26,25 +26,20 @@
       getAccessible(window.parent.document, [nsIAccessibleDocument]) :
       getAccessible(document, [nsIAccessibleDocument]);
     var testDoc = getAccessible(document, [nsIAccessibleDocument]);
     var iframeDoc = getAccessible("iframe").firstChild.
       QueryInterface(nsIAccessibleDocument);
 
     is(root.parentDocument, null,
        "Wrong parent document of root accessible");
-    ok(root.childDocumentCount >= 1,
+    is(root.childDocumentCount, 1,
        "Wrong child document count of root accessible");
-
-    var tabDocumentFound = false;
-    for (var i = 0; i < root.childDocumentCount && !tabDocumentFound; i++) {
-      tabDocumentFound = root.getChildDocumentAt(i) == tabDoc;
-    }
-    ok(tabDocumentFound,
-       "Tab document not found in children of the root accessible");
+    is(root.getChildDocumentAt(0), tabDoc,
+       "Wrong child document at index 0 of root accessible");
 
     is(tabDoc.parentDocument, root,
        "Wrong parent document of tab document");
     is(tabDoc.childDocumentCount, 1,
        "Wrong child document count of tab document");
     is(tabDoc.getChildDocumentAt(0), (tabDoc == testDoc ? iframeDoc : testDoc),
        "Wrong child document at index 0 of tab document");
 
--- a/accessible/tests/mochitest/tree/test_tabbrowser.xul
+++ b/accessible/tests/mochitest/tree/test_tabbrowser.xul
@@ -164,33 +164,16 @@
                     {
                       // #document ("about:mozilla")
                       role: ROLE_DOCUMENT
                       // children: [ ... ] // Ignore document content.
                     }
                   ]
                 }
               ]
-            },
-            {
-              // notificationbox
-              role: ROLE_PROPERTYPAGE,
-              children: [
-                {
-                  // browser
-                  role: ROLE_INTERNAL_FRAME,
-                  children: [
-                    {
-                      // #document ("about:newtab" preloaded)
-                      role: ROLE_APPLICATION
-                      // children: [ ... ] // Ignore document content.
-                    }
-                  ]
-                }
-              ]
             }
           ]
         };
 
         testAccessibleTree(tabBrowser().mTabBox.tabpanels, tabboxAccTree);
       }
 
       this.getID = function testTabHierarchy_getID()
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -149,16 +149,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
   "resource://gre/modules/PageThumbs.jsm");
 
 #ifdef MOZ_SAFE_BROWSING
 XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
   "resource://gre/modules/SafeBrowsing.jsm");
 #endif
 
+XPCOMUtils.defineLazyModuleGetter(this, "gBrowserNewTabPreloader",
+  "resource:///modules/BrowserNewTabPreloader.jsm", "BrowserNewTabPreloader");
+
 XPCOMUtils.defineLazyModuleGetter(this, "gCustomizationTabPreloader",
   "resource:///modules/CustomizationTabPreloader.jsm", "CustomizationTabPreloader");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Translation",
   "resource:///modules/translation/Translation.jsm");
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1508,146 +1508,16 @@
           <![CDATA[
             let shouldBeRemote = gMultiProcessBrowser &&
                                  E10SUtils.shouldBrowserBeRemote(aURL);
             return this.updateBrowserRemoteness(aBrowser, shouldBeRemote);
           ]]>
         </body>
       </method>
 
-      <field name="_preloadedBrowser">null</field>
-      <method name="_getPreloadedBrowser">
-        <body>
-          <![CDATA[
-            if (!this._isPreloadingEnabled()) {
-              return null;
-            }
-
-            // The preloaded browser might be null.
-            let browser = this._preloadedBrowser;
-
-            // Consume the browser.
-            this._preloadedBrowser = null;
-
-            // Attach the nsIFormFillController now that we know the browser
-            // will be used. If we do that before and the preloaded browser
-            // won't be consumed until shutdown then we leak a docShell.
-            if (browser && this.hasAttribute("autocompletepopup")) {
-              browser.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
-              browser.attachFormFill();
-            }
-
-            return browser;
-          ]]>
-        </body>
-      </method>
-
-      <method name="_isPreloadingEnabled">
-        <body>
-          <![CDATA[
-            // Preloading for the newtab page is enabled when the pref is true
-            // and the URL is "about:newtab". We do not support preloading for
-            // custom newtab URLs. We intentionally disable preloading for e10s
-            // as the goal here is that page loads do not affect tab animations.
-            return Services.prefs.getBoolPref("browser.newtab.preload") &&
-                   !Services.prefs.prefHasUserValue("browser.newtab.url") &&
-                   !gMultiProcessBrowser;
-          ]]>
-        </body>
-      </method>
-
-      <method name="_createPreloadBrowser">
-        <body>
-          <![CDATA[
-            // Do nothing if we have a preloaded browser already
-            // or preloading of newtab pages is disabled.
-            if (this._preloadedBrowser || !this._isPreloadingEnabled()) {
-              return;
-            }
-
-            let browser = this._createBrowser({isPreloadBrowser: true});
-            this._preloadedBrowser = browser;
-
-            let notificationbox = this.getNotificationBox(browser);
-            this.mPanelContainer.appendChild(notificationbox);
-
-            browser.loadURI(BROWSER_NEW_TAB_URL);
-            browser.docShellIsActive = false;
-          ]]>
-        </body>
-      </method>
-
-      <method name="_createBrowser">
-        <parameter name="aParams"/>
-        <body>
-          <![CDATA[
-            const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-
-            let remote = aParams && aParams.remote;
-            let uriIsAboutBlank = aParams && aParams.uriIsAboutBlank;
-            let isPreloadBrowser = aParams && aParams.isPreloadBrowser;
-
-            let b = document.createElementNS(NS_XUL, "browser");
-            b.setAttribute("type", "content-targetable");
-            b.setAttribute("message", "true");
-            b.setAttribute("messagemanagergroup", "browsers");
-            b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu"));
-            b.setAttribute("tooltip", this.getAttribute("contenttooltip"));
-
-            if (remote)
-              b.setAttribute("remote", "true");
-
-            if (window.gShowPageResizers && window.windowState == window.STATE_NORMAL) {
-              b.setAttribute("showresizer", "true");
-            }
-
-            if (!isPreloadBrowser && this.hasAttribute("autocompletepopup"))
-              b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
-
-            if (this.hasAttribute("selectpopup"))
-              b.setAttribute("selectpopup", this.getAttribute("selectpopup"));
-
-            b.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
-
-            // Create the browserStack container
-            var stack = document.createElementNS(NS_XUL, "stack");
-            stack.className = "browserStack";
-            stack.appendChild(b);
-            stack.setAttribute("flex", "1");
-
-            // Create the browserContainer
-            var browserContainer = document.createElementNS(NS_XUL, "vbox");
-            browserContainer.className = "browserContainer";
-            browserContainer.appendChild(stack);
-            browserContainer.setAttribute("flex", "1");
-
-            // Create the sidebar container
-            var browserSidebarContainer = document.createElementNS(NS_XUL,
-                                                                   "hbox");
-            browserSidebarContainer.className = "browserSidebarContainer";
-            browserSidebarContainer.appendChild(browserContainer);
-            browserSidebarContainer.setAttribute("flex", "1");
-
-            // Add the Message and the Browser to the box
-            var notificationbox = document.createElementNS(NS_XUL,
-                                                           "notificationbox");
-            notificationbox.setAttribute("flex", "1");
-            notificationbox.appendChild(browserSidebarContainer);
-
-            // Prevent the superfluous initial load of a blank document
-            // if we're going to load something other than about:blank.
-            if (!uriIsAboutBlank) {
-              b.setAttribute("nodefaultsrc", "true");
-            }
-
-            return b;
-          ]]>
-        </body>
-      </method>
-
       <method name="addTab">
         <parameter name="aURI"/>
         <parameter name="aReferrerURI"/>
         <parameter name="aCharset"/>
         <parameter name="aPostData"/>
         <parameter name="aOwner"/>
         <parameter name="aAllowThirdPartyFixup"/>
         <body>
@@ -1708,53 +1578,88 @@
             this._visibleTabs = null;
 
             this.tabContainer.appendChild(t);
 
             // If this new tab is owned by another, assert that relationship
             if (aOwner)
               t.owner = aOwner;
 
-            let b;
-            let usingPreloadedContent = false;
-            let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window);
-
-            // If we open a new tab with the newtab URL,
-            // check if there is a preloaded browser ready.
-            if (aURI == BROWSER_NEW_TAB_URL && !isPrivateWindow) {
-              b = this._getPreloadedBrowser();
-              usingPreloadedContent = !!b;
+            var b = document.createElementNS(
+              "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+                                             "browser");
+            b.setAttribute("type", "content-targetable");
+            b.setAttribute("message", "true");
+            b.setAttribute("messagemanagergroup", "browsers");
+            b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu"));
+            b.setAttribute("tooltip", this.getAttribute("contenttooltip"));
+
+            if (remote)
+              b.setAttribute("remote", "true");
+
+            if (window.gShowPageResizers && window.windowState == window.STATE_NORMAL) {
+              b.setAttribute("showresizer", "true");
             }
 
-            if (!b) {
-              // No preloaded browser found, create one.
-              b = this._createBrowser({remote, uriIsAboutBlank});
-            }
-
-            let notificationbox = this.getNotificationBox(b);
+            if (this.hasAttribute("autocompletepopup"))
+              b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
+
+            if (this.hasAttribute("selectpopup"))
+              b.setAttribute("selectpopup", this.getAttribute("selectpopup"));
+
+            b.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
+
+            // Create the browserStack container
+            var stack = document.createElementNS(NS_XUL, "stack");
+            stack.className = "browserStack";
+            stack.appendChild(b);
+            stack.setAttribute("flex", "1");
+
+            // Create the browserContainer
+            var browserContainer = document.createElementNS(NS_XUL, "vbox");
+            browserContainer.className = "browserContainer";
+            browserContainer.appendChild(stack);
+            browserContainer.setAttribute("flex", "1");
+
+            // Create the sidebar container
+            var browserSidebarContainer = document.createElementNS(NS_XUL,
+                                                                   "hbox");
+            browserSidebarContainer.className = "browserSidebarContainer";
+            browserSidebarContainer.appendChild(browserContainer);
+            browserSidebarContainer.setAttribute("flex", "1");
+
+            // Add the Message and the Browser to the box
+            var notificationbox = document.createElementNS(NS_XUL,
+                                                           "notificationbox");
+            notificationbox.setAttribute("flex", "1");
+            notificationbox.appendChild(browserSidebarContainer);
+
             var position = this.tabs.length - 1;
             var uniqueId = this._generateUniquePanelID();
             notificationbox.id = uniqueId;
             t.linkedPanel = uniqueId;
             t.linkedBrowser = b;
             this._tabForBrowser.set(b, t);
             t._tPos = position;
             t.lastAccessed = Date.now();
             this.tabContainer._setPositionalAttributes();
 
-            // Inject the <browser> into the DOM if necessary.
-            if (!notificationbox.parentNode) {
-              // NB: this appendChild call causes us to run constructors for the
-              // browser element, which fires off a bunch of notifications. Some
-              // of those notifications can cause code to run that inspects our
-              // state, so it is important that the tab element is fully
-              // initialized by this point.
-              this.mPanelContainer.appendChild(notificationbox);
+            // Prevent the superfluous initial load of a blank document
+            // if we're going to load something other than about:blank.
+            if (!uriIsAboutBlank) {
+              b.setAttribute("nodefaultsrc", "true");
             }
 
+            // NB: this appendChild call causes us to run constructors for the
+            // browser element, which fires off a bunch of notifications. Some
+            // of those notifications can cause code to run that inspects our
+            // state, so it is important that the tab element is fully
+            // initialized by this point.
+            this.mPanelContainer.appendChild(notificationbox);
+
             // We've waited until the tab is in the DOM to set the label. This
             // allows the TabLabelModified event to be properly dispatched.
             if (!aURI || isBlankPageURL(aURI)) {
               t.label = this.mStringBundle.getString("tabs.emptyTabTitle");
             }
 
             this.tabContainer.updateVisibility();
 
@@ -1764,31 +1669,38 @@
                                      .createInstance(Components.interfaces.nsIWebProgress);
             filter.addProgressListener(tabListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
             b.webProgress.addProgressListener(filter, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
             this.mTabListeners[position] = tabListener;
             this.mTabFilters[position] = filter;
 
             b.droppedLinkHandler = handleDroppedLink;
 
-            // Swap in a preloaded customize tab, if available.
-            if (aURI == "about:customizing") {
-              usingPreloadedContent = gCustomizationTabPreloader.newTab(t);
+            // If we just created a new tab that loads the default
+            // newtab url, swap in a preloaded page if possible.
+            // Do nothing if we're a private window.
+            let docShellsSwapped = false;
+            if (aURI == BROWSER_NEW_TAB_URL &&
+                !PrivateBrowsingUtils.isWindowPrivate(window) &&
+                !gMultiProcessBrowser) {
+              docShellsSwapped = gBrowserNewTabPreloader.newTab(t);
+            } else if (aURI == "about:customizing") {
+              docShellsSwapped = gCustomizationTabPreloader.newTab(t);
             }
 
             // Dispatch a new tab notification.  We do this once we're
             // entirely done, so that things are in a consistent state
             // even if the event listener opens or closes tabs.
             var evt = document.createEvent("Events");
             evt.initEvent("TabOpen", true, false);
             t.dispatchEvent(evt);
 
             // If we didn't swap docShells with a preloaded browser
             // then let's just continue loading the page normally.
-            if (!usingPreloadedContent && !uriIsAboutBlank) {
+            if (!docShellsSwapped && !uriIsAboutBlank) {
               // pretend the user typed this so it'll be available till
               // the document successfully loads
               if (aURI && gInitialPages.indexOf(aURI) == -1)
                 b.userTypedValue = aURI;
 
               let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
               if (aAllowThirdPartyFixup) {
                 flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
@@ -4359,19 +4271,16 @@
           }
 
           // XXXmano: this is a temporary workaround for bug 345399
           // We need to manually update the scroll buttons disabled state
           // if a tab was inserted to the overflow area or removed from it
           // without any scrolling and when the tabbar has already
           // overflowed.
           this.mTabstrip._updateScrollButtonsDisabledState();
-
-          // Preload the next about:newtab if there isn't one already.
-          this.tabbrowser._createPreloadBrowser();
         ]]></body>
       </method>
 
       <method name="_canAdvanceToTab">
         <parameter name="aTab"/>
         <body>
         <![CDATA[
           return !aTab.closing;
--- a/browser/base/content/test/newtab/browser_newtab_background_captures.js
+++ b/browser/base/content/test/newtab/browser_newtab_background_captures.js
@@ -6,16 +6,17 @@
  * when unhidden, do allow background captures.
  */
 
 const CAPTURE_PREF = "browser.pagethumbnails.capturing_disabled";
 
 function runTests() {
   let imports = {};
   Cu.import("resource://gre/modules/PageThumbs.jsm", imports);
+  Cu.import("resource:///modules/BrowserNewTabPreloader.jsm", imports);
 
   // Disable captures.
   let originalDisabledState = Services.prefs.getBoolPref(CAPTURE_PREF);
   Services.prefs.setBoolPref(CAPTURE_PREF, true);
 
   // Make sure the thumbnail doesn't exist yet.
   let url = "http://example.com/";
   let path = imports.PageThumbsStorage.getFilePathForURL(url);
@@ -25,39 +26,57 @@ function runTests() {
     file.remove(false);
   }
   catch (err) {}
 
   // Add a top site.
   yield setLinks("-1");
 
   // We need a handle to a hidden, pre-loaded newtab so we can verify that it
-  // doesn't allow background captures. Ensure we have a preloaded browser.
-  gBrowser._createPreloadBrowser();
+  // doesn't allow background captures.  Add a newtab, which triggers creation
+  // of a hidden newtab, and then keep calling BrowserNewTabPreloader.newTab
+  // until it returns true, meaning that it swapped the passed-in tab's docshell
+  // for the hidden newtab docshell.
+  let tab = gWindow.gBrowser.addTab("about:blank");
+  yield addNewTabPageTab();
+
+  // When newtab is loaded very quickly (which is what happens in 99% of cases)
+  // there is no need to wait so no tests are run. Because each test requires
+  // either a pass, fail or todo we run a dummy test here.
+  ok(true, "Each test requires at least one pass, fail or todo so here is a pass.");
 
-  // Wait for the preloaded browser to load.
-  yield waitForBrowserLoad(gBrowser._preloadedBrowser);
+  let swapWaitCount = 0;
+  let swapped = imports.BrowserNewTabPreloader.newTab(tab);
+  while (!swapped) {
+    if (++swapWaitCount == 10) {
+      ok(false, "Timed out waiting for newtab docshell swap.");
+      return;
+    }
+    // Give the hidden newtab some time to finish loading.
+    yield wait(2000);
+    info("Checking newtab swap " + swapWaitCount);
+    swapped = imports.BrowserNewTabPreloader.newTab(tab);
+  }
 
-  // We're now ready to use the preloaded browser.
-  BrowserOpenTab();
-  let tab = gBrowser.selectedTab;
+  // The tab's docshell is now the previously hidden newtab docshell.
   let doc = tab.linkedBrowser.contentDocument;
 
   // Enable captures.
   Services.prefs.setBoolPref(CAPTURE_PREF, false);
 
   // Showing the preloaded tab should trigger thumbnail capture.
   Services.obs.addObserver(function onCreate(subj, topic, data) {
     if (data != url)
       return;
     Services.obs.removeObserver(onCreate, "page-thumbnail:create");
-    ok(true, "thumbnail created after preloaded tab was shown");
-
     // Test finished!
     Services.prefs.setBoolPref(CAPTURE_PREF, originalDisabledState);
-    gBrowser.removeTab(tab);
     file.remove(false);
     TestRunner.next();
   }, "page-thumbnail:create", false);
 
   info("Waiting for thumbnail capture");
   yield true;
 }
+
+function wait(ms) {
+  setTimeout(TestRunner.next, ms);
+}
--- a/browser/base/content/test/newtab/head.js
+++ b/browser/base/content/test/newtab/head.js
@@ -369,37 +369,31 @@ function addNewTabPageTabPromise() {
       NewTabUtils.links.populateCache(function () {
         deferred.resolve(whenSearchInitDone());
       });
     } else {
       deferred.resolve();
     }
   }
 
+  // The new tab page might have been preloaded in the background.
+  if (browser.contentDocument.readyState == "complete") {
+    waitForCondition(() => !browser.contentDocument.hidden).then(whenNewTabLoaded);
+    return deferred.promise;
+  }
+
   // Wait for the new tab page to be loaded.
-  waitForBrowserLoad(browser, function () {
-    // Wait for the document to become visible in case it was preloaded.
-    waitForCondition(() => !browser.contentDocument.hidden).then(whenNewTabLoaded);
-  });
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    whenNewTabLoaded();
+  }, true);
 
   return deferred.promise;
 }
 
-function waitForBrowserLoad(browser, callback = TestRunner.next) {
-  if (browser.contentDocument.readyState == "complete") {
-    executeSoon(callback);
-    return;
-  }
-
-  browser.addEventListener("load", function onLoad() {
-    browser.removeEventListener("load", onLoad, true);
-    executeSoon(callback);
-  }, true);
-}
-
 /**
  * Compares the current grid arrangement with the given pattern.
  * @param the pattern (see below)
  * @param the array of sites to compare with (optional)
  *
  * Example: checkGrid("3p,2,,1p")
  * Result: We expect the first cell to contain the pinned site 'http://example3.com/'.
  *         The second cell contains 'http://example2.com/'. The third cell is empty.
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -48,16 +48,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource:///modules/WebappManager.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
                                   "resource://gre/modules/PageThumbs.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
                                   "resource://gre/modules/NewTabUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserNewTabPreloader",
+                                  "resource:///modules/BrowserNewTabPreloader.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizationTabPreloader",
                                   "resource:///modules/CustomizationTabPreloader.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PdfJs",
                                   "resource://pdf.js/PdfJs.jsm");
 
 #ifdef NIGHTLY_BUILD
 XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils",
@@ -783,16 +786,17 @@ BrowserGlue.prototype = {
     try {
       let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]
                          .getService(Ci.nsIAppStartup);
       appStartup.trackStartupCrashEnd();
     } catch (e) {
       Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e);
     }
 
+    BrowserNewTabPreloader.uninit();
     CustomizationTabPreloader.uninit();
     WebappManager.uninit();
 #ifdef NIGHTLY_BUILD
     if (Services.prefs.getBoolPref("dom.identity.enabled")) {
       SignInToWebsiteUX.uninit();
     }
 #endif
     webrtcUI.uninit();
new file mode 100644
--- /dev/null
+++ b/browser/modules/BrowserNewTabPreloader.jsm
@@ -0,0 +1,379 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["BrowserNewTabPreloader"];
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window%20id='win'/>";
+const NEWTAB_URL = "about:newtab";
+
+const PREF_NEWTAB_URL = "browser.newtab.url";
+const PREF_NEWTAB_PRELOAD = "browser.newtab.preload";
+
+// The interval between swapping in a preload docShell and kicking off the
+// next preload in the background.
+const PRELOADER_INTERVAL_MS = 600;
+// The number of miliseconds we'll wait after we received a notification that
+// causes us to update our list of browsers and tabbrowser sizes. This acts as
+// kind of a damper when too many events are occuring in quick succession.
+const PRELOADER_UPDATE_DELAY_MS = 3000;
+
+const TOPIC_TIMER_CALLBACK = "timer-callback";
+const TOPIC_DELAYED_STARTUP = "browser-delayed-startup-finished";
+const TOPIC_XUL_WINDOW_CLOSED = "xul-window-destroyed";
+
+const BROWSER_CONTENT_SCRIPT = "chrome://browser/content/content.js";
+
+function isPreloadingEnabled() {
+  return Services.prefs.getBoolPref(PREF_NEWTAB_PRELOAD) &&
+         !Services.prefs.prefHasUserValue(PREF_NEWTAB_URL);
+}
+
+function createTimer(obj, delay) {
+  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.init(obj, delay, Ci.nsITimer.TYPE_ONE_SHOT);
+  return timer;
+}
+
+function clearTimer(timer) {
+  if (timer) {
+    timer.cancel();
+  }
+  return null;
+}
+
+this.BrowserNewTabPreloader = {
+  uninit: function Preloader_uninit() {
+    HostFrame.destroy();
+    HiddenBrowsers.uninit();
+  },
+
+  newTab: function Preloader_newTab(aTab) {
+    if (!isPreloadingEnabled()) {
+      return false;
+    }
+
+    let win = aTab.ownerDocument.defaultView;
+    if (win.gBrowser) {
+      let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                     .getInterface(Ci.nsIDOMWindowUtils);
+
+      let {width, height} = utils.getBoundsWithoutFlushing(win.gBrowser);
+      let hiddenBrowser = HiddenBrowsers.get(width, height)
+      if (hiddenBrowser) {
+        return hiddenBrowser.swapWithNewTab(aTab);
+      }
+    }
+
+    return false;
+  }
+};
+
+Object.freeze(BrowserNewTabPreloader);
+
+let HiddenBrowsers = {
+  _browsers: null,
+  _updateTimer: null,
+
+  _topics: [
+    TOPIC_DELAYED_STARTUP,
+    TOPIC_XUL_WINDOW_CLOSED
+  ],
+
+  _init: function () {
+    this._browsers = new Map();
+    this._updateBrowserSizes();
+    this._topics.forEach(t => Services.obs.addObserver(this, t, false));
+  },
+
+  uninit: function () {
+    if (this._browsers) {
+      this._topics.forEach(t => Services.obs.removeObserver(this, t, false));
+      this._updateTimer = clearTimer(this._updateTimer);
+
+      for (let [key, browser] of this._browsers) {
+        browser.destroy();
+      }
+      this._browsers = null;
+    }
+  },
+
+  get: function (width, height) {
+    // Initialize if this is the first call.
+    if (!this._browsers) {
+      this._init();
+    }
+
+    let key = width + "x" + height;
+    if (!this._browsers.has(key)) {
+      // Update all browsers' sizes if we can't find a matching one.
+      this._updateBrowserSizes();
+    }
+
+    // We should now have a matching browser.
+    if (this._browsers.has(key)) {
+      return this._browsers.get(key);
+    }
+
+    // We should never be here. Return the first browser we find.
+    Cu.reportError("NewTabPreloader: no matching browser found after updating");
+    for (let [size, browser] of this._browsers) {
+      return browser;
+    }
+
+    // We should really never be here.
+    Cu.reportError("NewTabPreloader: not even a single browser was found?");
+    return null;
+  },
+
+  observe: function (subject, topic, data) {
+    if (topic === TOPIC_TIMER_CALLBACK) {
+      this._updateTimer = null;
+      this._updateBrowserSizes();
+    } else {
+      this._updateTimer = clearTimer(this._updateTimer);
+      this._updateTimer = createTimer(this, PRELOADER_UPDATE_DELAY_MS);
+    }
+  },
+
+  _updateBrowserSizes: function () {
+    let sizes = this._collectTabBrowserSizes();
+    let toRemove = [];
+
+    // Iterate all browsers and check that they
+    // each can be assigned to one of the sizes.
+    for (let [key, browser] of this._browsers) {
+      if (sizes.has(key)) {
+        // We already have a browser for that size, great!
+        sizes.delete(key);
+      } else {
+        // This browser is superfluous or needs to be resized.
+        toRemove.push(browser);
+        this._browsers.delete(key);
+      }
+    }
+
+    // Iterate all sizes that we couldn't find a browser for.
+    for (let [key, {width, height}] of sizes) {
+      let browser;
+      if (toRemove.length) {
+        // Let's just resize one of the superfluous
+        // browsers and put it back into the map.
+        browser = toRemove.shift();
+        browser.resize(width, height);
+      } else {
+        // No more browsers to reuse, create a new one.
+        browser = new HiddenBrowser(width, height);
+      }
+
+      this._browsers.set(key, browser);
+    }
+
+    // Finally, remove all browsers we don't need anymore.
+    toRemove.forEach(b => b.destroy());
+  },
+
+  _collectTabBrowserSizes: function () {
+    let sizes = new Map();
+
+    function tabBrowserBounds() {
+      let wins = Services.ww.getWindowEnumerator("navigator:browser");
+      while (wins.hasMoreElements()) {
+        let win = wins.getNext();
+        if (win.gBrowser) {
+          let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindowUtils);
+          yield utils.getBoundsWithoutFlushing(win.gBrowser);
+        }
+      }
+    }
+
+    // Collect the sizes of all <tabbrowser>s out there.
+    for (let {width, height} of tabBrowserBounds()) {
+      if (width > 0 && height > 0) {
+        let key = width + "x" + height;
+        if (!sizes.has(key)) {
+          sizes.set(key, {width: width, height: height});
+        }
+      }
+    }
+
+    return sizes;
+  }
+};
+
+function HiddenBrowser(width, height) {
+  this.resize(width, height);
+  this._createBrowser();
+}
+
+HiddenBrowser.prototype = {
+  _width: null,
+  _height: null,
+  _timer: null,
+
+  get isPreloaded() {
+    return this._browser &&
+           this._browser.contentDocument &&
+           this._browser.contentDocument.readyState === "complete" &&
+           this._browser.currentURI.spec === NEWTAB_URL;
+  },
+
+  swapWithNewTab: function (aTab) {
+    if (!this.isPreloaded || this._timer) {
+      return false;
+    }
+
+    let win = aTab.ownerDocument.defaultView;
+    let tabbrowser = win.gBrowser;
+
+    if (!tabbrowser) {
+      return false;
+    }
+
+    // Swap docShells.
+    tabbrowser.swapNewTabWithBrowser(aTab, this._browser);
+
+    // Load all delayed frame scripts attached to the "browers" message manager.
+    // The browser content script was already loaded, so don't load it again.
+    let mm = aTab.linkedBrowser.messageManager;
+    let scripts = win.getGroupMessageManager("browsers").getDelayedFrameScripts();
+    Array.forEach(scripts, ([script, runGlobal]) => {
+      if (script != BROWSER_CONTENT_SCRIPT) {
+        mm.loadFrameScript(script, true, runGlobal);
+      }
+    });
+
+    // Remove the browser, it will be recreated by a timer.
+    this._removeBrowser();
+
+    // Start a timer that will kick off preloading the next newtab page.
+    this._timer = createTimer(this, PRELOADER_INTERVAL_MS);
+
+    // Signal that we swapped docShells.
+    return true;
+  },
+
+  observe: function () {
+    this._timer = null;
+
+    // Start pre-loading the new tab page.
+    this._createBrowser();
+  },
+
+  resize: function (width, height) {
+    this._width = width;
+    this._height = height;
+    this._applySize();
+  },
+
+  destroy: function () {
+    this._removeBrowser();
+    this._timer = clearTimer(this._timer);
+  },
+
+  _applySize: function () {
+    if (this._browser) {
+      this._browser.style.width = this._width + "px";
+      this._browser.style.height = this._height + "px";
+    }
+  },
+
+  _createBrowser: function () {
+    HostFrame.get().then(aFrame => {
+      let doc = aFrame.document;
+      this._browser = doc.createElementNS(XUL_NS, "browser");
+      this._browser.setAttribute("type", "content");
+      this._browser.setAttribute("src", NEWTAB_URL);
+      this._applySize();
+      doc.getElementById("win").appendChild(this._browser);
+
+      // The browser might not have a docShell here if the HostFrame was
+      // destroyed while the promise was resolved. Simply bail out.
+      if (!this._browser.docShell) {
+        return;
+      }
+
+      // Let the docShell be inactive so that document.hidden=true.
+      this._browser.docShell.isActive = false;
+
+      this._browser.messageManager.loadFrameScript(BROWSER_CONTENT_SCRIPT,
+                                                   true);
+    });
+  },
+
+  _removeBrowser: function () {
+    if (this._browser) {
+      this._browser.remove();
+      this._browser = null;
+    }
+  }
+};
+
+let HostFrame = {
+  _frame: null,
+  _deferred: null,
+
+  get hiddenDOMDocument() {
+    return Services.appShell.hiddenDOMWindow.document;
+  },
+
+  get isReady() {
+    return this.hiddenDOMDocument.readyState === "complete";
+  },
+
+  get: function () {
+    if (!this._deferred) {
+      this._deferred = Promise.defer();
+      this._create();
+    }
+
+    return this._deferred.promise;
+  },
+
+  destroy: function () {
+    if (this._frame) {
+      if (!Cu.isDeadWrapper(this._frame)) {
+        this._frame.removeEventListener("load", this, true);
+        this._frame.remove();
+      }
+
+      this._frame = null;
+      this._deferred = null;
+    }
+  },
+
+  handleEvent: function () {
+    let contentWindow = this._frame.contentWindow;
+    if (contentWindow.location.href === XUL_PAGE) {
+      this._frame.removeEventListener("load", this, true);
+      this._deferred.resolve(contentWindow);
+    } else {
+      contentWindow.location = XUL_PAGE;
+    }
+  },
+
+  _create: function () {
+    if (this.isReady) {
+      let doc = this.hiddenDOMDocument;
+      this._frame = doc.createElementNS(HTML_NS, "iframe");
+      this._frame.addEventListener("load", this, true);
+      doc.documentElement.appendChild(this._frame);
+    } else {
+      let flags = Ci.nsIThread.DISPATCH_NORMAL;
+      Services.tm.currentThread.dispatch(() => this._create(), flags);
+    }
+  }
+};
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -6,16 +6,17 @@
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 XPCSHELL_TESTS_MANIFESTS += [
     'test/unit/social/xpcshell.ini',
     'test/xpcshell/xpcshell.ini',
 ]
 
 EXTRA_JS_MODULES += [
+    'BrowserNewTabPreloader.jsm',
     'BrowserUITelemetry.jsm',
     'CastingApps.jsm',
     'Chat.jsm',
     'ContentClick.jsm',
     'ContentLinkHandler.jsm',
     'ContentSearch.jsm',
     'ContentWebRTC.jsm',
     'CustomizationTabPreloader.jsm',
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -10,16 +10,19 @@ if (Cc === undefined) {
 }
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserNewTabPreloader",
+  "resource:///modules/BrowserNewTabPreloader.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizationTabPreloader",
   "resource:///modules/CustomizationTabPreloader.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
   "resource:///modules/ContentSearch.jsm");
 
 const SIMPLETEST_OVERRIDES =
   ["ok", "is", "isnot", "ise", "todo", "todo_is", "todo_isnot", "info", "expectAssertions", "requestCompleteLog"];
@@ -496,23 +499,17 @@ Tester.prototype = {
           socialSidebar.docShell.createAboutBlankContentViewer(null);
           socialSidebar.setAttribute("src", "about:blank");
 
           // Destroy BackgroundPageThumbs resources.
           let {BackgroundPageThumbs} =
             Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", {});
           BackgroundPageThumbs._destroy();
 
-          // Destroy preloaded browsers.
-          if (gBrowser._preloadedBrowser) {
-            let browser = gBrowser._preloadedBrowser;
-            gBrowser._preloadedBrowser = null;
-            gBrowser.getNotificationBox(browser).remove();
-          }
-
+          BrowserNewTabPreloader.uninit();
           CustomizationTabPreloader.uninit();
           SocialFlyout.unload();
           SocialShare.uninit();
           TabView.uninit();
         }
 
         // Schedule GC and CC runs before finishing in order to detect
         // DOM windows leaked by our tests or the tested code. Note that we