Bug 846921: record details about individual addons in XPI provider; r=unfocused,vladan

This commit is contained in:
Irving Reid 2013-10-11 13:13:31 -04:00
parent e04607ac02
commit 0d558f650a
8 changed files with 185 additions and 61 deletions

View File

@ -62,6 +62,8 @@ XPCOMUtils.defineLazyServiceGetter(this, "idleService",
"nsIIdleService");
XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
"resource://gre/modules/UpdateChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
"resource://gre/modules/AddonManager.jsm");
function generateUUID() {
let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
@ -158,9 +160,7 @@ TelemetryPing.prototype = {
appTimestamps = o.TelemetryTimestamps.get();
} catch (ex) {}
try {
let o = {};
Cu.import("resource://gre/modules/AddonManager.jsm", o);
ret.addonManager = o.AddonManagerPrivate.getSimpleMeasures();
ret.addonManager = AddonManagerPrivate.getSimpleMeasures();
} catch (ex) {}
if (si.process) {
@ -545,6 +545,7 @@ TelemetryPing.prototype = {
chromeHangs: Telemetry.chromeHangs,
lateWrites: Telemetry.lateWrites,
addonHistograms: this.getAddonHistograms(),
addonDetails: AddonManagerPrivate.getTelemetryDetails(),
info: info
};
@ -689,7 +690,7 @@ TelemetryPing.prototype = {
let observer = {
buffer: "",
onStreamComplete: function(loader, context, status, length, result) {
this.buffer = String.fromCharCode.apply(this, result);
this.buffer = String.fromCharCode.apply(this, result);
}
};

View File

@ -523,19 +523,48 @@ let Histogram = {
}
};
/*
* Helper function to render JS objects with white space between top level elements
* so that they look better in the browser
* @param aObject JavaScript object or array to render
* @return String
*/
function RenderObject(aObject) {
let output = "";
if (Array.isArray(aObject)) {
if (aObject.length == 0) {
return "[]";
}
output = "[" + JSON.stringify(aObject[0]);
for (let i = 1; i < aObject.length; i++) {
output += ", " + JSON.stringify(aObject[i]);
}
return output + "]";
}
let keys = Object.keys(aObject);
if (keys.length == 0) {
return "{}";
}
output = "{\"" + keys[0] + "\":\u00A0" + JSON.stringify(aObject[keys[0]]);
for (let i = 1; i < keys.length; i++) {
output += ", \"" + keys[i] + "\":\u00A0" + JSON.stringify(aObject[keys[i]]);
}
return output + "}";
};
let KeyValueTable = {
keysHeader: bundle.GetStringFromName("keysHeader"),
valuesHeader: bundle.GetStringFromName("valuesHeader"),
/**
* Fill out a 2-column table with keys and values
* Returns a 2-column table with keys and values
* @param aMeasurements Each key in this JS object is rendered as a row in
* the table with its corresponding value
* @param aKeysLabel Column header for the keys column
* @param aValuesLabel Column header for the values column
*/
render: function KeyValueTable_render(aTableID, aMeasurements) {
let table = document.getElementById(aTableID);
this.renderHeader(table);
render: function KeyValueTable_render(aMeasurements, aKeysLabel, aValuesLabel) {
let table = document.createElement("table");
this.renderHeader(table, aKeysLabel, aValuesLabel);
this.renderBody(table, aMeasurements);
return table;
},
/**
@ -543,15 +572,17 @@ let KeyValueTable = {
* Tabs & newlines added to cells to make it easier to copy-paste.
*
* @param aTable Table element
* @param aKeysLabel Column header for the keys column
* @param aValuesLabel Column header for the values column
*/
renderHeader: function KeyValueTable_renderHeader(aTable) {
renderHeader: function KeyValueTable_renderHeader(aTable, aKeysLabel, aValuesLabel) {
let headerRow = document.createElement("tr");
aTable.appendChild(headerRow);
let keysColumn = document.createElement("th");
keysColumn.appendChild(document.createTextNode(this.keysHeader + "\t"));
keysColumn.appendChild(document.createTextNode(aKeysLabel + "\t"));
let valuesColumn = document.createElement("th");
valuesColumn.appendChild(document.createTextNode(this.valuesHeader + "\n"));
valuesColumn.appendChild(document.createTextNode(aValuesLabel + "\n"));
headerRow.appendChild(keysColumn);
headerRow.appendChild(valuesColumn);
@ -567,7 +598,7 @@ let KeyValueTable = {
renderBody: function KeyValueTable_renderBody(aTable, aMeasurements) {
for (let [key, value] of Iterator(aMeasurements)) {
if (typeof value == "object") {
value = JSON.stringify(value);
value = RenderObject(value);
}
let newRow = document.createElement("tr");
@ -584,6 +615,28 @@ let KeyValueTable = {
}
};
let AddonDetails = {
tableIDTitle: bundle.GetStringFromName("addonTableID"),
tableDetailsTitle: bundle.GetStringFromName("addonTableDetails"),
/**
* Render the addon details section as a series of headers followed by key/value tables
* @param aSections Object containing the details sections to render
*/
render: function AddonDetails_render(aSections) {
let addonSection = document.getElementById("addon-details");
for (let provider in aSections) {
let providerSection = document.createElement("h2");
let titleText = bundle.formatStringFromName("addonProvider", [provider], 1);
providerSection.appendChild(document.createTextNode(titleText));
addonSection.appendChild(providerSection);
addonSection.appendChild(
KeyValueTable.render(aSections[provider],
this.tableIDTitle, this.tableDetailsTitle));
}
}
};
/**
* Helper function for showing "No data collected" message for a section
*
@ -813,10 +866,15 @@ function sortStartupMilestones(aSimpleMeasurements) {
function displayPingData() {
let ping = TelemetryPing.getPayload();
let keysHeader = bundle.GetStringFromName("keysHeader");
let valuesHeader = bundle.GetStringFromName("valuesHeader");
// Show simple measurements
let simpleMeasurements = sortStartupMilestones(ping.simpleMeasurements);
if (Object.keys(simpleMeasurements).length) {
KeyValueTable.render("simple-measurements-table", simpleMeasurements);
let simpleSection = document.getElementById("simple-measurements");
simpleSection.appendChild(KeyValueTable.render(simpleMeasurements,
keysHeader, valuesHeader));
} else {
showEmptySectionMessage("simple-measurements-section");
}
@ -825,10 +883,19 @@ function displayPingData() {
// Show basic system info gathered
if (Object.keys(ping.info).length) {
KeyValueTable.render("system-info-table", ping.info);
let infoSection = document.getElementById("system-info");
infoSection.appendChild(KeyValueTable.render(ping.info,
keysHeader, valuesHeader));
} else {
showEmptySectionMessage("system-info-section");
}
let addonDetails = ping.addonDetails;
if (Object.keys(addonDetails).length) {
AddonDetails.render(addonDetails);
} else {
showEmptySectionMessage("addon-details-section");
}
}
window.addEventListener("load", onLoad, false);

View File

@ -75,8 +75,6 @@
<span class="toggle-caption hidden">&aboutTelemetry.toggleOff;</span>
<span class="empty-caption hidden">&aboutTelemetry.emptySection;</span>
<div id="simple-measurements" class="data hidden">
<table id="simple-measurements-table">
</table>
</div>
</section>
@ -101,8 +99,15 @@
<span class="toggle-caption hidden">&aboutTelemetry.toggleOff;</span>
<span class="empty-caption hidden">&aboutTelemetry.emptySection;</span>
<div id="system-info" class="data hidden">
<table id="system-info-table">
</table>
</div>
</section>
<section id="addon-details-section" class="data-section">
<h1 class="section-name">&aboutTelemetry.addonDetailsSection;</h1>
<span class="toggle-caption">&aboutTelemetry.toggleOn;</span>
<span class="toggle-caption hidden">&aboutTelemetry.toggleOff;</span>
<span class="empty-caption hidden">&aboutTelemetry.emptySection;</span>
<div id="addon-details" class="data hidden">
</div>
</section>

View File

@ -28,6 +28,10 @@
Simple Measurements
">
<!ENTITY aboutTelemetry.addonDetailsSection "
Add-on Details
">
<!ENTITY aboutTelemetry.lateWritesSection "
Late Writes
">

View File

@ -45,3 +45,11 @@ enableTelemetry = Enable Telemetry
keysHeader = Property
valuesHeader = Value
addonTableID = Add-on ID
addonTableDetails = Details
# Note to translators:
# - The %1$S will be replaced with the name of an Add-on Provider (e.g. "XPI", "Plugin")
addonProvider = %1$S Provider

View File

@ -396,6 +396,9 @@ var AddonManagerInternal = {
providers: [],
types: {},
startupChanges: {},
// Store telemetry details per addon provider
telemetryDetails: {},
// A read-only wrapper around the types dictionary
typesProxy: Proxy.create({
@ -458,6 +461,10 @@ var AddonManagerInternal = {
this.recordTimestamp("AMI_startup_begin");
// clear this for xpcshell test restarts
for (let provider in this.telemetryDetails)
delete this.telemetryDetails[provider];
let appChanged = undefined;
let oldAppVersion = null;
@ -2192,12 +2199,20 @@ this.AddonManagerPrivate = {
return this._simpleMeasures;
},
getTelemetryDetails: function AMP_getTelemetryDetails() {
return AddonManagerInternal.telemetryDetails;
},
setTelemetryDetails: function AMP_setTelemetryDetails(aProvider, aDetails) {
AddonManagerInternal.telemetryDetails[aProvider] = aDetails;
},
// Start a timer, record a simple measure of the time interval when
// timer.done() is called
simpleTimer: function(aName) {
let startTime = Date.now();
return {
done: () => AddonManagerPrivate.recordSimpleMeasure(aName, Date.now() - startTime)
done: () => this.recordSimpleMeasure(aName, Date.now() - startTime)
};
}
};

View File

@ -1335,28 +1335,33 @@ function recursiveRemove(aFile) {
}
/**
* Returns the timestamp of the most recently modified file in a directory,
* Returns the timestamp and leaf file name of the most recently modified
* entry in a directory,
* or simply the file's own timestamp if it is not a directory.
*
* @param aFile
* A non-null nsIFile object
* @return Epoch time, as described above. 0 for an empty directory.
* @return [File Name, Epoch time], as described above.
*/
function recursiveLastModifiedTime(aFile) {
try {
let modTime = aFile.lastModifiedTime;
let fileName = aFile.leafName;
if (aFile.isFile())
return aFile.lastModifiedTime;
return [fileName, modTime];
if (aFile.isDirectory()) {
let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
let entry, time;
let maxTime = aFile.lastModifiedTime;
let entry;
while ((entry = entries.nextFile)) {
time = recursiveLastModifiedTime(entry);
maxTime = Math.max(time, maxTime);
let [subName, subTime] = recursiveLastModifiedTime(entry);
if (subTime > modTime) {
modTime = subTime;
fileName = subName;
}
}
entries.close();
return maxTime;
return [fileName, modTime];
}
}
catch (e) {
@ -1364,7 +1369,7 @@ function recursiveLastModifiedTime(aFile) {
}
// If the file is something else, just ignore it.
return 0;
return ["", 0];
}
/**
@ -1543,10 +1548,22 @@ var XPIProvider = {
enabledAddons: null,
// An array of add-on IDs of add-ons that were inactive during startup
inactiveAddonIDs: [],
// Count of unpacked add-ons
unpackedAddons: 0,
// Keep track of startup phases for telemetry
runPhase: XPI_STARTING,
// Keep track of the newest file in each add-on, in case we want to
// report it to telemetry.
_mostRecentlyModifiedFile: {},
// Per-addon telemetry information
_telemetryDetails: {},
/*
* Set a value in the telemetry hash for a given ID
*/
setTelemetry: function XPI_setTelemetry(aId, aName, aValue) {
if (!this._telemetryDetails[aId])
this._telemetryDetails[aId] = {};
this._telemetryDetails[aId][aName] = aValue;
},
/**
* Adds or updates a URI mapping for an Addon.id.
@ -1689,6 +1706,11 @@ var XPIProvider = {
this.installLocationsByName = {};
// Hook for tests to detect when saving database at shutdown time fails
this._shutdownError = null;
// Clear this at startup for xpcshell test restarts
this._telemetryDetails = {};
// Register our details structure with AddonManager
AddonManagerPrivate.setTelemetryDetails("XPI", this._telemetryDetails);
AddonManagerPrivate.recordTimestamp("XPI_startup_begin");
@ -2033,18 +2055,22 @@ var XPIProvider = {
let addonStates = {};
aLocation.addonLocations.forEach(function(file) {
let id = aLocation.getIDForLocation(file);
let unpacked = 0;
let [modFile, modTime] = recursiveLastModifiedTime(file);
addonStates[id] = {
descriptor: file.persistentDescriptor,
mtime: recursiveLastModifiedTime(file)
mtime: modTime
};
try {
// get the install.rdf update time, if any
file.append(FILE_INSTALL_MANIFEST);
let rdfTime = file.lastModifiedTime;
addonStates[id].rdfTime = rdfTime;
this.unpackedAddons += 1;
unpacked = 1;
}
catch (e) { }
this._mostRecentlyModifiedFile[id] = modFile;
this.setTelemetry(id, "unpacked", unpacked);
}, this);
return addonStates;
@ -2061,7 +2087,6 @@ var XPIProvider = {
*/
getInstallLocationStates: function XPI_getInstallLocationStates() {
let states = [];
this.unpackedAddons = 0;
this.installLocations.forEach(function(aLocation) {
let addons = aLocation.addonLocations;
if (addons.length == 0)
@ -3006,11 +3031,6 @@ var XPIProvider = {
let changed = false;
let knownLocations = XPIDatabase.getInstallLocations();
// Gather stats for addon telemetry
let modifiedUnpacked = 0;
let modifiedExManifest = 0;
let modifiedXPI = 0;
// The install locations are iterated in reverse order of priority so when
// there are multiple add-ons installed with the same ID the one that
// should be visible is the first one encountered.
@ -3045,15 +3065,22 @@ var XPIProvider = {
if (aOldAddon.visible && !aOldAddon.active)
XPIProvider.inactiveAddonIDs.push(aOldAddon.id);
// Check if the add-on is unpacked, and has had other files changed
// on disk without the install.rdf manifest being changed
if ((addonState.rdfTime) && (aOldAddon.updateDate != addonState.mtime)) {
modifiedUnpacked += 1;
if (aOldAddon.updateDate >= addonState.rdfTime)
modifiedExManifest += 1;
}
else if (aOldAddon.updateDate != addonState.mtime) {
modifiedXPI += 1;
// Check if the add-on has been changed outside the XPI provider
if (aOldAddon.updateDate != addonState.mtime) {
// Is the add-on unpacked?
if (addonState.rdfTime) {
// Was the addon manifest "install.rdf" modified, or some other file?
if (addonState.rdfTime > aOldAddon.updateDate) {
this.setTelemetry(aOldAddon.id, "modifiedInstallRDF", 1);
}
else {
this.setTelemetry(aOldAddon.id, "modifiedFile",
this._mostRecentlyModifiedFile[aOldAddon.id]);
}
}
else {
this.setTelemetry(aOldAddon.id, "modifiedXPI", 1);
}
}
// The add-on has changed if the modification time has changed, or
@ -3107,12 +3134,6 @@ var XPIProvider = {
}, this);
}
// Tell Telemetry what we found
AddonManagerPrivate.recordSimpleMeasure("modifiedUnpacked", modifiedUnpacked);
if (modifiedUnpacked > 0)
AddonManagerPrivate.recordSimpleMeasure("modifiedExceptInstallRDF", modifiedExManifest);
AddonManagerPrivate.recordSimpleMeasure("modifiedXPI", modifiedXPI);
// Cache the new install location states
let cache = JSON.stringify(this.getInstallLocationStates());
Services.prefs.setCharPref(PREF_INSTALL_CACHE, cache);
@ -3263,7 +3284,6 @@ var XPIProvider = {
ERROR("Failed to process extension changes at startup", e);
}
}
AddonManagerPrivate.recordSimpleMeasure("installedUnpacked", this.unpackedAddons);
if (aAppChanged) {
// When upgrading the app and using a custom skin make sure it is still
@ -3973,6 +3993,7 @@ var XPIProvider = {
if (Services.appinfo.inSafeMode)
return;
let timeStart = new Date();
if (aMethod == "startup") {
LOG("Registering manifest for " + aFile.path);
Components.manager.addBootstrappedManifestLocation(aFile);
@ -4019,6 +4040,7 @@ var XPIProvider = {
LOG("Removing manifest for " + aFile.path);
Components.manager.removeBootstrappedManifestLocation(aFile);
}
this.setTelemetry(aId, aMethod + "_MS", new Date() - timeStart);
}
},
@ -5331,7 +5353,8 @@ AddonInstall.prototype = {
// Update the metadata in the database
this.addon._sourceBundle = file;
this.addon._installLocation = this.installLocation;
this.addon.updateDate = recursiveLastModifiedTime(file); // XXX sync recursive scan
let [mFile, mTime] = recursiveLastModifiedTime(file);
this.addon.updateDate = mTime;
this.addon.visible = true;
if (isUpgrade) {
this.addon = XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,

View File

@ -16,7 +16,8 @@ const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride",
"registerProvider", "unregisterProvider",
"addStartupChange", "removeStartupChange",
"recordTimestamp", "recordSimpleMeasure",
"getSimpleMeasures", "simpleTimer"];
"getSimpleMeasures", "simpleTimer",
"setTelemetryDetails", "getTelemetryDetails"];
function test_functions() {
for (let prop in AddonManager) {