Bug 846921: record details about individual addons in XPI provider; r=unfocused,vladan
authorIrving Reid <irving@mozilla.com>
Fri, 11 Oct 2013 13:13:31 -0400
changeset 164308 0a9e2dd65d142660f5a089f143ddf1eb58a571fc
parent 164307 c180e7c60c14cd1923bb3e034d94556142a40623
child 164309 1144854a01d663bef5cbd0dc86775cdb2f18f41a
child 164328 6de71ba24d505f52d6bbb9cca9172b2687d7ef04
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersunfocused, vladan
bugs846921
milestone27.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 846921: record details about individual addons in XPI provider; r=unfocused,vladan
toolkit/components/telemetry/TelemetryPing.js
toolkit/content/aboutTelemetry.js
toolkit/content/aboutTelemetry.xhtml
toolkit/locales/en-US/chrome/global/aboutTelemetry.dtd
toolkit/locales/en-US/chrome/global/aboutTelemetry.properties
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
--- a/toolkit/components/telemetry/TelemetryPing.js
+++ b/toolkit/components/telemetry/TelemetryPing.js
@@ -57,16 +57,18 @@ function getLocale() {
 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
                                    "@mozilla.org/base/telemetry;1",
                                    "nsITelemetry");
 XPCOMUtils.defineLazyServiceGetter(this, "idleService",
                                    "@mozilla.org/widget/idleservice;1",
                                    "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();
   // strip {}
   return str.substring(1, str.length - 1);
 }
 
 /**
@@ -153,19 +155,17 @@ TelemetryPing.prototype = {
     // Look for app-specific timestamps
     var appTimestamps = {};
     try {
       let o = {};
       Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", o);
       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) {
       for each (let field in Object.keys(si)) {
         if (field == "process")
           continue;
         ret[field] = si[field] - si.process
       }
@@ -540,16 +540,17 @@ TelemetryPing.prototype = {
     let payloadObj = {
       ver: PAYLOAD_VERSION,
       simpleMeasurements: simpleMeasurements,
       histograms: this.getHistograms(Telemetry.histogramSnapshots),
       slowSQL: Telemetry.slowSQL,
       chromeHangs: Telemetry.chromeHangs,
       lateWrites: Telemetry.lateWrites,
       addonHistograms: this.getAddonHistograms(),
+      addonDetails: AddonManagerPrivate.getTelemetryDetails(),
       info: info
     };
 
     if (Object.keys(this._slowSQLStartup.mainThread).length
       || Object.keys(this._slowSQLStartup.otherThreads).length) {
       payloadObj.slowSQLStartup = this._slowSQLStartup;
     }
 
@@ -684,17 +685,17 @@ TelemetryPing.prototype = {
     payloadStream.data = this.gzipCompressString(JSON.stringify(ping.payload));
     request.send(payloadStream);
   },
 
   gzipCompressString: function gzipCompressString(string) {
     let observer = {
       buffer: "",
       onStreamComplete: function(loader, context, status, length, result) {
-	this.buffer = String.fromCharCode.apply(this, result);
+        this.buffer = String.fromCharCode.apply(this, result);
       }
     };
 
     let scs = Cc["@mozilla.org/streamConverters;1"]
               .getService(Ci.nsIStreamConverterService);
     let listener = Cc["@mozilla.org/network/stream-loader;1"]
                   .createInstance(Ci.nsIStreamLoader);
     listener.init(observer);
--- a/toolkit/content/aboutTelemetry.js
+++ b/toolkit/content/aboutTelemetry.js
@@ -518,77 +518,130 @@ let Histogram = {
       // Add bucket label
       barDiv.appendChild(document.createTextNode(label));
 
       aDiv.appendChild(barDiv);
     }
   }
 };
 
-let KeyValueTable = {
+/*
+ * 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 + "}";
+};
 
-  keysHeader: bundle.GetStringFromName("keysHeader"),
-
-  valuesHeader: bundle.GetStringFromName("valuesHeader"),
-
+let KeyValueTable = {
   /**
-   * 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;
   },
 
   /**
    * Create the table header
    * 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);
   },
 
   /**
    * Create the table body
    * Tabs & newlines added to cells to make it easier to copy-paste.
    *
    * @param aTable Table element
    * @param aMeasurements Key/value map
    */
   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");
       aTable.appendChild(newRow);
 
       let keyField = document.createElement("td");
       keyField.appendChild(document.createTextNode(key + "\t"));
       newRow.appendChild(keyField);
 
       let valueField = document.createElement("td");
       valueField.appendChild(document.createTextNode(value + "\n"));
       newRow.appendChild(valueField);
     }
   }
 };
 
