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
☠☠ backed out by 832a874a7ccc ☠ ☠
authorBenjamin Smedberg <benjamin@smedbergs.us>
Fri, 13 Mar 2015 10:55:06 -0400
changeset 266509 d7a94bbfa3ef6a267d711d4e806fd3c6bbde0a77
parent 266508 5e9ed1f7aebe5a49f84c2e5d82c2da85de16bcb7
child 266510 9623fa2b2e16de005b1508ba257e78b53c55270b
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
bugs1140558
milestone39.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 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
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
@@ -342,26 +342,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;
@@ -419,29 +413,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.
@@ -473,19 +464,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.
@@ -501,26 +491,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");
 
@@ -764,18 +752,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
@@ -819,23 +805,16 @@ let Impl = {
       return;
     }
 
     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("shutdown - 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
@@ -1230,28 +1230,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();