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
--- 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 ? 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.
+ * @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 => {
+ // 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.
+ aCallback(addons.filter(addon => addon.isActive ||
+ (addon.type == "experiment" && !addon.appDisabled)));
+ });
+ 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 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,
+ });
+ }
+
+ 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
@@ -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();
});