Bug 1140558 - Part 1 - Switch TelemetryEnvironment to a model which keeps track of the current state constantly and makes the current environment available synchronously. r=vladan/Dexter,a=lmandel
authorBenjamin Smedberg <benjamin@smedbergs.us>
Fri, 13 Mar 2015 10:55:06 -0400
changeset 267212 79582fe6d87e5202c533c97aeed9fbf297415a10
parent 267211 aabff522c149885e5ad4b96c0364bc6727f62a6e
child 267213 aab52686db29026236fb63a53b3defc891617fba
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvladan, Dexter, lmandel
bugs1140558
milestone39.0a2
Bug 1140558 - Part 1 - Switch TelemetryEnvironment to a model which keeps track of the current state constantly and makes the current environment available synchronously. r=vladan/Dexter,a=lmandel
toolkit/components/telemetry/TelemetryEnvironment.jsm
toolkit/components/telemetry/TelemetryPing.jsm
toolkit/components/telemetry/TelemetrySession.jsm
toolkit/components/telemetry/docs/environment.rst
toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
--- a/toolkit/components/telemetry/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -4,16 +4,17 @@
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "TelemetryEnvironment",
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const myScope = this;
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/PromiseUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
@@ -24,16 +25,112 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
 #endif
 XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
                                   "resource://gre/modules/ProfileAge.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
                                   "resource://gre/modules/UpdateChannel.jsm");
 