+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
  *
  * @param aSectionID ID of the section element that needs to be changed
  */
 function showEmptySectionMessage(aSectionID) {
   let sectionElement = document.getElementById(aSectionID);
 
@@ -808,27 +861,41 @@ function sortStartupMilestones(aSimpleMe
   }
 
   return result;
 }
 
 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");
   }
 
   LateWritesSingleton.renderLateWrites(ping.lateWrites);
 
   // 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);
--- a/toolkit/content/aboutTelemetry.xhtml
+++ b/toolkit/content/aboutTelemetry.xhtml
@@ -70,18 +70,16 @@
     </section>
 
     <section id="simple-measurements-section" class="data-section">
       <h1 class="section-name">&aboutTelemetry.simpleMeasurementsSection;</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="simple-measurements" class="data hidden">
-        <table id="simple-measurements-table">
-        </table>
       </div>
     </section>
 
     <section id="late-writes-section" class="data-section">
       <h1 class="section-name">&aboutTelemetry.lateWritesSection;</h1>
       <span class="toggle-caption">&aboutTelemetry.toggleOn;</span>
       <span class="toggle-caption hidden">&aboutTelemetry.toggleOff;</span>
       <span class="empty-caption hidden">&aboutTelemetry.emptySection;</span>
@@ -96,18 +94,25 @@
     </section>
 
     <section id="system-info-section" class="data-section">
       <h1 class="section-name">&aboutTelemetry.systemInfoSection;</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="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>
 
     <section id="addon-histograms-section" class="data-section">
       <h1 class="section-name">&aboutTelemetry.addonHistogramsSection;</h1>
       <span class="toggle-caption">&aboutTelemetry.toggleOn;</span>
       <span class="toggle-caption hidden">&aboutTelemetry.toggleOff;</span>
       <span class="empty-caption hidden">&aboutTelemetry.emptySection;</span>
--- a/toolkit/locales/en-US/chrome/global/aboutTelemetry.dtd
+++ b/toolkit/locales/en-US/chrome/global/aboutTelemetry.dtd
@@ -23,16 +23,20 @@
 <!ENTITY aboutTelemetry.histogramsSection "
   Histograms
 ">
 
 <!ENTITY aboutTelemetry.simpleMeasurementsSection "
   Simple Measurements
 ">
 
+<!ENTITY aboutTelemetry.addonDetailsSection "
+  Add-on Details
+">
+
 <!ENTITY aboutTelemetry.lateWritesSection "
   Late Writes
 ">
 
 <!ENTITY aboutTelemetry.systemInfoSection "
   System Information
 ">
 
