Backed out 14 changesets (bug 1143714, bug 1143796, bug 1139751, bug 1139460, bug 1140558) for Win PGO xpcshell failures
authorWes Kocher <wkocher@mozilla.com>
Wed, 01 Apr 2015 20:52:33 -0700
changeset 237160 6901a267f856b3b5f4c72d7eb160fefb9dc3e25c
parent 237159 516efc52a1abba365d4ce07f3991844ee50b6de7
child 237161 2c7c65e46790ee86b84ca99571cfd130ee11fd98
child 237203 cf8864126c58f90e76a6dce89cec4bdd17586eb9
push id12147
push userkwierso@gmail.com
push dateThu, 02 Apr 2015 03:53:16 +0000
treeherderfx-team@6901a267f856 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1143714, 1143796, 1139751, 1139460, 1140558
milestone40.0a1
backs outd2567d89cda3792419dffd672c00c5b52014cedf
a8edee74d07f6e992ffaf2cb82247398b1750cf6
613fd260f64630c524e05b16ec8605ead384a880
7a6f6bdd6edf5d9f344ad06ea696c920bca17ea8
fdf9d0174142501b063088fde6574851bd2377e9
18989d1ebd43519f9fa7b3149beba4150938203a
a4f545b715ae26d4c92ec99c1d4375cae99ecc9c
b2e92f548736591a93e6775ffa7d14ee40110f87
a082c774db0ab92bab1c1d124a1a53a7ff1294e4
f9f66f6aaa8679739bfc70fbd4a3087cc88277cb
d8b62b11c43ea2ddb01ccd6f48ec4c3a513b3f32
77090798e88cb908cb6aca0fe0671e231daec3ce
ea6da072eb0cbabda48f5c5cee06bd411877efb1
58c2eb92a95925e54633afd7299aa6cd4209a197
Backed out 14 changesets (bug 1143714, bug 1143796, bug 1139751, bug 1139460, bug 1140558) for Win PGO xpcshell failures Backed out changeset d2567d89cda3 (bug 1139751) Backed out changeset a8edee74d07f (bug 1139751) Backed out changeset 613fd260f646 (bug 1143796) Backed out changeset 7a6f6bdd6edf (bug 1143796) Backed out changeset fdf9d0174142 (bug 1143714) Backed out changeset 18989d1ebd43 (bug 1140558) Backed out changeset a4f545b715ae (bug 1140558) Backed out changeset b2e92f548736 (bug 1140558) Backed out changeset a082c774db0a (bug 1140558) Backed out changeset f9f66f6aaa86 (bug 1139460) Backed out changeset d8b62b11c43e (bug 1139460) Backed out changeset 77090798e88c (bug 1139460) Backed out changeset ea6da072eb0c (bug 1139460) Backed out changeset 58c2eb92a959 (bug 1139460)
browser/experiments/Experiments.jsm
testing/modules/Assert.jsm
testing/modules/tests/xpcshell/test_assert.js
toolkit/components/telemetry/TelemetryEnvironment.jsm
toolkit/components/telemetry/TelemetryPing.jsm
toolkit/components/telemetry/TelemetrySession.jsm
toolkit/components/telemetry/docs/environment.rst
toolkit/components/telemetry/docs/pings.rst
toolkit/components/telemetry/tests/unit/head.js
toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
toolkit/components/telemetry/tests/unit/test_TelemetryPing.js
toolkit/components/telemetry/tests/unit/test_TelemetryPingShutdown.js
toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
toolkit/components/telemetry/tests/unit/xpcshell.ini
toolkit/modules/ObjectUtils.jsm
toolkit/modules/moz.build
toolkit/modules/tests/xpcshell/test_ObjectUtils.js
toolkit/modules/tests/xpcshell/xpcshell.ini
--- a/browser/experiments/Experiments.jsm
+++ b/browser/experiments/Experiments.jsm
@@ -134,16 +134,38 @@ function configureLogging() {
     } else {
       gLogger.removeAppender(gLogAppenderDump);
       gLogAppenderDump = null;
     }
     gLogDumping = logDumping;
   }
 }
 
+// Takes an array of promises and returns a promise that is resolved once all of
+// them are rejected or resolved.
+function allResolvedOrRejected(promises) {
+  if (!promises.length) {
+    return Promise.resolve([]);
+  }
+
+  let countdown = promises.length;
+  let deferred = Promise.defer();
+
+  for (let p of promises) {
+    let helper = () => {
+      if (--countdown == 0) {
+        deferred.resolve();
+      }
+    };
+    Promise.resolve(p).then(helper, helper);
+  }
+
+  return deferred.promise;
+}
+
 // Loads a JSON file using OS.file. file is a string representing the path
 // of the file to be read, options contains additional options to pass to
 // OS.File.read.
 // Returns a Promise resolved with the json payload or rejected with
 // OS.File.Error or JSON.parse() errors.
 function loadJSONAsync(file, options) {
   return Task.spawn(function() {
     let rawData = yield OS.File.read(file, options);
--- a/testing/modules/Assert.jsm
+++ b/testing/modules/Assert.jsm
@@ -12,17 +12,16 @@
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "Assert"
 ];
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-Components.utils.import("resource://gre/modules/ObjectUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 /**
  * 1. The assert module provides functions that throw AssertionError's when
  * particular conditions are not met.
  *
  * To use the module you'll need to instantiate it first, which allows consumers
@@ -254,19 +253,105 @@ proto.notEqual = function notEqual(actua
  * @param actual
  *        (mixed) Test subject to be evaluated as equivalent to `expected`, including nested properties
  * @param expected
  *        (mixed) Test reference to evaluate against `actual`
  * @param message (optional)
  *        (string) Short explanation of the expected result
  */
 proto.deepEqual = function deepEqual(actual, expected, message) {
-  this.report(!ObjectUtils.deepEqual(actual, expected), actual, expected, message, "deepEqual");
+  this.report(!_deepEqual(actual, expected), actual, expected, message, "deepEqual");
 };
 
+function _deepEqual(actual, expected) {
+  // 7.1. All identical values are equivalent, as determined by ===.
+  if (actual === expected) {
+    return true;
+  // 7.2. If the expected value is a Date object, the actual value is
+  // equivalent if it is also a Date object that refers to the same time.
+  } else if (instanceOf(actual, "Date") && instanceOf(expected, "Date")) {
+    if (isNaN(actual.getTime()) && isNaN(expected.getTime()))
+      return true;
+    return actual.getTime() === expected.getTime();
+  // 7.3 If the expected value is a RegExp object, the actual value is
+  // equivalent if it is also a RegExp object with the same source and
+  // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).
+  } else if (instanceOf(actual, "RegExp") && instanceOf(expected, "RegExp")) {
+    return actual.source === expected.source &&
+           actual.global === expected.global &&
+           actual.multiline === expected.multiline &&
+           actual.lastIndex === expected.lastIndex &&
+           actual.ignoreCase === expected.ignoreCase;
+  // 7.4. Other pairs that do not both pass typeof value == "object",
+  // equivalence is determined by ==.
+  } else if (typeof actual != "object" && typeof expected != "object") {
+    return actual == expected;
+  // 7.5 For all other Object pairs, including Array objects, equivalence is
+  // determined by having the same number of owned properties (as verified
+  // with Object.prototype.hasOwnProperty.call), the same set of keys
+  // (although not necessarily the same order), equivalent values for every
+  // corresponding key, and an identical 'prototype' property. Note: this
+  // accounts for both named and indexed properties on Arrays.
+  } else {
+    return objEquiv(actual, expected);
+  }
+}
+
+function isUndefinedOrNull(value) {
+  return value === null || value === undefined;
+}
+
+function isArguments(object) {
+  return instanceOf(object, "Arguments");
+}
+
+function objEquiv(a, b) {
+  if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) {
+    return false;
+  }
+  // An identical 'prototype' property.
+  if (a.prototype !== b.prototype) {
+    return false;
+  }
+  // Object.keys may be broken through screwy arguments passing. Converting to
+  // an array solves the problem.
+  if (isArguments(a)) {
+    if (!isArguments(b)) {
+      return false;
+    }
+    a = pSlice.call(a);
+    b = pSlice.call(b);
+    return _deepEqual(a, b);
+  }
+  let ka, kb, key, i;
+  try {
+    ka = Object.keys(a);
+    kb = Object.keys(b);
+  } catch (e) {
+    // Happens when one is a string literal and the other isn't
+    return false;
+  }
+  // Having the same number of owned properties (keys incorporates
+  // hasOwnProperty)
+  if (ka.length != kb.length)
+    return false;
+  // The same set of keys (although not necessarily the same order),
+  ka.sort();
+  kb.sort();
+  // Equivalent values for every corresponding key, and possibly expensive deep 
+  // test
+  for (i = ka.length - 1; i >= 0; i--) {
+    key = ka[i];
+    if (!_deepEqual(a[key], b[key])) {
+      return false;
+    }
+  }
+  return true;
+}
+
 /**
  * 8. The non-equivalence assertion tests for any deep inequality.
  * assert.notDeepEqual(actual, expected, message_opt);
  *
  * @param actual
  *        (mixed) Test subject to be evaluated as NOT equivalent to `expected`, including nested properties
  * @param expected
  *        (mixed) Test reference to evaluate against `actual`
--- a/testing/modules/tests/xpcshell/test_assert.js
+++ b/testing/modules/tests/xpcshell/test_assert.js
@@ -204,16 +204,34 @@ function run_test() {
 
   // use a fn to validate error object
   assert.throws(makeBlock(thrower, TypeError), function(err) {
     if ((err instanceof TypeError) && /test/.test(err)) {
       return true;
     }
   });
 
+  // Make sure deepEqual doesn't loop forever on circular refs
+
+  let b = {};
+  b.b = b;
+
+  let c = {};
+  c.b = c;
+
+  let gotError = false;
+  try {
+    assert.deepEqual(b, c);
+  } catch (e) {
+    gotError = true;
+  }
+
+  dump("All OK\n");
+  assert.ok(gotError);
+
   function testAssertionMessage(actual, expected) {
     try {
       assert.equal(actual, "");
     } catch (e) {
       assert.equal(e.toString(),
           ["AssertionError:", expected, "==", '""'].join(" "));
     }
   }
--- a/toolkit/components/telemetry/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -4,143 +4,36 @@
 
 "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");
-Cu.import("resource://gre/modules/ObjectUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
                                   "resource://gre/modules/ctypes.jsm");
 #ifndef MOZ_WIDGET_GONK
 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");
 
-const CHANGE_THROTTLE_INTERVAL_MS = 5 * 60 * 1000;
-
-/**
- * This is a policy object used to override behavior for testing.
- */
-let Policy = {
-  now: () => new Date(),
-};
-
-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";
@@ -243,21 +136,19 @@ function getGfxField(aPropertyName, aDef
 
 /**
  * Get the information about a graphic adapter.
  *
  * @param aSuffix A suffix to add to the properties names.
  * @return An object containing the adapter properties.
  */
 function getGfxAdapter(aSuffix = "") {
-  // Note that gfxInfo, and so getGfxField, might return "Unknown" for the RAM on failures,
-  // not null.
-  let memoryMB = parseInt(getGfxField("adapterRAM" + aSuffix, null), 10);
-  if (Number.isNaN(memoryMB)) {
-    memoryMB = null;
+  let memoryMB = getGfxField("adapterRAM" + aSuffix, null);
+  if (memoryMB) {
+    memoryMB = parseInt(memoryMB, 10);
   }
 
   return {
     description: getGfxField("adapterDescription" + aSuffix, null),
     vendorID: getGfxField("adapterVendorID" + aSuffix, null),
     deviceID: getGfxField("adapterDeviceID" + aSuffix, null),
     subsysID: getGfxField("adapterSubsysID" + aSuffix, null),
     RAM: memoryMB,
@@ -322,484 +213,205 @@ function getServicePack() {
       minor: null,
     };
   } finally {
     kernel32.close();
   }
 }
 #endif
 
-/**
- * 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;
+this.TelemetryEnvironment = {
+  _shutdown: true,
 
-  // The pending task blocks addon manager shutdown. It can either be the initial load
-  // or a change load.
-  this._pendingTask = null;
+  // 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.
 
-  // 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);
-    }
+  // A map of watched preferences which trigger an Environment change when modified.
+  // Every entry contains a recording policy (RECORD_PREF_*).
+  _watchedPrefs: null,
 
-    this._pendingTask = this._updateAddons().then(
-      () => { this._pendingTask = null; },
-      (err) => {
-        this._environment._log.error("init - Exception in _updateAddons", err);
-        this._pendingTask = null;
-      }
-    );
+  // The Addons change listener, init by |_startWatchingAddons| .
+  _addonsListener: null,
 
-    return this._pendingTask;
-  },
+  // 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,
 
   /**
-   * Register an addon listener and watch for changes.
+   * Initialize TelemetryEnvironment.
    */
-  watchForChanges: function() {
-    this._loaded = true;
-    AddonManager.addAddonListener(this);
-    Services.obs.addObserver(this, EXPERIMENTS_CHANGED_TOPIC, false);
-  },
-
-  // AddonListener
-  onEnabled: function() {
-    this._onAddonChange();
-  },
-  onDisabled: function() {
-    this._onAddonChange();
-  },
-  onInstalled: function() {
-    this._onAddonChange();
-  },
-  onUninstalling: function() {
-    this._onAddonChange();
-  },
-
-  _onAddonChange: function() {
-    this._environment._log.trace("_onAddonChange");
-    this._checkForChanges("addons-changed");
-  },
-
-  // nsIObserver
-  observe: function (aSubject, aTopic, aData) {
-    this._environment._log.trace("observe - Topic " + aTopic);
-    this._checkForChanges("experiment-changed");
-  },
-
-  _checkForChanges: function(changeReason) {
-    if (this._pendingTask) {
-      this._environment._log.trace("_checkForChanges - task already pending, dropping change with reason " + changeReason);
+  init: function () {
+    if (!this._shutdown) {
+      this._log.error("init - Already initialized");
       return;
     }
 
-    this._pendingTask = this._updateAddons().then(
-      (result) => {
-        this._pendingTask = null;
-        if (result.changed) {
-          this._environment._onEnvironmentChange(changeReason, result.oldEnvironment);
-        }
-      },
-      (err) => {
-        this._pendingTask = null;
-        this._environment._log.error("_checkForChanges: Error collecting addons", err);
-      });
-  },
+    this._configureLog();
+    this._log.trace("init");
+    this._shutdown = false;
+    this._startWatchingPrefs();
+    this._startWatchingAddons();
 
-  _shutdownBlocker: function() {
-    if (this._loaded) {
-      AddonManager.removeAddonListener(this);
-      Services.obs.removeObserver(this, EXPERIMENTS_CHANGED_TOPIC);
-    }
-    return this._pendingTask;
+    AddonManager.shutdown.addBlocker("TelemetryEnvironment: caching addons",
+                                      () => this._blockAddonManagerShutdown(),
+                                      () => this._getState());
   },
 
   /**
-   * 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<Object> This returns a Promise resolved with a status object with the following members:
-   *   changed - Whether the environment changed.
-   *   oldEnvironment - Only set if a change occured, contains the environment data before the change.
+   * Shutdown TelemetryEnvironment.
+   * @return Promise<> that is resolved when shutdown is finished.
    */
-  _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,
-    };
-
-    let result = {
-      changed: !ObjectUtils.deepEqual(addons, this._environment._currentEnvironment.addons),
-    };
-
-    if (result.changed) {
-      this._environment._log.trace("_updateAddons: addons differ");
-      result.oldEnvironment = Cu.cloneInto(this._environment._currentEnvironment, myScope);
-      this._environment._currentEnvironment.addons = addons;
+  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");
+      }
+      return;
     }
 
-    return result;
-  }),
-
-  /**
-   * Get the addon data in object form.
-   * @return Promise<object> containing the addon data.
-   */
-  _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));
+    this._log.trace("shutdown");
+    this._shutdown = true;
+    this._stopWatchingPrefs();
+    this._stopWatchingAddons();
+    this._changeListeners.clear();
+    yield this._collectTask;
 
-      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()),
-      };
-    }
-
-    return activeTheme;
+    this._cachedAddons = null;
   }),
 
