Bug 1358907 Avoid reading XPI database at startup draft
authorAndrew Swan <aswan@mozilla.com>
Tue, 16 May 2017 22:26:01 -0700
changeset 579862 51f530ea5712af3ef1e5395239e2864342037f57
parent 578818 b8e9b674033bcd1f3a4c59b9d0ee7619c1a17cc5
child 629134 b4cf55cdcbd2a02774c5b4b41459a2640127fb40
push id59387
push useraswan@mozilla.com
push dateWed, 17 May 2017 21:24:44 +0000
bugs1358907
milestone55.0a1
Bug 1358907 Avoid reading XPI database at startup Experiments and TelemetryEnvironment were both calling AddonManager.getAddonsByTypes() during startup which caused the XPI database to be loaded. We can get some basic information about addons without the XPI database so now we defer that work until after startup. MozReview-Commit-ID: Fj6z5eYgYYC
browser/experiments/Experiments.jsm
toolkit/components/telemetry/TelemetryEnvironment.jsm
toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
--- a/browser/experiments/Experiments.jsm
+++ b/browser/experiments/Experiments.jsm
@@ -160,17 +160,17 @@ function loadJSONAsync(file, options) {
 // Returns a promise that is resolved with the AddonInstall for that URL.
 function addonInstallForURL(url, hash) {
   return AddonManager.getInstallForURL(url, null, "application/x-xpinstall", hash);
 }
 
 // Returns a promise that is resolved with an Array<Addon> of the installed
 // experiment addons.
 function installedExperimentAddons() {
-  return AddonManager.getAddonsByTypes(["experiment"]).then(addons => {
+  return AddonManager.getActiveAddons(["experiment"]).then(addons => {
     return addons.filter(a => !a.appDisabled);
   });
 }
 
 // Takes an Array<Addon> and returns a promise that is resolved when the
 // addons are uninstalled.
 function uninstallAddons(addons) {
   let ids = new Set(addons.map(addon => addon.id));
--- a/toolkit/components/telemetry/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -17,30 +17,35 @@ Cu.import("resource://gre/modules/Prefer
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
 Cu.import("resource://gre/modules/ObjectUtils.jsm");
 Cu.import("resource://gre/modules/TelemetryController.jsm", this);
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
 const Utils = TelemetryUtils;
 
+const { AddonManager, AddonManagerPrivate } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+
 XPCOMUtils.defineLazyModuleGetter(this, "AttributionCode",
                                   "resource:///modules/AttributionCode.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
                                   "resource://gre/modules/ctypes.jsm");
-Cu.import("resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
                                   "resource://gre/modules/ProfileAge.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
                                   "resource://gre/modules/UpdateUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
                                   "resource://gre/modules/WindowsRegistry.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "aomStartup",
+                                   "@mozilla.org/addons/addon-manager-startup;1",
+                                   "amIAddonManagerStartup");
+
 // The maximum length of a string (e.g. description) in the addons section.
 const MAX_ADDON_STRING_LENGTH = 100;
 // The maximum length of a string value in the settings.attribution object.
 const MAX_ATTRIBUTION_STRING_LENGTH = 100;
 // The maximum lengths for the experiment id and branch in the experiments section.
 const MAX_EXPERIMENT_ID_LENGTH = 100;
 const MAX_EXPERIMENT_BRANCH_LENGTH = 100;
 
@@ -470,16 +475,30 @@ EnvironmentAddonBuilder.prototype = {
     // unfortunate reality of life.
     try {
       AddonManager.shutdown.addBlocker("EnvironmentAddonBuilder",
         () => this._shutdownBlocker());
     } catch (err) {
       return Promise.reject(err);
     }
 
+    // If the addon database has not been loaded, listen for when it gets
+    // loaded so we can immediately gather full data at that time.
+    if (!AddonManagerPrivate.isDBLoaded()) {
+      const ADDON_LOAD_NOTIFICATION = "xpi-database-loaded";
+      let self = this;
+      Services.obs.addObserver({
+        observe(subject, topic, data) {
+          Services.obs.removeObserver(this, ADDON_LOAD_NOTIFICATION);
+          self.watchForChanges();
+          self._checkForChanges(ADDON_LOAD_NOTIFICATION);
+        },
+      }, ADDON_LOAD_NOTIFICATION);
+    }
+
     this._pendingTask = this._updateAddons().then(
       () => { this._pendingTask = null; },
       (err) => {
         this._environment._log.error("init - Exception in _updateAddons", err);
         this._pendingTask = null;
       }
     );
 
@@ -589,70 +608,69 @@ EnvironmentAddonBuilder.prototype = {
   },
 
   /**
    * Get the addon data in object form.
    * @return Promise<object> containing the addon data.
    */
   async _getActiveAddons() {
     // Request addons, asynchronously.
-    let allAddons = await AddonManager.getAddonsByTypes(["extension", "service"]);
+    let allAddons = await AddonManager.getActiveAddons(["extension", "service"]);
 
     let activeAddons = {};
     for (let addon of allAddons) {
-      // Skip addons which are not active.
-      if (!addon.isActive) {
-        continue;
-      }
-
       // Weird addon data in the wild can lead to exceptions while collecting
       // the data.
       try {
         // Make sure to have valid dates.
-        let installDate = new Date(Math.max(0, addon.installDate));
         let updateDate = new Date(Math.max(0, addon.updateDate));
 
         activeAddons[addon.id] = {
-          blocklisted: (addon.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
-          description: limitStringToLength(addon.description, MAX_ADDON_STRING_LENGTH),
-          name: limitStringToLength(addon.name, MAX_ADDON_STRING_LENGTH),
-          userDisabled: enforceBoolean(addon.userDisabled),
-          appDisabled: addon.appDisabled,
           version: limitStringToLength(addon.version, MAX_ADDON_STRING_LENGTH),
           scope: addon.scope,
           type: addon.type,
-          foreignInstall: enforceBoolean(addon.foreignInstall),
-          hasBinaryComponents: addon.hasBinaryComponents,
-          installDay: Utils.millisecondsToDays(installDate.getTime()),
           updateDay: Utils.millisecondsToDays(updateDate.getTime()),
-          signedState: addon.signedState,
           isSystem: addon.isSystem,
           isWebExtension: addon.isWebExtension,
           multiprocessCompatible: Boolean(addon.multiprocessCompatible),
         };
 
-        if (addon.signedState !== undefined)
-          activeAddons[addon.id].signedState = addon.signedState;
-
+        // getActiveAddons() gives limited data during startup and full
+        // data after the addons database is loaded.  Use the presence of
+        // the "appDisabled" property as the indicator for which we have.
+        if (addon.appDisabled !== undefined) {
+          let installDate = new Date(Math.max(0, addon.installDate));
+          Object.assign(activeAddons[addon.id], {
+            blocklisted: (addon.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
+            description: limitStringToLength(addon.description, MAX_ADDON_STRING_LENGTH),
+            name: limitStringToLength(addon.name, MAX_ADDON_STRING_LENGTH),
+            userDisabled: enforceBoolean(addon.userDisabled),
+            appDisabled: addon.appDisabled,
+            foreignInstall: enforceBoolean(addon.foreignInstall),
+            hasBinaryComponents: addon.hasBinaryComponents,
+            installDay: Utils.millisecondsToDays(installDate.getTime()),
+            signedState: addon.signedState,
+          });
+        }
       } catch (ex) {
         this._environment._log.error("_getActiveAddons - An addon was discarded due to an error", ex);
         continue;
       }
     }
 
     return activeAddons;
   },
 
   /**
    * Get the currently active theme data in object form.
    * @return Promise<object> containing the active theme data.
    */
   async _getActiveTheme() {
     // Request themes, asynchronously.
-    let themes = await AddonManager.getAddonsByTypes(["theme"]);
+    let themes = await AddonManager.getActiveAddons(["theme"]);
 
     let activeTheme = {};
     // We only store information about the active theme.
     let theme = themes.find(theme => theme.isActive);
     if (theme) {
       // Make sure to have valid dates.
       let installDate = new Date(Math.max(0, theme.installDate));
       let updateDate = new Date(Math.max(0, theme.updateDate));
@@ -807,17 +825,19 @@ function EnvironmentCache() {
   p.push(this._updateProfile());
   if (AppConstants.MOZ_BUILD_APP == "browser") {
     p.push(this._updateAttribution());
   }
 
   let setup = () => {
     this._initTask = null;
     this._startWatchingPrefs();
-    this._addonBuilder.watchForChanges();
+    if (AddonManagerPrivate.isDBLoaded()) {
+      this._addonBuilder.watchForChanges();
+    }
     this._updateGraphicsFeatures();
     return this.currentEnvironment;
   };
 
   this._initTask = Promise.all(p)
     .then(
       () => setup(),
       (err) => {
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -1,12 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://gre/modules/AddonManager.jsm");
+const {AddonManager, AddonManagerPrivate} = Cu.import("resource://gre/modules/AddonManager.jsm", {});
 Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this);
 Cu.import("resource://gre/modules/Preferences.jsm", this);
 Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://testing-common/AddonManagerTesting.jsm");
 Cu.import("resource://testing-common/httpd.js");
 Cu.import("resource://testing-common/MockRegistrar.jsm", this);
 Cu.import("resource://gre/modules/FileUtils.jsm");
@@ -650,48 +650,58 @@ function checkSystemSection(data) {
     let features = gfxInfo.getFeatures();
     Assert.equal(features.compositor, gfxData.features.compositor);
     Assert.equal(features.gpuProcess.status, gfxData.features.gpuProcess.status);
     Assert.equal(features.opengl, gfxData.features.opengl);
     Assert.equal(features.webgl, gfxData.features.webgl);
   } catch (e) {}
 }
 
-function checkActiveAddon(data) {
+function checkActiveAddon(data, partialRecord) {
   let signedState = mozinfo.addon_signing ? "number" : "undefined";
   // system add-ons have an undefined signState
   if (data.isSystem)
     signedState = "undefined";
 
   const EXPECTED_ADDON_FIELDS_TYPES = {
-    blocklisted: "boolean",
-    name: "string",
-    userDisabled: "boolean",
-    appDisabled: "boolean",
     version: "string",
     scope: "number",
     type: "string",
-    foreignInstall: "boolean",
-    hasBinaryComponents: "boolean",
-    installDay: "number",
     updateDay: "number",
-    signedState,
     isSystem: "boolean",
     isWebExtension: "boolean",
     multiprocessCompatible: "boolean",
   };
 
-  for (let f in EXPECTED_ADDON_FIELDS_TYPES) {
-    Assert.ok(f in data, f + " must be available.");
-    Assert.equal(typeof data[f], EXPECTED_ADDON_FIELDS_TYPES[f],
-                 f + " must have the correct type.");
+  const FULL_ADDON_FIELD_TYPES = {
+    blocklisted: "boolean",
+    name: "string",
+    userDisabled: "boolean",
+    appDisabled: "boolean",
+    foreignInstall: "boolean",
+    hasBinaryComponents: "boolean",
+    installDay: "number",
+    signedState,
+  };
+
+  let fields = EXPECTED_ADDON_FIELDS_TYPES;
+  if (!partialRecord) {
+    fields = Object.assign({}, fields, FULL_ADDON_FIELD_TYPES);
   }
 
-  // We check "description" separately, as it can be null.
-  Assert.ok(checkNullOrString(data.description));
+  for (let [name, type] of Object.entries(fields)) {
+    Assert.ok(name in data, name + " must be available.");
+    Assert.equal(typeof data[name], type,
+                 name + " must have the correct type.");
+  }
+
+  if (!partialRecord) {
+    // We check "description" separately, as it can be null.
+    Assert.ok(checkNullOrString(data.description));
+  }
 }
 
 function checkPlugin(data) {
   const EXPECTED_PLUGIN_FIELDS_TYPES = {
     name: "string",
     version: "string",
     description: "string",
     blocklisted: "boolean",
@@ -741,32 +751,32 @@ function checkActiveGMPlugin(data) {
   // GMP plugin version defaults to null until GMPDownloader runs to update it.
   if (data.version) {
     Assert.equal(typeof data.version, "string");
   }
   Assert.equal(typeof data.userDisabled, "boolean");
   Assert.equal(typeof data.applyBackgroundUpdates, "number");
 }
 
-function checkAddonsSection(data, expectBrokenAddons) {
+function checkAddonsSection(data, expectBrokenAddons, partialAddonsRecords) {
   const EXPECTED_FIELDS = [
     "activeAddons", "theme", "activePlugins", "activeGMPlugins", "activeExperiment",
     "persona",
   ];
 
   Assert.ok("addons" in data, "There must be an addons section in Environment.");
   for (let f of EXPECTED_FIELDS) {
     Assert.ok(f in data.addons, f + " must be available.");
   }
 
   // Check the active addons, if available.
   if (!expectBrokenAddons) {
     let activeAddons = data.addons.activeAddons;
     for (let addon in activeAddons) {
-      checkActiveAddon(activeAddons[addon]);
+      checkActiveAddon(activeAddons[addon], partialAddonsRecords);
     }
   }
 
   // Check "theme" structure.
   if (Object.keys(data.addons.theme).length !== 0) {
     checkTheme(data.addons.theme);
   }
 
@@ -805,23 +815,29 @@ function checkExperimentsSection(data) {
 
     // Check that we have valid experiment info.
     let experimentData = experiments[id];
     Assert.ok("branch" in experimentData, "The experiment must have branch data.")
     Assert.ok(checkString(experimentData.branch), "The experiment data must be valid.");
   }
 }
 
-function checkEnvironmentData(data, isInitial = false, expectBrokenAddons = false) {
+function checkEnvironmentData(data, options = {}) {
+  const {
+    isInitial = false,
+    expectBrokenAddons = false,
+    partialAddonsRecords = false,
+  } = options;
+
   checkBuildSection(data);
   checkSettingsSection(data);
   checkProfileSection(data);
   checkPartnerSection(data, isInitial);
   checkSystemSection(data);
-  checkAddonsSection(data, expectBrokenAddons);
+  checkAddonsSection(data, expectBrokenAddons, partialAddonsRecords);
   checkExperimentsSection(data);
 }
 
 add_task(async function setup() {
   // Load a custom manifest to provide search engine loading from JAR files.
   do_load_manifest("chrome.manifest");
   registerFakeSysInfo();
   spoofGfxAdapter();
@@ -833,16 +849,24 @@ add_task(async function setup() {
   let system_addon = FileUtils.File(distroDir.path);
   system_addon.append("tel-system-xpi@tests.mozilla.org.xpi");
   system_addon.lastModifiedTime = SYSTEM_ADDON_INSTALL_DATE;
   loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
 
   // Spoof the persona ID.
   LightweightThemeManager.currentTheme =
     spoofTheme(PERSONA_ID, PERSONA_NAME, PERSONA_DESCRIPTION);
+
+  // The test runs in a fresh profile so starting the AddonManager causes
+  // the addons database to be created (as does setting new theme).
+  // For test_addonsStartup below, we want to test a "warm" startup where
+  // there is already a database on disk.  Simulate that here by just
+  // restarting the AddonManager.
+  await AddonTestUtils.promiseRestartManager();
+
   // Register a fake plugin host for consistent flash version data.
   registerFakePluginHost();
 
   // Setup a webserver to serve Addons, Plugins, etc.
   gHttpServer = new HttpServer();
   gHttpServer.start(-1);
   let port = gHttpServer.identity.primaryPort;
   gHttpRoot = "http://localhost:" + port + "/";
@@ -864,23 +888,47 @@ add_task(async function setup() {
   }
 
   await spoofProfileReset();
   TelemetryEnvironment.delayedInit();
 });
 
 add_task(async function test_checkEnvironment() {
   let environmentData = await TelemetryEnvironment.onInitialized();
-  checkEnvironmentData(environmentData, true);
+  checkEnvironmentData(environmentData, {isInitial: true, partialAddonsRecords: true});
 
   spoofPartnerInfo();
   Services.obs.notifyObservers(null, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC);
 
   environmentData = TelemetryEnvironment.currentEnvironment;
-  checkEnvironmentData(environmentData);
+  checkEnvironmentData(environmentData, {partialAddonsRecords: true});
+});
+
+add_task(async function test_addonsStartup() {
+  // During startup we have partial addon records (see checkActiveAddon
+  // in this file).  Test that after startup we get complete records.
+
+  // First make sure we got our initial data without reading the DB
+  Assert.equal(AddonManagerPrivate.isDBLoaded(), false,
+               "addons database is not loaded");
+
+  // Now simulate startup finishing and make sure telemetry has
+  // had a chance to react.
+  let changePromise = new Promise(resolve => {
+    TelemetryEnvironment.registerChangeListener("blabbityblah", () => {
+      TelemetryEnvironment.unregisterChangeListener("blabbityblah");
+      resolve();
+    });
+  });
+
+  Services.obs.notifyObservers(null, "sessionstore-windows-restored");
+  await changePromise;
+
+  // And now we should have complete addon data in the environment.
+  checkEnvironmentData(TelemetryEnvironment.currentEnvironment);
 });
 
 add_task(async function test_prefWatchPolicies() {
   const PREF_TEST_1 = "toolkit.telemetry.test.pref_new";
   const PREF_TEST_2 = "toolkit.telemetry.test.pref1";
   const PREF_TEST_3 = "toolkit.telemetry.test.pref2";
   const PREF_TEST_4 = "toolkit.telemetry.test.pref_old";
   const PREF_TEST_5 = "toolkit.telemetry.test.requiresRestart";
@@ -1396,17 +1444,17 @@ add_task(async function test_collectionW
   checkpointPromise = registerCheckpointPromise(2);
   await AddonManagerTesting.installXPIFromURL(ADDON_INSTALL_URL);
   await checkpointPromise;
   assertCheckpoint(2);
 
   // Check that the new environment contains the Social addon installed with the broken
   // manifest and the rest of the data.
   let data = TelemetryEnvironment.currentEnvironment;
-  checkEnvironmentData(data, false, true /* expect broken addons*/);
+  checkEnvironmentData(data, {expectBrokenAddons: true});
 
   let activeAddons = data.addons.activeAddons;
   Assert.ok(BROKEN_ADDON_ID in activeAddons,
             "The addon with the broken manifest must be reported.");
   Assert.equal(activeAddons[BROKEN_ADDON_ID].version, null,
                "null should be reported for invalid data.");
   Assert.ok(ADDON_ID in activeAddons,
             "The valid addon must be reported.");
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -2494,16 +2494,54 @@ var AddonManagerInternal = {
           addons.push(...providerAddons);
       }
 
       return addons;
     })();
   },
 
   /**
+   * Gets active add-ons of specific types.
+   *
+   * This is similar to getAddonsByTypes() but it may return a limited
+   * amount of information about only active addons.  Consequently, it
+   * can be implemented by providers using only immediately available
+   * data as opposed to getAddonsByTypes which may require I/O).
+   *
+   * @param  aTypes
+   *         An optional array of types to retrieve. Each type is a string name
+   */
+  async getActiveAddons(aTypes) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
+    if (aTypes && !Array.isArray(aTypes))
+      throw Components.Exception("aTypes must be an array or null",
+                                 Cr.NS_ERROR_INVALID_ARG);
+
+    let addons = [];
+
+    for (let provider of this.providers) {
+      let providerAddons;
+      if ("getActiveAddons" in provider) {
+        providerAddons = await promiseCallProvider(provider, "getActiveAddons", aTypes);
+      } else {
+        providerAddons = await promiseCallProvider(provider, "getAddonsByTypes", aTypes);
+        providerAddons = providerAddons.filter(a => a.isActive);
+      }
+
+      if (providerAddons)
+        addons.push(...providerAddons);
+    }
+
+    return addons;
+  },
+
+  /**
    * Asynchronously gets all installed add-ons.
    */
   getAllAddons() {
     if (!gStarted)
       throw Components.Exception("AddonManager is not initialized",
                                  Cr.NS_ERROR_NOT_INITIALIZED);
 
     return this.getAddonsByTypes(null);
@@ -3262,16 +3300,21 @@ this.AddonManagerPrivate = {
 
     return AddonManagerInternal._getProviderByName("XPIProvider")
                                .isTemporaryInstallID(extensionId);
   },
 
   set nonMpcDisabled(val) {
     gNonMpcDisabled = val;
   },
+
+  isDBLoaded() {
+    let provider = AddonManagerInternal._getProviderByName("XPIProvider");
+    return provider.isDBLoaded;
+  },
 };
 
 /**
  * This is the public API that UI and developers should be calling. All methods
  * just forward to AddonManagerInternal.
  */
 this.AddonManager = {
   // Constants for the AddonInstall.state property
@@ -3581,16 +3624,22 @@ this.AddonManager = {
   },
 
   getAddonsByTypes(aTypes, aCallback) {
     return promiseOrCallback(
       AddonManagerInternal.getAddonsByTypes(aTypes),
       aCallback);
   },
 
+  getActiveAddons(aTypes, aCallback) {
+    return promiseOrCallback(
+      AddonManagerInternal.getActiveAddons(aTypes),
+      aCallback);
+  },
+
   getAllAddons(aCallback) {
     return promiseOrCallback(
       AddonManagerInternal.getAllAddons(),
       aCallback);
   },
 
   getInstallsByTypes(aTypes, aCallback) {
     return promiseOrCallback(
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -376,16 +376,24 @@ const RESTARTLESS_TYPES = new Set([
 const SIGNED_TYPES = new Set([
   "apiextension",
   "extension",
   "experiment",
   "webextension",
   "webextension-theme",
 ]);
 
+const ALL_TYPES = new Set([
+  "dictionary",
+  "extension",
+  "experiment",
+  "locale",
+  "theme",
+]);
+
 // This is a random number array that can be used as "salt" when generating
 // an automatic ID based on the directory path of an add-on. It will prevent
 // someone from creating an ID for a permanent add-on that could be replaced
 // by a temporary add-on (because that would be confusing, I guess).
 const TEMP_INSTALL_ID_GEN_SESSION =
   new Uint8Array(Float64Array.of(Math.random()).buffer);
 
 // Whether add-on signing is required.
@@ -2895,16 +2903,22 @@ this.XPIProvider = {
   _telemetryDetails: {},
   // A Map from an add-on install to its ID
   _addonFileMap: new Map(),
   // Flag to know if ToolboxProcess.jsm has already been loaded by someone or not
   _toolboxProcessLoaded: false,
   // Have we started shutting down bootstrap add-ons?
   _closing: false,
 
+  // Check if the XPIDatabase has been loaded (without actually
+  // triggering unwanted imports or I/O)
+  get isDBLoaded() {
+    return gLazyObjectsLoaded && XPIDatabase.initialized;
+  },
+
   /**
    * Returns true if the add-on with the given ID is currently active,
    * without forcing the add-ons database to load.
    *
    * @param {string} addonId
    *        The ID of the add-on to check.
    * @returns {boolean}
    */
@@ -3337,16 +3351,31 @@ this.XPIProvider = {
       Services.obs.addObserver({
         observe(aSubject, aTopic, aData) {
           AddonManagerPrivate.recordTimestamp("XPI_finalUIStartup");
           XPIProvider.runPhase = XPI_AFTER_UI_STARTUP;
           Services.obs.removeObserver(this, "final-ui-startup");
         }
       }, "final-ui-startup");
 
+      // Try to load the XPI database after other important startup
+      // work is done and we have some time.
+      if (!this.isDBLoaded) {
+        Services.obs.addObserver({
+          observe(subject, topic, data) {
+            Services.obs.removeObserver(this, "sessionstore-windows-restored");
+
+            // It would be nice to defer some of the work here until we
+            // have idle time but we can't yet use requestIdleCallback()
+            // from chrome.  See bug 1358476.
+            XPIDatabase.asyncLoadDB();
+          },
+        }, "sessionstore-windows-restored");
+      }
+
       AddonManagerPrivate.recordTimestamp("XPI_startup_end");
 
       this.extensionsActive = true;
       this.runPhase = XPI_BEFORE_UI_STARTUP;
 
       let timerManager = Cc["@mozilla.org/updates/timer-manager;1"].
                          getService(Ci.nsIUpdateTimerManager);
       timerManager.registerTimer("xpi-signature-verification", () => {
@@ -4603,23 +4632,76 @@ this.XPIProvider = {
    *
    * @param  aTypes
    *         An array of types to fetch. Can be null to get all types.
    * @param  aCallback
    *         A callback to pass an array of Addons to
    */
   getAddonsByTypes(aTypes, aCallback) {
     let typesToGet = getAllAliasesForTypes(aTypes);
+    if (typesToGet && typesToGet.every(type => !ALL_TYPES.has(type))) {
+      aCallback([]);
+      return;
+    }
 
     XPIDatabase.getVisibleAddons(typesToGet, function(aAddons) {
       aCallback(aAddons.map(a => a.wrapper));
     });
   },
 
   /**
+   * Called to get active Addons of a particular type
+   *
+   * @param  aTypes
+   *         An array of types to fetch. Can be null to get all types.
+   * @param  aCallback
+   *         A callback to pass an array of Addons to
+   */
+  getActiveAddons(aTypes, aCallback) {
+    // If we already have the database loaded, returning full info is fast.
+    if (this.isDBLoaded) {
+      this.getAddonsByTypes(aTypes, addons => {
+        aCallback(addons.filter(addon => addon.isActive));
+      });
+      return;
+    }
+
+    // Construct addon-like objects with the information we already
+    // have in memory.
+    if (!XPIStates.db) {
+      throw new Error("XPIStates not yet initialized");
+    }
+
+    let result = [];
+    for (let [locName, data] of XPIStates.db.entries()) {
+      let location = this.installLocationsByName[locName];
+      if (!location) {
+        logger.error(`Found non-existent install location ${locName}`);
+        continue;
+      }
+      let {scope, isSystem} = location;
+      for (let [id, state] of data.entries()) {
+        result.push({
+          id,
+          version: state.version,
+          type: state.type || "extension",
+          updateDate: state.lastModifiedTime,
+          scope,
+          isSystem,
+          isWebExtension: isWebExtension(state.type),
+          multiprocessCompatible: !state.enableShims,
+        });
+      }
+    }
+
+    aCallback(result);
+  },
+
+
+  /**
    * Obtain an Addon having the specified Sync GUID.
    *
    * @param  aGUID
    *         String GUID of add-on to retrieve
    * @param  aCallback
    *         A callback to pass the Addon to. Receives null if not found.
    */
   getAddonBySyncGUID(aGUID, aCallback) {
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -599,16 +599,17 @@ this.XPIDatabase = {
           return this.addonDB;
         }
         logger.debug("Finished async read of XPI database, parsing...");
         let decodeTimer = AddonManagerPrivate.simpleTimer("XPIDB_decode_MS");
         let decoder = new TextDecoder();
         let data = decoder.decode(byteArray);
         decodeTimer.done();
         this.parseDB(data, true);
+        Services.obs.notifyObservers(this.addonDB, "xpi-database-loaded");
         return this.addonDB;
       })
     .catch(
       error => {
         if (this.addonDB) {
           logger.debug("Synchronous load completed while waiting for async load");
           return this.addonDB;
         }