--- a/toolkit/locales/en-US/chrome/global/aboutTelemetry.properties
+++ b/toolkit/locales/en-US/chrome/global/aboutTelemetry.properties
@@ -40,8 +40,16 @@ histogramSum = sum
 
 disableTelemetry = Disable Telemetry
 
 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
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -391,16 +391,19 @@ var gHotfixID = null;
 var AddonManagerInternal = {
   managerListeners: [],
   installListeners: [],
   addonListeners: [],
   typeListeners: [],
   providers: [],
   types: {},
   startupChanges: {},
+  // Store telemetry details per addon provider
+  telemetryDetails: {},
+
 
   // A read-only wrapper around the types dictionary
   typesProxy: Proxy.create({
     getOwnPropertyDescriptor: function typesProxy_getOwnPropertyDescriptor(aName) {
       if (!(aName in AddonManagerInternal.types))
         return undefined;
 
       return {
@@ -453,16 +456,20 @@ var AddonManagerInternal = {
    * them.
    */
   startup: function AMI_startup() {
     if (gStarted)
       return;
 
     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;
     try {
       oldAppVersion = Services.prefs.getCharPref(PREF_EM_LAST_APP_VERSION);
       appChanged = Services.appinfo.version != oldAppVersion;
     }
     catch (e) { }
@@ -2187,22 +2194,30 @@ this.AddonManagerPrivate = {
   recordSimpleMeasure: function AMP_recordSimpleMeasure(name, value) {
     this._simpleMeasures[name] = value;
   },
 
   getSimpleMeasures: function AMP_getSimpleMeasures() {
     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)
     };
   }
 };
 
 /**
  * This is the public API that UI and developers should be calling. All methods
  * just forward to AddonManagerInternal.
  */
--- a/toolkit/mozapps/extensions/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/XPIProvider.jsm
@@ -1330,46 +1330,51 @@ function recursiveRemove(aFile) {
   }
   catch (e) {
     ERROR("Failed to remove empty directory " + aFile.path, e);
     throw e;
   }
 }
 
 /**
- * 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) {
     WARN("Problem getting last modified time for " + aFile.path, e);
   }
 
   // If the file is something else, just ignore it.
-  return 0;
+  return ["", 0];
 }
 
 /**
  * Gets a snapshot of directory entries.
  *
  * @param  aDir
  *         Directory to look at
  * @param  aSortEntries
@@ -1538,20 +1543,32 @@ var XPIProvider = {
 
   // True if all of the add-ons found during startup were installed in the
   // application install location
   allAppGlobal: true,
   // A string listing the enabled add-ons for annotating crash reports
   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.
    *
    * Mappings should not be removed at any point. This is so that the mappings
    * will be still valid after an add-on gets disabled or uninstalled, as
    * consumers may still have URIs of (leaked) resources they want to map.
    */
@@ -1684,16 +1701,21 @@ var XPIProvider = {
   startup: function XPI_startup(aAppChanged, aOldAppVersion, aOldPlatformVersion) {
     LOG("startup");
     this.runPhase = XPI_STARTING;
     this.installs = [];
     this.installLocations = [];
     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");
 
     function addDirectoryInstallLocation(aName, aKey, aPaths, aScope, aLocked) {
       try {
         var dir = FileUtils.getDir(aKey, aPaths);
       }
       catch (e) {
@@ -2028,45 +2050,48 @@ var XPIProvider = {
    *         property which contains the add-ons dir/file descriptor and an
    *         mtime property which contains the add-on's last modified time as
    *         the number of milliseconds since the epoch.
    */
   getAddonStates: function XPI_getAddonStates(aLocation) {
     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;
   },
 
   /**
    * Gets an array of install location states which uniquely describes all
    * installed add-ons with the add-on's InstallLocation name and last modified
    * time. This function may be expensive because of the getAddonStates() call.
    *
    * @return an array of add-on states for each install location. Each state
    *         is an object with a name property holding the location's name and
    *         an addons property holding the add-on states for the location
    */
   getInstallLocationStates: function XPI_getInstallLocationStates() {
     let states = [];
-    this.unpackedAddons = 0;
     this.installLocations.forEach(function(aLocation) {
       let addons = aLocation.addonLocations;
       if (addons.length == 0)
         return;
 
       let locationState = {
         name: aLocation.name,
         addons: this.getAddonStates(aLocation)
@@ -3001,21 +3026,16 @@ var XPIProvider = {
       }
 
       return false;
     }
 
     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.
     aState.reverse().forEach(function(aSt) {
 
       // We can't include the install location directly in the state as it has
       // to be cached as JSON.
       let installLocation = this.installLocationsByName[aSt.name];
@@ -3040,25 +3060,32 @@ var XPIProvider = {
           if (aOldAddon.id in addonStates) {
             let addonState = addonStates[aOldAddon.id];
             delete addonStates[aOldAddon.id];
 
             // Remember add-ons that were inactive during startup
             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
             // we have an updated manifest for it. Also reload the metadata for
             // add-ons in the application directory when the application version
             // has changed
             if (aOldAddon.id in aManifests[installLocation.name] ||
                 aOldAddon.updateDate != addonState.mtime ||
@@ -3102,22 +3129,16 @@ var XPIProvider = {
     // database.
     for (let location of knownLocations) {
       let addons = XPIDatabase.getAddonsInLocation(location);
       addons.forEach(function(aOldAddon) {
         changed = removeMetadata(aOldAddon) || changed;
       }, 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);
     this.persistBootstrappedAddons();
 
     // Clear out any cached migration data.
     XPIDatabase.migrateData = null;
 
@@ -3258,17 +3279,16 @@ var XPIProvider = {
                                                          aAppChanged,
                                                          aOldAppVersion,
                                                          aOldPlatformVersion);
         }
         catch (e) {
           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
         // compatible otherwise switch back the default
         if (this.currentSkin != this.defaultSkin) {
           let oldSkin = XPIDatabase.getVisibleAddonForInternalName(this.currentSkin);
           if (!oldSkin || isAddonDisabled(oldSkin))
             this.enableDefaultTheme();
@@ -3968,16 +3988,17 @@ var XPIProvider = {
    *         the params argument
    */
   callBootstrapMethod: function XPI_callBootstrapMethod(aId, aVersion, aType, aFile,
                                                         aMethod, aReason, aExtraParams) {
     // Never call any bootstrap methods in safe mode
     if (Services.appinfo.inSafeMode)
       return;
 
+    let timeStart = new Date();
     if (aMethod == "startup") {
       LOG("Registering manifest for " + aFile.path);
       Components.manager.addBootstrappedManifestLocation(aFile);
     }
 
     try {
       // Load the scope if it hasn't already been loaded
       if (!(aId in this.bootstrapScopes))
@@ -4014,16 +4035,17 @@ var XPIProvider = {
         WARN("Exception running bootstrap method " + aMethod + " on " + aId, e);
       }
     }
     finally {
       if (aMethod == "shutdown") {
         LOG("Removing manifest for " + aFile.path);
         Components.manager.removeBootstrappedManifestLocation(aFile);
       }
+      this.setTelemetry(aId, aMethod + "_MS", new Date() - timeStart);
     }
   },
 
   /**
    * Updates the disabled state for an add-on. Its appDisabled property will be
    * calculated and if the add-on is changed the database will be saved and
    * appropriate notifications will be sent out to the registered AddonListeners.
    *
@@ -5326,17 +5348,18 @@ AddonInstall.prototype = {
         let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
         let file = this.installLocation.installAddon(this.addon.id, stagedAddon,
                                                      existingAddonID);
         cleanStagingDir(stagedAddon.parent, []);
 
         // 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,
                                                         file.persistentDescriptor);
         }
         else {
           this.addon.installDate = this.addon.updateDate;
           this.addon.active = (this.addon.visible && !isAddonDisabled(this.addon))
--- a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
@@ -11,17 +11,18 @@ const IGNORE = ["escapeAddonURI", "shoul
                 "addManagerListener", "removeManagerListener",
                 "mapURIToAddonID"];
 
 const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride",
                         "AddonScreenshot", "AddonType", "startup", "shutdown",
                         "registerProvider", "unregisterProvider",
                         "addStartupChange", "removeStartupChange",
                         "recordTimestamp", "recordSimpleMeasure",
-                        "getSimpleMeasures", "simpleTimer"];
+                        "getSimpleMeasures", "simpleTimer",
+                        "setTelemetryDetails", "getTelemetryDetails"];
 
 function test_functions() {
   for (let prop in AddonManager) {
     if (typeof AddonManager[prop] != "function")
       continue;
     if (IGNORE.indexOf(prop) != -1)
       continue;