Bug 1358907 Part 1 Addon Manager hooks for startup telemetry r=kmag
authorAndrew Swan <aswan@mozilla.com>
Thu, 18 May 2017 13:07:14 -0700
changeset 359206 ac260c2c361fff0960130113e6ec32b73687900f
parent 359205 0e48946fd355251d61767f7ec6dd4a712bc36d9d
child 359207 a8ba0313fbffe131069b461dc6495c8a9f8d5729
push id31850
push userryanvm@gmail.com
push dateFri, 19 May 2017 15:47:16 +0000
treeherdermozilla-central@c800b6dfca67 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1358907
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1358907 Part 1 Addon Manager hooks for startup telemetry r=kmag Add AddonManager.getActiveAddons() which can be called during startup to get a limited amount of information about active addons without forcing an unwated read of the extensions database. MozReview-Commit-ID: Fj6z5eYgYYC
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
--- 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 callProvider(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 ? provider.isDBLoaded : false;
+  },
 };
 
 /**
  * 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
@@ -390,16 +390,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.
@@ -2914,16 +2922,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}
    */
@@ -3356,16 +3370,32 @@ 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");
 
+      // Once other important startup work is finished, try to load the
+      // XPI database so that the telemetry environment can be populated
+      // with detailed addon information.
+      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", () => {
@@ -4643,23 +4673,77 @@ 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.some(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.
+   * @returns {Promise<Array<Addon>>}
+   */
+  getActiveAddons(aTypes) {
+    // If we already have the database loaded, returning full info is fast.
+    if (this.isDBLoaded) {
+      return new Promise(resolve => {
+        this.getAddonsByTypes(aTypes, addons => {
+          // The thing with experiments is an ugly hack but we want
+          // Experiments.jsm to use this interface instead of getAddonsByTypes.
+          // They'll go away at some point and we can forget this ever happened.
+          resolve(addons.filter(addon => addon.isActive ||
+                                       (addon.type == "experiment" && !addon.appDisabled)));
+        });
+      });
+    }
+
+    // Construct addon-like objects with the information we already
+    // have in memory.
+    if (!XPIStates.db) {
+      return Promise.reject(new Error("XPIStates not yet initialized"));
+    }
+
+    let result = [];
+    for (let addon of XPIStates.enabledAddons()) {
+      let location = this.installLocationsByName[addon.location.name];
+      let scope, isSystem;
+      if (location) {
+        ({scope, isSystem} = location);
+      }
+      result.push({
+        id: addon.id,
+        version: addon.version,
+        type: addon.type,
+        updateDate: addon.lastModifiedTime,
+        scope,
+        isSystem,
+        isWebExtension: isWebExtension(addon),
+        multiprocessCompatible: addon.multiprocessCompatible,
+      });
+    }
+
+    return Promise.resolve(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
@@ -441,16 +441,17 @@ this.XPIDatabase = {
       if (fstream)
         fstream.close();
     }
     // If an async load was also in progress, record in telemetry.
     if (this._dbPromise) {
       AddonManagerPrivate.recordSimpleMeasure("XPIDB_overlapped_load", 1);
     }
     this._dbPromise = Promise.resolve(this.addonDB);
+    Services.obs.notifyObservers(this.addonDB, "xpi-database-loaded");
   },
 
   /**
    * Parse loaded data, reconstructing the database if the loaded data is not valid
    * @param aRebuildOnError
    *        If true, synchronously reconstruct the database from installed add-ons
    */
   parseDB(aData, aRebuildOnError) {
@@ -567,17 +568,17 @@ this.XPIDatabase = {
       return this._dbPromise;
     }
 
     logger.debug("Starting async load of XPI database " + this.jsonFile.path);
     AddonManagerPrivate.recordSimpleMeasure("XPIDB_async_load", XPIProvider.runPhase);
     let readOptions = {
       outExecutionDuration: 0
     };
-    return this._dbPromise = OS.File.read(this.jsonFile.path, null, readOptions).then(
+    this._dbPromise = OS.File.read(this.jsonFile.path, null, readOptions).then(
       byteArray => {
         logger.debug("Async JSON file read took " + readOptions.outExecutionDuration + " MS");
         AddonManagerPrivate.recordSimpleMeasure("XPIDB_asyncRead_MS",
           readOptions.outExecutionDuration);
 
         if (this.addonDB) {
           logger.debug("Synchronous load completed while waiting for async load");
           return this.addonDB;
@@ -599,16 +600,22 @@ this.XPIDatabase = {
         if (error.becauseNoSuchFile) {
           this.upgradeDB(true);
         } else {
           // it's there but unreadable
           this.rebuildUnreadableDB(error, true);
         }
         return this.addonDB;
       });
+
+    this._dbPromise.then(() => {
+      Services.obs.notifyObservers(this.addonDB, "xpi-database-loaded");
+    });
+
+    return this._dbPromise;
   },
 
   /**
    * Rebuild the database from addon install directories. If this.migrateData
    * is available, uses migrated information for settings on the addons found
    * during rebuild
    * @param aRebuildOnError
    *         A boolean indicating whether add-on information should be loaded
--- a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
@@ -18,19 +18,20 @@ const IGNORE_PRIVATE = ["AddonAuthor", "
                         "AddonScreenshot", "AddonType", "startup", "shutdown",
                         "addonIsActive", "registerProvider", "unregisterProvider",
                         "addStartupChange", "removeStartupChange",
                         "getNewSideloads",
                         "recordTimestamp", "recordSimpleMeasure",
                         "recordException", "getSimpleMeasures", "simpleTimer",
                         "setTelemetryDetails", "getTelemetryDetails",
                         "callNoUpdateListeners", "backgroundUpdateTimerHandler",
-                        "hasUpgradeListener", "getUpgradeListener"];
+                        "hasUpgradeListener", "getUpgradeListener",
+                        "isDBLoaded"];
 
-function test_functions() {
+async function test_functions() {
   for (let prop in AddonManager) {
     if (IGNORE.indexOf(prop) != -1)
       continue;
     if (typeof AddonManager[prop] != "function")
       continue;
 
     let args = [];
 
@@ -42,19 +43,24 @@ function test_functions() {
       // callback in the second argument.
       if (AddonManager[prop].length > 1) {
         args.push(undefined, () => {});
       } else {
         args.push(() => {});
       }
     }
 
+    // Clean this up in bug 1365720
+    if (prop == "getActiveAddons") {
+      args = [];
+    }
+
     try {
       do_print("AddonManager." + prop);
-      AddonManager[prop](...args);
+      await AddonManager[prop](...args);
       do_throw(prop + " did not throw an exception");
     } catch (e) {
       if (e.result != Components.results.NS_ERROR_NOT_INITIALIZED)
         do_throw(prop + " threw an unexpected exception: " + e);
     }
   }
 
   for (let prop in AddonManagerPrivate) {
@@ -69,15 +75,19 @@ function test_functions() {
       do_throw(prop + " did not throw an exception");
     } catch (e) {
       if (e.result != Components.results.NS_ERROR_NOT_INITIALIZED)
         do_throw(prop + " threw an unexpected exception: " + e);
     }
   }
 }
 
+add_task(async function() {
+  await test_functions();
+  startupManager();
+  shutdownManager();
+  await test_functions();
+});
+
 function run_test() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
-  test_functions();
-  startupManager();
-  shutdownManager();
-  test_functions();
+  run_next_test();
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
@@ -318,17 +318,17 @@ add_task(async function test_experiments
     id: extensionId,
     type: 256,
     version: "0.1",
     name: "Meh API",
   });
 
   await promiseInstallAllFiles([addonFile]);
 
-  let addons = await AddonManager.getAddonsByTypes(["apiextension"]);
+  let addons = await AddonManager.getAddonsByTypes(["extension"]);
   let addon = addons.pop();
   equal(addon.id, extensionId, "Add-on should be installed as an API extension");
 
   addons = await AddonManager.getAddonsByTypes(["extension"]);
   equal(addons.pop().id, extensionId, "Add-on type should be aliased to extension");
 
   addon.uninstall();
 });