Bug 1358907 Part 1 Addon Manager hooks for startup telemetry draft
authorAndrew Swan <aswan@mozilla.com>
Thu, 18 May 2017 13:07:14 -0700
changeset 583373 e032b0944c80a3ab260982f3c219ce4039f61f17
parent 578818 b8e9b674033bcd1f3a4c59b9d0ee7619c1a17cc5
child 583374 15caa3bde2531b0177334cc225841e4f7795e450
child 584110 c391410c07ed5a9462ca7dec8c01037c9ab14466
push id60376
push useraswan@mozilla.com
push dateWed, 24 May 2017 00:29:41 +0000
bugs1358907
milestone55.0a1
Bug 1358907 Part 1 Addon Manager hooks for startup telemetry 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
@@ -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,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", () => {
@@ -4603,23 +4633,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
@@ -457,16 +457,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) {
@@ -583,17 +584,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;
@@ -615,16 +616,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();
 });