+var gGlobalEnvironment;
+function getGlobal() {
+  if (!gGlobalEnvironment) {
+    gGlobalEnvironment = new EnvironmentCache();
+  }
+  return gGlobalEnvironment;
+}
+
+const TelemetryEnvironment = {
+  get currentEnvironment() {
+    return getGlobal().currentEnvironment;
+  },
+
+  onInitialized: function() {
+    return getGlobal().onInitialized();
+  },
+
+  registerChangeListener: function(name, listener) {
+    return getGlobal().registerChangeListener(name, listener);
+  },
+
+  unregisterChangeListener: function(name) {
+    return getGlobal().unregisterChangeListener(name);
+  },
+
+  // Policy to use when saving preferences. Exported for using them in tests.
+  RECORD_PREF_STATE: 1, // Don't record the preference value
+  RECORD_PREF_VALUE: 2, // We only record user-set prefs.
+  RECORD_PREF_NOTIFY_ONLY: 3, // Record nothing, just notify of changes.
+
+  // Testing method
+  _watchPreferences: function(prefMap) {
+    return getGlobal()._watchPreferences(prefMap);
+  },
+};
+
+const DEFAULT_ENVIRONMENT_PREFS = new Map([
+  ["app.feedback.baseURL", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["app.support.baseURL", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["accessibility.browsewithcaret", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["accessibility.force_disabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["app.update.auto", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["app.update.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["app.update.interval", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["app.update.service.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["app.update.silent", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["app.update.url", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["browser.cache.disk.enable", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["browser.cache.disk.capacity", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["browser.cache.memory.enable", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["browser.cache.offline.enable", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["browser.formfill.enable", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["browser.newtab.url", TelemetryEnvironment.RECORD_PREF_STATE],
+  ["browser.newtabpage.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["browser.newtabpage.enhanced", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["browser.polaris.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["browser.shell.checkDefaultBrowser", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["browser.startup.homepage", TelemetryEnvironment.RECORD_PREF_STATE],
+  ["browser.startup.page", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["devtools.chrome.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["devtools.debugger.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["devtools.debugger.remote-enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["dom.ipc.plugins.asyncInit", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["dom.ipc.plugins.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["experiments.manifest.uri", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["extensions.blocklist.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["extensions.blocklist.url", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["extensions.strictCompatibility", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["extensions.update.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["extensions.update.url", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["extensions.update.background.url", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["general.smoothScroll", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["gfx.direct2d.disabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["gfx.direct2d.force-enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["gfx.direct2d.use1_1", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["layers.acceleration.disabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["layers.acceleration.force-enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["layers.async-pan-zoom.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["layers.async-video-oop.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["layers.async-video.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["layers.componentalpha.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["layers.d3d11.disable-warp", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["layers.d3d11.force-warp", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["layers.prefer-d3d9", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["layers.prefer-opengl", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["layout.css.devPixelsPerPx", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["network.proxy.autoconfig_url", TelemetryEnvironment.RECORD_PREF_STATE],
+  ["network.proxy.http", TelemetryEnvironment.RECORD_PREF_STATE],
+  ["network.proxy.ssl", TelemetryEnvironment.RECORD_PREF_STATE],
+  ["pdfjs.disabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["places.history.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["privacy.trackingprotection.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["privacy.donottrackheader.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
+  ["services.sync.serverURL", TelemetryEnvironment.RECORD_PREF_STATE],
+]);
+
 const LOGGER_NAME = "Toolkit.Telemetry";
 
 const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
 const PREF_DISTRIBUTION_ID = "distribution.id";
 const PREF_DISTRIBUTION_VERSION = "distribution.version";
 const PREF_DISTRIBUTOR = "app.distributor";
 const PREF_DISTRIBUTOR_CHANNEL = "app.distributor.channel";
 const PREF_E10S_ENABLED = "browser.tabs.remote.autostart";
@@ -213,205 +310,486 @@ function getServicePack() {
       minor: null,
     };
   } finally {
     kernel32.close();
   }
 }
 #endif
 
-this.TelemetryEnvironment = {
-  _shutdown: true,
+/**
+ * Encapsulates the asynchronous magic interfacing with the addon manager. The builder
+ * is owned by a parent environment object and is an addon listener.
+ */
+function EnvironmentAddonBuilder(environment) {
+  this._environment = environment;
 
-  // A map of (sync) listeners that will be called on environment changes.
-  _changeListeners: new Map(),
-  // Async task for collecting the environment data.
-  _collectTask: null,
-
-  // Policy to use when saving preferences. Exported for using them in tests.
-  RECORD_PREF_STATE: 1, // Don't record the preference value
-  RECORD_PREF_VALUE: 2, // We only record user-set prefs.
-  RECORD_PREF_NOTIFY_ONLY: 3, // Record nothing, just notify of changes.
+  // The pending task blocks addon manager shutdown. It can either be the initial load
+  // or a change load.
+  this._pendingTask = null;
 
-  // A map of watched preferences which trigger an Environment change when modified.
-  // Every entry contains a recording policy (RECORD_PREF_*).
-  _watchedPrefs: null,
+  // Set to true once initial load is complete and we're watching for changes.
+  this._loaded = false;
+}
+EnvironmentAddonBuilder.prototype = {
+  /**
+   * Get the initial set of addons.
+   * @returns Promise<void> when the initial load is complete.
+   */
+  init: function() {
+    // Some tests don't initialize the addon manager. This accounts for the
+    // unfortunate reality of life.
+    try {
+      AddonManager.shutdown.addBlocker("EnvironmentAddonBuilder",
+        () => this._shutdownBlocker());
+    } catch (err) {
+      return Promise.reject(err);
+    }
 
-  // The Addons change listener, init by |_startWatchingAddons| .
-  _addonsListener: null,
+    this._pendingTask = this._updateAddons().then(
+      () => { this._pendingTask = null; },
+      (err) => {
+        this._environment._log.error("init - Exception in _updateAddons", err);
+        this._pendingTask = null;
+      }
+    );
 
-  // AddonManager may shut down before us, in which case we cache the addons here.
-  // It is always null if the AM didn't shut down before us.
-  // If cached, it is an object containing the addon information, suitable for use
-  // in the environment data.
-  _cachedAddons: null,
+    return this._pendingTask;
+  },
 
   /**
-   * Initialize TelemetryEnvironment.
+   * Register an addon listener and watch for changes.
    */
-  init: function () {
-    if (!this._shutdown) {
-      this._log.error("init - Already initialized");
+  watchForChanges: function() {
+    this._loaded = true;
+    AddonManager.addAddonListener(this);
+    Services.obs.addObserver(this, EXPERIMENTS_CHANGED_TOPIC, false);
+  },
+
+  // AddonListener
+  onEnabled: function() {
+    this._onChange();
+  },
+  onDisabled: function() {
+    this._onChange();
+  },
+  onInstalled: function() {
+    this._onChange();
+  },
+  onUninstalling: function() {
+    this._onChange();
+  },
+
+  _onChange: function() {
+    if (this._pendingTask) {
+      this._environment._log.trace("_onChange - task already pending");
       return;
     }
 
-    this._configureLog();
-    this._log.trace("init");
-    this._shutdown = false;
-    this._startWatchingPrefs();
-    this._startWatchingAddons();
+    this._environment._log.trace("_onChange");
+    this._pendingTask = this._updateAddons().then(
+      (changed) => {
+        this._pendingTask = null;
+        if (changed) {
+          this._environment._onEnvironmentChange("addons-changed");
+        }
+      },
+      (err) => {
+        this._pendingTask = null;
+        this._environment._log.error("Error collecting addons", err);
+      });
+  },
+
+  // nsIObserver
+  observe: function (aSubject, aTopic, aData) {
+    this._environment._log.trace("observe - Topic " + aTopic);
 
-    AddonManager.shutdown.addBlocker("TelemetryEnvironment: caching addons",
-                                      () => this._blockAddonManagerShutdown(),
-                                      () => this._getState());
+    if (aTopic == EXPERIMENTS_CHANGED_TOPIC) {
+      if (this._pendingTask) {
+        return;
+      }
+      this._pendingTask = this._updateAddons().then(
+        (changed) => {
+          this._pendingTask = null;
+          if (changed) {
+            this._environment._onEnvironmentChange("experiment-changed");
+          }
+        },
+        (err) => {
+          this._pendingTask = null;
+          this._environment._log.error("observe: Error collecting addons", err);
+        });
+    }
+  },
+
+  _shutdownBlocker: function() {
+    if (this._loaded) {
+      AddonManager.removeAddonListener(this);
+      Services.obs.removeObserver(this, EXPERIMENTS_CHANGED_TOPIC);
+    }
+    return this._pendingTask;
   },
 
   /**
-   * Shutdown TelemetryEnvironment.
-   * @return Promise<> that is resolved when shutdown is finished.
+   * Collect the addon data for the environment.
+   *
+   * This should only be called from _pendingTask; otherwise we risk
+   * running this during addon manager shutdown.
+   *
+   * @returns Promise<bool> whether the environment changed.
+   */
+  _updateAddons: Task.async(function* () {
+    this._environment._log.trace("_updateAddons");
+    let personaId = null;
+#ifndef MOZ_WIDGET_GONK
+    let theme = LightweightThemeManager.currentTheme;
+    if (theme) {
+      personaId = theme.id;
+    }
+#endif
+
+    let addons = {
+      activeAddons: yield this._getActiveAddons(),
+      theme: yield this._getActiveTheme(),
+      activePlugins: this._getActivePlugins(),
+      activeGMPlugins: yield this._getActiveGMPlugins(),
+      activeExperiment: this._getActiveExperiment(),
+      persona: personaId,
+    };
+
+    if (JSON.stringify(addons) !=
+        JSON.stringify(this._environment._currentEnvironment.addons)) {
+      this._environment._log.trace("_updateAddons: addons differ");
+      this._environment._currentEnvironment.addons = addons;
+      return true;
+    }
+    this._environment._log.trace("_updateAddons: no changes found");
+    return false;
+  }),
+
+  /**
+   * Get the addon data in object form.
+   * @return Promise<object> containing the addon data.
    */
-  shutdown: Task.async(function* () {
-    if (this._shutdown) {
-      if (this._log) {
-        this._log.error("shutdown - Already shut down");
-      } else {
-        Cu.reportError("TelemetryEnvironment.shutdown - Already shut down");
+  _getActiveAddons: Task.async(function* () {
+    // Request addons, asynchronously.
+    let allAddons = yield promiseGetAddonsByTypes(["extension", "service"]);
+
+    let activeAddons = {};
+    for (let addon of allAddons) {
+      // Skip addons which are not active.
+      if (!addon.isActive) {
+        continue;
       }
-      return;
+
+      // Make sure to have valid dates.
+      let installDate = new Date(Math.max(0, addon.installDate));
+      let updateDate = new Date(Math.max(0, addon.updateDate));
+
+      activeAddons[addon.id] = {
+        blocklisted: (addon.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
+        description: addon.description,
+        name: addon.name,
+        userDisabled: addon.userDisabled,
+        appDisabled: addon.appDisabled,
+        version: addon.version,
+        scope: addon.scope,
+        type: addon.type,
+        foreignInstall: addon.foreignInstall,
+        hasBinaryComponents: addon.hasBinaryComponents,
+        installDay: truncateToDays(installDate.getTime()),
+        updateDay: truncateToDays(updateDate.getTime()),
+      };
+    }
+
+    return activeAddons;
+  }),
+
+  /**
+   * Get the currently active theme data in object form.
+   * @return Promise<object> containing the active theme data.
+   */
+  _getActiveTheme: Task.async(function* () {
+    // Request themes, asynchronously.
+    let themes = yield promiseGetAddonsByTypes(["theme"]);
+
+    let activeTheme = {};
+    // We only store information about the active theme.
+    let theme = themes.find(theme => theme.isActive);
+    if (theme) {
+      // Make sure to have valid dates.
+      let installDate = new Date(Math.max(0, theme.installDate));
+      let updateDate = new Date(Math.max(0, theme.updateDate));
+
+      activeTheme = {
+        id: theme.id,
+        blocklisted: (theme.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
+        description: theme.description,
+        name: theme.name,
+        userDisabled: theme.userDisabled,
+        appDisabled: theme.appDisabled,
+        version: theme.version,
+        scope: theme.scope,
+        foreignInstall: theme.foreignInstall,
+        hasBinaryComponents: theme.hasBinaryComponents,
+        installDay: truncateToDays(installDate.getTime()),
+        updateDay: truncateToDays(updateDate.getTime()),
+      };
     }
 
-    this._log.trace("shutdown");
-    this._shutdown = true;
-    this._stopWatchingPrefs();
-    this._stopWatchingAddons();
-    this._changeListeners.clear();
-    yield this._collectTask;
+    return activeTheme;
+  }),
+
+  /**
+   * Get the plugins data in object form.
+   * @return Object containing the plugins data.
+   */
+  _getActivePlugins: function () {
+    let pluginTags =
+      Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost).getPluginTags({});
+
+    let activePlugins = [];
+    for (let tag of pluginTags) {
+      // Skip plugins which are not active.
+      if (tag.disabled) {
+        continue;
+      }
+
+      // Make sure to have a valid date.
+      let updateDate = new Date(Math.max(0, tag.lastModifiedTime));
 
-    this._cachedAddons = null;
+      activePlugins.push({
+        name: tag.name,
+        version: tag.version,
+        description: tag.description,
+        blocklisted: tag.blocklisted,
+        disabled: tag.disabled,
+        clicktoplay: tag.clicktoplay,
+        mimeTypes: tag.getMimeTypes({}),
+        updateDay: truncateToDays(updateDate.getTime()),
+      });
+    }
+
+    return activePlugins;
+  },
+
+  /**
+   * Get the GMPlugins data in object form.
+   * @return Object containing the GMPlugins data.
+   *
+   * This should only be called from _pendingTask; otherwise we risk
+   * running this during addon manager shutdown.
+   */
+  _getActiveGMPlugins: Task.async(function* () {
+    // Request plugins, asynchronously.
+    let allPlugins = yield promiseGetAddonsByTypes(["plugin"]);
+
+    let activeGMPlugins = {};
+    for (let plugin of allPlugins) {
+      // Only get GM Plugin info.
+      if (!plugin.isGMPlugin) {
+        continue;
+      }
+
+      activeGMPlugins[plugin.id] = {
+        version: plugin.version,
+        userDisabled: plugin.userDisabled,
+        applyBackgroundUpdates: plugin.applyBackgroundUpdates,
+      };
+    }
+
+    return activeGMPlugins;
   }),
 
-  _configureLog: function () {
-    if (this._log) {
-      return;
+  /**
+   * Get the active experiment data in object form.
+   * @return Object containing the active experiment data.
+   */
+  _getActiveExperiment: function () {
+    let experimentInfo = {};
+    try {
+      let scope = {};
+      Cu.import("resource:///modules/experiments/Experiments.jsm", scope);
+      let experiments = scope.Experiments.instance();
+      let activeExperiment = experiments.getActiveExperimentID();
+      if (activeExperiment) {
+        experimentInfo.id = activeExperiment;
+        experimentInfo.branch = experiments.getActiveExperimentBranch();
+      }
+    } catch(e) {
+      // If this is not Firefox, the import will fail.
     }
-    this._log = Log.repository.getLoggerWithMessagePrefix(
-                                 LOGGER_NAME, "TelemetryEnvironment::");
+
+    return experimentInfo;
+  },
+};
+
+function EnvironmentCache() {
+  this._log = Log.repository.getLoggerWithMessagePrefix(
+    LOGGER_NAME, "TelemetryEnvironment::");
+  this._log.trace("constructor");
+
+  this._shutdown = false;
+
+  // A map of listeners that will be called on environment changes.
+  this._changeListeners = new Map();
+
+  // A map of watched preferences which trigger an Environment change when
+  // modified. Every entry contains a recording policy (RECORD_PREF_*).
+  this._watchedPrefs = DEFAULT_ENVIRONMENT_PREFS;
+
+  this._currentEnvironment = {
+    build: this._getBuild(),
+    partner: this._getPartner(),
+    system: this._getSystem(),
+  };
+
+  this._updateSettings();
+
+#ifndef MOZ_WIDGET_ANDROID
+  this._currentEnvironment.profile = {};
+#endif
+
+  // Build the remaining asynchronous parts of the environment. Don't register change listeners
+  // until the initial environment has been built.
+
+  this._addonBuilder = new EnvironmentAddonBuilder(this);
+
+  this._initTask = Promise.all([this._addonBuilder.init(), this._updateProfile()])
+    .then(
+      () => {
+        this._initTask = null;
+        this._startWatchingPrefs();
+        this._addonBuilder.watchForChanges();
+        return this.currentEnvironment;
+      },
+      (err) => {
+        // log errors but eat them for consumers
+        this._log.error("error while initializing", err);
+        this._initTask = null;
+        this._startWatchingPrefs();
+        this._addonBuilder.watchForChanges();
+        return this.currentEnvironment;
+      });
+}
+EnvironmentCache.prototype = {
+  /**
+   * The current environment data. The returned data is cloned to avoid
+   * unexpected sharing or mutation.
+   * @returns object
+   */
+  get currentEnvironment() {
+    return Cu.cloneInto(this._currentEnvironment, myScope);
+  },
+
+  /**
+   * Wait for the current enviroment to be fully initialized.
+   * @returns Promise<object>
+   */
+  onInitialized: function() {
+    if (this._initTask) {
+      return this._initTask;
+    }
+    return Promise.resolve(this.currentEnvironment);
   },
 
   /**
    * Register a listener for environment changes.
-   * It's fine to call this on an unitialized TelemetryEnvironment.
-   * @param name The name of the listener - good for debugging purposes.
-   * @param listener A JS callback function.
+   * @param name The name of the listener. If a new listener is registered
+   *             with the same name, the old listener will be replaced.
+   * @param listener function(reason, environment)
    */
   registerChangeListener: function (name, listener) {
-    this._configureLog();
     this._log.trace("registerChangeListener for " + name);
     if (this._shutdown) {
-      this._log.warn("registerChangeListener - already shutdown")
+      this._log.warn("registerChangeListener - already shutdown");
       return;
     }
     this._changeListeners.set(name, listener);
   },
 
   /**
    * Unregister from listening to environment changes.
    * It's fine to call this on an unitialized TelemetryEnvironment.
    * @param name The name of the listener to remove.
    */
   unregisterChangeListener: function (name) {
-    this._configureLog();
     this._log.trace("unregisterChangeListener for " + name);
     if (this._shutdown) {
-      this._log.warn("registerChangeListener - already shutdown")
+      this._log.warn("registerChangeListener - already shutdown");
       return;
     }
     this._changeListeners.delete(name);
   },
 
   /**
    * Only used in tests, set the preferences to watch.
    * @param aPreferences A map of preferences names and their recording policy.
    */
   _watchPreferences: function (aPreferences) {
-    if (this._watchedPrefs) {
-      this._stopWatchingPrefs();
-    }
-
+    this._stopWatchingPrefs();
     this._watchedPrefs = aPreferences;
+    this._updateSettings();
     this._startWatchingPrefs();
   },
 
   /**
    * Get an object containing the values for the watched preferences. Depending on the
    * policy, the value for a preference or whether it was changed by user is reported.
    *
    * @return An object containing the preferences values.
    */
   _getPrefData: function () {
-    if (!this._watchedPrefs) {
-      return {};
-    }
-
     let prefData = {};
     for (let pref in this._watchedPrefs) {
       // Only record preferences if they are non-default and policy allows recording.
       if (!Preferences.isSet(pref) ||
-          this._watchedPrefs[pref] == this.RECORD_PREF_NOTIFY_ONLY) {
+          this._watchedPrefs[pref] == TelemetryEnvironment.RECORD_PREF_NOTIFY_ONLY) {
         continue;
       }
 
       // Check the policy for the preference and decide if we need to store its value
       // or whether it changed from the default value.
       let prefValue = undefined;
-      if (this._watchedPrefs[pref] == this.RECORD_PREF_STATE) {
-        prefValue = null;
+      if (this._watchedPrefs[pref] == TelemetryEnvironment.RECORD_PREF_STATE) {
+        prefValue = "<user-set>";
       } else {
         prefValue = Preferences.get(pref, null);
       }
       prefData[pref] = prefValue;
     }
     return prefData;
   },
 
   /**
    * Start watching the preferences.
    */
   _startWatchingPrefs: function () {
     this._log.trace("_startWatchingPrefs - " + this._watchedPrefs);
 
-    if (!this._watchedPrefs) {
-      return;
-    }
-
     for (let pref in this._watchedPrefs) {
       Preferences.observe(pref, this._onPrefChanged, this);
     }
   },
 
+  _onPrefChanged: function() {
+    this._log.trace("_onPrefChanged");
+    this._updateSettings();
+    this._onEnvironmentChange("pref-changed");
+  },
+
   /**
    * Do not receive any more change notifications for the preferences.
    */
   _stopWatchingPrefs: function () {
     this._log.trace("_stopWatchingPrefs");
 
-    if (!this._watchedPrefs) {
-      return;
-    }
-
     for (let pref in this._watchedPrefs) {
       Preferences.ignore(pref, this._onPrefChanged, this);
     }
-
-    this._watchedPrefs = null;
-  },
-
-  _onPrefChanged: function () {
-    this._log.trace("_onPrefChanged");
-    this._onEnvironmentChange("pref-changed");
   },
 
   /**
    * Get the build data in object form.
    * @return Object containing the build data.
    */
   _getBuild: function () {
     let buildData = {
@@ -465,26 +843,25 @@ this.TelemetryEnvironment = {
         return null;
       }
     }
 
     return null;
   },
 
   /**
-   * Get the settings data in object form.
-   * @return Object containing the settings.
+   * Update the cached settings data.
    */
-  _getSettings: function () {
+  _updateSettings: function () {
     let updateChannel = null;
     try {
       updateChannel = UpdateChannel.get();
     } catch (e) {}
 
-    return {
+    this._currentEnvironment.settings = {
       blocklistEnabled: Preferences.get(PREF_BLOCKLIST_ENABLED, true),
 #ifndef MOZ_WIDGET_ANDROID
       isDefaultBrowser: this._isDefaultBrowser(),
 #endif
       e10sEnabled: Preferences.get(PREF_E10S_ENABLED, false),
       telemetryEnabled: Preferences.get(PREF_TELEMETRY_ENABLED, false),
       locale: getBrowserLocale(),
       update: {
@@ -492,33 +869,30 @@ this.TelemetryEnvironment = {
         enabled: Preferences.get(PREF_UPDATE_ENABLED, true),
         autoDownload: Preferences.get(PREF_UPDATE_AUTODOWNLOAD, true),
       },
       userPrefs: this._getPrefData(),
     };
   },
 
   /**
-   * Get the profile data in object form.
-   * @return Object containing the profile data.
+   * Update the cached profile data.
+   * @returns Promise<> resolved when the I/O is complete.
    */
-  _getProfile: Task.async(function* () {
+  _updateProfile: Task.async(function* () {
     let profileAccessor = new ProfileAge(null, this._log);
 
     let creationDate = yield profileAccessor.created;
     let resetDate = yield profileAccessor.reset;
 
-    let profileData = {
-      creationDate: truncateToDays(creationDate),
-    };
-
+    this._currentEnvironment.profile.creationDate =
+      truncateToDays(creationDate);
     if (resetDate) {
-      profileData.resetDate = truncateToDays(resetDate);
+      this._currentEnvironment.profile.resetDate = truncateToDays(resetDate);
     }
-    return profileData;
   }),
 
   /**
    * Get the partner data in object form.
    * @return Object containing the partner data.
    */
   _getPartner: function () {
     let partnerData = {
@@ -526,17 +900,17 @@ this.TelemetryEnvironment = {
       distributionVersion: Preferences.get(PREF_DISTRIBUTION_VERSION, null),
       partnerId: Preferences.get(PREF_PARTNER_ID, null),
       distributor: Preferences.get(PREF_DISTRIBUTOR, null),
       distributorChannel: Preferences.get(PREF_DISTRIBUTOR_CHANNEL, null),
     };
 
     // Get the PREF_APP_PARTNER_BRANCH branch and append its children to partner data.
     let partnerBranch = Services.prefs.getBranch(PREF_APP_PARTNER_BRANCH);
-    partnerData.partnerNames = partnerBranch.getChildList("")
+    partnerData.partnerNames = partnerBranch.getChildList("");
 
     return partnerData;
   },
 
   /**
    * Get the CPU information.
    * @return Object containing the CPU information data.
    */
@@ -680,398 +1054,25 @@ this.TelemetryEnvironment = {
       device: this._getDeviceData(),
 #endif
       os: this._getOSData(),
       hdd: this._getHDDData(),
       gfx: this._getGFXData(),
     };
   },
 
-  /**
-   * Get the addon data in object form.
-   * @return Object containing the addon data.
-   *
-   * This should only be called from the environment collection task
-   * or _blockAddonManagerShutdown, otherwise we risk running this
-   * during addon manager shutdown.
-   */
-  _getActiveAddons: Task.async(function* () {
-
-    // Request addons, asynchronously.
-    let allAddons = yield promiseGetAddonsByTypes(["extension", "service"]);
-
-    let activeAddons = {};
-    for (let addon of allAddons) {
-      // Skip addons which are not active.
-      if (!addon.isActive) {
-        continue;
-      }
-
-      // Make sure to have valid dates.
-      let installDate = new Date(Math.max(0, addon.installDate));
-      let updateDate = new Date(Math.max(0, addon.updateDate));
-
-      activeAddons[addon.id] = {
-        blocklisted: (addon.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
-        description: addon.description,
-        name: addon.name,
-        userDisabled: addon.userDisabled,
-        appDisabled: addon.appDisabled,
-        version: addon.version,
-        scope: addon.scope,
-        type: addon.type,
-        foreignInstall: addon.foreignInstall,
-        hasBinaryComponents: addon.hasBinaryComponents,
-        installDay: truncateToDays(installDate.getTime()),
-        updateDay: truncateToDays(updateDate.getTime()),
-      };
-    }
-
-    return activeAddons;
-  }),
-
-  /**
-   * Get the currently active theme data in object form.
-   * @return Object containing the active theme data.
-   *
-   * This should only be called from the environment collection task
-   * or _blockAddonManagerShutdown, otherwise we risk running this
-   * during addon manager shutdown.
-   */
-  _getActiveTheme: Task.async(function* () {
-    // Request themes, asynchronously.
-    let themes = yield promiseGetAddonsByTypes(["theme"]);
-
-    let activeTheme = {};
-    // We only store information about the active theme.
-    let theme = themes.find(theme => theme.isActive);
-    if (theme) {
-      // Make sure to have valid dates.
-      let installDate = new Date(Math.max(0, theme.installDate));
-      let updateDate = new Date(Math.max(0, theme.updateDate));
-
-      activeTheme = {
-        id: theme.id,
-        blocklisted: (theme.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
-        description: theme.description,
-        name: theme.name,
-        userDisabled: theme.userDisabled,
-        appDisabled: theme.appDisabled,
-        version: theme.version,
-        scope: theme.scope,
-        foreignInstall: theme.foreignInstall,
-        hasBinaryComponents: theme.hasBinaryComponents,
-        installDay: truncateToDays(installDate.getTime()),
-        updateDay: truncateToDays(updateDate.getTime()),
-      };
-    }
-
-    return activeTheme;
-  }),
-
-  /**
-   * Get the plugins data in object form.
-   * @return Object containing the plugins data.
-   */
-  _getActivePlugins: function () {
-    let pluginTags =
-      Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost).getPluginTags({});
-
-    let activePlugins = [];
-    for (let tag of pluginTags) {
-      // Skip plugins which are not active.
-      if (tag.disabled) {
-        continue;
-      }
-
-      // Make sure to have a valid date.
-      let updateDate = new Date(Math.max(0, tag.lastModifiedTime));
-
-      activePlugins.push({
-        name: tag.name,
-        version: tag.version,
-        description: tag.description,
-        blocklisted: tag.blocklisted,
-        disabled: tag.disabled,
-        clicktoplay: tag.clicktoplay,
-        mimeTypes: tag.getMimeTypes({}),
-        updateDay: truncateToDays(updateDate.getTime()),
-      });
-    }
-
-    return activePlugins;
-  },
-
-  /**
-   * Get the GMPlugins data in object form.
-   * @return Object containing the GMPlugins data.
-   *
-   * This should only be called from the environment collection task
-   * or _blockAddonManagerShutdown, otherwise we risk running this
-   * during addon manager shutdown.
-   */
-  _getActiveGMPlugins: Task.async(function* () {
-    // Request plugins, asynchronously.
-    let allPlugins = yield promiseGetAddonsByTypes(["plugin"]);
-
-    let activeGMPlugins = {};
-    for (let plugin of allPlugins) {
-      // Only get GM Plugin info.
-      if (!plugin.isGMPlugin) {
-        continue;
-      }
-
-      activeGMPlugins[plugin.id] = {
-        version: plugin.version,
-        userDisabled: plugin.userDisabled,
-        applyBackgroundUpdates: plugin.applyBackgroundUpdates,
-      };
-    }
-
-    return activeGMPlugins;
-  }),
-
-  /**
-   * Get the active experiment data in object form.
-   * @return Object containing the active experiment data.
-   */
-  _getActiveExperiment: function () {
-    let experimentInfo = {};
-    try {
-      let scope = {};
-      Cu.import("resource:///modules/experiments/Experiments.jsm", scope);
-      let experiments = scope.Experiments.instance()
-      let activeExperiment = experiments.getActiveExperimentID();
-      if (activeExperiment) {
-        experimentInfo.id = activeExperiment;
-        experimentInfo.branch = experiments.getActiveExperimentBranch();
-      }
-    } catch(e) {
-      // If this is not Firefox, the import will fail.
-      return experimentInfo;
-    }
-
-    return experimentInfo;
-  },
-
-  /**
-   * Get the addon data in object form.
-   * @return Object containing the addon data.
-   *
-   * This should only be called from the environment collection task
-   * or _blockAddonManagerShutdown, otherwise we risk running this
-   * during addon manager shutdown.
-   */
-  _getAddons: Task.async(function* () {
-    // AddonManager may have shutdown already, in which case we should have cached addon data.
-    // It can't shutdown during the collection here because we have a blocker on the AMs
-    // shutdown barrier that waits for the collect task.
-    let addons = this._cachedAddons || {};
-    if (!this._cachedAddons) {
-      addons.activeAddons = yield this._getActiveAddons();
-      addons.activeTheme = yield this._getActiveTheme();
-      addons.activeGMPlugins = yield this._getActiveGMPlugins();
-    }
-
-    let personaId = null;
-#ifndef MOZ_WIDGET_GONK
-    let theme = LightweightThemeManager.currentTheme;
-    if (theme) {
-      personaId = theme.id;
-    }
-#endif
-
-    let addonData = {
-      activeAddons: addons.activeAddons,
-      theme: addons.activeTheme,
-      activePlugins: this._getActivePlugins(),
-      activeGMPlugins: addons.activeGMPlugins,
-      activeExperiment: this._getActiveExperiment(),
-      persona: personaId,
-    };
-
-    return addonData;
-  }),
-
-  /**
-   * Start watching the addons for changes.
-   */
-  _startWatchingAddons: function () {
-    // Define a listener to catch addons changes from the AddonManager. This part is
-    // tricky, as we only want to detect when the set of active addons changes without
-    // getting double notifications.
-    //
-    // We identified the following cases:
-    //
-    // * onEnabled:   Gets called when a restartless addon is enabled. Doesn't get called
-    //                if the restartless addon is installed and directly enabled.
-    // * onDisabled:  Gets called when disabling a restartless addon or can get called when
-    //                uninstalling a restartless addon from the UI (see bug 612168).
-    // * onInstalled: Gets called for all addon installs.
-    // * onUninstalling: Called the moment before addon uninstall happens.
-
-    this._addonsListener = {
-      onEnabled: addon => {
-        this._log.trace("_addonsListener - onEnabled " + addon.id);
-        this._onActiveAddonsChanged(addon)
-      },
-      onDisabled: addon => {
-        this._log.trace("_addonsListener - onDisabled " + addon.id);
-        this._onActiveAddonsChanged(addon);
-      },
-      onInstalled: addon => {
-        this._log.trace("_addonsListener - onInstalled " + addon.id +
-                        ", isActive: " + addon.isActive);
-        if (addon.isActive) {
-          this._onActiveAddonsChanged(addon);
-        }
-      },
-      onUninstalling: (addon, requiresRestart) => {
-        this._log.trace("_addonsListener - onUninstalling " + addon.id +
-                        ", isActive: " + addon.isActive +
-                        ", requiresRestart: " + requiresRestart);
-        if (!addon.isActive || requiresRestart) {
-          return;
-        }
-        this._onActiveAddonsChanged(addon);
-      },
-    };
-
-    AddonManager.addAddonListener(this._addonsListener);
-
-    // Watch for experiment changes as well.
-    Services.obs.addObserver(this, EXPERIMENTS_CHANGED_TOPIC, false);
-  },
-
-  /**
-   * Stop watching addons for changes.
-   */
-  _stopWatchingAddons: function () {
-    if (this._addonsListener) {
-      AddonManager.removeAddonListener(this._addonsListener);
-      Services.obs.removeObserver(this, EXPERIMENTS_CHANGED_TOPIC);
-    }
-    this._addonsListener = null;
-  },
-
-  /**
-   * Triggered when an addon changes its state.
-   * @param aAddon The addon which triggered the change.
-   */
-  _onActiveAddonsChanged: function (aAddon) {
-    const INTERESTING_ADDONS = [ "extension", "plugin", "service", "theme" ];
-
-    this._log.trace("_onActiveAddonsChanged - id " + aAddon.id + ", type " + aAddon.type);
-
-    if (INTERESTING_ADDONS.find(addon => addon == aAddon.type)) {
-      this._onEnvironmentChange("addons-changed");
-    }
-  },
-
-  /**
-   * Handle experiment change notifications.
-   */
-  observe: function (aSubject, aTopic, aData) {
-    this._log.trace("observe - Topic " + aTopic);
-
-    if (aTopic == EXPERIMENTS_CHANGED_TOPIC) {
-      this._onEnvironmentChange("experiment-changed");
-    }
-  },
-
-  /**
-   * Get the environment data in object form.
-   * @return Promise<Object> Resolved with the data on success, otherwise rejected.
-   */
-  getEnvironmentData: function() {
-    if (this._shutdown) {
-      this._log.error("getEnvironmentData - Already shut down");
-      return Promise.reject("Already shutdown");
-    }
-
-    this._log.trace("getEnvironmentData");
-    if (this._collectTask) {
-      return this._collectTask;
-    }
-
-    this._collectTask = this._doGetEnvironmentData();
-    let clear = () => this._collectTask = null;
-    this._collectTask.then(clear, clear);
-    return this._collectTask;
-  },
-
-  _doGetEnvironmentData: Task.async(function* () {
-    this._log.trace("getEnvironmentData");
-
-    // Define the data collection function for each section.
-    let sections = {
-      "build" : () => this._getBuild(),
-      "settings": () => this._getSettings(),
-#ifndef MOZ_WIDGET_ANDROID
-      "profile": () => this._getProfile(),
-#endif
-      "partner": () => this._getPartner(),
-      "system": () => this._getSystem(),
-      "addons": () => this._getAddons(),
-    };
-
-    let data = {};
-    // Recover from exceptions in the collection functions and populate the data
-    // object. We want to recover so that, in the worst-case, we only lose some data
-    // sections instead of all.
-    for (let s in sections) {
-      try {
-        data[s] = yield sections[s]();
-      } catch (e) {
-        this._log.error("_doGetEnvironmentData - There was an exception collecting " + s, e);
-      }
-    }
-
-    return data;
-  }),
-
   _onEnvironmentChange: function (what) {
     this._log.trace("_onEnvironmentChange for " + what);
     if (this._shutdown) {
       this._log.trace("_onEnvironmentChange - Already shut down.");
       return;
     }
 
     for (let [name, listener] of this._changeListeners) {
       try {
         this._log.debug("_onEnvironmentChange - calling " + name);
-        listener();
+        listener(what, this.currentEnvironment);
       } catch (e) {
-        this._log.warning("_onEnvironmentChange - listener " + name + " caught error", e);
+        this._log.error("_onEnvironmentChange - listener " + name + " caught error", e);
       }
     }
   },
-
-  /**
-   * This blocks the AddonManager shutdown barrier, it caches addons we might need later.
-   * It also lets an active collect task finish first as it may still access the AM.
-   */
-  _blockAddonManagerShutdown: Task.async(function*() {
-    this._log.trace("_blockAddonManagerShutdown");
-
-    this._stopWatchingAddons();
-
-    this._cachedAddons = {
-      activeAddons: yield this._getActiveAddons(),
-      activeTheme: yield this._getActiveTheme(),
-      activeGMPlugins: yield this._getActiveGMPlugins(),
-    };
-
-    yield this._collectTask;
-  }),
-
-  /**
-   * Get an object describing the current state of this module for AsyncShutdown diagnostics.
-   */
-  _getState: function() {
-    return {
-      shutdown: this._shutdown,
-      hasCollectTask: !!this._collectTask,
-      hasAddonsListener: !!this._addonsListener,
-      hasCachedAddons: !!this._cachedAddons,
-    };
-  },
 };
--- a/toolkit/components/telemetry/TelemetryPing.jsm
+++ b/toolkit/components/telemetry/TelemetryPing.jsm
@@ -345,26 +345,20 @@ let Impl = {
       payload: aPayload,
     };
 
     if (aOptions.addClientId) {
       pingData.clientId = this._clientID;
     }
 
     if (aOptions.addEnvironment) {
-      return TelemetryEnvironment.getEnvironmentData().then(environment => {
-        pingData.environment = environment;
-        return pingData;
-      },
-      error => {
-        this._log.error("assemblePing - Rejection", error);
-      });
+      pingData.environment = TelemetryEnvironment.currentEnvironment;
     }
 
-    return Promise.resolve(pingData);
+    return pingData;
   },
 
   popPayloads: function popPayloads() {
     this._log.trace("popPayloads");
     function payloadIter() {
       let iterator = TelemetryFile.popPendingPings();
       for (let data of iterator) {
         yield data;
@@ -422,29 +416,26 @@ let Impl = {
    *                  environment data.
    *
    * @returns {Promise} A promise that resolves when the ping is sent.
    */
   send: function send(aType, aPayload, aOptions) {
     this._log.trace("send - Type " + aType + ", Server " + this._server +
                     ", aOptions " + JSON.stringify(aOptions));
 
-    let promise = this.assemblePing(aType, aPayload, aOptions)
-        .then(pingData => {
-          // Once ping is assembled, send it along with the persisted ping in the backlog.
-          let p = [
-            // Persist the ping if sending it fails.
-            this.doPing(pingData, false)
-                .catch(() => TelemetryFile.savePing(pingData, true)),
-            this.sendPersistedPings(),
-          ];
-          return Promise.all(p);
-        },
-        error => this._log.error("send - Rejection", error));
+    let pingData = this.assemblePing(aType, aPayload, aOptions);
+    // Once ping is assembled, send it along with the persisted pings in the backlog.
+    let p = [
+      // Persist the ping if sending it fails.
+      this.doPing(pingData, false)
+          .catch(() => TelemetryFile.savePing(pingData, true)),
+      this.sendPersistedPings(),
+    ];
 
+    let promise = Promise.all(p);
     this._trackPendingPingTask(promise);
     return promise;
   },
 
   /**
    * Send the persisted pings to the server.
    *
    * @return Promise A promise that is resolved when all pings finished sending or failed.
@@ -476,19 +467,18 @@ let Impl = {
    *                  environment data.
    *
    * @returns {Promise} A promise that resolves when all the pings are saved to disk.
    */
   savePendingPings: function savePendingPings(aType, aPayload, aOptions) {
     this._log.trace("savePendingPings - Type " + aType + ", Server " + this._server +
                     ", aOptions " + JSON.stringify(aOptions));
 
-    return this.assemblePing(aType, aPayload, aOptions)
-        .then(pingData => TelemetryFile.savePendingPings(pingData),
-              error => this._log.error("savePendingPings - Rejection", error));
+    let pingData = this.assemblePing(aType, aPayload, aOptions);
+    return TelemetryFile.savePendingPings(pingData);
   },
 
   /**
    * Save a ping to disk.
    *
    * @param {String} aType The type of the ping.
    * @param {Object} aPayload The actual data payload for the ping.
    * @param {Object} aOptions Options object.
@@ -504,26 +494,24 @@ let Impl = {
    *
    * @returns {Promise} A promise that resolves with the ping id when the ping is saved to
    *                    disk.
    */
   savePing: function savePing(aType, aPayload, aOptions) {
     this._log.trace("savePing - Type " + aType + ", Server " + this._server +
                     ", aOptions " + JSON.stringify(aOptions));
 
-    return this.assemblePing(aType, aPayload, aOptions)
-      .then(pingData => {
-        if ("filePath" in aOptions) {
-          return TelemetryFile.savePingToFile(pingData, aOptions.filePath, aOptions.overwrite)
-                              .then(() => { return pingData.id; });
-        } else {
-          return TelemetryFile.savePing(pingData, aOptions.overwrite)
-                              .then(() => { return pingData.id; });
-        }
-      }, error => this._log.error("savePing - Rejection", error));
+    let pingData = this.assemblePing(aType, aPayload, aOptions);
+    if ("filePath" in aOptions) {
+      return TelemetryFile.savePingToFile(pingData, aOptions.filePath, aOptions.overwrite)
+                          .then(() => { return pingData.id; });
+    } else {
+      return TelemetryFile.savePing(pingData, aOptions.overwrite)
+                          .then(() => { return pingData.id; });
+    }
   },
 
   onPingRequestFinished: function(success, startTime, ping, isPersisted) {
     this._log.trace("onPingRequestFinished - success: " + success + ", persisted: " + isPersisted);
 
     let hping = Telemetry.getHistogramById("TELEMETRY_PING");
     let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
 
@@ -770,18 +758,16 @@ let Impl = {
     // Delay full telemetry initialization to give the browser time to
     // run various late initializers. Otherwise our gathered memory
     // footprint and other numbers would be too optimistic.
     this._delayedInitTaskDeferred = Promise.defer();
     this._delayedInitTask = new DeferredTask(function* () {
       try {
         this._initialized = true;
 
-        yield TelemetryEnvironment.init();
-
         yield TelemetryFile.loadSavedPings();
         // If we have any TelemetryPings lying around, we'll be aggressive
         // and try to send them all off ASAP.
         if (TelemetryFile.pingsOverdue > 0) {
           this._log.trace("setupChromeProcess - Sending " + TelemetryFile.pingsOverdue +
                           " overdue pings now.");
           // It doesn't really matter what we pass to this.send as a reason,
           // since it's never sent to the server. All that this.send does with
@@ -837,23 +823,16 @@ let Impl = {
     this._pendingPingRequests.clear();
 
     // Now do an orderly shutdown.
     try {
       // First wait for clients processing shutdown.
       yield this._shutdownBarrier.wait();
       // Then wait for any outstanding async ping activity.
       yield this._connectionsBarrier.wait();
-
-      // Should down dependent components.
-      try {
-        yield TelemetryEnvironment.shutdown();
-      } catch (e) {
-        this._log.error("_cleanupOnShutdown - environment shutdown failure", e);
-      }
     } finally {
       // Reset state.
       this._initialized = false;
       this._initStarted = false;
     }
   }),
 
   shutdown: function() {
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -1512,31 +1512,31 @@ let Impl = {
 
         Telemetry.asyncFetchTelemetryData(function () {});
 
 #if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
         // Check for a previously written aborted session ping.
         yield this._checkAbortedSessionPing();
 
         TelemetryEnvironment.registerChangeListener(ENVIRONMENT_CHANGE_LISTENER,
-                                                    () => this._onEnvironmentChange());
+                                                    (reason, data) => this._onEnvironmentChange(reason, data));
         // Write the first aborted-session ping as early as possible. Just do that
         // if we are not testing, since calling Telemetry.reset() will make a previous
         // aborted ping a pending ping.
         if (!testing) {
           yield this._saveAbortedSessionPing();
         }
 
         // Start the scheduler.
         TelemetryScheduler.init();
 #endif
 
         this._delayedInitTaskDeferred.resolve();
       } catch (e) {
-        this._delayedInitTaskDeferred.reject();
+        this._delayedInitTaskDeferred.reject(e);
       } finally {
         this._delayedInitTask = null;
         this._delayedInitTaskDeferred = null;
       }
     }.bind(this), testing ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY);
 
     this._delayedInitTask.arm();
     return this._delayedInitTaskDeferred.promise;
@@ -1978,18 +1978,18 @@ let Impl = {
     let filePath = OS.Path.join(dataDir, SESSION_STATE_FILE_NAME);
     try {
       yield CommonUtils.writeJSON(sessionData, filePath);
     } catch(e) {
       this._log.error("_saveSessionData - Failed to write session data to " + filePath, e);
     }
   }),
 
-  _onEnvironmentChange: function() {
-    this._log.trace("_onEnvironmentChange");
+  _onEnvironmentChange: function(reason, data) {
+    this._log.trace("_onEnvironmentChange", reason);
     let payload = this.getSessionPayload(REASON_ENVIRONMENT_CHANGE, true);
 
     let clonedPayload = Cu.cloneInto(payload, myScope);
     TelemetryScheduler.reschedulePings(REASON_ENVIRONMENT_CHANGE, clonedPayload);
 
     let options = {
       retentionDays: RETENTION_DAYS,
       addClientId: true,
--- a/toolkit/components/telemetry/docs/environment.rst
+++ b/toolkit/components/telemetry/docs/environment.rst
@@ -31,18 +31,21 @@ Structure::
         telemetryEnabled: <bool>, // false on failure
         locale: <string>, // e.g. "it", null on failure
         update: {
           channel: <string>, // e.g. "release", null on failure
           enabled: <bool>, // true on failure
           autoDownload: <bool>, // true on failure
         },
         userPrefs: {
-          // Two possible behaviours: values of the whitelisted prefs, or for some prefs we
-          // only record they are present with value being set to null.
+          // Only prefs which are changed from the default value are listed
+          // in this block
+          "pref.name.value": value // some prefs send the value
+          "pref.name.url": "<user-set>" // For some privacy-sensitive prefs
+            // only the fact that the value has been changed is recorded
         },
       },
       profile: { // This section is not available on Android.
         creationDate: <integer>, // integer days since UNIX epoch, e.g. 16446
         resetDate: <integer>, // integer days since UNIX epoch, e.g. 16446 - optional
       },
       partner: {
         distributionId: <string>, // pref "distribution.id", null on failure
@@ -174,8 +177,13 @@ Structure::
         ],
         activeExperiment: { // section is empty if there's no active experiment
             id: <string>, // id
             branch: <string>, // branch name
         },
         persona: <string>, // id of the current persona, null on GONK
       },
     }
+
+Some parts of the environment must be fetched asynchronously at startup. If a session is very short or terminates early, the following items may be missing from the environment:
+
+- profile
+- addons
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -608,228 +608,180 @@ function isRejected(promise) {
     promise.then(() => resolve(false), () => resolve(true));
   });
 }
 
 add_task(function* asyncSetup() {
   yield spoofProfileReset();
 });
 
-add_task(function* test_initAndShutdown() {
-  // Check that init and shutdown work properly.
-  TelemetryEnvironment.init();
-  yield TelemetryEnvironment.shutdown();
-  TelemetryEnvironment.init();
-  yield TelemetryEnvironment.shutdown();
-
-  // A double init should be silently handled.
-  TelemetryEnvironment.init();
-  TelemetryEnvironment.init();
-
-  // getEnvironmentData should return a sane result.
-  let data = yield TelemetryEnvironment.getEnvironmentData();
-  Assert.ok(!!data);
-
-  // The change listener registration should silently fail after shutdown.
-  yield TelemetryEnvironment.shutdown();
-  TelemetryEnvironment.registerChangeListener("foo", () => {});
-  TelemetryEnvironment.unregisterChangeListener("foo");
-
-  // Shutting down again should be ignored.
-  yield TelemetryEnvironment.shutdown();
-
-  // Getting the environment data should reject after shutdown.
-  Assert.ok(yield isRejected(TelemetryEnvironment.getEnvironmentData()));
-});
-
-add_task(function* test_changeNotify() {
-  TelemetryEnvironment.init();
-
-  // Register some listeners
-  let results = new Array(4).fill(false);
-  for (let i=0; i<results.length; ++i) {
-    let k = i;
-    TelemetryEnvironment.registerChangeListener("test"+k, () => results[k] = true);
-  }
-  // Trigger environment change notifications.
-  // TODO: test with proper environment changes, not directly.
-  TelemetryEnvironment._onEnvironmentChange("foo");
-  Assert.ok(results.every(val => val), "All change listeners should have been notified.");
-  results.fill(false);
-  TelemetryEnvironment._onEnvironmentChange("bar");
-  Assert.ok(results.every(val => val), "All change listeners should have been notified.");
-
-  // Unregister listeners
-  for (let i=0; i<4; ++i) {
-    TelemetryEnvironment.unregisterChangeListener("test"+i);
-  }
-});
-
 add_task(function* test_checkEnvironment() {
-  yield TelemetryEnvironment.init();
-  let environmentData = yield TelemetryEnvironment.getEnvironmentData();
-
+  let environmentData = yield TelemetryEnvironment.onInitialized();
   checkEnvironmentData(environmentData);
-
-  yield TelemetryEnvironment.shutdown();
 });
 
 add_task(function* test_prefWatchPolicies() {
   const PREF_TEST_1 = "toolkit.telemetry.test.pref_new";
   const PREF_TEST_2 = "toolkit.telemetry.test.pref1";
   const PREF_TEST_3 = "toolkit.telemetry.test.pref2";
+  const PREF_TEST_4 = "toolkit.telemetry.test.pref_old";
 
   const expectedValue = "some-test-value";
 
   let prefsToWatch = {};
   prefsToWatch[PREF_TEST_1] = TelemetryEnvironment.RECORD_PREF_VALUE;
   prefsToWatch[PREF_TEST_2] = TelemetryEnvironment.RECORD_PREF_STATE;
   prefsToWatch[PREF_TEST_3] = TelemetryEnvironment.RECORD_PREF_STATE;
+  prefsToWatch[PREF_TEST_4] = TelemetryEnvironment.RECORD_PREF_VALUE;
 
-  yield TelemetryEnvironment.init();
+  Preferences.set(PREF_TEST_4, expectedValue);
 
   // Set the Environment preferences to watch.
   TelemetryEnvironment._watchPreferences(prefsToWatch);
   let deferred = PromiseUtils.defer();
+
+  // Check that the pref values are missing or present as expected
+  Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1], undefined);
+  Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_4], expectedValue);
+
   TelemetryEnvironment.registerChangeListener("testWatchPrefs", deferred.resolve);
 
   // Trigger a change in the watched preferences.
   Preferences.set(PREF_TEST_1, expectedValue);
   Preferences.set(PREF_TEST_2, false);
   yield deferred.promise;
 
   // Unregister the listener.
   TelemetryEnvironment.unregisterChangeListener("testWatchPrefs");
 
   // Check environment contains the correct data.
-  let environmentData = yield TelemetryEnvironment.getEnvironmentData();
-
-  let userPrefs = environmentData.settings.userPrefs;
+  let userPrefs = TelemetryEnvironment.currentEnvironment.settings.userPrefs;
 
   Assert.equal(userPrefs[PREF_TEST_1], expectedValue,
                "Environment contains the correct preference value.");
-  Assert.equal(userPrefs[PREF_TEST_2], null,
-               "Report that the pref was user set and has no value.");
+  Assert.equal(userPrefs[PREF_TEST_2], "<user-set>",
+               "Report that the pref was user set but the value is not shown.");
   Assert.ok(!(PREF_TEST_3 in userPrefs),
             "Do not report if preference not user set.");
-
-  yield TelemetryEnvironment.shutdown();
 });
 
 add_task(function* test_prefWatch_prefReset() {
   const PREF_TEST = "toolkit.telemetry.test.pref1";
 
   let prefsToWatch = {};
   prefsToWatch[PREF_TEST] = TelemetryEnvironment.RECORD_PREF_STATE;
   // Set the preference to a non-default value.
   Preferences.set(PREF_TEST, false);
 
-  yield TelemetryEnvironment.init();
-
   // Set the Environment preferences to watch.
   TelemetryEnvironment._watchPreferences(prefsToWatch);
   let deferred = PromiseUtils.defer();
   TelemetryEnvironment.registerChangeListener("testWatchPrefs_reset", deferred.resolve);
 
+  Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], "<user-set>");
+
   // Trigger a change in the watched preferences.
   Preferences.reset(PREF_TEST);
   yield deferred.promise;
 
+  Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], undefined);
+
   // Unregister the listener.
   TelemetryEnvironment.unregisterChangeListener("testWatchPrefs_reset");
-  yield TelemetryEnvironment.shutdown();
 });
 
 add_task(function* test_addonsWatch_InterestingChange() {
   const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
   const ADDON_ID = "tel-restartless-xpi@tests.mozilla.org";
   // We only expect a single notification for each install, uninstall, enable, disable.
   const EXPECTED_NOTIFICATIONS = 4;
 
-  yield TelemetryEnvironment.init();
   let deferred = PromiseUtils.defer();
   let receivedNotifications = 0;
 
   let registerCheckpointPromise = (aExpected) => {
     return new Promise(resolve => TelemetryEnvironment.registerChangeListener(
-      "testWatchAddons_Changes" + aExpected, () => {
+      "testWatchAddons_Changes" + aExpected, (reason, data) => {
+        Assert.equal(reason, "addons-changed");
         receivedNotifications++;
         resolve();
       }));
   };
 
   let assertCheckpoint = (aExpected) => {
     Assert.equal(receivedNotifications, aExpected);
     TelemetryEnvironment.unregisterChangeListener("testWatchAddons_Changes" + aExpected);
   };
 
   // Test for receiving one notification after each change.
   let checkpointPromise = registerCheckpointPromise(1);
   yield AddonTestUtils.installXPIFromURL(ADDON_INSTALL_URL);
   yield checkpointPromise;
   assertCheckpoint(1);
-  
+  Assert.ok(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons);
+
   checkpointPromise = registerCheckpointPromise(2);
   let addon = yield AddonTestUtils.getAddonById(ADDON_ID);
   addon.userDisabled = true;
   yield checkpointPromise;
   assertCheckpoint(2);
+  Assert.ok(!(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons));
 
   checkpointPromise = registerCheckpointPromise(3);
   addon.userDisabled = false;
   yield checkpointPromise;
   assertCheckpoint(3);
+  Assert.ok(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons);
 
   checkpointPromise = registerCheckpointPromise(4);
   yield AddonTestUtils.uninstallAddonByID(ADDON_ID);
   yield checkpointPromise;
   assertCheckpoint(4);
-
-  yield TelemetryEnvironment.shutdown();
+  Assert.ok(!(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons));
 
   Assert.equal(receivedNotifications, EXPECTED_NOTIFICATIONS,
                "We must only receive the notifications we expect.");
 });
 
 add_task(function* test_pluginsWatch_Add() {
   if (gIsAndroid) {
     Assert.ok(true, "Skipping: there is no Plugin Manager on Android.");
     return;
   }
 
-  yield TelemetryEnvironment.init();
+  Assert.equal(TelemetryEnvironment.currentEnvironment.addons.activePlugins.length, 1);
 
   let newPlugin = new PluginTag(PLUGIN2_NAME, PLUGIN2_DESC, PLUGIN2_VERSION, true);
   gInstalledPlugins.push(newPlugin);
 
   let deferred = PromiseUtils.defer();
   let receivedNotifications = 0;
-  let callback = () => {
+  let callback = (reason, data) => {
     receivedNotifications++;
+    Assert.equal(reason, "addons-changed");
     deferred.resolve();
   };
   TelemetryEnvironment.registerChangeListener("testWatchPlugins_Add", callback);
 
   Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC, null);
   yield deferred.promise;
 
+  Assert.equal(TelemetryEnvironment.currentEnvironment.addons.activePlugins.length, 2);
+
   TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Add");
-  yield TelemetryEnvironment.shutdown();
 
   Assert.equal(receivedNotifications, 1, "We must only receive one notification.");
 });
 
 add_task(function* test_pluginsWatch_Remove() {
   if (gIsAndroid) {
     Assert.ok(true, "Skipping: there is no Plugin Manager on Android.");
     return;
   }
 
-  yield TelemetryEnvironment.init();
-
   // Find the test plugin.
   let plugin = gInstalledPlugins.find(plugin => (plugin.name == PLUGIN2_NAME));
   Assert.ok(plugin, "The test plugin must exist.");
 
   // Remove it from the PluginHost.
   gInstalledPlugins = gInstalledPlugins.filter(p => p != plugin);
 
   let deferred = PromiseUtils.defer();
@@ -839,38 +791,43 @@ add_task(function* test_pluginsWatch_Rem
     deferred.resolve();
   };
   TelemetryEnvironment.registerChangeListener("testWatchPlugins_Remove", callback);
 
   Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC, null);
   yield deferred.promise;
 
   TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Remove");
-  yield TelemetryEnvironment.shutdown();
 
   Assert.equal(receivedNotifications, 1, "We must only receive one notification.");
 });
 
 add_task(function* test_addonsWatch_NotInterestingChange() {
   // We are not interested to dictionary addons changes.
   const DICTIONARY_ADDON_INSTALL_URL = gDataRoot + "dictionary.xpi";
   const INTERESTING_ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
-  yield TelemetryEnvironment.init();
 
-  let receivedNotifications = 0;
+  let receivedNotification = false;
+  let deferred = PromiseUtils.defer();
   TelemetryEnvironment.registerChangeListener("testNotInteresting",
-                                              () => receivedNotifications++);
+    () => {
+      Assert.ok(!receivedNotification, "Should not receive multiple notifications");
+      receivedNotification = true;
+      deferred.resolve();
+    });
 
   yield AddonTestUtils.installXPIFromURL(DICTIONARY_ADDON_INSTALL_URL);
   yield AddonTestUtils.installXPIFromURL(INTERESTING_ADDON_INSTALL_URL);
 
-  Assert.equal(receivedNotifications, 1, "We must receive only one notification.");
+  yield deferred.promise;
+  Assert.ok(!("telemetry-dictionary@tests.mozilla.org" in
+              TelemetryEnvironment.currentEnvironment.addons.activeAddons),
+            "Dictionaries should not appear in active addons.");
 
   TelemetryEnvironment.unregisterChangeListener("testNotInteresting");
-  yield TelemetryEnvironment.shutdown();
 });
 
 add_task(function* test_addonsAndPlugins() {
   const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
   const ADDON_ID = "tel-restartless-xpi@tests.mozilla.org";
   const ADDON_INSTALL_DATE = truncateToDays(Date.now());
   const EXPECTED_ADDON_DATA = {
     blocklisted: false,
@@ -891,22 +848,20 @@ add_task(function* test_addonsAndPlugins
     name: FLASH_PLUGIN_NAME,
     version: FLASH_PLUGIN_VERSION,
     description: FLASH_PLUGIN_DESC,
     blocklisted: false,
     disabled: false,
     clicktoplay: true,
   };
 
-  yield TelemetryEnvironment.init();
-
   // Install an addon so we have some data.
   yield AddonTestUtils.installXPIFromURL(ADDON_INSTALL_URL);
 
-  let data = yield TelemetryEnvironment.getEnvironmentData();
+  let data = TelemetryEnvironment.currentEnvironment;
   checkEnvironmentData(data);
 
   // Check addon data.
   Assert.ok(ADDON_ID in data.addons.activeAddons, "We must have one active addon.");
   let targetAddon = data.addons.activeAddons[ADDON_ID];
   for (let f in EXPECTED_ADDON_DATA) {
     Assert.equal(targetAddon[f], EXPECTED_ADDON_DATA[f], f + " must have the correct value.");
   }
@@ -926,15 +881,13 @@ add_task(function* test_addonsAndPlugins
 
   // Check plugin mime types.
   Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE1));
   Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE2));
   Assert.ok(!targetPlugin.mimeTypes.find(m => m == "Not There."));
 
   let personaId = (gIsGonk) ? null : PERSONA_ID;
   Assert.equal(data.addons.persona, personaId, "The correct Persona Id must be reported.");
-
-  yield TelemetryEnvironment.shutdown();
 });
 
 add_task(function*() {
   do_test_finished();
 });
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -1229,28 +1229,30 @@ add_task(function* test_savedSessionData
 
   const PREF_TEST = "toolkit.telemetry.test.pref1";
   Preferences.reset(PREF_TEST);
   let prefsToWatch = {};
   prefsToWatch[PREF_TEST] = TelemetryEnvironment.RECORD_PREF_VALUE;
 
   // We expect one new subsession when starting TelemetrySession and one after triggering
   // an environment change.
-  const expectedSubsessions = sessionState.profileSubsessionCounter + 2;
+  const expectedSubsessions = sessionState.profileSubsessionCounter + 3;
   const expectedUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
   fakeGenerateUUID(generateUUID, () => expectedUUID);
 
   if (gIsAndroid) {
     // We don't support subsessions yet on Android, so skip the next checks.
     return;
   }
 
   // Start TelemetrySession so that it loads the session data file.
   yield TelemetrySession.reset();
   // Watch a test preference, trigger and environment change and wait for it to propagate.
+
+  // _watchPreferences triggers a subsession notification
   TelemetryEnvironment._watchPreferences(prefsToWatch);
   let changePromise = new Promise(resolve =>
     TelemetryEnvironment.registerChangeListener("test_fake_change", resolve));
   Preferences.set(PREF_TEST, 1);
   yield changePromise;
   TelemetryEnvironment.unregisterChangeListener("test_fake_change");
 
   let payload = TelemetrySession.getPayload();