-  /**
-   * 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 _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,
-      };
+  _configureLog: function () {
+    if (this._log) {
+      return;
     }
-
-    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;
-  },
-};
-
-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();
-
-  // The last change date for the environment, used to throttle environment changes.
-  this._lastEnvironmentChangeDate = null;
-
-  // 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);
+    this._log = Log.repository.getLoggerWithMessagePrefix(
+                                 LOGGER_NAME, "TelemetryEnvironment::");
   },
 
   /**
    * Register a listener for environment changes.
-   * @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, oldEnvironment) - Will receive a reason for
-                     the change and the environment data before the change.
+   * 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.
    */
   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) {
-    this._stopWatchingPrefs();
+    if (this._watchedPrefs) {
+      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] == TelemetryEnvironment.RECORD_PREF_NOTIFY_ONLY) {
+          this._watchedPrefs[pref] == this.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] == TelemetryEnvironment.RECORD_PREF_STATE) {
-        prefValue = "<user-set>";
+      if (this._watchedPrefs[pref] == this.RECORD_PREF_STATE) {
+        prefValue = null;
       } 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");
-    let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
-    this._updateSettings();
-    this._onEnvironmentChange("pref-changed", oldEnvironment);
-  },
-
   /**
    * 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 = {
@@ -853,25 +465,26 @@ EnvironmentCache.prototype = {
         return null;
       }
     }
 
     return null;
   },
 
   /**
-   * Update the cached settings data.
+   * Get the settings data in object form.
+   * @return Object containing the settings.
    */
