Backed out 2 changesets (bug 1556789) on request from aryx for causing Bug 1565836. a=backout
authorBrindusan Cristian <cbrindusan@mozilla.com>
Sat, 13 Jul 2019 20:52:45 +0300
changeset 543196 74c0d994aa18fdc4b30d18ecb4d5de95b82298d6
parent 543195 3e793ca066f227fcc3c25e31925d3729ca8593f8
child 543203 a9b83366be5c94c9ef74fa66c7713b3044f7c75f
push id11848
push userffxbld-merge
push dateMon, 26 Aug 2019 19:26:25 +0000
treeherdermozilla-beta@9b31bfdfac10 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs1556789, 1565836
milestone70.0a1
backs outca5fdf3a0ddd0031b91162390a9ee8508cbe159c
19419ff4e756c9760a2f4354feb866d1911962c5
first release with
nightly linux32
74c0d994aa18 / 70.0a1 / 20190713175429 / files
nightly linux64
74c0d994aa18 / 70.0a1 / 20190713175429 / files
nightly mac
74c0d994aa18 / 70.0a1 / 20190713175429 / files
nightly win32
74c0d994aa18 / 70.0a1 / 20190713175429 / files
nightly win64
74c0d994aa18 / 70.0a1 / 20190713175429 / 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
Backed out 2 changesets (bug 1556789) on request from aryx for causing Bug 1565836. a=backout Backed out changeset ca5fdf3a0ddd (bug 1556789) Backed out changeset 19419ff4e756 (bug 1556789)
browser/base/content/test/performance/browser_startup_mainthreadio.js
browser/components/extensions/parent/ext-chrome-settings-overrides.js
browser/components/extensions/test/xpcshell/head.js
browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js
browser/components/extensions/test/xpcshell/test_ext_urlbar.js
browser/components/preferences/in-content/tests/browser.ini
browser/components/preferences/in-content/tests/browser_engines.js
browser/components/search/test/browser/browser_contextmenu.js
browser/components/search/test/marionette/test_engines_on_restart.py
browser/components/tests/unit/test_distribution.js
browser/components/urlbar/tests/unit/head.js
testing/firefox-ui/tests/functional/private_browsing/test_about_private_browsing.py
testing/profiles/common/user.js
toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
toolkit/components/places/tests/unifiedcomplete/test_PlacesSearchAutocompleteProvider.js
toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
toolkit/components/places/tests/unit/head_bookmarks.js
toolkit/components/search/SearchEngine.jsm
toolkit/components/search/SearchService.jsm
toolkit/components/search/SearchUtils.jsm
toolkit/components/search/nsISearchService.idl
toolkit/components/search/tests/xpcshell/data/invalid-extension/invalid/manifest.json
toolkit/components/search/tests/xpcshell/data/invalid-extension/list.json
toolkit/components/search/tests/xpcshell/head_search.js
toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js
toolkit/components/search/tests/xpcshell/test_async_distribution.js
toolkit/components/search/tests/xpcshell/test_list_json_searchorder.js
toolkit/components/search/tests/xpcshell/test_migrateWebExtensionEngine.js
toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js
toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js
toolkit/components/search/tests/xpcshell/test_require_engines_for_cache.js
toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js
toolkit/components/search/tests/xpcshell/test_validate_engines.js
toolkit/components/search/tests/xpcshell/test_webextensions_install_failure.js
toolkit/components/search/tests/xpcshell/xpcshell.ini
toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py
toolkit/components/telemetry/tests/unit/head.js
--- a/browser/base/content/test/performance/browser_startup_mainthreadio.js
+++ b/browser/base/content/test/performance/browser_startup_mainthreadio.js
@@ -582,17 +582,17 @@ const startupPhases = {
       path: "GreD:omni.ja",
       condition: WIN,
       stat: 1,
     },
     {
       // bug 1543090
       path: "XCurProcD:omni.ja",
       condition: WIN,
-      stat: 8, // search engine loading can cause extra stats on win
+      stat: 2,
     },
   ],
 
   // Things that are expected to be completely out of the startup path
   // and loaded lazily when used for the first time by the user should
   // be blacklisted here.
   "before becoming idle": [
     {
@@ -686,17 +686,16 @@ const startupPhases = {
       path: "ProfD:",
       condition: WIN,
       ignoreIfUnused: true,
       stat: 3,
     },
     {
       // bug 1543090
       path: "XCurProcD:omni.ja",
-      ignoreIfUnused: true,
       condition: WIN,
       stat: 7,
     },
     {
       // bug 1003968
       path: "XREAppDist:searchplugins",
       condition: WIN,
       ignoreIfUnused: true, // with WebRender enabled this may happen during "before handling user events"
--- a/browser/components/extensions/parent/ext-chrome-settings-overrides.js
+++ b/browser/components/extensions/parent/ext-chrome-settings-overrides.js
@@ -305,44 +305,43 @@ this.chrome_settings_overrides = class e
       pendingSearchSetupTasks.set(extension.id, searchStartupPromise);
     }
   }
 
   async processSearchProviderManifestEntry() {
     let { extension } = this;
     let { manifest } = extension;
     let searchProvider = manifest.chrome_settings_overrides.search_provider;
-    let handleIsDefault =
-      searchProvider.is_default && !extension.addonData.builtIn;
-    let engineName = searchProvider.name.trim();
-    // Builtin extensions are never marked with is_default.  We can safely wait on
-    // the search service to fully initialize before handling these extensions.
-    if (handleIsDefault) {
+    if (searchProvider.is_default) {
       await searchInitialized;
       if (!this.extension) {
         Cu.reportError(
           `Extension shut down before search provider was registered`
         );
         return;
       }
+    }
+
+    let engineName = searchProvider.name.trim();
+    if (searchProvider.is_default) {
       let engine = Services.search.getEngineByName(engineName);
       let defaultEngines = await Services.search.getDefaultEngines();
       if (
         engine &&
         defaultEngines.some(defaultEngine => defaultEngine.name == engineName)
       ) {
         // Needs to be called every time to handle reenabling, but
         // only sets default for install or enable.
         await this.setDefault(engineName);
         // For built in search engines, we don't do anything further
         return;
       }
     }
     await this.addSearchEngine();
-    if (handleIsDefault) {
+    if (searchProvider.is_default) {
       if (extension.startupReason === "ADDON_INSTALL") {
         // Don't ask if it already the current engine
         let engine = Services.search.getEngineByName(engineName);
         let defaultEngine = await Services.search.getDefault();
         if (defaultEngine.name != engine.name) {
           let subject = {
             wrappedJSObject: {
               // This is a hack because we don't have the browser of
@@ -413,30 +412,66 @@ this.chrome_settings_overrides = class e
         "enable",
         extension.id
       );
     }
   }
 
   async addSearchEngine() {
     let { extension } = this;
+    let isCurrent = false;
+    let index = -1;
+    if (
+      extension.startupReason === "ADDON_UPGRADE" &&
+      !extension.addonData.builtIn
+    ) {
+      let engines = await Services.search.getEnginesByExtensionID(extension.id);
+      if (engines.length > 0) {
+        let firstEngine = engines[0];
+        let firstEngineName = firstEngine.name;
+        // There can be only one engine right now
+        isCurrent =
+          (await Services.search.getDefault()).name == firstEngineName;
+        // Get position of engine and store it
+        index = (await Services.search.getEngines())
+          .map(engine => engine.name)
+          .indexOf(firstEngineName);
+        await Services.search.removeEngine(firstEngine);
+      }
+    }
     try {
-      // This is safe to await prior to SearchService.init completing.
       let engines = await Services.search.addEnginesFromExtension(extension);
-      if (engines[0]) {
+      if (engines.length > 0) {
         await ExtensionSettingsStore.addSetting(
           extension.id,
           DEFAULT_SEARCH_STORE_TYPE,
           ENGINE_ADDED_SETTING_NAME,
           engines[0].name
         );
       }
+      if (
+        extension.startupReason === "ADDON_UPGRADE" &&
+        !extension.addonData.builtIn
+      ) {
+        let engines = await Services.search.getEnginesByExtensionID(
+          extension.id
+        );
+        let engine = Services.search.getEngineByName(engines[0].name);
+        if (isCurrent) {
+          await Services.search.setDefault(engine);
+        }
+        if (index != -1) {
+          await Services.search.moveEngine(engine, index);
+        }
+      }
     } catch (e) {
       Cu.reportError(e);
+      return false;
     }
+    return true;
   }
 };
 
 ExtensionPreferencesManager.addSetting("homepage_override", {
   prefNames: [HOMEPAGE_PREF, HOMEPAGE_EXTENSION_CONTROLLED],
   // ExtensionPreferencesManager will call onPrefsChanged when control changes
   // and it updates the preferences. We are passed the item from
   // ExtensionSettingsStore that details what is in control. If there is an id
--- a/browser/components/extensions/test/xpcshell/head.js
+++ b/browser/components/extensions/test/xpcshell/head.js
@@ -15,22 +15,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
   ExtensionTestUtils: "resource://testing-common/ExtensionXPCShellUtils.jsm",
   FileUtils: "resource://gre/modules/FileUtils.jsm",
   HttpServer: "resource://testing-common/httpd.js",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   Schemas: "resource://gre/modules/Schemas.jsm",
   TestUtils: "resource://testing-common/TestUtils.jsm",
 });
 
-// For search related tests, reduce what is happening.  Search tests cover
-// these otherwise.
-Services.prefs.setCharPref("browser.search.region", "US");
-Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
-Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
-
 Services.prefs.setBoolPref("extensions.webextensions.remote", false);
 
 ExtensionTestUtils.init(this);
 
 /**
  * Creates a new HttpServer for testing, and begins listening on the
  * specified port. Automatically shuts down the server when the test
  * unit ends.
--- 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
@@ -13,22 +13,17 @@ AddonTestUtils.overrideCertDB();
 AddonTestUtils.createAppInfo(
   "xpcshell@tests.mozilla.org",
   "XPCShell",
   "1",
   "42"
 );
 
 add_task(async function setup() {
-  Services.prefs.setCharPref("browser.search.region", "US");
-  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
-  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
-
   await AddonTestUtils.promiseStartupManager();
-  await Services.search.init();
 });
 
 add_task(async function test_overrides_update_removal() {
   /* This tests the scenario where the manifest key for homepage and/or
    * search_provider are removed between updates and therefore the
    * settings are expected to revert.  It also tests that an extension
    * can make a builtin extension the default extension without user
    * interaction.  */
--- a/browser/components/extensions/test/xpcshell/test_ext_urlbar.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_urlbar.js
@@ -25,17 +25,16 @@ AddonTestUtils.createAppInfo(
 const ORIGINAL_NOTIFICATION_TIMEOUT =
   UrlbarProviderExtension.notificationTimeout;
 
 add_task(async function startup() {
   Services.prefs.setCharPref("browser.search.region", "US");
   Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
   Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
   await AddonTestUtils.promiseStartupManager();
-  await Services.search.init(true);
 
   // Add a test engine and make it default so that when we do searches below,
   // Firefox doesn't try to include search suggestions from the actual default
   // engine from over the network.
   let engine = await Services.search.addEngineWithDetails("Test engine", {
     template: "http://example.com/?s=%S",
   });
   Services.search.defaultEngine = engine;
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -1,15 +1,12 @@
 [DEFAULT]
 prefs =
   extensions.formautofill.available='on'
   extensions.formautofill.creditCards.available=true
-# turn off geo updates for search related tests
-  browser.search.region=US
-  browser.search.geoSpecificDefaults=false
 support-files =
   head.js
   privacypane_tests_perwindow.js
   addons/pl-dictionary.xpi
   addons/set_homepage.xpi
   addons/set_newtab.xpi
 
 [browser_applications_selection.js]
--- a/browser/components/preferences/in-content/tests/browser_engines.js
+++ b/browser/components/preferences/in-content/tests/browser_engines.js
@@ -1,13 +1,10 @@
 // Test Engine list
 add_task(async function() {
-  // running stand-alone, be sure to wait for init
-  await Services.search.init();
-
   let prefs = await openPreferencesViaOpenPreferencesAPI("search", {
     leaveOpen: true,
   });
   is(prefs.selectedPane, "paneSearch", "Search pane is selected by default");
   let doc = gBrowser.contentDocument;
 
   let tree = doc.querySelector("#engineList");
   ok(
--- a/browser/components/search/test/browser/browser_contextmenu.js
+++ b/browser/components/search/test/browser/browser_contextmenu.js
@@ -1,18 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  *  * http://creativecommons.org/publicdomain/zero/1.0/ */
 /*
  * Test searching for the selected text using the context menu
  */
 
-const { SearchExtensionLoader } = ChromeUtils.import(
-  "resource://gre/modules/SearchUtils.jsm"
-);
-
 const ENGINE_NAME = "mozSearch";
 const ENGINE_ID = "mozsearch-engine@search.mozilla.org";
 
 add_task(async function() {
   // We want select events to be fired.
   await SpecialPowers.pushPrefEnv({
     set: [["dom.select_events.enabled", true]],
   });
@@ -27,17 +23,17 @@ add_task(async function() {
     .getProtocolHandler("resource")
     .QueryInterface(Ci.nsIResProtocolHandler);
   let originalSubstitution = resProt.getSubstitution("search-extensions");
   resProt.setSubstitution(
     "search-extensions",
     Services.io.newURI("file://" + searchExtensions.path)
   );
 
-  await SearchExtensionLoader.installAddons([ENGINE_ID]);
+  await Services.search.ensureBuiltinExtension(ENGINE_ID);
 
   let engine = await Services.search.getEngineByName(ENGINE_NAME);
   ok(engine, "Got a search engine");
   let defaultEngine = await Services.search.getDefault();
   await Services.search.setDefault(engine);
 
   let contextMenu = document.getElementById("contentAreaContextMenu");
   ok(contextMenu, "Got context menu XUL");
--- a/browser/components/search/test/marionette/test_engines_on_restart.py
+++ b/browser/components/search/test/marionette/test_engines_on_restart.py
@@ -10,18 +10,17 @@ from marionette_harness.marionette_test 
 
 
 class TestEnginesOnRestart(MarionetteTestCase):
 
     def setUp(self):
         super(TestEnginesOnRestart, self).setUp()
         self.marionette.enforce_gecko_prefs({
             'browser.search.log': True,
-            'browser.search.geoSpecificDefaults': False,
-            'browser.search.addonLoadTimeout': 0
+            'browser.search.geoSpecificDefaults': False
         })
 
     def get_default_search_engine(self):
         """Retrieve the identifier of the default search engine."""
 
         script = """\
         let [resolve] = arguments;
         let searchService = Components.classes[
--- a/browser/components/tests/unit/test_distribution.js
+++ b/browser/components/tests/unit/test_distribution.js
@@ -275,17 +275,12 @@ add_task(async function() {
     "Language Set"
   );
 
   Services.prefs.setCharPref(
     "distribution.searchplugins.defaultLocale",
     "de-DE"
   );
 
-  // Turn off region updates and timeouts for search service
-  Services.prefs.setCharPref("browser.search.region", "DE");
-  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
-  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
-
   await Services.search.init();
   var engine = Services.search.getEngineByName("Google");
   Assert.equal(engine.description, "override-de-DE");
 });
--- a/browser/components/urlbar/tests/unit/head.js
+++ b/browser/components/urlbar/tests/unit/head.js
@@ -30,21 +30,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
   UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
   UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm",
   UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
   UrlbarResult: "resource:///modules/UrlbarResult.jsm",
   UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
 });
 const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
 
-// Turn off region updates and timeouts for search service
-Services.prefs.setCharPref("browser.search.region", "US");
-Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
-Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
-
 /**
  * @param {string} searchString The search string to insert into the context.
  * @param {object} properties Overrides for the default values.
  * @returns {UrlbarQueryContext} Creates a dummy query context with pre-filled
  *          required options.
  */
 function createContext(searchString = "foo", properties = {}) {
   let context = new UrlbarQueryContext({
@@ -194,19 +189,16 @@ async function addTestEngine(basename, h
     }, "browser-search-engine-modified");
 
     info("Adding engine from URL: " + dataUrl + basename);
     Services.search.addEngine(dataUrl + basename, null, false);
   });
 }
 
 /**
- * WARNING: use of this function may result in intermittent failures when tests
- * run in parallel due to reliance on port 9000.  Duplicated in/from unifiedcomplete.
- *
  * Sets up a search engine that provides some suggestions by appending strings
  * onto the search query.
  *
  * @param {function} suggestionsFn
  *        A function that returns an array of suggestion strings given a
  *        search string.  If not given, a default function is used.
  * @returns {nsISearchEngine} The new engine.
  */
--- a/testing/firefox-ui/tests/functional/private_browsing/test_about_private_browsing.py
+++ b/testing/firefox-ui/tests/functional/private_browsing/test_about_private_browsing.py
@@ -11,20 +11,16 @@ from marionette_harness import Marionett
 
 class TestAboutPrivateBrowsingWithSearch(PuppeteerMixin, MarionetteTestCase):
 
     def setUp(self):
         super(TestAboutPrivateBrowsingWithSearch, self).setUp()
 
         # Use a fake local support URL
         support_url = 'about:blank?'
-        self.marionette.enforce_gecko_prefs({
-            'browser.search.geoSpecificDefaults': False,
-            'browser.search.addonLoadTimeout': 0
-        })
         self.marionette.set_pref('app.support.baseURL', support_url)
         self.pb_url = support_url + 'private-browsing-myths'
 
     def tearDown(self):
         try:
             self.puppeteer.windows.close_all([self.browser])
             self.browser.switch_to()
 
--- a/testing/profiles/common/user.js
+++ b/testing/profiles/common/user.js
@@ -19,20 +19,16 @@ user_pref("browser.newtabpage.activity-s
 // Background thumbnails in particular cause grief, and disabling thumbnails
 // in general can't hurt - we re-enable them when tests need them.
 user_pref("browser.pagethumbnails.capturing_disabled", true);
 // Tell the search service we are running in the US.  This also has the desired
 // side-effect of preventing our geoip lookup.
 user_pref("browser.search.region", "US");
 // This will prevent HTTP requests for region defaults.
 user_pref("browser.search.geoSpecificDefaults", false);
-// Debug builds will timeout on the failsafe timeout for search init,
-// we just turn off the load timeout for tests in general.
-user_pref("browser.search.addonLoadTimeout", 0);
-
 // Disable webapp updates.  Yes, it is supposed to be an integer.
 user_pref("browser.webapps.checkForUpdates", 0);
 // We do not wish to display datareporting policy notifications as it might
 // cause other tests to fail. Tests that wish to test the notification functionality
 // should explicitly disable this pref.
 user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
 user_pref("dom.max_chrome_script_run_time", 0);
 user_pref("dom.max_script_run_time", 0); // no slow script dialogs
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -69,24 +69,17 @@ AddonTestUtils.init(this, false);
 AddonTestUtils.createAppInfo(
   "xpcshell@tests.mozilla.org",
   "XPCShell",
   "42",
   "42"
 );
 
 add_task(async function setup() {
-  // Tell the search service we are running in the US.  This also has the
-  // desired side-effect of preventing our geoip lookup.
-  Services.prefs.setCharPref("browser.search.region", "US");
-  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
-  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
-
   await AddonTestUtils.promiseStartupManager();
-  await Services.search.init();
 });
 
 async function cleanup() {
   Services.prefs.clearUserPref("browser.urlbar.autoFill");
   Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
   let suggestPrefs = ["history", "bookmark", "openpage", "searches"];
   for (let type of suggestPrefs) {
     Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
@@ -562,19 +555,16 @@ function addTestEngine(basename, httpSer
     }, "browser-search-engine-modified");
 
     info("Adding engine from URL: " + dataUrl + basename);
     Services.search.addEngine(dataUrl + basename, null, false);
   });
 }
 
 /**
- * WARNING: use of this function may result in intermittent failures when tests
- * run in parallel due to reliance on port 9000.
- *
  * Sets up a search engine that provides some suggestions by appending strings
  * onto the search query.
  *
  * @param   {function} suggestionsFn
  *          A function that returns an array of suggestion strings given a
  *          search string.  If not given, a default function is used.
  * @returns {nsISearchEngine} The new engine.
  */
@@ -611,17 +601,16 @@ add_task(async function ensure_search_en
   registerCleanupFunction(() => Services.prefs.clearUserPref(geoPref));
   // Remove any existing engines before adding ours.
   for (let engine of await Services.search.getEngines()) {
     await Services.search.removeEngine(engine);
   }
   await Services.search.addEngineWithDetails("MozSearch", {
     method: "GET",
     template: "http://s.example.com/search",
-    isBuiltin: true,
   });
   let engine = Services.search.getEngineByName("MozSearch");
   await Services.search.setDefault(engine);
 });
 
 /**
  * Add a adaptive result for a given (url, string) tuple.
  * @param {string} aUrl
--- a/toolkit/components/places/tests/unifiedcomplete/test_PlacesSearchAutocompleteProvider.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_PlacesSearchAutocompleteProvider.js
@@ -1,24 +1,22 @@
 /* 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/. */
 
 const { PlacesSearchAutocompleteProvider } = ChromeUtils.import(
   "resource://gre/modules/PlacesSearchAutocompleteProvider.jsm"
 );
 
-add_task(async function setup() {
+add_task(async function() {
+  await Services.search.init();
   // Tell the search service we are running in the US.  This also has the
   // desired side-effect of preventing our geoip lookup.
   Services.prefs.setCharPref("browser.search.region", "US");
   Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
-  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
-
-  await Services.search.init();
 
   Services.search.restoreDefaultEngines();
   Services.search.resetToOriginalDefaultEngine();
 });
 
 add_task(async function search_engine_match() {
   let engine = await Services.search.getDefault();
   let domain = engine.getResultDomain();
@@ -35,19 +33,19 @@ add_task(async function no_match() {
     await PlacesSearchAutocompleteProvider.engineForDomainPrefix("test")
   );
 });
 
 add_task(async function hide_search_engine_nomatch() {
   let engine = await Services.search.getDefault();
   let domain = engine.getResultDomain();
   let token = domain.substr(0, 1);
-  let promiseTopic = promiseSearchTopic("engine-removed");
+  let promiseTopic = promiseSearchTopic("engine-changed");
   await Promise.all([Services.search.removeEngine(engine), promiseTopic]);
-  Assert.ok(engine.hidden, "engine was hidden rather than removed");
+  Assert.ok(engine.hidden);
   let matchedEngine = await PlacesSearchAutocompleteProvider.engineForDomainPrefix(
     token
   );
   Assert.ok(!matchedEngine || matchedEngine.getResultDomain() != domain);
   engine.hidden = false;
   await TestUtils.waitForCondition(() =>
     PlacesSearchAutocompleteProvider.engineForDomainPrefix(token)
   );
@@ -160,45 +158,41 @@ add_task(async function test_parseSubmis
   // Most of the logic of parseSubmissionURL is tested in the search service
   // itself, thus we only do a sanity check of the wrapper here.
   let engine = await Services.search.getDefault();
   let submissionURL = engine.getSubmission("terms").uri.spec;
 
   let result = PlacesSearchAutocompleteProvider.parseSubmissionURL(
     submissionURL
   );
-  Assert.equal(
-    result.engineName,
-    engine.name,
-    "parsed submissionURL has matching engine name"
-  );
+  Assert.equal(result.engineName, engine.name);
   Assert.equal(result.terms, "terms");
 
   result = PlacesSearchAutocompleteProvider.parseSubmissionURL(
     "http://example.org/"
   );
   Assert.equal(result, null);
 });
 
 add_task(async function test_builtin_aliased_search_engine_match() {
   let engine = await PlacesSearchAutocompleteProvider.engineForAlias("@google");
-  Assert.ok(engine, "matched an engine with an alias");
-  Assert.equal(engine.name, "Google", "correct engine for alias");
+  Assert.ok(engine);
+  Assert.equal(engine.name, "Google");
   let promiseTopic = promiseSearchTopic("engine-changed");
   await Promise.all([Services.search.removeEngine(engine), promiseTopic]);
   let matchedEngine = await PlacesSearchAutocompleteProvider.engineForAlias(
     "@google"
   );
   Assert.ok(!matchedEngine);
   engine.hidden = false;
   await TestUtils.waitForCondition(() =>
     PlacesSearchAutocompleteProvider.engineForAlias("@google")
   );
   engine = await PlacesSearchAutocompleteProvider.engineForAlias("@google");
-  Assert.ok(engine, "matched an engine with an alias");
+  Assert.ok(engine);
 });
 
 function promiseSearchTopic(expectedVerb) {
   return new Promise(resolve => {
     Services.obs.addObserver(function observe(subject, topic, verb) {
       info("browser-search-engine-modified: " + verb);
       if (verb == expectedVerb) {
         Services.obs.removeObserver(observe, "browser-search-engine-modified");
--- a/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
+++ b/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
@@ -38,23 +38,19 @@ support-files =
 [test_keywords.js]
 [test_multi_word_search.js]
 [test_PlacesSearchAutocompleteProvider.js]
 skip-if = appname == "thunderbird"
 [test_preloaded_sites.js]
 [test_query_url.js]
 [test_remote_tab_matches.js]
 skip-if = !sync
+[test_search_engine_alias.js]
 [test_search_engine_default.js]
 [test_search_engine_host.js]
 [test_search_engine_restyle.js]
 [test_search_suggestions.js]
+[test_special_search.js]
 [test_swap_protocol.js]
 [test_tab_matches.js]
 [test_trimming.js]
 [test_visit_url.js]
 [test_word_boundary_search.js]
-# The following tests use addTestSuggestionsEngine which doesn't
-# play well when run in parallel.
-[test_search_engine_alias.js]
-run-sequentially = Test relies on port 9000, fails intermittently
-[test_special_search.js]
-run-sequentially = Test relies on port 9000, may fail intermittently
--- a/toolkit/components/places/tests/unit/head_bookmarks.js
+++ b/toolkit/components/places/tests/unit/head_bookmarks.js
@@ -13,21 +13,16 @@ var { Services } = ChromeUtils.import("r
   Services.scriptloader.loadSubScript(uri.spec, this);
 }
 
 // Put any other stuff relative to this test folder below.
 const { AddonTestUtils } = ChromeUtils.import(
   "resource://testing-common/AddonTestUtils.jsm"
 );
 
-// Turn off region updates and timeouts for search service
-Services.prefs.setCharPref("browser.search.region", "US");
-Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
-Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
-
 AddonTestUtils.init(this, false);
 AddonTestUtils.overrideCertDB();
 AddonTestUtils.createAppInfo(
   "xpcshell@tests.mozilla.org",
   "XPCShell",
   "1",
   "42"
 );
--- a/toolkit/components/search/SearchEngine.jsm
+++ b/toolkit/components/search/SearchEngine.jsm
@@ -43,17 +43,16 @@ const OPENSEARCH_NAMESPACES = [
 ];
 
 const OPENSEARCH_LOCALNAME = "OpenSearchDescription";
 
 const MOZSEARCH_NS_10 = "http://www.mozilla.org/2006/browser/search/";
 const MOZSEARCH_LOCALNAME = "SearchPlugin";
 
 const USER_DEFINED = "searchTerms";
-const SEARCH_TERM_PARAM = "{searchTerms}";
 
 // Custom search parameters
 const MOZ_PARAM_LOCALE = "moz:locale";
 const MOZ_PARAM_DIST_ID = "moz:distributionID";
 const MOZ_PARAM_OFFICIAL = "moz:official";
 
 // Supported OpenSearch parameters
 // See http://opensearch.a9.com/spec/1.1/querysyntax/#core
@@ -576,28 +575,18 @@ EngineURL.prototype = {
       postData.addHeader("Content-Type", "application/x-www-form-urlencoded");
       postData.setData(stringStream);
     }
 
     return new Submission(Services.io.newURI(url), postData);
   },
 
   _getTermsParameterName() {
-    if (this.params.length > 0) {
-      let queryParam = this.params.find(p => p.value == SEARCH_TERM_PARAM);
-      return queryParam ? queryParam.name : "";
-    }
-    // If an engine only used template, then params is empty, fall back to checking the template.
-    let params = new URL(this.template).searchParams;
-    for (let [name, value] of params.entries()) {
-      if (value == SEARCH_TERM_PARAM) {
-        return name;
-      }
-    }
-    return "";
+    let queryParam = this.params.find(p => p.value == "{" + USER_DEFINED + "}");
+    return queryParam ? queryParam.name : "";
   },
 
   _hasRelation(rel) {
     return this.rels.some(e => e == rel.toLowerCase());
   },
 
   _initWithJSON(json) {
     if (!json.params) {
@@ -820,18 +809,16 @@ SearchEngine.prototype = {
   // The number of days between update checks for new versions
   _updateInterval: null,
   // The url to check at for a new update
   _updateURL: null,
   // The url to check for a new icon
   _iconUpdateURL: null,
   /* The extension ID if added by an extension. */
   _extensionID: null,
-  /* The extension version if added by an extension. */
-  _version: null,
   // Built in search engine extensions.
   _isBuiltin: false,
 
   /**
    * Retrieves the data from the engine's file asynchronously.
    * The document element is placed in the engine's data field.
    *
    * @param {nsIFile} file
@@ -1411,17 +1398,16 @@ SearchEngine.prototype = {
    *   Any special Mozilla Parameters.
    * @param {string} [params.postParams]
    *   Any parameters for a POST method.
    * @param {string} params.template
    *   The url template.
    */
   _initFromMetadata(engineName, params) {
     this._extensionID = params.extensionID;
-    this._version = params.version;
     this._isBuiltin = !!params.isBuiltin;
 
     this._initEngineURLFromMetaData(SearchUtils.URL_TYPE.SEARCH, {
       method: (params.searchPostParams && "POST") || params.method || "GET",
       template: params.template,
       getParams: params.searchGetParams,
       postParams: params.searchPostParams,
       mozParams: params.mozParams,
@@ -1693,19 +1679,16 @@ SearchEngine.prototype = {
     this._metaData = json._metaData || {};
     this._isBuiltin = json._isBuiltin;
     if (json.filePath) {
       this._filePath = json.filePath;
     }
     if (json.extensionID) {
       this._extensionID = json.extensionID;
     }
-    if (json.version) {
-      this._version = json.version;
-    }
     for (let i = 0; i < json._urls.length; ++i) {
       let url = json._urls[i];
       let engineURL = new EngineURL(
         url.type || SearchUtils.URL_TYPE.SEARCH,
         url.method || "GET",
         url.template,
         url.resultDomain || undefined
       );
@@ -1754,19 +1737,16 @@ SearchEngine.prototype = {
     if (this._filePath) {
       // File path is stored so that we can remove legacy xml files
       // from the profile if the user removes the engine.
       json.filePath = this._filePath;
     }
     if (this._extensionID) {
       json.extensionID = this._extensionID;
     }
-    if (this._version) {
-      json.version = this._version;
-    }
 
     return json;
   },
 
   setAttr(name, val) {
     this._metaData[name] = val;
   },
 
--- a/toolkit/components/search/SearchService.jsm
+++ b/toolkit/components/search/SearchService.jsm
@@ -8,25 +8,25 @@ const { XPCOMUtils } = ChromeUtils.impor
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 const { PromiseUtils } = ChromeUtils.import(
   "resource://gre/modules/PromiseUtils.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
+  AddonManager: "resource://gre/modules/AddonManager.jsm",
   clearTimeout: "resource://gre/modules/Timer.jsm",
   DeferredTask: "resource://gre/modules/DeferredTask.jsm",
   ExtensionParent: "resource://gre/modules/ExtensionParent.jsm",
   getVerificationHash: "resource://gre/modules/SearchEngine.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   RemoteSettings: "resource://services-settings/remote-settings.js",
   RemoteSettingsClient: "resource://services-settings/RemoteSettingsClient.jsm",
   SearchEngine: "resource://gre/modules/SearchEngine.jsm",
-  SearchExtensionLoader: "resource://gre/modules/SearchUtils.jsm",
   SearchStaticData: "resource://gre/modules/SearchStaticData.jsm",
   SearchUtils: "resource://gre/modules/SearchUtils.jsm",
   Services: "resource://gre/modules/Services.jsm",
   setTimeout: "resource://gre/modules/Timer.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetters(this, {
   gEnvironment: ["@mozilla.org/process/environment;1", "nsIEnvironment"],
@@ -48,16 +48,24 @@ XPCOMUtils.defineLazyGetter(this, "distr
 // A text encoder to UTF8, used whenever we commit the cache to disk.
 XPCOMUtils.defineLazyGetter(this, "gEncoder", function() {
   return new TextEncoder();
 });
 
 // Directory service keys
 const NS_APP_DISTRIBUTION_SEARCH_DIR_LIST = "SrchPluginsDistDL";
 
+// We load plugins from EXT_SEARCH_PREFIX, where a list.json
+// file needs to exist to list available engines.
+const EXT_SEARCH_PREFIX = "resource://search-extensions/";
+const APP_SEARCH_PREFIX = "resource://search-plugins/";
+
+// The address we use to sign the built in search extensions with.
+const EXT_SIGNING_ADDRESS = "search.mozilla.org";
+
 const TOPIC_LOCALES_CHANGE = "intl:app-locales-changed";
 const QUIT_APPLICATION_TOPIC = "quit-application";
 
 // The following constants are left undocumented in nsISearchService.idl
 // For the moment, they are meant for testing/debugging purposes only.
 
 // Delay for batching invalidation of the JSON cache (ms)
 const CACHE_INVALIDATION_DELAY = 1000;
@@ -254,22 +262,21 @@ function fetchRegion(ss) {
     ERROR: 3,
     // Note that we expect to add finer-grained error types here later (eg,
     // dns error, network error, ssl error, etc) with .ERROR remaining as the
     // generic catch-all that doesn't fit into other categories.
   };
   let endpoint = Services.urlFormatter.formatURLPref(
     "browser.search.geoip.url"
   );
+  SearchUtils.log("_fetchRegion starting with endpoint " + endpoint);
   // As an escape hatch, no endpoint means no geoip.
   if (!endpoint) {
     return Promise.resolve();
   }
-  SearchUtils.log("_fetchRegion starting with endpoint " + endpoint);
-
   let startTime = Date.now();
   return new Promise(resolve => {
     // Instead of using a timeout on the xhr object itself, we simulate one
     // using a timer and let the XHR request complete.  This allows us to
     // capture reliable telemetry on what timeout value should actually be
     // used to ensure most users don't see one while not making it so large
     // that many users end up doing a sync init of the search service and thus
     // would see the jank that implies.
@@ -557,19 +564,16 @@ const gEmptyParseSubmissionResult = Obje
 /**
  * The search service handles loading and maintaining of search engines. It will
  * also work out the default lists for each locale/region.
  *
  * @implements {nsISearchService}
  */
 function SearchService() {
   this._initObservers = PromiseUtils.defer();
-  // This deferred promise is resolved once a set of engines have been
-  // parsed out of list.json, which happens in _loadEngines.
-  this._extensionLoadReady = PromiseUtils.defer();
 }
 
 SearchService.prototype = {
   classID: Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}"),
 
   // The current status of initialization. Note that it does not determine if
   // initialization is complete, only if an error has been encountered so far.
   _initRV: Cr.NS_OK,
@@ -631,17 +635,16 @@ SearchService.prototype = {
    *   to be absolutely certain of the correct default engine and/ or ordering of
    *   visible engines.
    * @returns {number}
    *   A Components.results success code on success, otherwise a failure code.
    */
   async _init(skipRegionCheck) {
     SearchUtils.log("_init start");
 
-    TelemetryStopwatch.start("SEARCH_SERVICE_INIT_MS");
     try {
       // See if we have a cache file so we don't have to parse a bunch of XML.
       let cache = await this._readCacheFile();
 
       // The init flow is not going to block on a fetch from an external service,
       // but we're kicking it off as soon as possible to prevent UI flickering as
       // much as possible.
       this._ensureKnownRegionPromise = ensureKnownRegion(this)
@@ -657,38 +660,35 @@ SearchService.prototype = {
 
       await this._loadEngines(cache);
 
       // Make sure the current list of engines is persisted, without the need to wait.
       SearchUtils.log("_init: engines loaded, writing cache");
       this._buildCache();
       this._addObservers();
     } catch (ex) {
-      // If loadEngines has a rejected promise chain, ex is undefined.
-      this._initRV =
-        ex && ex.result !== undefined ? ex.result : Cr.NS_ERROR_FAILURE;
+      this._initRV = ex.result !== undefined ? ex.result : Cr.NS_ERROR_FAILURE;
       SearchUtils.log(
-        "_init: failure initializing search: " + ex + "\n" + (ex && ex.stack)
+        "_init: failure initializng search: " + ex + "\n" + ex.stack
       );
     }
     gInitialized = true;
     if (Components.isSuccessCode(this._initRV)) {
-      TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
       this._initObservers.resolve(this._initRV);
     } else {
-      TelemetryStopwatch.cancel("SEARCH_SERVICE_INIT_MS");
       this._initObservers.reject(this._initRV);
     }
     Services.obs.notifyObservers(
       null,
       SearchUtils.TOPIC_SEARCH_SERVICE,
       "init-complete"
     );
 
     SearchUtils.log("_init: Completed _init");
+    return this._initRV;
   },
 
   /**
    * Obtains the current ignore list from remote settings. This includes
    * verifying the signature of the ignore list within the database.
    *
    * If the signature in the database is invalid, the database will be wiped
    * and the stored dump will be used, until the settings next update.
@@ -842,26 +842,30 @@ SearchService.prototype = {
     let val = this.getGlobalAttr(name);
     if (val && this.getGlobalAttr(name + "Hash") != getVerificationHash(val)) {
       SearchUtils.log("getVerifiedGlobalAttr, invalid hash for " + name);
       return "";
     }
     return val;
   },
 
-  // Some tests need to modify this url, they can do so through SearchUtils.
-  get _listJSONURL() {
-    return SearchUtils.LIST_JSON_URL;
-  },
+  _listJSONURL:
+    (AppConstants.platform == "android"
+      ? APP_SEARCH_PREFIX
+      : EXT_SEARCH_PREFIX) + "list.json",
 
   _engines: {},
   __sortedEngines: null,
   _visibleDefaultEngines: [],
   _searchDefault: null,
   _searchOrder: [],
+  // A Set of installed search extensions reported by AddonManager
+  // startup before SearchSevice has started. Will be installed
+  // during init().
+  _startupExtensions: new Set(),
 
   get _sortedEngines() {
     if (!this.__sortedEngines) {
       return this._buildSortedEngineList();
     }
     return this.__sortedEngines;
   },
 
@@ -1017,25 +1021,22 @@ SearchService.prototype = {
       !cache.engines ||
       cache.version != CACHE_VERSION ||
       cache.locale != Services.locale.requestedLocale ||
       cache.buildID != buildID ||
       cache.visibleDefaultEngines.length !=
         this._visibleDefaultEngines.length ||
       this._visibleDefaultEngines.some(notInCacheVisibleEngines);
 
-    this._engineLocales = this._enginesToLocales(engines);
-    this._extensionLoadReady.resolve();
-
     if (!rebuildCache) {
       SearchUtils.log("_loadEngines: loading from cache directories");
       this._loadEnginesFromCache(cache);
       if (Object.keys(this._engines).length) {
         SearchUtils.log("_loadEngines: done using existing cache");
-        return Promise.resolve();
+        return;
       }
       SearchUtils.log(
         "_loadEngines: No valid engines found in cache. Loading engines from disk."
       );
     }
 
     SearchUtils.log(
       "_loadEngines: Absent or outdated cache. Loading engines from disk."
@@ -1043,43 +1044,87 @@ SearchService.prototype = {
     for (let loadDir of distDirs) {
       let enginesFromDir = await this._loadEnginesFromDir(loadDir);
       enginesFromDir.forEach(this._addEngineToStore, this);
     }
     if (AppConstants.platform == "android") {
       let enginesFromURLs = await this._loadFromChromeURLs(engines, isReload);
       enginesFromURLs.forEach(this._addEngineToStore, this);
     } else {
-      return SearchExtensionLoader.installAddons(this._engineLocales.keys());
+      let engineList = this._enginesToLocales(engines);
+      for (let [id, locales] of engineList) {
+        await this.ensureBuiltinExtension(id, locales);
+      }
+
+      SearchUtils.log(
+        "_loadEngines: loading " +
+          this._startupExtensions.size +
+          " engines reported by AddonManager startup"
+      );
+      for (let extension of this._startupExtensions) {
+        await this._installExtensionEngine(extension, [DEFAULT_TAG], true);
+      }
     }
 
     SearchUtils.log(
       "_loadEngines: loading user-installed engines from the obsolete cache"
     );
     this._loadEnginesFromCache(cache, true);
 
     this._loadEnginesMetadataFromCache(cache);
 
     SearchUtils.log("_loadEngines: done using rebuilt cache");
-    return Promise.resolve();
+  },
+
+  /**
+   * Ensures a built in search WebExtension is installed, installing
+   * it if necessary.
+   *
+   * @param {string} id
+   *   The WebExtension ID.
+   * @param {Array<string>} locales
+   *   An array of locales to use for the WebExtension. If more than
+   *   one is specified, different versions of the same engine may
+   *   be installed.
+   */
+  async ensureBuiltinExtension(id, locales = [DEFAULT_TAG]) {
+    SearchUtils.log("ensureBuiltinExtension: " + id);
+    try {
+      let policy = WebExtensionPolicy.getByID(id);
+      if (!policy) {
+        SearchUtils.log("ensureBuiltinExtension: Installing " + id);
+        let path = EXT_SEARCH_PREFIX + id.split("@")[0] + "/";
+        await AddonManager.installBuiltinAddon(path);
+        policy = WebExtensionPolicy.getByID(id);
+      }
+      // On startup the extension may have not finished parsing the
+      // manifest, wait for that here.
+      await policy.readyPromise;
+      await this._installExtensionEngine(policy.extension, locales);
+      SearchUtils.log("ensureBuiltinExtension: " + id + " installed.");
+    } catch (err) {
+      Cu.reportError(
+        "Failed to install engine: " + err.message + "\n" + err.stack
+      );
+    }
   },
 
   /**
    * Converts array of engines into a Map of extensions + the locales
    * of those extensions to install.
    *
    * @param {array} engines
    *   An array of engines
-   * @returns {Map} A Map of extension IDs to locales.
+   * @returns {Map} A Map of extension names + locales.
    */
   _enginesToLocales(engines) {
     let engineLocales = new Map();
     for (let engine of engines) {
       let [extensionName, locale] = this._parseEngineName(engine);
-      let id = SearchUtils.makeExtensionId(extensionName);
+      let id = extensionName + "@" + EXT_SIGNING_ADDRESS;
       let locales = engineLocales.get(id) || new Set();
       locales.add(locale);
       engineLocales.set(id, locales);
     }
     return engineLocales;
   },
 
   /**
@@ -1152,24 +1197,19 @@ SearchService.prototype = {
       SearchUtils.log("_reInit: already re-initializing, bailing out.");
       return;
     }
     gReinitializing = true;
 
     // Start by clearing the initialized state, so we don't abort early.
     gInitialized = false;
 
-    // Reset any init promises synchronously before the async init below.
-    this._initObservers = PromiseUtils.defer();
-    this._extensionLoadReady = PromiseUtils.defer();
-    // If reset is called prior to reinit, be sure to mark init as started.
-    this._initStarted = true;
-
     (async () => {
       try {
+        this._initObservers = PromiseUtils.defer();
         if (this._batchTask) {
           SearchUtils.log("finalizing batch task");
           let task = this._batchTask;
           this._batchTask = null;
           // Tests manipulate the cache directly, so let's not double-write with
           // stale cache data here.
           if (origin == "test") {
             task.disarm();
@@ -1181,17 +1221,16 @@ SearchService.prototype = {
         // Clear the engines, too, so we don't stick with the stale ones.
         this._engines = {};
         this.__sortedEngines = null;
         this._currentEngine = null;
         this._visibleDefaultEngines = [];
         this._searchDefault = null;
         this._searchOrder = [];
         this._metaData = {};
-        this._engineLocales = null;
 
         // Tests that want to force a synchronous re-initialization need to
         // be notified when we are done uninitializing.
         Services.obs.notifyObservers(
           null,
           SearchUtils.TOPIC_SEARCH_SERVICE,
           "uninit-complete"
         );
@@ -1226,17 +1265,16 @@ SearchService.prototype = {
       } catch (err) {
         SearchUtils.log("Reinit failed: " + err);
         SearchUtils.log(err.stack);
         Services.obs.notifyObservers(
           null,
           SearchUtils.TOPIC_SEARCH_SERVICE,
           "reinit-failed"
         );
-        this._initObservers.reject();
       } finally {
         gReinitializing = false;
         Services.obs.notifyObservers(
           null,
           SearchUtils.TOPIC_SEARCH_SERVICE,
           "reinit-complete"
         );
       }
@@ -1250,18 +1288,16 @@ SearchService.prototype = {
     gInitialized = false;
     this._initObservers = PromiseUtils.defer();
     this._initStarted = this.__sortedEngines = this._currentEngine = this._searchDefault = null;
     this._startupExtensions = new Set();
     this._engines = {};
     this._visibleDefaultEngines = [];
     this._searchOrder = [];
     this._metaData = {};
-    this._extensionLoadReady = PromiseUtils.defer();
-    this._engineLocales = null;
   },
 
   /**
    * Read the cache file asynchronously.
    */
   async _readCacheFile() {
     let json;
     try {
@@ -1306,41 +1342,31 @@ SearchService.prototype = {
   },
 
   _addEngineToStore(engine) {
     if (this._engineMatchesIgnoreLists(engine)) {
       SearchUtils.log("_addEngineToStore: Ignoring engine");
       return;
     }
 
+    SearchUtils.log('_addEngineToStore: Adding engine: "' + engine.name + '"');
+
     // See if there is an existing engine with the same name. However, if this
     // engine is updating another engine, it's allowed to have the same name.
-    var matchingEngineUpdate =
-      engine._engineToUpdate &&
-      (engine.name == engine._engineToUpdate.name ||
-        (engine._extensionID &&
-          engine._extensionID == engine._engineToUpdate._extensionID));
-    if (engine.name in this._engines && !matchingEngineUpdate) {
+    var hasSameNameAsUpdate =
+      engine._engineToUpdate && engine.name == engine._engineToUpdate.name;
+    if (engine.name in this._engines && !hasSameNameAsUpdate) {
       SearchUtils.log("_addEngineToStore: Duplicate engine found, aborting!");
       return;
     }
 
     if (engine._engineToUpdate) {
-      SearchUtils.log(
-        '_addEngineToStore: Updating engine: "' + engine.name + '"'
-      );
       // We need to replace engineToUpdate with the engine that just loaded.
       var oldEngine = engine._engineToUpdate;
 
-      let index = -1;
-      if (this.__sortedEngines) {
-        index = this.__sortedEngines.indexOf(oldEngine);
-      }
-      let isCurrent = this._currentEngine == oldEngine;
-
       // Remove the old engine from the hash, since it's keyed by name, and our
       // name might change (the update might have a new name).
       delete this._engines[oldEngine.name];
 
       // Hack: we want to replace the old engine with the new one, but since
       // people may be holding refs to the nsISearchEngine objects themselves,
       // we'll just copy over all "private" properties (those without a getter
       // or setter) from one object to the other.
@@ -1349,28 +1375,18 @@ SearchService.prototype = {
           oldEngine[p] = engine[p];
         }
       }
       engine = oldEngine;
       engine._engineToUpdate = null;
 
       // Add the engine back
       this._engines[engine.name] = engine;
-      if (index >= 0) {
-        this.__sortedEngines[index] = engine;
-        this._saveSortedEngineList();
-      }
-      if (isCurrent) {
-        this._currentEngine = engine;
-      }
       SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.CHANGED);
     } else {
-      SearchUtils.log(
-        '_addEngineToStore: Adding engine: "' + engine.name + '"'
-      );
       // Not an update, just add the new engine.
       this._engines[engine.name] = engine;
       // Only add the engine to the list of sorted engines if the initial list
       // has already been built (i.e. if this.__sortedEngines is non-null). If
       // it hasn't, we're loading engines from disk and the sorted engine list
       // will be built once we need it.
       if (this.__sortedEngines) {
         this.__sortedEngines.push(engine);
@@ -1513,19 +1529,17 @@ SearchService.prototype = {
    */
   async _loadFromChromeURLs(urls, isReload = false) {
     let engines = [];
     for (let url of urls) {
       try {
         SearchUtils.log(
           "_loadFromChromeURLs: loading engine from chrome url: " + url
         );
-        let uri = Services.io.newURI(
-          SearchUtils.APP_SEARCH_PREFIX + url + ".xml"
-        );
+        let uri = Services.io.newURI(APP_SEARCH_PREFIX + url + ".xml");
         let engine = new SearchEngine({
           uri,
           readOnly: true,
         });
         await engine._initFromURI(uri);
         // If there is an existing engine with the same name then update that engine.
         // Only do this during reloads so it doesnt interfere with distribution
         // engines
@@ -1556,28 +1570,28 @@ SearchService.prototype = {
       );
       return [];
     }
 
     // Read list.json to find the engines we need to load.
     let request = new XMLHttpRequest();
     request.overrideMimeType("text/plain");
     let list = await new Promise(resolve => {
-      request.onload = event => {
+      request.onload = function(event) {
         resolve(event.target.responseText);
       };
-      request.onerror = event => {
+      request.onerror = function(event) {
         SearchUtils.log("_findEngines: failed to read " + this._listJSONURL);
         resolve();
       };
       request.open("GET", Services.io.newURI(this._listJSONURL).spec, true);
       request.send();
     });
 
-    return list !== undefined ? this._parseListJSON(list) : [];
+    return this._parseListJSON(list);
   },
 
   _parseListJSON(list) {
     let json;
     try {
       json = JSON.parse(list);
     } catch (e) {
       Cu.reportError("parseListJSON: Failed to parse list.json: " + e);
@@ -1723,17 +1737,17 @@ SearchService.prototype = {
       this._searchDefault = searchSettings[searchRegion].searchDefault;
     } else if ("searchDefault" in searchSettings.default) {
       this._searchDefault = searchSettings.default.searchDefault;
     } else {
       this._searchDefault = json.default.searchDefault;
     }
 
     if (!this._searchDefault) {
-      SearchUtils.log("parseListJSON: No searchDefault");
+      Cu.reportError("parseListJSON: No searchDefault");
     }
 
     if (
       searchRegion &&
       searchRegion in searchSettings &&
       "searchOrder" in searchSettings[searchRegion]
     ) {
       this._searchOrder = searchSettings[searchRegion].searchOrder;
@@ -1906,28 +1920,48 @@ SearchService.prototype = {
 
     return this._sortedEngines.filter(function(engine) {
       return !engine.hidden;
     });
   },
 
   // nsISearchService
   async init(skipRegionCheck = false) {
+    SearchUtils.log("SearchService.init");
     if (this._initStarted) {
       if (!skipRegionCheck) {
         await this._ensureKnownRegionPromise;
       }
       return this._initObservers.promise;
     }
+
+    TelemetryStopwatch.start("SEARCH_SERVICE_INIT_MS");
     this._initStarted = true;
-    SearchUtils.log("SearchService.init");
-
-    // Don't await on _init, _initObservers is resolved or rejected in _init.
-    this._init(skipRegionCheck);
-    return this._initObservers.promise;
+    try {
+      // Complete initialization by calling asynchronous initializer.
+      await this._init(skipRegionCheck);
+      TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
+    } catch (ex) {
+      if (ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {
+        // No need to pursue asynchronous because synchronous fallback was
+        // called and has finished.
+        TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
+      } else {
+        this._initObservers.reject(ex.result);
+        TelemetryStopwatch.cancel("SEARCH_SERVICE_INIT_MS");
+        throw ex;
+      }
+    }
+    if (!Components.isSuccessCode(this._initRV)) {
+      throw Components.Exception(
+        "SearchService initialization failed",
+        this._initRV
+      );
+    }
+    return this._initRV;
   },
 
   get isInitialized() {
     return gInitialized;
   },
 
   // reInit is currently only exposed for testing purposes
   async reInit(skipRegionCheck) {
@@ -2044,122 +2078,120 @@ SearchService.prototype = {
       ) {
         return engine;
       }
     }
     return null;
   },
 
   async addEngineWithDetails(name, details) {
-    // We only enforce init when called via the IDL API.  Internally we are adding engines
-    // during init and do not wait on this.
-    if (!gInitialized) {
+    SearchUtils.log('addEngineWithDetails: Adding "' + name + '".');
+    let isCurrent = false;
+    var params = details;
+
+    let isBuiltin = !!params.isBuiltin;
+    // We install search extensions during the init phase, both built in
+    // web extensions freshly installed (via addEnginesFromExtension) or
+    // user installed extensions being reenabled calling this directly.
+    if (!gInitialized && !isBuiltin && !params.initEngine) {
       await this.init(true);
     }
-    return this._addEngineWithDetails(name, details);
-  },
-
-  async _addEngineWithDetails(name, params) {
-    SearchUtils.log('addEngineWithDetails: Adding "' + name + '".');
-
     if (!name) {
       SearchUtils.fail("Invalid name passed to addEngineWithDetails!");
     }
     if (!params.template) {
       SearchUtils.fail("Invalid template passed to addEngineWithDetails!");
     }
     let existingEngine = this._engines[name];
     if (existingEngine) {
-      // Is this a webextension update?  If not we're dealing with legacy opensearch or an override attempt.
-      let webExtUpdate =
+      if (
         params.extensionID &&
-        params.extensionID === existingEngine._extensionID;
-      if (!webExtUpdate) {
-        let webExtBuiltin = params.extensionID && params.isBuiltin;
-        // Is the existing engine a distribution engine?
-        if (
-          webExtBuiltin &&
-          existingEngine._loadPath.startsWith(
-            `[profile]/distribution/searchplugins/`
-          )
-        ) {
-          SearchExtensionLoader.reject(
-            params.extensionID,
-            new Error(
-              `${params.extensionID} cannot override distribution engine.`
-            )
-          );
-          return null;
-        } else if (
-          params.extensionID &&
-          existingEngine._loadPath.startsWith(
-            `jar:[profile]/extensions/${params.extensionID}`
-          )
-        ) {
-          // We uninstall the legacy engine, but we don't need to wait or do anything else here,
-          // _addEngineToStore will handle updating the engine data we're using.
-          this._removeEngineInstall(existingEngine);
-        } else {
-          SearchUtils.fail(
-            `An engine with the name ${name} already exists!`,
-            Cr.NS_ERROR_FILE_ALREADY_EXISTS
-          );
-        }
+        existingEngine._loadPath.startsWith(
+          `jar:[profile]/extensions/${params.extensionID}`
+        )
+      ) {
+        // This is a legacy extension engine that needs to be migrated to WebExtensions.
+        isCurrent = this.defaultEngine == existingEngine;
+        await this.removeEngine(existingEngine);
+      } else {
+        SearchUtils.fail(
+          "An engine with that name already exists!",
+          Cr.NS_ERROR_FILE_ALREADY_EXISTS
+        );
       }
     }
 
     let newEngine = new SearchEngine({
       name,
-      readOnly: !!params.isBuiltin,
+      readOnly: isBuiltin,
       sanitizeName: true,
     });
     newEngine._initFromMetadata(name, params);
     newEngine._loadPath = "[other]addEngineWithDetails";
     if (params.extensionID) {
       newEngine._loadPath += ":" + params.extensionID;
     }
-    newEngine._engineToUpdate = existingEngine;
 
     this._addEngineToStore(newEngine);
+    if (isCurrent) {
+      this.defaultEngine = newEngine;
+    }
     return newEngine;
   },
 
   async addEnginesFromExtension(extension) {
     SearchUtils.log("addEnginesFromExtension: " + extension.id);
-    // Wait for the list.json engines to be parsed before
-    // allowing addEnginesFromExtension to continue.  This delays early start
-    // extensions until we are at a stage that they can be handled.
-    await this._extensionLoadReady.promise;
-    let locales = this._engineLocales.get(extension.id) || [DEFAULT_TAG];
+    if (extension.addonData.builtIn) {
+      SearchUtils.log("addEnginesFromExtension: Ignoring builtIn engine.");
+      return [];
+    }
+    // If we havent started SearchService yet, store this extension
+    // to install in SearchService.init().
+    if (!gInitialized) {
+      this._startupExtensions.add(extension);
+      return [];
+    }
+    return this._installExtensionEngine(extension, [DEFAULT_TAG]);
+  },
+
+  async _installExtensionEngine(extension, locales, initEngine) {
+    SearchUtils.log("installExtensionEngine: " + extension.id);
 
     let installLocale = async locale => {
       let manifest =
         locale === DEFAULT_TAG
           ? extension.manifest
           : await extension.getLocalizedManifest(locale);
-      return this._addEngineForManifest(extension, manifest, locale);
+      return this._addEngineForManifest(
+        extension,
+        manifest,
+        locale,
+        initEngine
+      );
     };
 
     let engines = [];
     for (let locale of locales) {
       SearchUtils.log(
         "addEnginesFromExtension: installing locale: " +
           extension.id +
           ":" +
           locale
       );
-      engines.push(installLocale(locale));
+      engines.push(await installLocale(locale));
     }
-    return Promise.all(engines).then(installedEngines => {
-      SearchExtensionLoader.resolve(extension.id);
-      return installedEngines;
-    });
+    return engines;
   },
 
-  async _addEngineForManifest(extension, manifest, locale = DEFAULT_TAG) {
+  async _addEngineForManifest(
+    extension,
+    manifest,
+    locale = DEFAULT_TAG,
+    initEngine = false
+  ) {
     let { IconDetails } = ExtensionParent;
 
     // General set of icons for an engine.
     let icons = extension.manifest.icons;
     let iconList = [];
     if (icons) {
       iconList = Object.entries(icons).map(icon => {
         return {
@@ -2204,20 +2236,20 @@ SearchService.prototype = {
       extensionID: extension.id,
       isBuiltin: extension.addonData.builtIn,
       // suggest_url doesn't currently get encoded.
       suggestURL: searchProvider.suggest_url,
       suggestPostParams: searchProvider.suggest_url_post_params,
       suggestGetParams: searchProvider.suggest_url_get_params,
       queryCharset: searchProvider.encoding || "UTF-8",
       mozParams: searchProvider.params,
-      version: extension.version,
+      initEngine,
     };
 
-    return this._addEngineWithDetails(params.name, params);
+    return this.addEngineWithDetails(params.name, params);
   },
 
   async addEngine(engineURL, iconURL, confirm, extensionID) {
     SearchUtils.log('addEngine: Adding "' + engineURL + '".');
     await this.init(true);
     let errCode;
     try {
       var engine = new SearchEngine({
@@ -2255,29 +2287,16 @@ SearchService.prototype = {
 
   async removeWebExtensionEngine(id) {
     SearchUtils.log("removeWebExtensionEngine: " + id);
     for (let engine of await this.getEnginesByExtensionID(id)) {
       await this.removeEngine(engine);
     }
   },
 
-  async _removeEngineInstall(engine) {
-    // Make sure there is a file and this is not a webextension.
-    if (!engine._filePath || engine._extensionID) {
-      return;
-    }
-    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-    file.persistentDescriptor = engine._filePath;
-    if (file.exists()) {
-      file.remove(false);
-    }
-    engine._filePath = null;
-  },
-
   async removeEngine(engine) {
     await this.init(true);
     if (!engine) {
       SearchUtils.fail("no engine passed to removeEngine!");
     }
 
     var engineToRemove = null;
     for (var e in this._engines) {
@@ -2299,17 +2318,24 @@ SearchService.prototype = {
 
     if (engineToRemove._readOnly || engineToRemove.isBuiltin) {
       // Just hide it (the "hidden" setter will notify) and remove its alias to
       // avoid future conflicts with other engines.
       engineToRemove.hidden = true;
       engineToRemove.alias = null;
     } else {
       // Remove the engine file from disk if we had a legacy file in the profile.
-      this._removeEngineInstall(engineToRemove);
+      if (engineToRemove._filePath) {
+        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+        file.persistentDescriptor = engineToRemove._filePath;
+        if (file.exists()) {
+          file.remove(false);
+        }
+        engineToRemove._filePath = null;
+      }
 
       // Remove the engine from _sortedEngines
       var index = this._sortedEngines.indexOf(engineToRemove);
       if (index == -1) {
         SearchUtils.fail(
           "Can't find engine to remove in _sortedEngines!",
           Cr.NS_ERROR_FAILURE
         );
--- a/toolkit/components/search/SearchUtils.jsm
+++ b/toolkit/components/search/SearchUtils.jsm
@@ -1,62 +1,39 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* eslint no-shadow: error, mozilla/no-aArgs: error */
 
 "use strict";
 
-var EXPORTED_SYMBOLS = ["SearchUtils", "SearchExtensionLoader"];
+var EXPORTED_SYMBOLS = ["SearchUtils"];
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
-  AddonManager: "resource://gre/modules/AddonManager.jsm",
-  AppConstants: "resource://gre/modules/AppConstants.jsm",
-  PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
   Services: "resource://gre/modules/Services.jsm",
-  clearTimeout: "resource://gre/modules/Timer.jsm",
-  setTimeout: "resource://gre/modules/Timer.jsm",
 });
 
 const BROWSER_SEARCH_PREF = "browser.search.";
 
-const EXT_SEARCH_PREFIX = "resource://search-extensions/";
-const APP_SEARCH_PREFIX = "resource://search-plugins/";
-
-// By the time we start loading an extension, it should load much
-// faster than 1000ms.  This simply ensures we resolve all the
-// promises and let search init complete if something happens.
-XPCOMUtils.defineLazyPreferenceGetter(
-  this,
-  "ADDON_LOAD_TIMEOUT",
-  BROWSER_SEARCH_PREF + "addonLoadTimeout",
-  1000
-);
-
 XPCOMUtils.defineLazyPreferenceGetter(
   this,
   "loggingEnabled",
   BROWSER_SEARCH_PREF + "log",
   false
 );
 
 var SearchUtils = {
-  APP_SEARCH_PREFIX,
+  APP_SEARCH_PREFIX: "resource://search-plugins/",
 
   BROWSER_SEARCH_PREF,
-  EXT_SEARCH_PREFIX,
-  LIST_JSON_URL:
-    (AppConstants.platform == "android"
-      ? APP_SEARCH_PREFIX
-      : EXT_SEARCH_PREFIX) + "list.json",
 
   /**
    * Topic used for events involving the service itself.
    */
   TOPIC_SEARCH_SERVICE: "browser-search-service",
 
   // See documentation in nsISearchService.idl.
   TOPIC_ENGINE_MODIFIED: "browser-search-engine-modified",
@@ -113,16 +90,17 @@ var SearchUtils = {
   /**
    * Outputs text to the JavaScript console as well as to stdout.
    *
    * @param {string} text
    *   The message to log.
    */
   log(text) {
     if (loggingEnabled) {
+      dump("*** Search: " + text + "\n");
       Services.console.logStringMessage(text);
     }
   },
 
   /**
    * Logs the failure message (if browser.search.log is enabled) and throws.
    * @param {string} message
    *   A message to display
@@ -167,140 +145,9 @@ var SearchUtils = {
         null /* triggeringPrincipal */,
         Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
         Ci.nsIContentPolicy.TYPE_OTHER
       );
     } catch (ex) {}
 
     return null;
   },
-
-  makeExtensionId(name) {
-    return name + "@search.mozilla.org";
-  },
-
-  getExtensionUrl(id) {
-    return EXT_SEARCH_PREFIX + id.split("@")[0] + "/";
-  },
 };
-
-/**
- * SearchExtensionLoader provides a simple install function that
- * returns a set of promises.  The caller (SearchService) must resolve
- * each extension id once it has handled the final part of the install
- * (creating the SearchEngine).  Once they are resolved, the extensions
- * are fully functional, in terms of the SearchService, and initialization
- * can be completed.
- *
- * When an extension is installed (that has a search provider), the
- * extension system will call ss.addEnginesFromExtension. When that is
- * completed, SearchService calls back to resolve the promise.
- */
-const SearchExtensionLoader = {
-  _promises: new Map(),
-  // strict is used in tests, causes load errors to reject.
-  _strict: false,
-  // chaos mode is used in search config tests.  It bypasses
-  // reloading extensions, otherwise over the course of these
-  // tests we do over 700K reloads of extensions.
-  _chaosMode: false,
-
-  /**
-   * Creates a deferred promise for an extension install.
-   * @param {string} id the extension id.
-   * @returns {Promise}
-   */
-  _addPromise(id) {
-    let deferred = PromiseUtils.defer();
-    // We never want to have some uncaught problem stop the SearchService
-    // init from completing, so timeout the promise.
-    if (ADDON_LOAD_TIMEOUT > 0) {
-      deferred.timeout = setTimeout(() => {
-        deferred.reject(id, new Error("addon install timed out."));
-        this._promises.delete(id);
-      }, ADDON_LOAD_TIMEOUT);
-    }
-    this._promises.set(id, deferred);
-    return deferred.promise;
-  },
-
-  /**
-   * @param {string} id the extension id to resolve.
-   */
-  resolve(id) {
-    if (this._promises.has(id)) {
-      let deferred = this._promises.get(id);
-      if (deferred.timeout) {
-        clearTimeout(deferred.timeout);
-      }
-      deferred.resolve();
-      this._promises.delete(id);
-    }
-  },
-
-  /**
-   * @param {string} id the extension id to reject.
-   * @param {object} error The error to log when rejecting.
-   */
-  reject(id, error) {
-    if (this._promises.has(id)) {
-      let deferred = this._promises.get(id);
-      if (deferred.timeout) {
-        clearTimeout(deferred.timeout);
-      }
-      // We don't want to reject here because that will reject the promise.all
-      // and stop the searchservice init.  Log the error, and resolve the promise.
-      // strict mode can be used by tests to force an exception to occur.
-      Cu.reportError(`Addon install for search engine ${id} failed: ${error}`);
-      if (this._strict) {
-        deferred.reject();
-      } else {
-        deferred.resolve();
-      }
-      this._promises.delete(id);
-    }
-  },
-
-  _reset() {
-    SearchUtils.log(`SearchExtensionLoader.reset`);
-    for (let id of this._promises.keys()) {
-      this.reject(id, new Error(`installAddons reset during install`));
-    }
-    this._promises = new Map();
-  },
-
-  /**
-   * Tell AOM to install a set of built-in extensions.  If the extension is
-   * already installed, it will be reinstalled.
-   *
-   * @param {Array} engineIDList is an array of extension IDs.
-   * @returns {Promise} resolved when all engines have finished installation.
-   */
-  async installAddons(engineIDList) {
-    SearchUtils.log(`SearchExtensionLoader.installAddons`);
-    // If SearchService calls us again, it is being re-inited.  reset ourselves.
-    this._reset();
-    let promises = [];
-    for (let id of engineIDList) {
-      promises.push(this._addPromise(id));
-      let path = SearchUtils.getExtensionUrl(id);
-      SearchUtils.log(
-        `SearchExtensionLoader.installAddons: installing ${id} at ${path}`
-      );
-      if (this._chaosMode) {
-        // If the extension is already loaded, we do not reload the extension.  Instead
-        // we call back to search service directly.
-        let policy = WebExtensionPolicy.getByID(id);
-        if (policy) {
-          Services.search.addEnginesFromExtension(policy.extension);
-          continue;
-        }
-      }
-      // The AddonManager will install the engine asynchronously
-      AddonManager.installBuiltinAddon(path).catch(error => {
-        // Catch any install errors and propogate.
-        this.reject(id, error);
-      });
-    }
-
-    return Promise.all(promises);
-  },
-};
--- a/toolkit/components/search/nsISearchService.idl
+++ b/toolkit/components/search/nsISearchService.idl
@@ -216,16 +216,18 @@ interface nsISearchService : nsISupports
    */
   Promise init();
 
   /**
    * Exposed for testing.
    */
   void reInit([optional] in boolean skipRegionCheck);
   void reset();
+  Promise ensureBuiltinExtension(in AString id,
+                                [optional] in jsval locales);
 
   /**
    * Determine whether initialization has been completed.
    *
    * Clients of the service can use this attribute to quickly determine whether
    * initialization is complete, and decide to trigger some immediate treatment,
    * to launch asynchronous initialization or to bailout.
    *
deleted file mode 100644
--- a/toolkit/components/search/tests/xpcshell/data/invalid-extension/invalid/manifest.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-  "name": "Invalid",
-  "description": "Invalid Engine",
-  "manifest_version": 2,
-  "version": "1.0",
-  "applications": {
-    "gecko": {
-      "id": "invalid@search.mozilla.org"
-    }
-  },
-  "hidden": true,
-  "chrome_settings_overrides": {
-    "search_provider": {
-      "name": "Invalid",
-      "search_url": "ssh://duckduckgo.com/",
-      "suggest_url": "ssh://ac.duckduckgo.com/ac/q={searchTerms}&type=list"
-    }
-  }
-}
deleted file mode 100644
--- a/toolkit/components/search/tests/xpcshell/data/invalid-extension/list.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "default": {
-    "visibleDefaultEngines": [
-      "invalid"
-    ]
-  }
-}
--- a/toolkit/components/search/tests/xpcshell/head_search.js
+++ b/toolkit/components/search/tests/xpcshell/head_search.js
@@ -36,20 +36,16 @@ const CACHE_FILENAME = "search.json.mozl
 
 // nsSearchService.js uses Services.appinfo.name to build a salt for a hash.
 // eslint-disable-next-line mozilla/use-services
 var XULRuntime = Cc["@mozilla.org/xre/runtime;1"].getService(Ci.nsIXULRuntime);
 
 // Expand the amount of information available in error logs
 Services.prefs.setBoolPref("browser.search.log", true);
 
-// Some tests load tons of extensions and will timeout, disable the timeout
-// here to allow tests to be slow.
-Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
-
 // The geo-specific search tests assume certain prefs are already setup, which
 // might not be true when run in comm-central etc.  So create them here.
 Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", true);
 Services.prefs.setIntPref("browser.search.geoip.timeout", 3000);
 // But still disable geoip lookups - tests that need it will re-configure this.
 Services.prefs.setCharPref("browser.search.geoip.url", "");
 // Also disable region defaults - tests using it will also re-configure it.
 Services.prefs
--- a/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js
@@ -1,55 +1,36 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-Cu.importGlobalProperties(["fetch"]);
-
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 XPCOMUtils.defineLazyModuleGetters(this, {
   AddonTestUtils: "resource://testing-common/AddonTestUtils.jsm",
   ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
   OS: "resource://gre/modules/osfile.jsm",
-  SearchExtensionLoader: "resource://gre/modules/SearchUtils.jsm",
   SearchTestUtils: "resource://testing-common/SearchTestUtils.jsm",
-  SearchUtils: "resource://gre/modules/SearchUtils.jsm",
   Services: "resource://gre/modules/Services.jsm",
 });
 
 const GLOBAL_SCOPE = this;
 
 const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
 const URLTYPE_SEARCH_HTML = "text/html";
 const SUBMISSION_PURPOSES = [
   "searchbar",
   "keyword",
   "contextmenu",
   "homepage",
   "newtab",
 ];
 
-const ORIG_LIST_JSON_URL = SearchUtils.LIST_JSON_URL;
-SearchExtensionLoader._chaosMode = true;
-
-function traverse(obj, fun) {
-  for (var i in obj) {
-    let r = fun.apply(this, [i, obj[i]]);
-    if (r !== undefined) {
-      obj[i] = r;
-    }
-    if (obj[i] !== null && typeof obj[i] == "object") {
-      traverse(obj[i], fun);
-    }
-  }
-}
-
 /**
  * This class implements the test harness for search configuration tests.
  * These tests are designed to ensure that the correct search engines are
  * loaded for the various region/locale configurations.
  *
  * The configuration for each test is represented by an object having the
  * following properties:
  *
@@ -95,69 +76,31 @@ class SearchConfigTest {
     // This is intended for development-only. Setting it to true restricts the
     // set of locales and regions that are covered, to provide tests that are
     // quicker to run.
     // Turning it on will generate one error at the end of the test, as a reminder
     // that it needs to be changed back before shipping.
     this._testDebug = false;
   }
 
-  // Search init loads a bunch of extensions which can take a lot of time.
-  // This reduces that down to any default engines, and the engine that is
-  // specifically being tested.
-  async setupListJSON() {
-    let origListJSON = await fetch(ORIG_LIST_JSON_URL).then(req => req.json());
-    let target = this._config.identifier;
-    let listJSON = Object.assign({}, origListJSON);
-    // XXX these are all possible "searchDefault" engines in list.json.  Since the searchDefault
-    // value is a localized name, we have no good way to map these.  We need the engines that
-    // could be default, along with the engine being tested.
-    let defaults = ["google", "yandex", "baidu"];
-    // info(`testing with default engines ${defaults.values()}`)
-    traverse(listJSON, (key, val) => {
-      if (key === "searchOrder") {
-        return val.filter(
-          v =>
-            v.startsWith(target) ||
-            defaults.filter(d => v.startsWith(d)).length > 0
-        );
-      }
-      if (key === "visibleDefaultEngines") {
-        return val.filter(
-          v =>
-            v.startsWith(target) ||
-            defaults.filter(d => v.startsWith(d)).length > 0
-        );
-      }
-      return val;
-    });
-    SearchUtils.LIST_JSON_URL =
-      "data:application/json," + JSON.stringify(listJSON);
-  }
-
   /**
    * Sets up the test.
    */
   async setup() {
-    await this.setupListJSON();
-
     AddonTestUtils.init(GLOBAL_SCOPE);
     AddonTestUtils.createAppInfo(
       "xpcshell@tests.mozilla.org",
       "XPCShell",
       "42",
       "42"
     );
 
     // Disable region checks.
     Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
     Services.prefs.setCharPref("browser.search.geoip.url", "");
-    Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
-    // Shut off a bunch of logging from AOM.
-    Services.prefs.setBoolPref("extensions.logging.enabled", false);
 
     await AddonTestUtils.promiseStartupManager();
     await Services.search.init();
 
     // Note: we don't use the helper function here, so that we have at least
     // one message output per process.
     Assert.ok(
       Services.search.isInitialized,
--- a/toolkit/components/search/tests/xpcshell/test_async_distribution.js
+++ b/toolkit/components/search/tests/xpcshell/test_async_distribution.js
@@ -6,26 +6,24 @@ add_task(async function setup() {
 });
 
 add_task(async function test_async_distribution() {
   configureToLoadJarEngines();
   installDistributionEngine();
 
   Assert.ok(!Services.search.isInitialized);
 
-  let aStatus = await Services.search.init();
-  Assert.ok(Components.isSuccessCode(aStatus));
-  Assert.ok(Services.search.isInitialized);
+  return Services.search.init().then(function search_initialized(aStatus) {
+    Assert.ok(Components.isSuccessCode(aStatus));
+    Assert.ok(Services.search.isInitialized);
 
-  // test that the engine from the distribution overrides our jar engine
-  let engines = await Services.search.getEngines();
-  Assert.equal(engines.length, 1);
+    // test that the engine from the distribution overrides our jar engine
+    return Services.search.getEngines().then(engines => {
+      Assert.equal(engines.length, 1);
 
-  let engine = Services.search.getEngineByName("bug645970");
-  Assert.ok(!!engine, "engine is installed");
+      let engine = Services.search.getEngineByName("bug645970");
+      Assert.notEqual(engine, null);
 
-  // check the engine we have is actually the one from the distribution
-  Assert.equal(
-    engine.description,
-    "override",
-    "distribution engine override installed"
-  );
+      // check the engine we have is actually the one from the distribution
+      Assert.equal(engine.description, "override");
+    });
+  });
 });
--- a/toolkit/components/search/tests/xpcshell/test_list_json_searchorder.js
+++ b/toolkit/components/search/tests/xpcshell/test_list_json_searchorder.js
@@ -1,33 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* Check default search engine is picked from list.json searchDefault */
 
 "use strict";
 
 add_task(async function setup() {
-  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
-  Services.prefs.setCharPref("browser.search.geoip.url", "");
-  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
-
   await AddonTestUtils.promiseStartupManager();
 });
 
 // Override list.json with test data from data/list.json
 // and check that searchOrder is working
 add_task(async function test_searchOrderJSON() {
   let url = "resource://test/data/";
   let resProt = Services.io
     .getProtocolHandler("resource")
     .QueryInterface(Ci.nsIResProtocolHandler);
   resProt.setSubstitution("search-extensions", Services.io.newURI(url));
 
-  await Services.search.init();
+  await asyncReInit();
 
   Assert.ok(Services.search.isInitialized, "search initialized");
   Assert.equal(
     Services.search.defaultEngine.name,
     kTestEngineName,
     "expected test list JSON default search engine"
   );
 
--- a/toolkit/components/search/tests/xpcshell/test_migrateWebExtensionEngine.js
+++ b/toolkit/components/search/tests/xpcshell/test_migrateWebExtensionEngine.js
@@ -1,160 +1,48 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const { ExtensionTestUtils } = ChromeUtils.import(
-  "resource://testing-common/ExtensionXPCShellUtils.jsm"
-);
-ExtensionTestUtils.init(this);
-
-AddonTestUtils.usePrivilegedSignatures = false;
-AddonTestUtils.overrideCertDB();
-
 const kSearchEngineID = "addEngineWithDetails_test_engine";
 const kExtensionID = "test@example.com";
 
 const kSearchEngineDetails = {
   template: "http://example.com/?search={searchTerms}",
   description: "Test Description",
   iconURL:
     "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
   suggestURL: "http://example.com/?suggest={searchTerms}",
   alias: "alias_foo",
+  extensionID: kExtensionID,
 };
 
 add_task(async function setup() {
   await AddonTestUtils.promiseStartupManager();
-  await Services.search.init();
 });
 
 add_task(async function test_migrateLegacyEngine() {
+  Assert.ok(!Services.search.isInitialized);
+
   await Services.search.addEngineWithDetails(
     kSearchEngineID,
     kSearchEngineDetails
   );
 
   // Modify the loadpath so it looks like an legacy plugin loadpath
   let engine = Services.search.getEngineByName(kSearchEngineID);
-  Assert.ok(!!engine, "opensearch engine installed");
   engine.wrappedJSObject._loadPath = `jar:[profile]/extensions/${kExtensionID}.xpi!/engine.xml`;
-  await Services.search.setDefault(engine);
-  Assert.equal(
-    engine.name,
-    Services.search.defaultEngine.name,
-    "set engine to default"
-  );
+  engine.wrappedJSObject._extensionID = null;
 
-  // We assume the default engines are installed, so our position will be after the default engine.
-  // This sets up the test to later test the engine position after updates.
-  let allEngines = await Services.search.getEngines();
-  Assert.ok(
-    allEngines.length > 2,
-    "default engines available " + allEngines.length
-  );
-  let origIndex = allEngines.map(e => e.name).indexOf(kSearchEngineID);
-  Assert.ok(
-    origIndex > 1,
-    "opensearch engine installed at position " + origIndex
+  // This should replace the existing engine
+  await Services.search.addEngineWithDetails(
+    kSearchEngineID,
+    kSearchEngineDetails
   );
-  await Services.search.moveEngine(engine, origIndex - 1);
-  let index = (await Services.search.getEngines())
-    .map(e => e.name)
-    .indexOf(kSearchEngineID);
-  Assert.equal(
-    origIndex - 1,
-    index,
-    "opensearch engine moved to position " + index
-  );
-
-  // Replace the opensearch extension with a webextension
-  let extensionInfo = {
-    useAddonManager: "permanent",
-    manifest: {
-      version: "1.0",
-      applications: {
-        gecko: {
-          id: kExtensionID,
-        },
-      },
-      chrome_settings_overrides: {
-        search_provider: {
-          name: kSearchEngineID,
-          search_url: "https://example.com/?q={searchTerms}",
-        },
-      },
-    },
-  };
-
-  let extension = ExtensionTestUtils.loadExtension(extensionInfo);
-  await extension.startup();
 
   engine = Services.search.getEngineByName(kSearchEngineID);
   Assert.equal(
     engine.wrappedJSObject._loadPath,
     "[other]addEngineWithDetails:" + kExtensionID
   );
   Assert.equal(engine.wrappedJSObject._extensionID, kExtensionID);
-  Assert.equal(engine.wrappedJSObject._version, "1.0");
-  index = (await Services.search.getEngines())
-    .map(e => e.name)
-    .indexOf(kSearchEngineID);
-  Assert.equal(origIndex - 1, index, "webext position " + index);
-  Assert.equal(
-    engine.name,
-    Services.search.defaultEngine.name,
-    "engine stil default"
-  );
-
-  extensionInfo.manifest.version = "2.0";
-  await extension.upgrade(extensionInfo);
-  await AddonTestUtils.waitForSearchProviderStartup(extension);
-
-  engine = Services.search.getEngineByName(kSearchEngineID);
-  Assert.equal(
-    engine.wrappedJSObject._loadPath,
-    "[other]addEngineWithDetails:" + kExtensionID
-  );
-  Assert.equal(engine.wrappedJSObject._extensionID, kExtensionID);
-  Assert.equal(engine.wrappedJSObject._version, "2.0");
-  index = (await Services.search.getEngines())
-    .map(e => e.name)
-    .indexOf(kSearchEngineID);
-  Assert.equal(origIndex - 1, index, "webext position " + index);
-  Assert.equal(
-    engine.name,
-    Services.search.defaultEngine.name,
-    "engine stil default"
-  );
-
-  // A different extension cannot use the same name
-  extensionInfo.manifest.applications.gecko.id = "takeover@search.foo";
-  let otherExt = ExtensionTestUtils.loadExtension(extensionInfo);
-  await otherExt.startup();
-  // Verify correct owner
-  engine = Services.search.getEngineByName(kSearchEngineID);
-  Assert.equal(
-    engine.wrappedJSObject._extensionID,
-    kExtensionID,
-    "prior search engine could not be overwritten"
-  );
-  // Verify no engine installed
-  let engines = await Services.search.getEnginesByExtensionID(
-    "takeover@search.foo"
-  );
-  Assert.equal(engines.length, 0, "no search engines installed");
-  await otherExt.unload();
-
-  // An opensearch engine cannot replace a webextension.
-  try {
-    await Services.search.addEngineWithDetails(
-      kSearchEngineID,
-      kSearchEngineDetails
-    );
-    Assert.ok(false, "unable to install opensearch over webextension");
-  } catch (e) {
-    Assert.ok(true, "unable to install opensearch over webextension");
-  }
-
-  await extension.unload();
 });
--- a/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js
+++ b/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js
@@ -13,17 +13,17 @@ add_task(async function setup() {
 });
 
 add_task(async function test_parseSubmissionURL() {
   // Hide the default engines to prevent them from being used in the search.
   for (let engine of await Services.search.getEngines()) {
     await Services.search.removeEngine(engine);
   }
 
-  let engines = await addTestEngines([
+  let [engine1, engine2, engine3, engine4] = await addTestEngines([
     { name: "Test search engine", xmlFileName: "engine.xml" },
     { name: "Test search engine (fr)", xmlFileName: "engine-fr.xml" },
     {
       name: "bacon_addParam",
       details: {
         alias: "bacon_addParam",
         description: "Search Bacon",
         method: "GET",
@@ -47,147 +47,128 @@ add_task(async function test_parseSubmis
         alias: "bacon",
         description: "Search Bacon",
         method: "GET",
         template: "http://www.bacon.moz/search?q={searchTerms}",
       },
     },
   ]);
 
-  engines[2].addParam("q", "{searchTerms}", null);
-  engines[3].addParam("q", "{searchTerms}", null);
-
-  function testParseSubmissionURL(url, engine, terms = "", offsetTerm) {
-    let result = Services.search.parseSubmissionURL(url);
-    Assert.equal(result.engine.name, engine.name, "engine matches");
-    Assert.equal(result.terms, terms, "term matches");
-    if (offsetTerm) {
-      Assert.ok(
-        url.slice(result.termsOffset).startsWith(offsetTerm),
-        "offset term matches"
-      );
-      Assert.equal(
-        result.termsLength,
-        offsetTerm.length,
-        "offset term length matches"
-      );
-    } else {
-      Assert.equal(result.termsOffset, url.length, "no term offset");
-    }
-  }
+  engine3.addParam("q", "{searchTerms}", null);
+  engine4.addParam("q", "{searchTerms}", null);
 
   // Test the first engine, whose URLs use UTF-8 encoding.
-  info("URLs use UTF-8 encoding");
-  testParseSubmissionURL(
-    "http://www.google.com/search?foo=bar&q=caff%C3%A8",
-    engines[0],
-    "caff\u00E8",
-    "caff%C3%A8"
-  );
+  let url = "http://www.google.com/search?foo=bar&q=caff%C3%A8";
+  let result = Services.search.parseSubmissionURL(url);
+  Assert.equal(result.engine, engine1);
+  Assert.equal(result.terms, "caff\u00E8");
+  Assert.ok(url.slice(result.termsOffset).startsWith("caff%C3%A8"));
+  Assert.equal(result.termsLength, "caff%C3%A8".length);
 
   // The second engine uses a locale-specific domain that is an alternate domain
   // of the first one, but the second engine should get priority when matching.
   // The URL used with this engine uses ISO-8859-1 encoding instead.
-  info("URLs use alternate domain and ISO-8859-1 encoding");
-  testParseSubmissionURL(
-    "http://www.google.fr/search?q=caff%E8",
-    engines[1],
-    "caff\u00E8",
-    "caff%E8"
-  );
+  url = "http://www.google.fr/search?q=caff%E8";
+  result = Services.search.parseSubmissionURL(url);
+  Assert.equal(result.engine, engine2);
+  Assert.equal(result.terms, "caff\u00E8");
+  Assert.ok(url.slice(result.termsOffset).startsWith("caff%E8"));
+  Assert.equal(result.termsLength, "caff%E8".length);
 
   // Test a domain that is an alternate domain of those defined.  In this case,
   // the first matching engine from the ordered list should be returned.
-  info("URLs use alternate domain");
-  testParseSubmissionURL(
-    "http://www.google.co.uk/search?q=caff%C3%A8",
-    engines[0],
-    "caff\u00E8",
-    "caff%C3%A8"
-  );
+  url = "http://www.google.co.uk/search?q=caff%C3%A8";
+  result = Services.search.parseSubmissionURL(url);
+  Assert.equal(result.engine, engine1);
+  Assert.equal(result.terms, "caff\u00E8");
+  Assert.ok(url.slice(result.termsOffset).startsWith("caff%C3%A8"));
+  Assert.equal(result.termsLength, "caff%C3%A8".length);
 
   // We support parsing URLs from a dynamically added engine.  Those engines use
   // windows-1252 encoding by default.
-  info("URLs use windows-1252");
-  testParseSubmissionURL(
-    "http://www.bacon.test/find?q=caff%E8",
-    engines[2],
-    "caff\u00E8",
-    "caff%E8"
-  );
+  url = "http://www.bacon.test/find?q=caff%E8";
+  result = Services.search.parseSubmissionURL(url);
+  Assert.equal(result.engine, engine3);
+  Assert.equal(result.terms, "caff\u00E8");
+  Assert.ok(url.slice(result.termsOffset).startsWith("caff%E8"));
+  Assert.equal(result.termsLength, "caff%E8".length);
+
+  // Test URLs with unescaped unicode characters.
+  url = "http://www.google.com/search?q=foo+b\u00E4r";
+  result = Services.search.parseSubmissionURL(url);
+  Assert.equal(result.engine, engine1);
+  Assert.equal(result.terms, "foo b\u00E4r");
+  Assert.ok(url.slice(result.termsOffset).startsWith("foo+b\u00E4r"));
+  Assert.equal(result.termsLength, "foo+b\u00E4r".length);
 
-  info("URLs with unescaped unicode characters");
-  testParseSubmissionURL(
-    "http://www.google.com/search?q=foo+b\u00E4r",
-    engines[0],
-    "foo b\u00E4r",
-    "foo+b\u00E4r"
+  // Test search engines with unescaped IDNs.
+  url = "http://www.b\u00FCcher.ch/search?q=foo+bar";
+  result = Services.search.parseSubmissionURL(url);
+  Assert.equal(result.engine, engine4);
+  Assert.equal(result.terms, "foo bar");
+  Assert.ok(url.slice(result.termsOffset).startsWith("foo+bar"));
+  Assert.equal(result.termsLength, "foo+bar".length);
+
+  // Test search engines with escaped IDNs.
+  url = "http://www.xn--bcher-kva.ch/search?q=foo+bar";
+  result = Services.search.parseSubmissionURL(url);
+  Assert.equal(result.engine, engine4);
+  Assert.equal(result.terms, "foo bar");
+  Assert.ok(url.slice(result.termsOffset).startsWith("foo+bar"));
+  Assert.equal(result.termsLength, "foo+bar".length);
+
+  // Parsing of parameters from an engine template URL is not supported.
+  Assert.equal(
+    Services.search.parseSubmissionURL("http://www.bacon.moz/search?q=").engine,
+    null
   );
-
-  info("URLs with unescaped IDNs");
-  testParseSubmissionURL(
-    "http://www.b\u00FCcher.ch/search?q=foo+bar",
-    engines[3],
-    "foo bar",
-    "foo+bar"
+  Assert.equal(
+    Services.search.parseSubmissionURL("https://duckduckgo.com?q=test").engine,
+    null
   );
-
-  info("URLs with escaped IDNs");
-  testParseSubmissionURL(
-    "http://www.xn--bcher-kva.ch/search?q=foo+bar",
-    engines[3],
-    "foo bar",
-    "foo+bar"
+  Assert.equal(
+    Services.search.parseSubmissionURL("https://duckduckgo.com/?q=test").engine,
+    null
   );
 
-  info("URLs with engines using template params, no value");
-  testParseSubmissionURL("http://www.bacon.moz/search?q=", engines[5]);
-
-  info("URLs with engines using template params");
-  testParseSubmissionURL(
-    "https://duckduckgo.com?q=test",
-    engines[4],
-    "test",
-    "test"
-  );
+  // HTTP and HTTPS schemes are interchangeable.
+  url = "https://www.google.com/search?q=caff%C3%A8";
+  result = Services.search.parseSubmissionURL(url);
+  Assert.equal(result.engine, engine1);
+  Assert.equal(result.terms, "caff\u00E8");
+  Assert.ok(url.slice(result.termsOffset).startsWith("caff%C3%A8"));
 
-  info("HTTP and HTTPS schemes are interchangeable.");
-  testParseSubmissionURL(
-    "https://www.google.com/search?q=caff%C3%A8",
-    engines[0],
-    "caff\u00E8",
-    "caff%C3%A8"
+  // Decoding search terms with multiple spaces should work.
+  result = Services.search.parseSubmissionURL(
+    "http://www.google.com/search?q=+with++spaces+"
   );
+  Assert.equal(result.engine, engine1);
+  Assert.equal(result.terms, " with  spaces ");
 
-  info("Decoding search terms with multiple spaces should work.");
-  testParseSubmissionURL(
-    "http://www.google.com/search?q=+with++spaces+",
-    engines[0],
-    " with  spaces ",
-    "+with++spaces+"
-  );
+  // An empty query parameter should work the same.
+  url = "http://www.google.com/search?q=";
+  result = Services.search.parseSubmissionURL(url);
+  Assert.equal(result.engine, engine1);
+  Assert.equal(result.terms, "");
+  Assert.equal(result.termsOffset, url.length);
 
-  info("An empty query parameter should work the same.");
-  testParseSubmissionURL("http://www.google.com/search?q=", engines[0]);
-
-  // These test slightly different so we don't use testParseSubmissionURL.
-  info("There should be no match when the path is different.");
-  let result = Services.search.parseSubmissionURL(
+  // There should be no match when the path is different.
+  result = Services.search.parseSubmissionURL(
     "http://www.google.com/search/?q=test"
   );
   Assert.equal(result.engine, null);
   Assert.equal(result.terms, "");
   Assert.equal(result.termsOffset, -1);
 
-  info("There should be no match when the argument is different.");
+  // There should be no match when the argument is different.
   result = Services.search.parseSubmissionURL(
     "http://www.google.com/search?q2=test"
   );
   Assert.equal(result.engine, null);
   Assert.equal(result.terms, "");
   Assert.equal(result.termsOffset, -1);
 
-  info("There should be no match for URIs that are not HTTP or HTTPS.");
+  // There should be no match for URIs that are not HTTP or HTTPS.
   result = Services.search.parseSubmissionURL("file://localhost/search?q=test");
   Assert.equal(result.engine, null);
   Assert.equal(result.terms, "");
   Assert.equal(result.termsOffset, -1);
 });
--- a/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js
+++ b/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js
@@ -30,18 +30,17 @@ add_task(async function run_test() {
   for (let engine of data.engines) {
     if (engine._name == "bug645970") {
       engine.filePath = file.path;
     }
   }
 
   await promiseSaveCacheData(data);
 
-  Services.search.reset();
-  await Services.search.init();
+  await asyncReInit();
 
   // test the engine is loaded ok.
   let engine = Services.search.getEngineByName("bug645970");
   Assert.notEqual(engine, null);
 
   // remove the engine and verify the file has been removed too.
   await Services.search.removeEngine(engine);
   Assert.ok(!file.exists());
deleted file mode 100644
--- a/toolkit/components/search/tests/xpcshell/test_require_engines_for_cache.js
+++ /dev/null
@@ -1,16 +0,0 @@
-"strict";
-
-// https://bugzilla.mozilla.org/show_bug.cgi?id=1255605
-add_task(async function skip_writing_cache_without_engines() {
-  Services.prefs.setCharPref("browser.search.region", "US");
-  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
-  await AddonTestUtils.promiseStartupManager();
-
-  useTestEngines("no-extensions");
-  Assert.strictEqual(
-    0,
-    (await Services.search.getEngines()).length,
-    "no engines loaded"
-  );
-  Assert.ok(!removeCacheFile(), "empty cache file was not created.");
-});
--- a/toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js
+++ b/toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js
@@ -1,50 +1,72 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 add_task(async function setup() {
-  Services.prefs.setCharPref("browser.search.region", "US");
-  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
   configureToLoadJarEngines();
   await AddonTestUtils.promiseStartupManager();
 });
 
 add_task(async function ignore_cache_files_without_engines() {
   let commitPromise = promiseAfterCache();
   let engineCount = (await Services.search.getEngines()).length;
-  Assert.equal(engineCount, 1, "one engine installed on search init");
+  Assert.equal(engineCount, 1);
 
   // Wait for the file to be saved to disk, so that we can mess with it.
   await commitPromise;
 
   // Remove all engines from the cache file.
   let cache = await promiseCacheData();
   cache.engines = [];
   await promiseSaveCacheData(cache);
 
   // Check that after an async re-initialization, we still have the same engine count.
   commitPromise = promiseAfterCache();
   await asyncReInit();
-  Assert.equal(
-    engineCount,
-    (await Services.search.getEngines()).length,
-    "Search got correct number of engines"
-  );
+  Assert.equal(engineCount, (await Services.search.getEngines()).length);
   await commitPromise;
 
   // Check that after a sync re-initialization, we still have the same engine count.
   await promiseSaveCacheData(cache);
   let unInitPromise = SearchTestUtils.promiseSearchNotification(
     "uninit-complete"
   );
   let reInitPromise = asyncReInit();
   await unInitPromise;
-  Assert.ok(!Services.search.isInitialized, "Search is not initialized");
+  Assert.ok(!Services.search.isInitialized);
   // Synchronously check the engine count; will force a sync init.
-  Assert.equal(
-    engineCount,
-    (await Services.search.getEngines()).length,
-    "Search got correct number of engines"
-  );
-  Assert.ok(Services.search.isInitialized, "Search is initialized");
+  Assert.equal(engineCount, (await Services.search.getEngines()).length);
+  Assert.ok(Services.search.isInitialized);
   await reInitPromise;
 });
+
+add_task(async function skip_writing_cache_without_engines() {
+  let unInitPromise = SearchTestUtils.promiseSearchNotification(
+    "uninit-complete"
+  );
+  let reInitPromise = asyncReInit();
+  await unInitPromise;
+
+  // Configure so that no engines will be found.
+  Assert.ok(removeCacheFile());
+  let resProt = Services.io
+    .getProtocolHandler("resource")
+    .QueryInterface(Ci.nsIResProtocolHandler);
+  resProt.setSubstitution(
+    "search-extensions",
+    Services.io.newURI("about:blank")
+  );
+
+  // Let the async-reInit happen.
+  await reInitPromise;
+  Assert.strictEqual(0, (await Services.search.getEngines()).length);
+
+  // Trigger yet another re-init, to flush of any pending cache writing task.
+  unInitPromise = SearchTestUtils.promiseSearchNotification("uninit-complete");
+  reInitPromise = asyncReInit();
+  await unInitPromise;
+
+  // Now check that a cache file doesn't exist.
+  Assert.ok(!removeCacheFile());
+
+  await reInitPromise;
+});
--- a/toolkit/components/search/tests/xpcshell/test_validate_engines.js
+++ b/toolkit/components/search/tests/xpcshell/test_validate_engines.js
@@ -4,76 +4,44 @@
 // Ensure all the engines defined in list.json are valid by
 // creating a new list.json that contains every engine and
 // loading them all.
 
 "use strict";
 
 Cu.importGlobalProperties(["fetch"]);
 
-const { SearchUtils, SearchExtensionLoader } = ChromeUtils.import(
-  "resource://gre/modules/SearchUtils.jsm"
+const { SearchService } = ChromeUtils.import(
+  "resource://gre/modules/SearchService.jsm"
 );
+const LIST_JSON_URL = "resource://search-extensions/list.json";
 
 function traverse(obj, fun) {
   for (var i in obj) {
     fun.apply(this, [i, obj[i]]);
     if (obj[i] !== null && typeof obj[i] == "object") {
       traverse(obj[i], fun);
     }
   }
 }
 
-add_task(async function setup() {
-  // Read all the builtin engines and locales, create a giant list.json
-  // that includes everything.
-  let engines = await fetch(SearchUtils.LIST_JSON_URL).then(req => req.json());
+const ss = new SearchService();
+
+add_task(async function test_validate_engines() {
+  let engines = await fetch(LIST_JSON_URL).then(req => req.json());
 
   let visibleDefaultEngines = new Set();
   traverse(engines, (key, val) => {
     if (key === "visibleDefaultEngines") {
       val.forEach(engine => visibleDefaultEngines.add(engine));
     }
   });
 
   let listjson = {
     default: {
       visibleDefaultEngines: Array.from(visibleDefaultEngines),
     },
   };
-  SearchUtils.LIST_JSON_URL =
-    "data:application/json," + JSON.stringify(listjson);
-
-  // Set strict so the addon install promise is rejected.  This causes
-  // search.init to throw the error, and this test fails.
-  SearchExtensionLoader._strict = true;
-  await AddonTestUtils.promiseStartupManager();
-});
+  ss._listJSONURL = "data:application/json," + JSON.stringify(listjson);
 
-add_task(async function test_validate_engines() {
-  // All engines should parse and init should work fine.
-  await Services.search
-    .init()
-    .then(() => {
-      ok(true, "all engines parsed and loaded");
-    })
-    .catch(() => {
-      ok(false, "an engine failed to parse and load");
-    });
+  await AddonTestUtils.promiseStartupManager();
+  await ss.init();
 });
-
-add_task(async function test_install_timeout_failure() {
-  // Set an incredibly unachievable timeout here and make sure
-  // that init throws.  We're loading every engine/locale combo under the
-  // sun, it's unlikely we could intermittently succeed in loading
-  // them all.
-  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 1);
-  removeCacheFile();
-  Services.search.reset();
-  await Services.search
-    .init()
-    .then(() => {
-      ok(false, "search init did not time out");
-    })
-    .catch(error => {
-      equal(Cr.NS_ERROR_FAILURE, error, "search init timed out");
-    });
-});
deleted file mode 100644
--- a/toolkit/components/search/tests/xpcshell/test_webextensions_install_failure.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-"use strict";
-
-const { SearchExtensionLoader } = ChromeUtils.import(
-  "resource://gre/modules/SearchUtils.jsm"
-);
-const { ExtensionTestUtils } = ChromeUtils.import(
-  "resource://testing-common/ExtensionXPCShellUtils.jsm"
-);
-
-ExtensionTestUtils.init(this);
-AddonTestUtils.usePrivilegedSignatures = false;
-AddonTestUtils.overrideCertDB();
-
-add_task(async function test_install_manifest_failure() {
-  // Force addon loading to reject on errors
-  SearchExtensionLoader._strict = true;
-  useTestEngines("invalid-extension");
-  await AddonTestUtils.promiseStartupManager();
-
-  await Services.search
-    .init()
-    .then(() => {
-      ok(false, "search init did not throw");
-    })
-    .catch(e => {
-      equal(Cr.NS_ERROR_FAILURE, e, "search init error");
-    });
-});
--- a/toolkit/components/search/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -43,18 +43,16 @@ support-files =
   data/test-extensions/plainengine/favicon.ico
   data/test-extensions/plainengine/manifest.json
   data/test-extensions/special-engine/favicon.ico
   data/test-extensions/special-engine/manifest.json
   data/test-extensions/multilocale/favicon.ico
   data/test-extensions/multilocale/manifest.json
   data/test-extensions/multilocale/_locales/af/messages.json
   data/test-extensions/multilocale/_locales/an/messages.json
-  data/invalid-extension/list.json
-  data/invalid-extension/invalid/manifest.json
 tags=searchmain
 
 [test_nocache.js]
 [test_big_icon.js]
 [test_bug930456.js]
 [test_bug930456_child.js]
 skip-if = true # Is confusing
 [test_engine_set_alias.js]
@@ -96,25 +94,23 @@ tags = addons
 [test_async_disthidden.js]
 [test_rel_searchform.js]
 [test_reloadEngines.js]
 [test_remove_profile_engine.js]
 [test_selectedEngine.js]
 [test_geodefaults.js]
 [test_hidden.js]
 [test_currentEngine_fallback.js]
-[test_require_engines_for_cache.js]
 [test_require_engines_in_cache.js]
 skip-if = (verify && !debug && (os == 'linux'))
 [test_svg_icon.js]
 [test_addEngineWithDetails.js]
 [test_addEngineWithDetailsObject.js]
 [test_addEngineWithExtensionID.js]
 [test_chromeresource_icon2.js]
 [test_engineUpdate.js]
 [test_paramSubstitution.js]
 [test_migrateWebExtensionEngine.js]
 [test_sendSubmissionURL.js]
 [test_validate_engines.js]
 [test_validate_manifests.js]
 [test_webextensions_install.js]
-[test_webextensions_install_failure.js]
 [test_purpose.js]
--- a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py
@@ -20,24 +20,20 @@ class TelemetryTestRunner(BaseMarionette
         # Select the appropriate GeckoInstance
         kwargs["app"] = "fxdesktop"
 
         prefs = kwargs.pop("prefs", {})
 
         # Set Firefox Client Telemetry specific preferences
         prefs.update(
             {
-                # Force search region to DE and disable geo lookups.
-                "browser.search.region": "DE",
-                "browser.search.geoSpecificDefaults": False,
-                # Turn off timeouts for loading search extensions
-                "browser.search.addonLoadTimeout": 0,
-                "browser.search.log": True,
-                # geoip is skipped if url is empty (bug 1545207)
-                "browser.search.geoip.url": "",
+                # Fake the geoip lookup to always return Germany to:
+                #   * avoid net access in tests
+                #   * stabilize browser.search.region to avoid an extra subsession (bug 1545207)
+                "browser.search.geoip.url": "data:application/json,{\"country_code\": \"DE\"}",
                 # Disable smart sizing because it changes prefs at startup. (bug 1547750)
                 "browser.cache.disk.smart_size.enabled": False,
                 "toolkit.telemetry.server": "{}/pings".format(SERVER_URL),
                 "toolkit.telemetry.initDelay": 1,
                 "toolkit.telemetry.minSubsessionLength": 0,
                 "datareporting.healthreport.uploadEnabled": True,
                 "datareporting.policy.dataSubmissionEnabled": True,
                 "datareporting.policy.dataSubmissionPolicyBypassNotification": True,
--- a/toolkit/components/telemetry/tests/unit/head.js
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -479,21 +479,16 @@ function setEmptyPrefWatchlist() {
     "resource://gre/modules/TelemetryEnvironment.jsm"
   );
   return TelemetryEnvironment.onInitialized().then(() =>
     TelemetryEnvironment.testWatchPreferences(new Map())
   );
 }
 
 if (runningInParent) {
-  // Turn off region updates and timeouts for search service
-  Services.prefs.setCharPref("browser.search.region", "US");
-  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
-  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
-
   // Set logging preferences for all the tests.
   Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
   // Telemetry archiving should be on.
   Services.prefs.setBoolPref(TelemetryUtils.Preferences.ArchiveEnabled, true);
   // Telemetry xpcshell tests cannot show the infobar.
   Services.prefs.setBoolPref(
     TelemetryUtils.Preferences.BypassNotification,
     true