-  _updateSettings: function () {
+  _getSettings: function () {
     let updateChannel = null;
     try {
       updateChannel = UpdateChannel.get();
     } catch (e) {}
 
-    this._currentEnvironment.settings = {
+    return {
       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: {
@@ -879,30 +492,33 @@ EnvironmentCache.prototype = {
         enabled: Preferences.get(PREF_UPDATE_ENABLED, true),
         autoDownload: Preferences.get(PREF_UPDATE_AUTODOWNLOAD, true),
       },
       userPrefs: this._getPrefData(),
     };
   },
 
   /**
-   * Update the cached profile data.
-   * @returns Promise<> resolved when the I/O is complete.
+   * Get the profile data in object form.
+   * @return Object containing the profile data.
    */
-  _updateProfile: Task.async(function* () {
+  _getProfile: Task.async(function* () {
     let profileAccessor = new ProfileAge(null, this._log);
 
     let creationDate = yield profileAccessor.created;
     let resetDate = yield profileAccessor.reset;
 
-    this._currentEnvironment.profile.creationDate =
-      truncateToDays(creationDate);
+    let profileData = {
+      creationDate: truncateToDays(creationDate),
+    };
+
     if (resetDate) {
-      this._currentEnvironment.profile.resetDate = truncateToDays(resetDate);
+      profileData.resetDate = truncateToDays(resetDate);
     }
+    return profileData;
   }),
 
   /**
    * Get the partner data in object form.
    * @return Object containing the partner data.
    */
   _getPartner: function () {
     let partnerData = {
@@ -910,17 +526,17 @@ EnvironmentCache.prototype = {
       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.
    */
@@ -1064,37 +680,398 @@ EnvironmentCache.prototype = {
       device: this._getDeviceData(),
 #endif
       os: this._getOSData(),
       hdd: this._getHDDData(),
       gfx: this._getGFXData(),
     };
   },
 
-  _onEnvironmentChange: function (what, oldEnvironment) {
+  /**
+   * 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;
     }
 
-    // We are already skipping change events in _checkChanges if there is a pending change task running.
-    let now = Policy.now();
-    if (this._lastEnvironmentChangeDate &&
-        (CHANGE_THROTTLE_INTERVAL_MS >=
-         (now.getTime() - this._lastEnvironmentChangeDate.getTime()))) {
-      this._log.trace("_onEnvironmentChange - throttling changes, now: " + now +
-                      ", last change: " + this._lastEnvironmentChangeDate);
-      return;
-    }
-
-    this._lastEnvironmentChangeDate = now;
-
     for (let [name, listener] of this._changeListeners) {
       try {
         this._log.debug("_onEnvironmentChange - calling " + name);
-        listener(what, oldEnvironment);
+        listener();
       } catch (e) {
-        this._log.error("_onEnvironmentChange - listener " + name + " caught error", e);
+        this._log.warning("_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
@@ -4,26 +4,23 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
-const myScope = this;
 
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/debug.js", this);
 Cu.import("resource://gre/modules/Services.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
-Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
-Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/DeferredTask.jsm", this);
 Cu.import("resource://gre/modules/Preferences.jsm");
 
 const LOGGER_NAME = "Toolkit.Telemetry";
 const LOGGER_PREFIX = "TelemetryPing::";
 
 const PREF_BRANCH = "toolkit.telemetry.";
 const PREF_BRANCH_LOG = PREF_BRANCH + "log.";
@@ -37,18 +34,16 @@ const PREF_FHR_UPLOAD_ENABLED = "datarep
 const PING_FORMAT_VERSION = 4;
 
 // Delay before intializing telemetry (ms)
 const TELEMETRY_DELAY = 60000;
 // Delay before initializing telemetry if we're testing (ms)
 const TELEMETRY_TEST_DELAY = 100;
 // The number of days to keep pings serialised on the disk in case of failures.
 const DEFAULT_RETENTION_DAYS = 14;
-// Timeout after which we consider a ping submission failed.
-const PING_SUBMIT_TIMEOUT_MS = 2 * 60 * 1000;
 
 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
                                    "@mozilla.org/base/telemetry;1",
                                    "nsITelemetry");
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryFile",
                                   "resource://gre/modules/TelemetryFile.jsm");
@@ -171,17 +166,16 @@ this.TelemetryPing = Object.freeze({
    * @param {Object} aPayload The actual data payload for the ping.
    * @param {Object} [aOptions] Options object.
    * @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
    *                 if sending fails.
    * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
    *                  id, false otherwise.
    * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
    *                  environment data.
-   * @param {Object}  [aOptions.overrideEnvironment=null] set to override the environment data.
    * @returns {Promise} A promise that resolves when the ping is sent.
    */
   send: function(aType, aPayload, aOptions = {}) {
     let options = aOptions;
     options.retentionDays = aOptions.retentionDays || DEFAULT_RETENTION_DAYS;
     options.addClientId = aOptions.addClientId || false;
     options.addEnvironment = aOptions.addEnvironment || false;
 
@@ -195,17 +189,16 @@ this.TelemetryPing = Object.freeze({
    * @param {Object} aPayload The actual data payload for the ping.
    * @param {Object} [aOptions] Options object.
    * @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
    *                 if sending fails.
    * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
    *                  id, false otherwise.
    * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
    *                  environment data.
-   * @param {Object}  [aOptions.overrideEnvironment=null] set to override the environment data.
    * @returns {Promise} A promise that resolves when the pings are saved.
    */
   savePendingPings: function(aType, aPayload, aOptions = {}) {
     let options = aOptions;
     options.retentionDays = aOptions.retentionDays || DEFAULT_RETENTION_DAYS;
     options.addClientId = aOptions.addClientId || false;
     options.addEnvironment = aOptions.addEnvironment || false;
 
@@ -221,17 +214,16 @@ this.TelemetryPing = Object.freeze({
    * @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
    *                 if sending fails.
    * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
    *                  id, false otherwise.
    * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
    *                  environment data.
    * @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
    *                  if found.
-   * @param {Object}  [aOptions.overrideEnvironment=null] set to override the environment data.
    * @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
    *                 ping location if not provided.
    *
    * @returns {Promise<Integer>} A promise that resolves with the ping id when the ping is
    *                             saved to disk.
    */
   savePing: function(aType, aPayload, aOptions = {}) {
     let options = aOptions;
@@ -269,25 +261,17 @@ let Impl = {
   // Undefined if this is not the first run, or the previous build ID is unknown.
   _previousBuildID: undefined,
   _clientID: null,
   // A task performing delayed initialization
   _delayedInitTask: null,
   // The deferred promise resolved when the initialization task completes.
   _delayedInitTaskDeferred: null,
 
-  // This is a public barrier Telemetry clients can use to add blockers to the shutdown
-  // of TelemetryPing.
-  // After this barrier, clients can not submit Telemetry pings anymore.
   _shutdownBarrier: new AsyncShutdown.Barrier("TelemetryPing: Waiting for clients."),
-  // This is a private barrier blocked by pending async ping activity (sending & saving).
-  _connectionsBarrier: new AsyncShutdown.Barrier("TelemetryPing: Waiting for pending ping activity"),
-
-  // This tracks all pending ping requests to the server.
-  _pendingPingRequests: new Map(),
 
   /**
    * Get the data for the "application" section of the ping.
    */
   _getApplicationSection: function() {
     // Querying architecture and update channel can throw. Make sure to recover and null
     // those fields.
     let arch = null;
@@ -321,48 +305,48 @@ let Impl = {
    *
    * @param {String} aType The type of the ping.
    * @param {Object} aPayload The actual data payload for the ping.
    * @param {Object} aOptions Options object.
    * @param {Boolean} aOptions.addClientId true if the ping should contain the client
    *                  id, false otherwise.
    * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
    *                  environment data.
-   * @param {Object}  [aOptions.overrideEnvironment=null] set to override the environment data.
    *
    * @returns Promise<Object> A promise that resolves when the ping is completely assembled.
    */
   assemblePing: function assemblePing(aType, aPayload, aOptions = {}) {
     this._log.trace("assemblePing - Type " + aType + ", Server " + this._server +
                     ", aOptions " + JSON.stringify(aOptions));
 
-    // Clone the payload data so we don't race against unexpected changes in subobjects that are
-    // still referenced by other code.
-    // We can't trust all callers to do this properly on their own.
-    let payload = Cu.cloneInto(aPayload, myScope);
-
     // Fill the common ping fields.
     let pingData = {
       type: aType,
       id: generateUUID(),
       creationDate: (new Date()).toISOString(),
       version: PING_FORMAT_VERSION,
       application: this._getApplicationSection(),
       payload: aPayload,
     };
 
     if (aOptions.addClientId) {
       pingData.clientId = this._clientID;
     }
 
     if (aOptions.addEnvironment) {
-      pingData.environment = aOptions.overrideEnvironment || TelemetryEnvironment.currentEnvironment;
+      return TelemetryEnvironment.getEnvironmentData().then(environment => {
+        pingData.environment = environment;
+        return pingData;
+      },
+      error => {
+        this._log.error("assemblePing - Rejection", error);
+      });
     }
 
-    return pingData;
+    return Promise.resolve(pingData);
   },
 
   popPayloads: function popPayloads() {
     this._log.trace("popPayloads");
     function payloadIter() {
       let iterator = TelemetryFile.popPendingPings();
       for (let data of iterator) {
         yield data;
@@ -376,24 +360,16 @@ let Impl = {
   /**
    * Only used in tests.
    */
   setServer: function (aServer) {
     this._server = aServer;
   },
 
   /**
-   * Track any pending ping send and save tasks through the promise passed here.
-   * This is needed to block shutdown on any outstanding ping activity.
-   */
-  _trackPendingPingTask: function (aPromise) {
-    this._connectionsBarrier.client.addBlocker("Waiting for ping task", aPromise);
-  },
-
-  /**
    * Adds a ping to the pending ping list by moving it to the saved pings directory
    * and adding it to the pending ping list.
    *
    * @param {String} aPingPath The path of the ping to add to the pending ping list.
    * @param {Boolean} [aRemoveOriginal] If true, deletes the ping at aPingPath after adding
    *                  it to the saved pings directory.
    * @return {Promise} Resolved when the ping is correctly moved to the saved pings directory.
    */
@@ -413,78 +389,69 @@ let Impl = {
    * @param {Object} aPayload The actual data payload for the ping.
    * @param {Object} aOptions Options object.
    * @param {Number} aOptions.retentionDays The number of days to keep the ping on disk
    *                 if sending fails.
    * @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
    *                  false otherwise.
    * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
    *                  environment data.
-   * @param {Object}  aOptions.overrideEnvironment set to override the 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 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;
+    return 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));
   },
 
   /**
    * Send the persisted pings to the server.
-   *
-   * @return Promise A promise that is resolved when all pings finished sending or failed.
    */
   sendPersistedPings: function sendPersistedPings() {
     this._log.trace("sendPersistedPings");
-
     let pingsIterator = Iterator(this.popPayloads());
-    let p = [for (data of pingsIterator) this.doPing(data, true).catch((e) => {
-      this._log.error("sendPersistedPings - doPing rejected", e);
-    })];
-
-    let promise = Promise.all(p);
-    this._trackPendingPingTask(promise);
-    return promise;
+    let p = [data for (data in pingsIterator)].map(data => this.doPing(data, true));
+    return Promise.all(p);
   },
 
   /**
    * Saves all the pending pings, plus the passed one, 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.
    * @param {Number} aOptions.retentionDays The number of days to keep the ping on disk
    *                 if sending fails.
    * @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
    *                  false otherwise.
    * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
    *                  environment data.
-   * @param {Object}  [aOptions.overrideEnvironment=null] set to override the 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));
 
-    let pingData = this.assemblePing(aType, aPayload, aOptions);
-    return TelemetryFile.savePendingPings(pingData);
+    return this.assemblePing(aType, aPayload, aOptions)
+        .then(pingData => TelemetryFile.savePendingPings(pingData),
+              error => this._log.error("savePendingPings - Rejection", error));
   },
 
   /**
    * 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.
@@ -492,37 +459,38 @@ let Impl = {
    *                 if sending fails.
    * @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
    *                  false otherwise.
    * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
    *                  environment data.
    * @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
    * @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
    *                 ping location if not provided.
-   * @param {Object}  [aOptions.overrideEnvironment=null] set to override the environment data.
    *
    * @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));
 
-    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; });
-    }
+    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));
   },
 
-  onPingRequestFinished: function(success, startTime, ping, isPersisted) {
-    this._log.trace("onPingRequestFinished - success: " + success + ", persisted: " + isPersisted);
+  finishPingRequest: function finishPingRequest(success, startTime, ping, isPersisted) {
+    this._log.trace("finishPingRequest - Success " + success + ", Persisted " + isPersisted);
 
     let hping = Telemetry.getHistogramById("TELEMETRY_PING");
     let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
 
     hsuccess.add(success);
     hping.add(new Date() - startTime);
 
     if (success && isPersisted) {
@@ -562,103 +530,63 @@ let Impl = {
     }
 
     let slug = pathComponents.join("/");
     return "/submit/telemetry/" + slug;
   },
 
   doPing: function doPing(ping, isPersisted) {
     this._log.trace("doPing - Server " + this._server + ", Persisted " + isPersisted);
-
-    const isNewPing = isNewPingFormat(ping);
-    const version = isNewPing ? PING_FORMAT_VERSION : 1;
-    const url = this._server + this.submissionPath(ping) + "?v=" + version;
-
+    let deferred = Promise.defer();
+    let isNewPing = isNewPingFormat(ping);
+    let version = isNewPing ? PING_FORMAT_VERSION : 1;
+    let url = this._server + this.submissionPath(ping) + "?v=" + version;
     let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
                   .createInstance(Ci.nsIXMLHttpRequest);
     request.mozBackgroundRequest = true;
-    request.timeout = PING_SUBMIT_TIMEOUT_MS;
-
     request.open("POST", url, true);
     request.overrideMimeType("text/plain");
     request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
 
-    this._pendingPingRequests.set(url, request);
+    let startTime = new Date();
 
-    let startTime = new Date();
-    let deferred = PromiseUtils.defer();
-
-    let onRequestFinished = (success, event) => {
-      let onCompletion = () => {
+    function handler(success) {
+      let handleCompletion = event => {
         if (success) {
           deferred.resolve();
         } else {
           deferred.reject(event);
         }
       };
 
-      this._pendingPingRequests.delete(url);
-      this.onPingRequestFinished(success, startTime, ping, isPersisted)
-        .then(() => onCompletion(),
-              (error) => {
-                this._log.error("doPing - request success: " + success + ", error" + error);
-                onCompletion();
-              });
-    };
-
-    let errorhandler = (event) => {
-      this._log.error("doPing - error making request to " + url + ": " + event.type);
-      onRequestFinished(false, event);
-    };
-    request.onerror = errorhandler;
-    request.ontimeout = errorhandler;
-    request.onabort = errorhandler;
-
-    request.onload = (event) => {
-      let status = request.status;
-      let statusClass = status - (status % 100);
-      let success = false;
-
-      if (statusClass === 200) {
-        // We can treat all 2XX as success.
-        this._log.info("doPing - successfully loaded, status: " + status);
-        success = true;
-      } else if (statusClass === 400) {
-        // 4XX means that something with the request was broken.
-        this._log.error("doPing - error submitting to " + url + ", status: " + status
-                        + " - ping request broken?");
-        // TODO: we should handle this better, but for now we should avoid resubmitting
-        // broken requests by pretending success.
-        success = true;
-      } else if (statusClass === 500) {
-        // 5XX means there was a server-side error and we should try again later.
-        this._log.error("doPing - error submitting to " + url + ", status: " + status
-                        + " - server error, should retry later");
-      } else {
-        // We received an unexpected status codes.
-        this._log.error("doPing - error submitting to " + url + ", status: " + status
-                        + ", type: " + event.type);
-      }
-
-      onRequestFinished(success, event);
-    };
+      return function(event) {
+        this.finishPingRequest(success, startTime, ping, isPersisted)
+          .then(() => handleCompletion(event),
+                error => {
+                  this._log.error("doPing - Request Success " + success + ", Error " +
+                                  error);
+                  handleCompletion(event);
+                });
+      };
+    }
+    request.addEventListener("error", handler(false).bind(this), false);
+    request.addEventListener("load", handler(true).bind(this), false);
 
     // If that's a legacy ping format, just send its payload.
     let networkPayload = isNewPing ? ping : ping.payload;
     request.setRequestHeader("Content-Encoding", "gzip");
     let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                     .createInstance(Ci.nsIScriptableUnicodeConverter);
     converter.charset = "UTF-8";
     let utf8Payload = converter.ConvertFromUnicode(JSON.stringify(networkPayload));
     utf8Payload += converter.Finish();
     let payloadStream = Cc["@mozilla.org/io/string-input-stream;1"]
                         .createInstance(Ci.nsIStringInputStream);
     payloadStream.data = this.gzipCompressString(utf8Payload);
     request.send(payloadStream);
-
     return deferred.promise;
   },
 
   gzipCompressString: function gzipCompressString(string) {
     let observer = {
       buffer: "",
       onStreamComplete: function(loader, context, status, length, result) {
         this.buffer = String.fromCharCode.apply(this, result);
@@ -765,16 +693,18 @@ 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
@@ -807,69 +737,51 @@ let Impl = {
     AsyncShutdown.sendTelemetry.addBlocker("TelemetryPing: shutting down",
                                            () => this.shutdown(),
                                            () => this._getState());
 
     this._delayedInitTask.arm();
     return this._delayedInitTaskDeferred.promise;
   },
 
-  // Do proper shutdown waiting and cleanup.
-  _cleanupOnShutdown: Task.async(function*() {
-    if (!this._initialized) {
-      return;
-    }
-
-    // Abort any pending ping XHRs.
-    for (let [url, request] of this._pendingPingRequests) {
-      this._log.trace("_cleanupOnShutdown - aborting ping request for " + url);
-      try {
-        request.abort();
-      } catch (e) {
-        this._log.error("_cleanupOnShutdown - failed to abort request to " + url, e);
-      }
-    }
-    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();
-    } finally {
-      // Reset state.
-      this._initialized = false;
-      this._initStarted = false;
-    }
-  }),
-
   shutdown: function() {
     this._log.trace("shutdown");
 
+    let cleanup = () => {
+      if (!this._initialized) {
+        return;
+      }
+      let reset = () => {
+        this._initialized = false;
+        this._initStarted = false;
+      };
+      return this._shutdownBarrier.wait().then(
+               () => TelemetryEnvironment.shutdown().then(reset, reset));
+    };
+
     // We can be in one the following states here:
     // 1) setupTelemetry was never called
     // or it was called and
     //   2) _delayedInitTask was scheduled, but didn't run yet.
     //   3) _delayedInitTask is running now.
     //   4) _delayedInitTask finished running already.
 
     // This handles 1).
     if (!this._initStarted) {
       return Promise.resolve();
     }
 
     // This handles 4).
     if (!this._delayedInitTask) {
       // We already ran the delayed initialization.
-      return this._cleanupOnShutdown();
+      return cleanup();
     }
 
     // This handles 2) and 3).
-    return this._delayedInitTask.finalize().then(() => this._cleanupOnShutdown());
+    return this._delayedInitTask.finalize().then(cleanup);
   },
 
   /**
    * This observer drives telemetry.
    */
   observe: function (aSubject, aTopic, aData) {
     // The logger might still be not available at this point.
     if (!this._log) {
@@ -904,13 +816,11 @@ let Impl = {
   /**
    * Get an object describing the current state of this module for AsyncShutdown diagnostics.
    */
   _getState: function() {
     return {
       initialized: this._initialized,
       initStarted: this._initStarted,
       haveDelayedInitTask: !!this._delayedInitTask,
-      shutdownBarrier: this._shutdownBarrier.state,
-      connectionsBarrier: this._connectionsBarrier.state,
     };
   },
 };
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -42,17 +42,19 @@ const REASON_SAVED_SESSION = "saved-sess
 const REASON_IDLE_DAILY = "idle-daily";
 const REASON_GATHER_PAYLOAD = "gather-payload";
 const REASON_TEST_PING = "test-ping";
 const REASON_ENVIRONMENT_CHANGE = "environment-change";
 const REASON_SHUTDOWN = "shutdown";
 
 const ENVIRONMENT_CHANGE_LISTENER = "TelemetrySession::onEnvironmentChange";
 
-const MS_IN_ONE_HOUR  = 60 * 60 * 1000;
+const SEC_IN_ONE_DAY  = 24 * 60 * 60;
+const MS_IN_ONE_DAY   = SEC_IN_ONE_DAY * 1000;
+
 const MIN_SUBSESSION_LENGTH_MS = 10 * 60 * 1000;
 
 // This is the HG changeset of the Histogram.json file, used to associate
 // submitted ping data with its histogram definition (bug 832007)
 #expand const HISTOGRAMS_FILE_VERSION = "__HISTOGRAMS_FILE_VERSION__";
 
 const LOGGER_NAME = "Toolkit.Telemetry";
 const LOGGER_PREFIX = "TelemetrySession::";
@@ -79,18 +81,16 @@ const MAX_NUM_CONTENT_PAYLOADS = 10;
 // Do not gather data more than once a minute
 const TELEMETRY_INTERVAL = 60000;
 // Delay before intializing telemetry (ms)
 const TELEMETRY_DELAY = 60000;
 // Delay before initializing telemetry if we're testing (ms)
 const TELEMETRY_TEST_DELAY = 100;
 // Execute a scheduler tick every 5 minutes.
 const SCHEDULER_TICK_INTERVAL_MS = 5 * 60 * 1000;
-// When user is idle, execute a scheduler tick every 60 minutes.
-const SCHEDULER_TICK_IDLE_INTERVAL_MS = 60 * 60 * 1000;
 // The maximum number of times a scheduled operation can fail.
 const SCHEDULER_RETRY_ATTEMPTS = 3;
 
 // The tolerance we have when checking if it's midnight (15 minutes).
 const SCHEDULER_MIDNIGHT_TOLERANCE_MS = 15 * 60 * 1000;
 
 // Coalesce the daily and aborted-session pings if they are both due within
 // two minutes from each other.
@@ -190,39 +190,29 @@ function truncateToDays(date) {
  * @return {Boolean} True if the absolute time difference is within the tolerance, false
  *                   otherwise.
  */
 function areTimesClose(t1, t2, tolerance) {
   return Math.abs(t1 - t2) <= tolerance;
 }
 
 /**
- * Get the next midnight for a date.
- * @param {Object} date The date object to check.
- * @return {Object} The Date object representing the next midnight.
- */
-function getNextMidnight(date) {
-  let nextMidnight = new Date(truncateToDays(date));
-  nextMidnight.setDate(nextMidnight.getDate() + 1);
-  return nextMidnight;
-}
-
-/**
  * Get the midnight which is closer to the provided date.
  * @param {Object} date The date object to check.
  * @return {Object} The Date object representing the closes midnight, or null if midnight
  *                  is not within the midnight tolerance.
  */
 function getNearestMidnight(date) {
   let lastMidnight = truncateToDays(date);
   if (areTimesClose(date.getTime(), lastMidnight.getTime(), SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
     return lastMidnight;
   }
 
-  const nextMidnightDate = getNextMidnight(date);
+  let nextMidnightDate = new Date(lastMidnight);
+  nextMidnightDate.setDate(nextMidnightDate.getDate() + 1);
   if (areTimesClose(date.getTime(), nextMidnightDate.getTime(), SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
     return nextMidnightDate;
   }
   return null;
 }
 
 /**
  * Get the ping type based on the payload.
@@ -421,116 +411,82 @@ let TelemetryScheduler = {
 
   _log: null,
 
   // The number of times a daily ping fails.
   _dailyPingRetryAttempts: 0,
 
   // The timer which drives the scheduler.
   _schedulerTimer: null,
-  // The interval used by the scheduler timer.
-  _schedulerInterval: 0,
   _shuttingDown: true,
-  _isUserIdle: false,
 
   /**
    * Initialises the scheduler and schedules the first daily/aborted session pings.
    */
   init: function() {
     this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, "TelemetryScheduler::");
     this._log.trace("init");
     this._shuttingDown = false;
-    this._isUserIdle = false;
     // Initialize the last daily ping and aborted session last due times to the current time.
     // Otherwise, we might end up sending daily pings even if the subsession is not long enough.
     let now = Policy.now();
     this._lastDailyPingTime = now.getTime();
     this._lastSessionCheckpointTime = now.getTime();
     this._rescheduleTimeout();
-    idleService.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
   },
 
   /**
    * Reschedules the tick timer.
    */
   _rescheduleTimeout: function() {
-    this._log.trace("_rescheduleTimeout - isUserIdle: " + this._isUserIdle);
+    this._log.trace("_rescheduleTimeout");
     if (this._shuttingDown) {
       this._log.warn("_rescheduleTimeout - already shutdown");
       return;
     }
 
     if (this._schedulerTimer) {
       Policy.clearSchedulerTickTimeout(this._schedulerTimer);
     }
 
-    const now = Policy.now();
-    let timeout = SCHEDULER_TICK_INTERVAL_MS;
-
-    // When the user is idle we want to fire the timer less often.
-    if (this._isUserIdle) {
-      timeout = SCHEDULER_TICK_IDLE_INTERVAL_MS;
-      // We need to make sure though that we don't miss sending pings around
-      // midnight when we use the longer idle intervals.
-      const nextMidnight = getNextMidnight(now);
-      timeout = Math.min(timeout, nextMidnight.getTime() - now.getTime());
-    }
-
-    this._log.trace("_rescheduleTimeout - scheduling next tick for " + new Date(now.getTime() + timeout));
     this._schedulerTimer =
-      Policy.setSchedulerTickTimeout(() => this._onSchedulerTick(), timeout);
-  },
-
-  _sentDailyPingToday: function(nowDate) {
-    // This is today's date and also the previous midnight (0:00).
-    const todayDate = truncateToDays(nowDate);
-    const nearestMidnight = getNearestMidnight(nowDate);
-    // If we are close to midnight, we check against that, otherwise against the last midnight.
-    const checkDate = nearestMidnight || todayDate;
-    // We consider a ping sent for today if it occured after midnight, or prior within the tolerance.
-    return (this._lastDailyPingTime >= (checkDate.getTime() - SCHEDULER_MIDNIGHT_TOLERANCE_MS));
+      Policy.setSchedulerTickTimeout(() => this._onSchedulerTick(), SCHEDULER_TICK_INTERVAL_MS);
   },
 
   /**
    * Checks if we can send a daily ping or not.
    * @param {Object} nowDate A date object.
    * @return {Boolean} True if we can send the daily ping, false otherwise.
    */
   _isDailyPingDue: function(nowDate) {
-    const sentPingToday = this._sentDailyPingToday(nowDate);
-
-    // The daily ping is not due if we already sent one today.
-    if (sentPingToday) {
-      this._log.trace("_isDailyPingDue - already sent one today");
-      return false;
-    }
-
-    const nearestMidnight = getNearestMidnight(nowDate);
-    if (!sentPingToday && !nearestMidnight) {
-      // Computer must have gone to sleep, the daily ping is overdue.
-      this._log.trace("_isDailyPingDue - daily ping is overdue... computer went to sleep?");
+    let nearestMidnight = getNearestMidnight(nowDate);
+    if (nearestMidnight) {
+      let subsessionLength = Math.abs(nowDate.getTime() - this._lastDailyPingTime);
+      if (subsessionLength < MIN_SUBSESSION_LENGTH_MS) {
+        // Generating a daily ping now would create a very short subsession.
+        return false;
+      } else if (areTimesClose(this._lastDailyPingTime, nearestMidnight.getTime(),
+                               SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
+        // We've already sent a ping for this midnight.
+        return false;
+      }
       return true;
     }
 
-    // Avoid overly short sessions.
-    const timeSinceLastDaily = nowDate.getTime() - this._lastDailyPingTime;
-    if (timeSinceLastDaily < MIN_SUBSESSION_LENGTH_MS) {
-      this._log.trace("_isDailyPingDue - delaying daily to keep minimum session length");
-      return false;
+    let lastDailyPingDate = truncateToDays(new Date(this._lastDailyPingTime));
+    // This is today's date and also the previous midnight (0:00).
+    let todayDate = truncateToDays(nowDate);
+    // Check that _lastDailyPingTime isn't today nor within SCHEDULER_MIDNIGHT_TOLERANCE_MS of the
+    // *previous* midnight.
+    if ((lastDailyPingDate.getTime() != todayDate.getTime()) &&
+        !areTimesClose(this._lastDailyPingTime, todayDate.getTime(), SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
+      // Computer must have gone to sleep, the daily ping is overdue.
+      return true;
     }
-
-    // To fight jank, we allow daily pings to be collected on user idle before midnight
-    // within the tolerance interval.
-    if (!this._isUserIdle && (nowDate.getTime() < nearestMidnight.getTime())) {
-      this._log.trace("_isDailyPingDue - waiting for user idle period");
-      return false;
-    }
-
-    this._log.trace("_isDailyPingDue - is due");
-    return true;
+    return false;
   },
 
   /**
    * An helper function to save an aborted-session ping.
    * @param {Number} now The current time, in milliseconds.
    * @param {Object} [competingPayload=null] If we are coalescing the daily and the
    *                 aborted-session pings, this is the payload for the former. Note
    *                 that the reason field of this payload will be changed.
@@ -538,43 +494,24 @@ let TelemetryScheduler = {
    */
   _saveAbortedPing: function(now, competingPayload=null) {
     this._lastSessionCheckpointTime = now;
     return Impl._saveAbortedSessionPing(competingPayload)
                 .catch(e => this._log.error("_saveAbortedPing - Failed", e));
   },
 
   /**
-   * The notifications handler.
-   */
-  observe: function(aSubject, aTopic, aData) {
-    this._log.trace("observe - aTopic: " + aTopic);
-    switch(aTopic) {
-      case "idle":
-        // If the user is idle, increase the tick interval.
-        this._isUserIdle = true;
-        return this._onSchedulerTick();
-        break;
-      case "active":
-        // User is back to work, restore the original tick interval.
-        this._isUserIdle = false;
-        return this._onSchedulerTick();
-        break;
-    }
-  },
-
-  /**
    * Performs a scheduler tick. This function manages Telemetry recurring operations.
    * @return {Promise} A promise, only used when testing, resolved when the scheduled
    *                   operation completes.
    */
   _onSchedulerTick: function() {
     if (this._shuttingDown) {
       this._log.warn("_onSchedulerTick - already shutdown.");
-      return Promise.reject(new Error("Already shutdown."));
+      return;
     }
 
     let promise = Promise.resolve();
     try {
       promise = this._schedulerTickLogic();
     } catch (e) {
       this._log.error("_onSchedulerTick - There was an exception", e);
     } finally {
@@ -708,18 +645,16 @@ let TelemetryScheduler = {
     }
 
     this._log.trace("shutdown");
     if (this._schedulerTimer) {
       Policy.clearSchedulerTickTimeout(this._schedulerTimer);
       this._schedulerTimer = null;
     }
 
-    idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
-
     this._shuttingDown = true;
   }
 };
 
 this.EXPORTED_SYMBOLS = ["TelemetrySession"];
 
 this.TelemetrySession = Object.freeze({
   Constants: Object.freeze({
@@ -1577,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,
-                                                    (reason, data) => this._onEnvironmentChange(reason, data));
+                                                    () => this._onEnvironmentChange());
         // 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(e);
+        this._delayedInitTaskDeferred.reject();
       } finally {
         this._delayedInitTask = null;
         this._delayedInitTaskDeferred = null;
       }
     }.bind(this), testing ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY);
 
     this._delayedInitTask.arm();
     return this._delayedInitTaskDeferred.promise;
@@ -1982,18 +1917,17 @@ let Impl = {
       addClientId: true,
       addEnvironment: true,
     };
 
     let promise = TelemetryPing.send(getPingType(payload), payload, options);
 #if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
     // If required, also save the payload as an aborted session.
     if (saveAsAborted) {
-      let abortedPromise = this._saveAbortedSessionPing(payload);
-      promise = promise.then(() => abortedPromise);
+      return promise.then(() => this._saveAbortedSessionPing(payload));
     }
 #endif
     return promise;
   },
 
   /**
    * Loads session data from the session data file.
    * @return {Promise<boolean>} A promise which is resolved with a true argument when
@@ -2044,28 +1978,27 @@ 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(reason, oldEnvironment) {
-    this._log.trace("_onEnvironmentChange", reason);
+  _onEnvironmentChange: function() {
+    this._log.trace("_onEnvironmentChange");
     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,
       addEnvironment: true,
-      overrideEnvironment: oldEnvironment,
     };
     TelemetryPing.send(getPingType(payload), payload, options);
   },
 
   _isClassicReason: function(reason) {
     const classicReasons = [
       REASON_SAVED_SESSION,
       REASON_IDLE_DAILY,
@@ -2090,23 +2023,18 @@ let Impl = {
    * Deletes the aborted session ping. This is called during shutdown.
    * @return {Promise} Resolved when the aborted session ping is removed or if it doesn't
    *                   exist.
    */
   _removeAbortedSessionPing: function() {
     const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
                                    ABORTED_SESSION_FILE_NAME);
     try {
-      this._log.trace("_removeAbortedSessionPing - success");
       return OS.File.remove(FILE_PATH);
-    } catch (ex if ex.becauseNoSuchFile) {
-      this._log.trace("_removeAbortedSessionPing - no such file");
-    } catch (ex) {
-      this._log.error("_removeAbortedSessionPing - error removing ping", ex)
-    }
+    } catch (ex if ex.becauseNoSuchFile) { }
     return Promise.resolve();
   },
 
   /**
    * Check if there's any aborted session ping available. If so, tell TelemetryPing about
    * it.
    */
   _checkAbortedSessionPing: Task.async(function* () {
--- a/toolkit/components/telemetry/docs/environment.rst
+++ b/toolkit/components/telemetry/docs/environment.rst
@@ -4,23 +4,16 @@ Environment
 
 The environment consists of data that is expected to be characteristic for performance and other behavior and not expected to change too often.
 
 Changes to most of these data points are detected (where possible and sensible) and will lead to a session split in the :doc:`main-ping`.
 The environment data may also be submitted by other ping types.
 
 *Note:* This is not submitted with all ping types due to privacy concerns. This and other data is inspected under the `data collection policy <https://wiki.mozilla.org/Firefox/Data_Collection>`_.
 
-Some parts of the environment must be fetched asynchronously at startup. We don't want other Telemetry components to block on waiting for the environment, so some items may be missing from it until the async fetching finished.
-This currently affects the following sections:
-
-- profile
-- addons
-
-
 Structure::
 
     {
       build: {
         applicationId: <string>, // nsIXULAppInfo.ID
         applicationName: <string>, // "Firefox"
         architecture: <string>, // e.g. "x86", build architecture for the active build
         architecturesInBinary: <string>, // e.g. "i386-x86_64", from nsIMacUtils.architecturesInBinary, only present for mac universal builds
@@ -38,21 +31,18 @@ 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: {
-          // 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
+          // Two possible behaviours: values of the whitelisted prefs, or for some prefs we
+          // only record they are present with value being set to null.
         },
       },
       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
--- a/toolkit/components/telemetry/docs/pings.rst
+++ b/toolkit/components/telemetry/docs/pings.rst
@@ -14,22 +14,16 @@ It contains some basic information share
 Submission
 ==========
 
 Pings are submitted via a common API on ``TelemetryPing``. It allows callers to choose a custom retention period that determines how long pings are kept on disk if submission wasn't successful.
 If a ping failed to submit (e.g. because of missing internet connection), Telemetry will retry to submit it until its retention period is up.
 
 *Note:* the :doc:`main pings <main-ping>` are kept locally even after successful submission to enable the HealthReport and SelfSupport features. They will be deleted after their retention period of 180 days.
 
-The telemetry server team is working towards `the common services status codes <https://wiki.mozilla.org/CloudServices/DataPipeline/HTTPEdgeServerSpecification#Server_Responses>`_, but for now the following logic is sufficient for Telemetry:
-
-* `2XX` - success, don't resubmit
-* `4XX` - there was some problem with the request - the client should not try to resubmit as it would just receive the same response
-* `5XX` - there was a server-side error, the client should try to resubmit later
-
 Ping types
 ==========
 
 * :doc:`main <main-ping>` - contains the information collected by Telemetry (Histograms, hang stacks, ...)
 * :doc:`saved-session <main-ping>` - contains the *"classic"* Telemetry payload with measurements covering the whole browser session. Used to make storage of saved-session easier server-side.
 * ``activation`` - *planned* - sent right after installation or profile creation
 * ``upgrade`` - *planned* - sent right after an upgrade
 * ``deletion`` - *planned* - on opt-out we may have to tell the server to delete user data
--- a/toolkit/components/telemetry/tests/unit/head.js
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -4,22 +4,16 @@
 Components.utils.import("resource://gre/modules/TelemetryPing.jsm", this);
 Components.utils.import("resource://gre/modules/Services.jsm", this);
 
 const gIsWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
 const gIsMac = ("@mozilla.org/xpcom/mac-utils;1" in Components.classes);
 const gIsAndroid =  ("@mozilla.org/android/bridge;1" in Components.classes);
 const gIsGonk = ("@mozilla.org/cellbroadcast/gonkservice;1" in Components.classes);
 
-const MILLISECONDS_PER_MINUTE = 60 * 1000;
-const MILLISECONDS_PER_HOUR = 60 * MILLISECONDS_PER_MINUTE;
-const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR;
-
-const HAS_DATAREPORTINGSERVICE = "@mozilla.org/datareporting/service;1" in Components.classes;
-
 let gOldAppInfo = null;
 let gGlobalScope = this;
 
 function loadAddonManager(id, name, version, platformVersion) {
   let ns = {};
   Cu.import("resource://gre/modules/Services.jsm", ns);
   let head = "../../../../mozapps/extensions/test/xpcshell/head_addons.js";
   let file = do_get_file(head);
@@ -86,32 +80,15 @@ function createAppInfo(id, name, version
 
 // Fake the timeout functions for the TelemetryScheduler.
 function fakeSchedulerTimer(set, clear) {
   let session = Components.utils.import("resource://gre/modules/TelemetrySession.jsm");
   session.Policy.setSchedulerTickTimeout = set;
   session.Policy.clearSchedulerTickTimeout = clear;
 }
 
-// Fake the current date.
-function fakeNow(date) {
-  let session = Cu.import("resource://gre/modules/TelemetrySession.jsm");
-  session.Policy.now = () => date;
-  let environment = Cu.import("resource://gre/modules/TelemetryEnvironment.jsm");
-  environment.Policy.now = () => date;
-}
-
-// Return a date that is |offset| ms in the future from |date|.
-function futureDate(date, offset) {
-  return new Date(date.getTime() + offset);
-}
-
-function truncateToDays(aMsec) {
-  return Math.floor(aMsec / MILLISECONDS_PER_DAY);
-}
-
 // Set logging preferences for all the tests.
 Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
 Services.prefs.setBoolPref("toolkit.telemetry.log.dump", true);
 TelemetryPing.initLogging();
 
 // Avoid timers interrupting test behavior.
 fakeSchedulerTimer(() => {}, () => {});
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -22,35 +22,33 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 // The webserver hosting the addons.
 let gHttpServer = null;
 // The URL of the webserver root.
 let gHttpRoot = null;
 // The URL of the data directory, on the webserver.
 let gDataRoot = null;
 
-let gNow = new Date(2010, 1, 1, 12, 0, 0);
-fakeNow(gNow);
-
 const PLATFORM_VERSION = "1.9.2";
 const APP_VERSION = "1";
 const APP_ID = "xpcshell@tests.mozilla.org";
 const APP_NAME = "XPCShell";
 const APP_HOTFIX_VERSION = "2.3.4a";
 
 const DISTRIBUTION_ID = "distributor-id";
 const DISTRIBUTION_VERSION = "4.5.6b";
 const DISTRIBUTOR_NAME = "Some Distributor";
 const DISTRIBUTOR_CHANNEL = "A Channel";
 const PARTNER_NAME = "test";
 const PARTNER_ID = "NicePartner-ID-3785";
 
 const GFX_VENDOR_ID = "0xabcd";
 const GFX_DEVICE_ID = "0x1234";
 
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 // The profile reset date, in milliseconds (Today)
 const PROFILE_RESET_DATE_MS = Date.now();
 // The profile creation date, in milliseconds (Yesterday).
 const PROFILE_CREATION_DATE_MS = PROFILE_RESET_DATE_MS - MILLISECONDS_PER_DAY;
 
 const FLASH_PLUGIN_NAME = "Shockwave Flash";
 const FLASH_PLUGIN_DESC = "A mock flash plugin";
 const FLASH_PLUGIN_VERSION = "\u201c1.1.1.1\u201d";
@@ -172,16 +170,20 @@ function spoofPartnerInfo() {
   prefsToSpoof["mozilla.partner.id"] = PARTNER_ID;
 
   // Spoof the preferences.
   for (let pref in prefsToSpoof) {
     Preferences.set(pref, prefsToSpoof[pref]);
   }
 }
 
+function truncateToDays(aMsec) {
+  return Math.floor(aMsec / MILLISECONDS_PER_DAY);
+}
+
 /**
  * Check that a value is a string and not empty.
  *
  * @param aValue The variable to check.
  * @return True if |aValue| has type "string" and is not empty, False otherwise.
  */
 function checkString(aValue) {
   return (typeof aValue == "string") && (aValue != "");
@@ -594,195 +596,227 @@ 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() {
-  let environmentData = yield TelemetryEnvironment.onInitialized();
+  yield TelemetryEnvironment.init();
+  let environmentData = yield TelemetryEnvironment.getEnvironmentData();
+
   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";
-  gNow = futureDate(gNow, 10 * MILLISECONDS_PER_MINUTE);
-  fakeNow(gNow);
 
   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;
 
-  Preferences.set(PREF_TEST_4, expectedValue);
+  yield TelemetryEnvironment.init();
 
   // 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",
-    (reason, data) => deferred.resolve(data));
-  let oldEnvironmentData = TelemetryEnvironment.currentEnvironment;
+  TelemetryEnvironment.registerChangeListener("testWatchPrefs", deferred.resolve);
 
   // Trigger a change in the watched preferences.
   Preferences.set(PREF_TEST_1, expectedValue);
   Preferences.set(PREF_TEST_2, false);
-  let eventEnvironmentData = yield deferred.promise;
+  yield deferred.promise;
 
   // Unregister the listener.
   TelemetryEnvironment.unregisterChangeListener("testWatchPrefs");
 
   // Check environment contains the correct data.
-  Assert.deepEqual(oldEnvironmentData, eventEnvironmentData);
-  let userPrefs = TelemetryEnvironment.currentEnvironment.settings.userPrefs;
+  let environmentData = yield TelemetryEnvironment.getEnvironmentData();
+
+  let userPrefs = environmentData.settings.userPrefs;
 
   Assert.equal(userPrefs[PREF_TEST_1], expectedValue,
                "Environment contains the correct preference value.");
-  Assert.equal(userPrefs[PREF_TEST_2], "<user-set>",
-               "Report that the pref was user set but the value is not shown.");
+  Assert.equal(userPrefs[PREF_TEST_2], null,
+               "Report that the pref was user set and has no value.");
   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);
 
-  gNow = futureDate(gNow, 10 * MILLISECONDS_PER_MINUTE);
-  fakeNow(gNow);
+  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) => {
-    gNow = futureDate(gNow, 10 * MILLISECONDS_PER_MINUTE);
-    fakeNow(gNow);
     return new Promise(resolve => TelemetryEnvironment.registerChangeListener(
-      "testWatchAddons_Changes" + aExpected, (reason, data) => {
-        Assert.equal(reason, "addons-changed");
+      "testWatchAddons_Changes" + aExpected, () => {
         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);
-  Assert.ok(!(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons));
+
+  yield TelemetryEnvironment.shutdown();
 
   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;
   }
 
-  gNow = futureDate(gNow, 10 * MILLISECONDS_PER_MINUTE);
-  fakeNow(gNow);
-
-  Assert.equal(TelemetryEnvironment.currentEnvironment.addons.activePlugins.length, 1);
+  yield TelemetryEnvironment.init();
 
   let newPlugin = new PluginTag(PLUGIN2_NAME, PLUGIN2_DESC, PLUGIN2_VERSION, true);
   gInstalledPlugins.push(newPlugin);
 
   let deferred = PromiseUtils.defer();
   let receivedNotifications = 0;
-  let callback = (reason, data) => {
+  let callback = () => {
     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;
   }
 
-  gNow = futureDate(gNow, 10 * MILLISECONDS_PER_MINUTE);
-  fakeNow(gNow);
+  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);
 
@@ -793,46 +827,38 @@ 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";
-
-  gNow = futureDate(gNow, 10 * MILLISECONDS_PER_MINUTE);
-  fakeNow(gNow);
+  yield TelemetryEnvironment.init();
 
-  let receivedNotification = false;
-  let deferred = PromiseUtils.defer();
+  let receivedNotifications = 0;
   TelemetryEnvironment.registerChangeListener("testNotInteresting",
-    () => {
-      Assert.ok(!receivedNotification, "Should not receive multiple notifications");
-      receivedNotification = true;
-      deferred.resolve();
-    });
+                                              () => receivedNotifications++);
 
   yield AddonTestUtils.installXPIFromURL(DICTIONARY_ADDON_INSTALL_URL);
   yield AddonTestUtils.installXPIFromURL(INTERESTING_ADDON_INSTALL_URL);
 
-  yield deferred.promise;
-  Assert.ok(!("telemetry-dictionary@tests.mozilla.org" in
-              TelemetryEnvironment.currentEnvironment.addons.activeAddons),
-            "Dictionaries should not appear in active addons.");
+  Assert.equal(receivedNotifications, 1, "We must receive only one notification.");
 
   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,
@@ -853,20 +879,22 @@ 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 = TelemetryEnvironment.currentEnvironment;
+  let data = yield TelemetryEnvironment.getEnvironmentData();
   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.");
   }
@@ -886,52 +914,15 @@ 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.");
-});
 
-add_task(function* test_changeThrottling() {
-  const PREF_TEST = "toolkit.telemetry.test.pref1";
-  let prefsToWatch = {};
-  prefsToWatch[PREF_TEST] = TelemetryEnvironment.RECORD_PREF_STATE;
-  Preferences.reset(PREF_TEST);
-
-  gNow = futureDate(gNow, 10 * MILLISECONDS_PER_MINUTE);
-  fakeNow(gNow);
-
-  // Set the Environment preferences to watch.
-  TelemetryEnvironment._watchPreferences(prefsToWatch);
-  let deferred = PromiseUtils.defer();
-  let changeCount = 0;
-  TelemetryEnvironment.registerChangeListener("testWatchPrefs_throttling", () => {
-    ++changeCount;
-    deferred.resolve();
-  });
-
-  // The first pref change should trigger a notification.
-  Preferences.set(PREF_TEST, 1);
-  yield deferred.promise;
-  Assert.equal(changeCount, 1);
-
-  // We should only get a change notification for second of the following changes.
-  deferred = PromiseUtils.defer();
-  gNow = futureDate(gNow, MILLISECONDS_PER_MINUTE);
-  fakeNow(gNow);
-  Preferences.set(PREF_TEST, 2);
-  gNow = futureDate(gNow, 5 * MILLISECONDS_PER_MINUTE);
-  fakeNow(gNow);
-  Preferences.set(PREF_TEST, 3);
-  yield deferred.promise;
-
-  Assert.equal(changeCount, 2);
-
-  // Unregister the listener.
-  TelemetryEnvironment.unregisterChangeListener("testWatchPrefs_throttling");
+  yield TelemetryEnvironment.shutdown();
 });
 
 add_task(function*() {
   do_test_finished();
 });
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryPing.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryPing.js
@@ -30,16 +30,18 @@ const PLATFORM_VERSION = "1.9.2";
 const APP_VERSION = "1";
 const APP_NAME = "XPCShell";
 
 const PREF_BRANCH = "toolkit.telemetry.";
 const PREF_ENABLED = PREF_BRANCH + "enabled";
 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 const PREF_FHR_SERVICE_ENABLED = "datareporting.healthreport.service.enabled";
 
+const HAS_DATAREPORTINGSERVICE = "@mozilla.org/datareporting/service;1" in Cc;
+
 const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
 
 let gHttpServer = new HttpServer();
 let gServerStarted = false;
 let gRequestIterator = null;
 let gDataReportingClientID = null;
 
 XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
deleted file mode 100644
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryPingShutdown.js
+++ /dev/null
@@ -1,73 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// Test that TelemetryPing sends close to shutdown don't lead
-// to AsyncShutdown timeouts.
-
-"use strict";
-
-const { utils: Cu, interfaces: Ci, classes: Cc } = Components;
-
-Cu.import("resource://gre/modules/Services.jsm", this);
-Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
-Cu.import("resource://gre/modules/Timer.jsm", this);
-Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
-Cu.import("resource://gre/modules/AsyncShutdown.jsm", this);
-Cu.import("resource://testing-common/httpd.js", this);
-
-const PREF_BRANCH = "toolkit.telemetry.";
-const PREF_ENABLED = PREF_BRANCH + "enabled";
-const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
-
-function contentHandler(metadata, response)
-{
-  dump("contentHandler called for path: " + metadata._path + "\n");
-  // We intentionally don't finish writing the response here to let the
-  // client time out.
-  response.processAsync();
-  response.setHeader("Content-Type", "text/plain");
-}
-
-function run_test() {
-  // Addon manager needs a profile directory
-  do_get_profile();
-  loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
-
-  Services.prefs.setBoolPref(PREF_ENABLED, true);
-  Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
-
-  // Send the needed startup notifications to the datareporting service
-  // to ensure that it has been initialized.
-  if (HAS_DATAREPORTINGSERVICE) {
-    let drs = Cc["@mozilla.org/datareporting/service;1"]
-                .getService(Ci.nsISupports)
-                .wrappedJSObject;
-    drs.observe(null, "app-startup", null);
-    drs.observe(null, "profile-after-change", null);
-  }
-
-  run_next_test();
-}
-
-add_task(function* test_sendTimeout() {
-  const TIMEOUT = 100;
-  // Enable testing mode for AsyncShutdown, otherwise some testing-only functionality
-  // is not available.
-  Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
-  Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", TIMEOUT);
-
-  let httpServer = new HttpServer();
-  httpServer.registerPrefixHandler("/", contentHandler);
-  httpServer.start(-1);
-
-  yield TelemetryPing.setup();
-  TelemetryPing.setServer("http://localhost:" + httpServer.identity.primaryPort);
-  TelemetryPing.send("test-ping-type", {});
-
-  // Trigger the AsyncShutdown phase TelemetryPing hangs off.
-  AsyncShutdown.profileBeforeChange._trigger();
-  AsyncShutdown.sendTelemetry._trigger();
-
-  // If we get here, we didn't time out in the shutdown routines.
-  Assert.ok(true, "Didn't time out on shutdown.");
-});
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -56,25 +56,26 @@ const FAILED_PROFILE_LOCK_ATTEMPTS = 2;
 const PR_WRONLY = 0x2;
 const PR_CREATE_FILE = 0x8;
 const PR_TRUNCATE = 0x20;
 const RW_OWNER = parseInt("0600", 8);
 
 const NUMBER_OF_THREADS_TO_LAUNCH = 30;
 let gNumberOfThreadsLaunched = 0;
 
-const MS_IN_ONE_HOUR  = 60 * 60 * 1000;
-const MS_IN_ONE_DAY   = 24 * MS_IN_ONE_HOUR;
+const SEC_IN_ONE_DAY  = 24 * 60 * 60;
+const MS_IN_ONE_DAY   = SEC_IN_ONE_DAY * 1000;
 
 const PREF_BRANCH = "toolkit.telemetry.";
 const PREF_ENABLED = PREF_BRANCH + "enabled";
 const PREF_SERVER = PREF_BRANCH + "server";
 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 const PREF_FHR_SERVICE_ENABLED = "datareporting.healthreport.service.enabled";
 
+const HAS_DATAREPORTINGSERVICE = "@mozilla.org/datareporting/service;1" in Cc;
 const SESSION_RECORDER_EXPECTED = HAS_DATAREPORTINGSERVICE &&
                                   Preferences.get(PREF_FHR_SERVICE_ENABLED, true);
 
 const DATAREPORTING_DIR = "datareporting";
 const ABORTED_PING_FILE_NAME = "aborted-session-ping";
 const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
 
 const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
@@ -94,23 +95,16 @@ XPCOMUtils.defineLazyGetter(this, "gData
           .wrappedJSObject);
 
 function generateUUID() {
   let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
   // strip {}
   return str.substring(1, str.length - 1);
 }
 
-function truncateDateToDays(date) {
-  return new Date(date.getFullYear(),
-                  date.getMonth(),
-                  date.getDate(),
-                  0, 0, 0, 0);
-}
-
 function sendPing() {
   TelemetrySession.gatherStartup();
   if (gServerStarted) {
     TelemetryPing.setServer("http://localhost:" + gHttpServer.identity.primaryPort);
     return TelemetrySession.testPing();
   } else {
     TelemetryPing.setServer("http://doesnotexist");
     return TelemetrySession.testPing();
@@ -125,27 +119,31 @@ function wrapWithExceptionHandler(f) {
       dump("Caught exception: " + ex.message + "\n");
       dump(ex.stack);
       do_test_finished();
     }
   }
   return wrapper;
 }
 
+function futureDate(date, offset) {
+  return new Date(date.getTime() + offset);
+}
+
+function fakeNow(date) {
+  let session = Cu.import("resource://gre/modules/TelemetrySession.jsm");
+  session.Policy.now = () => date;
+}
+
 function fakeGenerateUUID(sessionFunc, subsessionFunc) {
   let session = Cu.import("resource://gre/modules/TelemetrySession.jsm");
   session.Policy.generateSessionUUID = sessionFunc;
   session.Policy.generateSubsessionUUID = subsessionFunc;
 }
 
-function fakeIdleNotification(topic) {
-  let session = Cu.import("resource://gre/modules/TelemetrySession.jsm");
-  return session.TelemetryScheduler.observe(null, topic, null);
-}
-
 function registerPingHandler(handler) {
   gHttpServer.registerPrefixHandler("/submit/telemetry/",
 				   wrapWithExceptionHandler(handler));
 }
 
 function setupTestData() {
   Telemetry.newHistogram(IGNORE_HISTOGRAM, "never", Telemetry.HISTOGRAM_BOOLEAN);
   Telemetry.histogramFrom(IGNORE_CLONED_HISTOGRAM, IGNORE_HISTOGRAM_TO_CLONE);
@@ -1018,20 +1016,18 @@ add_task(function* test_dailyDuplication
 
   let schedulerTickCallback = null;
   let now = new Date(2030, 1, 1, 0, 0, 0);
   fakeNow(now);
   // Fake scheduler functions to control daily collection flow in tests.
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
   yield TelemetrySession.setup();
 
-  // Make sure the daily ping gets triggered at midnight.
-  // We need to make sure that we trigger this after the period where we wait for
-  // the user to become idle.
-  let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
+  // Make sure the daily ping gets triggered just before midnight.
+  let firstDailyDue = new Date(2030, 1, 1, 23, 45, 0);
   fakeNow(firstDailyDue);
 
   // Run a scheduler tick: it should trigger the daily ping.
   Assert.ok(!!schedulerTickCallback);
   yield schedulerTickCallback();
 
   // Get the first daily ping.
   let request = yield gRequestIterator.next();
@@ -1043,16 +1039,17 @@ add_task(function* test_dailyDuplication
 
   // We don't expect to receive any other daily ping in this test, so assert if we do.
   registerPingHandler((req, res) => {
     Assert.ok(false, "No more daily pings should be sent/received in this test.");
   });
 
   // Set the current time to a bit after midnight.
   let secondDailyDue = new Date(firstDailyDue);
+  secondDailyDue.setDate(firstDailyDue.getDate() + 1);
   secondDailyDue.setHours(0);
   secondDailyDue.setMinutes(15);
   fakeNow(secondDailyDue);
 
   // Run a scheduler tick: it should NOT trigger the daily ping.
   Assert.ok(!!schedulerTickCallback);
   yield schedulerTickCallback();
 
@@ -1112,16 +1109,17 @@ add_task(function* test_dailyOverdue() {
 
 add_task(function* test_environmentChange() {
   if (gIsAndroid) {
     // We don't split subsessions on environment changes yet on Android.
     return;
   }
 
   let now = new Date(2040, 1, 1, 12, 0, 0);
+  let nowDay = new Date(2040, 1, 1, 0, 0, 0);
   let timerCallback = null;
   let timerDelay = null;
 
   gRequestIterator = Iterator(new Request());
 
   fakeNow(now);
 
   const PREF_TEST = "toolkit.telemetry.test.pref1";
@@ -1142,49 +1140,41 @@ add_task(function* test_environmentChang
 
   count.clear();
   keyed.clear();
   count.add(1);
   keyed.add("a", 1);
   keyed.add("b", 1);
 
   // Trigger and collect environment-change ping.
-  let startDay = truncateDateToDays(now);
-  now = futureDate(now, 10 * MILLISECONDS_PER_MINUTE);
-  fakeNow(now);
-
   Preferences.set(PREF_TEST, 1);
   let request = yield gRequestIterator.next();
   Assert.ok(!!request);
   let ping = decodeRequestPayload(request);
 
   Assert.equal(ping.type, PING_TYPE_MAIN);
-  Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], undefined);
+  Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], 1);
   Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE);
   let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
-  Assert.equal(subsessionStartDate.toISOString(), startDay.toISOString());
+  Assert.equal(subsessionStartDate.toISOString(), nowDay.toISOString());
 
   Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
   Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["a"].sum, 1);
 
   // Trigger and collect another ping. The histograms should be reset.
-  startDay = truncateDateToDays(now);
-  now = futureDate(now, 10 * MILLISECONDS_PER_MINUTE);
-  fakeNow(now);
-
   Preferences.set(PREF_TEST, 2);
   request = yield gRequestIterator.next();
   Assert.ok(!!request);
   ping = decodeRequestPayload(request);
 
   Assert.equal(ping.type, PING_TYPE_MAIN);
-  Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], 1);
+  Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], 2);
   Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE);
   subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
-  Assert.equal(subsessionStartDate.toISOString(), startDay.toISOString());
+  Assert.equal(subsessionStartDate.toISOString(), nowDay.toISOString());
 
   Assert.equal(ping.payload.histograms[COUNT_ID].sum, 0);
   Assert.deepEqual(ping.payload.keyedHistograms[KEYED_ID], {});
 });
 
 // Checks that an expired histogram file is deleted when loaded.
 add_task(function* test_runOldPingFile() {
   let histogramsFile = getSavedPingFile("old-histograms.dat");
@@ -1252,19 +1242,16 @@ add_task(function* test_savedSessionData
   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
-  fakeNow(new Date(2050, 1, 1, 12, 0, 0));
   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();
@@ -1494,17 +1481,17 @@ add_task(function* test_schedulerEnviron
   const PREF_TEST = "toolkit.telemetry.test.pref1";
   Preferences.reset(PREF_TEST);
   let prefsToWatch = {};
   prefsToWatch[PREF_TEST] = TelemetryEnvironment.RECORD_PREF_VALUE;
 
   gRequestIterator = Iterator(new Request());
 
   // Set a fake current date and start Telemetry.
-  let nowDate = new Date(2060, 10, 18, 0, 00, 0);
+  let nowDate = new Date(2009, 10, 18, 0, 00, 0);
   fakeNow(nowDate);
   let schedulerTickCallback = null;
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
   yield TelemetrySession.reset();
   TelemetryEnvironment._watchPreferences(prefsToWatch);
 
   // Set the current time at midnight.
   let future = futureDate(nowDate, MS_IN_ONE_DAY);
@@ -1615,112 +1602,16 @@ add_task(function* test_pingExtendedStat
   }
 
   Assert.ok("addonManager" in ping.payload.simpleMeasurements,
             "addonManager must be sent if the extended set is on.");
   Assert.ok("UITelemetry" in ping.payload.simpleMeasurements,
             "UITelemetry must be sent if the extended set is on.");
 });
 
-add_task(function* test_schedulerUserIdle() {
-  if (gIsAndroid || gIsGonk) {
-    // We don't have the aborted session or the daily ping here.
-    return;
-  }
-
-  const SCHEDULER_TICK_INTERVAL_MS = 5 * 60 * 1000;
-  const SCHEDULER_TICK_IDLE_INTERVAL_MS = 60 * 60 * 1000;
-
-  let now = new Date(2010, 1, 1, 11, 0, 0);
-  fakeNow(now);
-
-  let schedulerTimeout = 0;
-  fakeSchedulerTimer((callback, timeout) => {
-    schedulerTimeout = timeout;
-  }, () => {});
-  yield TelemetrySession.reset();
-  gRequestIterator = Iterator(new Request());
-
-  // When not idle, the scheduler should have a 5 minutes tick interval.
-  Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS);
-
-  // Send an "idle" notification to the scheduler.
-  fakeIdleNotification("idle");
-
-  // When idle, the scheduler should have a 1hr tick interval.
-  Assert.equal(schedulerTimeout, SCHEDULER_TICK_IDLE_INTERVAL_MS);
-
-  // Send an "active" notification to the scheduler.
-  fakeIdleNotification("active");
-
-  // When user is back active, the scheduler tick should be 5 minutes again.
-  Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS);
-
-  // We should not miss midnight when going to idle.
-  now.setHours(23);
-  now.setMinutes(50);
-  fakeIdleNotification("idle");
-  Assert.equal(schedulerTimeout, 10 * 60 * 1000);
-
-  yield TelemetrySession.shutdown();
-});
-
-add_task(function* test_sendDailyOnIdle() {
-  if (gIsAndroid || gIsGonk) {
-    // We don't have the aborted session or the daily ping here.
-    return;
-  }
-
-  let now = new Date(2040, 1, 1, 11, 0, 0);
-  fakeNow(now);
-
-  let schedulerTickCallback = 0;
-  fakeSchedulerTimer((callback, timeout) => {
-    schedulerTickCallback = callback;
-  }, () => {});
-  yield TelemetrySession.reset();
-
-  // Make sure we are not sending a daily before midnight when active.
-  now = new Date(2040, 1, 1, 23, 55, 0);
-  fakeNow(now);
-  registerPingHandler((req, res) => {
-    Assert.ok(false, "No daily ping should be received yet when the user is active.");
-  });
-  yield fakeIdleNotification("active");
-
-  // The Request constructor restores the previous ping handler.
-  gRequestIterator = Iterator(new Request());
-
-  // We should receive a daily ping after midnight.
-  now = new Date(2040, 1, 2, 0, 05, 0);
-  fakeNow(now);
-  yield schedulerTickCallback();
-
-  let request = yield gRequestIterator.next();
-  Assert.ok(!!request);
-  let ping = decodeRequestPayload(request);
-
-  Assert.equal(ping.type, PING_TYPE_MAIN);
-  Assert.equal(ping.payload.info.reason, REASON_DAILY);
-
-  // We should also trigger a ping when going idle shortly before next midnight.
-  now = new Date(2040, 1, 2, 23, 55, 0);
-  fakeNow(now);
-  yield fakeIdleNotification("idle");
-
-  request = yield gRequestIterator.next();
-  Assert.ok(!!request);
-  ping = decodeRequestPayload(request);
-
-  Assert.equal(ping.type, PING_TYPE_MAIN);
-  Assert.equal(ping.payload.info.reason, REASON_DAILY);
-
-  yield TelemetrySession.shutdown();
-});
-
 add_task(function* stopServer(){
   gHttpServer.stop(do_test_finished);
 });
 
 // An iterable sequence of http requests
 function Request() {
   let defers = [];
   let current = 0;
--- a/toolkit/components/telemetry/tests/unit/xpcshell.ini
+++ b/toolkit/components/telemetry/tests/unit/xpcshell.ini
@@ -28,17 +28,16 @@ skip-if = android_version == "18"
 [test_TelemetryLockCount.js]
 [test_TelemetryLog.js]
 [test_TelemetryPing.js]
 # Bug 676989: test fails consistently on Android
 # fail-if = os == "android"
 # Bug 1144395: crash on Android 4.3
 skip-if = android_version == "18"
 [test_TelemetryPing_idle.js]
-[test_TelemetryPingShutdown.js]
 [test_TelemetryStopwatch.js]
 [test_TelemetryPingBuildID.js]
 # Bug 1144395: crash on Android 4.3
 skip-if = android_version == "18"
 [test_ThirdPartyCookieProbe.js]
 skip-if = true # Bug 1149284
 [test_TelemetrySendOldPings.js]
 skip-if = debug == true || os == "android" # Disabled due to intermittent orange on Android
deleted file mode 100644
--- a/toolkit/modules/ObjectUtils.jsm
+++ /dev/null
@@ -1,131 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-// Portions of this file are originally from narwhal.js (http://narwhaljs.org)
-// Copyright (c) 2009 Thomas Robinson <280north.com>
-// MIT license: http://opensource.org/licenses/MIT
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = [
-  "ObjectUtils"
-];
-
-const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
-
-this.ObjectUtils = {
-  /**
-   * This tests objects & values for deep equality.
-   *
-   * We check using the most exact approximation of equality between two objects
-   * to keep the chance of false positives to a minimum.
-   * `JSON.stringify` is not designed to be used for this purpose; objects may
-   * have ambiguous `toJSON()` implementations that would influence the test.
-   *
-   * @param a (mixed) Object or value to be compared.
-   * @param b (mixed) Object or value to be compared.
-   * @return Boolean Whether the objects are deep equal.
-   */
-  deepEqual: function(a, b) {
-    return _deepEqual(a, b);
-  },
-};
-
-// ... Start of previously MIT-licensed code.
-// This deepEqual implementation is originally from narwhal.js (http://narwhaljs.org)
-// Copyright (c) 2009 Thomas Robinson <280north.com>
-// MIT license: http://opensource.org/licenses/MIT
-
-function _deepEqual(a, b) {
-  // The numbering below refers to sections in the CommonJS spec.
-
-  // 7.1 All identical values are equivalent, as determined by ===.
-  if (a === b) {
-    return true;
-  // 7.2 If the b value is a Date object, the a value is
-  // equivalent if it is also a Date object that refers to the same time.
-  } else if (instanceOf(a, "Date") && instanceOf(b, "Date")) {
-    if (isNaN(a.getTime()) && isNaN(b.getTime()))
-      return true;
-    return a.getTime() === b.getTime();
-  // 7.3 If the b value is a RegExp object, the a value is
-  // equivalent if it is also a RegExp object with the same source and
-  // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).
-  } else if (instanceOf(a, "RegExp") && instanceOf(b, "RegExp")) {
-    return a.source === b.source &&
-           a.global === b.global &&
-           a.multiline === b.multiline &&
-           a.lastIndex === b.lastIndex &&
-           a.ignoreCase === b.ignoreCase;
-  // 7.4 Other pairs that do not both pass typeof value == "object",
-  // equivalence is determined by ==.
-  } else if (typeof a != "object" && typeof b != "object") {
-    return a == b;
-  // 7.5 For all other Object pairs, including Array objects, equivalence is
-  // determined by having the same number of owned properties (as verified
-  // with Object.prototype.hasOwnProperty.call), the same set of keys
-  // (although not necessarily the same order), equivalent values for every
-  // corresponding key, and an identical 'prototype' property. Note: this
-  // accounts for both named and indexed properties on Arrays.
-  } else {
-    return objEquiv(a, b);
-  }
-}
-
-function instanceOf(object, type) {
-  return Object.prototype.toString.call(object) == "[object " + type + "]";
-}
-
-function isUndefinedOrNull(value) {
-  return value === null || value === undefined;
-}
-
-function isArguments(object) {
-  return instanceOf(object, "Arguments");
-}
-
-function objEquiv(a, b) {
-  if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) {
-    return false;
-  }
-  // An identical 'prototype' property.
-  if (a.prototype !== b.prototype) {
-    return false;
-  }
-  // Object.keys may be broken through screwy arguments passing. Converting to
-  // an array solves the problem.
-  if (isArguments(a)) {
-    if (!isArguments(b)) {
-      return false;
-    }
-    a = pSlice.call(a);
-    b = pSlice.call(b);
-    return _deepEqual(a, b);
-  }
-  let ka, kb;
-  try {
-    ka = Object.keys(a);
-    kb = Object.keys(b);
-  } catch (e) {
-    // Happens when one is a string literal and the other isn't
-    return false;
-  }
-  // Having the same number of owned properties (keys incorporates
-  // hasOwnProperty)
-  if (ka.length != kb.length)
-    return false;
-  // The same set of keys (although not necessarily the same order),
-  ka.sort();
-  kb.sort();
-  // Equivalent values for every corresponding key, and possibly expensive deep
-  // test
-  for (let key of ka) {
-    if (!_deepEqual(a[key], b[key])) {
-      return false;
-    }
-  }
-  return true;
-}
-
-// ... End of previously MIT-licensed code.
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -25,17 +25,16 @@ EXTRA_JS_MODULES += [
     'Finder.jsm',
     'Geometry.jsm',
     'Http.jsm',
     'InlineSpellChecker.jsm',
     'InlineSpellCheckerContent.jsm',
     'LoadContextInfo.jsm',
     'Log.jsm',
     'NewTabUtils.jsm',
-    'ObjectUtils.jsm',
     'PageMenu.jsm',
     'PageMetadata.jsm',
     'PerformanceStats.jsm',
     'PermissionsUtils.jsm',
     'PopupNotifications.jsm',
     'Preferences.jsm',
     'PrivateBrowsingUtils.jsm',
     'ProfileAge.jsm',
deleted file mode 100644
--- a/toolkit/modules/tests/xpcshell/test_ObjectUtils.js
+++ /dev/null
@@ -1,92 +0,0 @@
-Components.utils.import("resource://gre/modules/ObjectUtils.jsm");
-
-function run_test() {
-  run_next_test();
-}
-
-add_task(function* test_deepEqual() {
-  let deepEqual = ObjectUtils.deepEqual.bind(ObjectUtils);
-  // CommonJS 7.2
-  Assert.ok(deepEqual(new Date(2000, 3, 14), new Date(2000, 3, 14)), "deepEqual date");
-  Assert.ok(deepEqual(new Date(NaN), new Date(NaN)), "deepEqual invalid dates");
-
-  Assert.ok(!deepEqual(new Date(), new Date(2000, 3, 14)), "deepEqual date");
-
-  // 7.3
-  Assert.ok(deepEqual(/a/, /a/));
-  Assert.ok(deepEqual(/a/g, /a/g));
-  Assert.ok(deepEqual(/a/i, /a/i));
-  Assert.ok(deepEqual(/a/m, /a/m));
-  Assert.ok(deepEqual(/a/igm, /a/igm));
-  Assert.ok(!deepEqual(/ab/, /a/));
-  Assert.ok(!deepEqual(/a/g, /a/));
-  Assert.ok(!deepEqual(/a/i, /a/));
-  Assert.ok(!deepEqual(/a/m, /a/));
-  Assert.ok(!deepEqual(/a/igm, /a/im));
-
-  let re1 = /a/;
-  re1.lastIndex = 3;
-  Assert.ok(!deepEqual(re1, /a/));
-
-  // 7.4
-  Assert.ok(deepEqual(4, "4"), "deepEqual == check");
-  Assert.ok(deepEqual(true, 1), "deepEqual == check");
-  Assert.ok(!deepEqual(4, "5"), "deepEqual == check");
-
-  // 7.5
-  // having the same number of owned properties && the same set of keys
-  Assert.ok(deepEqual({a: 4}, {a: 4}));
-  Assert.ok(deepEqual({a: 4, b: "2"}, {a: 4, b: "2"}));
-  Assert.ok(deepEqual([4], ["4"]));
-  Assert.ok(!deepEqual({a: 4}, {a: 4, b: true}));
-  Assert.ok(deepEqual(["a"], {0: "a"}));
-
-  let a1 = [1, 2, 3];
-  let a2 = [1, 2, 3];
-  a1.a = "test";
-  a1.b = true;
-  a2.b = true;
-  a2.a = "test";
-  Assert.ok(!deepEqual(Object.keys(a1), Object.keys(a2)));
-  Assert.ok(deepEqual(a1, a2));
-
-  let nbRoot = {
-    toString: function() { return this.first + " " + this.last; }
-  };
-
-  function nameBuilder(first, last) {
-    this.first = first;
-    this.last = last;
-    return this;
-  }
-  nameBuilder.prototype = nbRoot;
-
-  function nameBuilder2(first, last) {
-    this.first = first;
-    this.last = last;
-    return this;
-  }
-  nameBuilder2.prototype = nbRoot;
-
-  let nb1 = new nameBuilder("Ryan", "Dahl");
-  let nb2 = new nameBuilder2("Ryan", "Dahl");
-
-  Assert.ok(deepEqual(nb1, nb2));
-
-  nameBuilder2.prototype = Object;
-  nb2 = new nameBuilder2("Ryan", "Dahl");
-  Assert.ok(!deepEqual(nb1, nb2));
-
-  // String literal + object
-  Assert.ok(!deepEqual("a", {}));
-
-  // Make sure deepEqual doesn't loop forever on circular refs
-
-  let b = {};
-  b.b = b;
-
-  let c = {};
-  c.b = c;
-
-  Assert.ok(!deepEqual(b, c));
-});
--- a/toolkit/modules/tests/xpcshell/xpcshell.ini
+++ b/toolkit/modules/tests/xpcshell/xpcshell.ini
@@ -11,17 +11,16 @@ support-files =
 [test_BinarySearch.js]
 [test_DeferredTask.js]
 [test_dict.js]
 [test_FileUtils.js]
 [test_GMPInstallManager.js]
 [test_Http.js]
 [test_Log.js]
 [test_NewTabUtils.js]
-[test_ObjectUtils.js]
 [test_PermissionsUtils.js]
 [test_Preferences.js]
 [test_Promise.js]
 [test_PromiseUtils.js]
 [test_propertyListsUtils.js]
 [test_readCertPrefs.js]
 [test_Services.js]
 [test_sqlite.js]