Bug 847662 - Part 3: Move provider management code into provider manager; r=rnewman, a=bajaj
authorGregory Szorc <gps@mozilla.com>
Mon, 11 Mar 2013 14:12:24 -0700
changeset 132439 51893bbcd397bb8fa15a76a9dc5fb0f2fcad6ece
parent 132438 7fa0ed380ba95d9f18eeb7916f369e2a9880369f
child 132440 1688de15d68b4476a47b6fc74f4188ca3323ef83
push id2323
push userbbajaj@mozilla.com
push dateMon, 01 Apr 2013 19:47:02 +0000
treeherdermozilla-beta@7712be144d91 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman, bajaj
bugs847662
milestone21.0a2
Bug 847662 - Part 3: Move provider management code into provider manager; r=rnewman, a=bajaj
services/healthreport/healthreporter.jsm
services/healthreport/tests/xpcshell/test_healthreporter.js
services/metrics/Metrics.jsm
services/metrics/modules-testing/mocks.jsm
services/metrics/providermanager.jsm
services/metrics/tests/xpcshell/test_metrics_provider_manager.js
--- a/services/healthreport/healthreporter.jsm
+++ b/services/healthreport/healthreporter.jsm
@@ -85,18 +85,16 @@ function AbstractHealthReporter(branch, 
   this._initializedDeferred = Promise.defer();
   this._shutdownRequested = false;
   this._shutdownInitiated = false;
   this._shutdownComplete = false;
   this._shutdownCompleteCallback = null;
 
   this._errors = [];
 
-  this._pullOnlyProviders = {};
-  this._pullOnlyProvidersRegistered = false;
   this._lastDailyDate = null;
 
   // Yes, this will probably run concurrently with remaining constructor work.
   let hasFirstRun = this._prefs.get("service.firstRun", false);
   this._initHistogram = hasFirstRun ? TELEMETRY_INIT : TELEMETRY_INIT_FIRSTRUN;
   this._dbOpenHistogram = hasFirstRun ? TELEMETRY_DB_OPEN : TELEMETRY_DB_OPEN_FIRSTRUN;
 
   TelemetryStopwatch.start(this._initHistogram, this);
@@ -172,22 +170,23 @@ AbstractHealthReporter.prototype = Objec
   _initializeProviderManager: function () {
     if (this._collector) {
       throw new Error("Provider manager has already been initialized.");
     }
 
     this._log.info("Initializing provider manager.");
     this._providerManager = new Metrics.ProviderManager(this._storage);
     this._providerManager.onProviderError = this._recordError.bind(this);
+    this._providerManager.onProviderInit = this._initProvider.bind(this);
     this._providerManagerInProgress = true;
 
     let catString = this._prefs.get("service.providerCategories") || "";
     if (catString.length) {
       for (let category of catString.split(",")) {
-        yield this.registerProvidersFromCategoryManager(category);
+        yield this._providerManager.registerProvidersFromCategoryManager(category);
       }
     }
   },
 
   _onProviderManagerInitialized: function () {
     TelemetryStopwatch.finish(this._initHistogram, this);
     delete this._initHistogram;
     this._log.debug("Provider manager initialized.");
@@ -424,183 +423,19 @@ AbstractHealthReporter.prototype = Objec
    * This will only return providers that are currently initialized. If
    * a provider is lazy initialized (like pull-only providers) this
    * will likely not return anything.
    */
   getProvider: function (name) {
     return this._providerManager.getProvider(name);
   },
 
-  /**
-   * Register a `Metrics.Provider` with this instance.
-   *
-   * This needs to be called or no data will be collected. See also
-   * `registerProvidersFromCategoryManager`.
-   *
-   * @param provider
-   *        (Metrics.Provider) The provider to register for collection.
-   */
-  registerProvider: function (provider) {
-    return this._providerManager.registerProvider(provider);
-  },
-
-  /**
-   * Registers a provider from its constructor function.
-   *
-   * If the provider is pull-only, it will be stashed away and
-   * initialized later. Null will be returned.
-   *
-   * If it is not pull-only, it will be initialized immediately and a
-   * promise will be returned. The promise will be resolved when the
-   * provider has finished initializing.
-   */
-  registerProviderFromType: function (type) {
-    let proto = type.prototype;
-    if (proto.pullOnly) {
-      this._log.info("Provider is pull-only. Deferring initialization: " +
-                     proto.name);
-      this._pullOnlyProviders[proto.name] = type;
-
-      return null;
-    }
-
-    let provider = this.initProviderFromType(type);
-    return this.registerProvider(provider);
-  },
-
-  /**
-   * Registers providers from a category manager category.
-   *
-   * This examines the specified category entries and registers found
-   * providers.
-   *
-   * Category entries are essentially JS modules and the name of the symbol
-   * within that module that is a `Metrics.Provider` instance.
-   *
-   * The category entry name is the name of the JS type for the provider. The
-   * value is the resource:// URI to import which makes this type available.
-   *
-   * Example entry:
-   *
-   *   FooProvider resource://gre/modules/foo.jsm
-   *
-   * One can register entries in the application's .manifest file. e.g.
-   *
-   *   category healthreport-js-provider FooProvider resource://gre/modules/foo.jsm
-   *
-   * Then to load them:
-   *
-   *   let reporter = getHealthReporter("healthreport.");
-   *   reporter.registerProvidersFromCategoryManager("healthreport-js-provider");
-   *
-   * @param category
-   *        (string) Name of category to query and load from.
-   */
-  registerProvidersFromCategoryManager: function (category) {
-    this._log.info("Registering providers from category: " + category);
-    let cm = Cc["@mozilla.org/categorymanager;1"]
-               .getService(Ci.nsICategoryManager);
-
-    let promises = [];
-    let enumerator = cm.enumerateCategory(category);
-    while (enumerator.hasMoreElements()) {
-      let entry = enumerator.getNext()
-                            .QueryInterface(Ci.nsISupportsCString)
-                            .toString();
-
-      let uri = cm.getCategoryEntry(category, entry);
-      this._log.info("Attempting to load provider from category manager: " +
-                     entry + " from " + uri);
-
-      try {
-        let ns = {};
-        Cu.import(uri, ns);
-
-        let promise = this.registerProviderFromType(ns[entry]);
-        if (promise) {
-          promises.push(promise);
-        }
-      } catch (ex) {
-        this._recordError("Error registering provider from category manager : " +
-                          entry + ": ", ex);
-        continue;
-      }
-    }
-
-    return Task.spawn(function wait() {
-      for (let promise of promises) {
-        yield promise;
-      }
-    });
-  },
-
-  initProviderFromType: function (providerType) {
-    let provider = new providerType();
+  _initProvider: function (provider) {
     provider.initPreferences(this._branch + "provider.");
     provider.healthReporter = this;
-
-    return provider;
-  },
-
-  /**
-   * Ensure that pull-only providers are registered.
-   */
-  ensurePullOnlyProvidersRegistered: function () {
-    if (this._pullOnlyProvidersRegistered) {
-      return Promise.resolve();
-    }
-
-    let onFinished = function () {
-      this._pullOnlyProvidersRegistered = true;
-
-      return Promise.resolve();
-    }.bind(this);
-
-    return Task.spawn(function registerPullProviders() {
-      for each (let providerType in this._pullOnlyProviders) {
-        try {
-          let provider = this.initProviderFromType(providerType);
-          yield this.registerProvider(provider);
-        } catch (ex) {
-          this._recordError("Error registering pull-only provider", ex);
-        }
-      }
-    }.bind(this)).then(onFinished, onFinished);
-  },
-
-  ensurePullOnlyProvidersUnregistered: function () {
-    if (!this._pullOnlyProvidersRegistered) {
-      return Promise.resolve();
-    }
-
-    let onFinished = function () {
-      this._pullOnlyProvidersRegistered = false;
-
-      return Promise.resolve();
-    }.bind(this);
-
-    return Task.spawn(function unregisterPullProviders() {
-      for (let provider of this._providerManager.providers) {
-        if (!provider.pullOnly) {
-          continue;
-        }
-
-        this._log.info("Shutting down pull-only provider: " +
-                       provider.name);
-
-        try {
-          yield provider.shutdown();
-        } catch (ex) {
-          this._recordError("Error when shutting down provider: " +
-                            provider.name, ex);
-        } finally {
-          this._providerManager.unregisterProvider(provider.name);
-        }
-      }
-    }.bind(this)).then(onFinished, onFinished);
   },
 
   /**
    * Record an exception for reporting in the payload.
    *
    * A side effect is the exception is logged.
    *
    * Note that callers need to be extra sensitive about ensuring personal
@@ -706,30 +541,30 @@ AbstractHealthReporter.prototype = Objec
    * @param asObject
    *        (bool) Whether to resolve an object or JSON-encoded string of that
    *        object (the default).
    *
    * @return Promise<Object | string>
    */
   collectAndObtainJSONPayload: function (asObject=false) {
     return Task.spawn(function collectAndObtain() {
-      yield this.ensurePullOnlyProvidersRegistered();
+      yield this._providerManager.ensurePullOnlyProvidersRegistered();
 
       let payload;
       let error;
 
       try {
         yield this.collectMeasurements();
         payload = yield this.getJSONPayload(asObject);
       } catch (ex) {
         error = ex;
         this._collectException("Error collecting and/or retrieving JSON payload",
                                ex);
       } finally {
-        yield this.ensurePullOnlyProvidersUnregistered();
+        yield this._providerManager.ensurePullOnlyProvidersUnregistered();
 
         if (error) {
           throw error;
         }
       }
 
       // We hold off throwing to ensure that behavior between finally
       // and generators and throwing is sane.
@@ -1122,26 +957,26 @@ HealthReporter.prototype = Object.freeze
 
   /**
    * Called to initiate a data upload.
    *
    * The passed argument is a `DataSubmissionRequest` from policy.jsm.
    */
   requestDataUpload: function (request) {
     return Task.spawn(function doUpload() {
-      yield this.ensurePullOnlyProvidersRegistered();
+      yield this._providerManager.ensurePullOnlyProvidersRegistered();
       try {
         yield this.collectMeasurements();
         try {
           yield this._uploadData(request);
         } catch (ex) {
           this._onSubmitDataRequestFailure(ex);
         }
       } finally {
-        yield this.ensurePullOnlyProvidersUnregistered();
+        yield this._providerManager.ensurePullOnlyProvidersUnregistered();
       }
     }.bind(this));
   },
 
   /**
    * Request that server data be deleted.
    *
    * If deletion is scheduled to occur immediately, a promise will be returned
--- a/services/healthreport/tests/xpcshell/test_healthreporter.js
+++ b/services/healthreport/tests/xpcshell/test_healthreporter.js
@@ -168,61 +168,42 @@ add_task(function test_shutdown_when_pro
   };
 
   // This will hang if shutdown logic is busted.
   reporter._waitForShutdown();
   do_check_eq(reporter.providerManagerShutdownCount, 1);
   do_check_eq(reporter.storageCloseCount, 1);
 });
 
-add_task(function test_register_providers_from_category_manager() {
-  const category = "healthreporter-js-modules";
-
-  let cm = Cc["@mozilla.org/categorymanager;1"]
-             .getService(Ci.nsICategoryManager);
-  cm.addCategoryEntry(category, "DummyProvider",
-                      "resource://testing-common/services/metrics/mocks.jsm",
-                      false, true);
-
-  let reporter = yield getReporter("category_manager");
-  try {
-    do_check_eq(reporter._providerManager._providers.size, 0);
-    yield reporter.registerProvidersFromCategoryManager(category);
-    do_check_eq(reporter._providerManager._providers.size, 1);
-  } finally {
-    reporter._shutdown();
-  }
-});
-
 // Pull-only providers are only initialized at collect time.
 add_task(function test_pull_only_providers() {
   const category = "healthreporter-constant-only";
 
   let cm = Cc["@mozilla.org/categorymanager;1"]
              .getService(Ci.nsICategoryManager);
   cm.addCategoryEntry(category, "DummyProvider",
                       "resource://testing-common/services/metrics/mocks.jsm",
                       false, true);
   cm.addCategoryEntry(category, "DummyConstantProvider",
                       "resource://testing-common/services/metrics/mocks.jsm",
                       false, true);
 
   let reporter = yield getReporter("constant_only_providers");
   try {
     do_check_eq(reporter._providerManager._providers.size, 0);
-    yield reporter.registerProvidersFromCategoryManager(category);
+    yield reporter._providerManager.registerProvidersFromCategoryManager(category);
     do_check_eq(reporter._providerManager._providers.size, 1);
     do_check_true(reporter._storage.hasProvider("DummyProvider"));
     do_check_false(reporter._storage.hasProvider("DummyConstantProvider"));
     do_check_neq(reporter.getProvider("DummyProvider"), null);
     do_check_null(reporter.getProvider("DummyConstantProvider"));
 
-    yield reporter.ensurePullOnlyProvidersRegistered();
+    yield reporter._providerManager.ensurePullOnlyProvidersRegistered();
     yield reporter.collectMeasurements();
-    yield reporter.ensurePullOnlyProvidersUnregistered();
+    yield reporter._providerManager.ensurePullOnlyProvidersUnregistered();
 
     do_check_eq(reporter._providerManager._providers.size, 1);
     do_check_true(reporter._storage.hasProvider("DummyConstantProvider"));
 
     let mID = reporter._storage.measurementID("DummyConstantProvider", "DummyMeasurement", 1);
     let values = yield reporter._storage.getMeasurementValues(mID);
     do_check_true(values.singular.size > 0);
   } finally {
@@ -231,17 +212,17 @@ add_task(function test_pull_only_provide
 });
 
 add_task(function test_collect_daily() {
   let reporter = yield getReporter("collect_daily");
 
   try {
     let now = new Date();
     let provider = new DummyProvider();
-    yield reporter.registerProvider(provider);
+    yield reporter._providerManager.registerProvider(provider);
     yield reporter.collectMeasurements();
 
     do_check_eq(provider.collectConstantCount, 1);
     do_check_eq(provider.collectDailyCount, 1);
 
     yield reporter.collectMeasurements();
     do_check_eq(provider.collectConstantCount, 1);
     do_check_eq(provider.collectDailyCount, 1);
@@ -290,17 +271,17 @@ add_task(function test_json_payload_simp
     reporter._shutdown();
   }
 });
 
 add_task(function test_json_payload_dummy_provider() {
   let reporter = yield getReporter("json_payload_dummy_provider");
 
   try {
-    yield reporter.registerProvider(new DummyProvider());
+    yield reporter._providerManager.registerProvider(new DummyProvider());
     yield reporter.collectMeasurements();
     let payload = yield reporter.getJSONPayload();
     print(payload);
     let o = JSON.parse(payload);
 
     let name = "DummyProvider.DummyMeasurement";
     do_check_eq(Object.keys(o.data.last).length, 1);
     do_check_true(name in o.data.last);
@@ -309,17 +290,17 @@ add_task(function test_json_payload_dumm
     reporter._shutdown();
   }
 });
 
 add_task(function test_collect_and_obtain_json_payload() {
   let reporter = yield getReporter("collect_and_obtain_json_payload");
 
   try {
-    yield reporter.registerProvider(new DummyProvider());
+    yield reporter._providerManager.registerProvider(new DummyProvider());
     let payload = yield reporter.collectAndObtainJSONPayload();
     do_check_eq(typeof payload, "string");
 
     let o = JSON.parse(payload);
     do_check_true("DummyProvider.DummyMeasurement" in o.data.last);
 
     payload = yield reporter.collectAndObtainJSONPayload(true);
     do_check_eq(typeof payload, "object");
@@ -338,17 +319,17 @@ add_task(function test_constant_only_pro
                       "resource://testing-common/services/metrics/mocks.jsm",
                       false, true);
   cm.addCategoryEntry(category, "DummyConstantProvider",
                       "resource://testing-common/services/metrics/mocks.jsm",
                       false, true);
 
   let reporter = yield getReporter("constant_only_providers_in_json_payload");
   try {
-    yield reporter.registerProvidersFromCategoryManager(category);
+    yield reporter._providerManager.registerProvidersFromCategoryManager(category);
 
     let payload = yield reporter.collectAndObtainJSONPayload();
     let o = JSON.parse(payload);
     do_check_true("DummyProvider.DummyMeasurement" in o.data.last);
     do_check_true("DummyConstantProvider.DummyMeasurement" in o.data.last);
 
     let providers = reporter._providerManager.providers;
     do_check_eq(providers.length, 1);
@@ -383,17 +364,17 @@ add_task(function test_constant_only_pro
   }
 });
 
 add_task(function test_json_payload_multiple_days() {
   let reporter = yield getReporter("json_payload_multiple_days");
 
   try {
     let provider = new DummyProvider();
-    yield reporter.registerProvider(provider);
+    yield reporter._providerManager.registerProvider(provider);
 
     let now = new Date();
     let m = provider.getMeasurement("DummyMeasurement", 1);
     for (let i = 0; i < 200; i++) {
       let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY);
       yield m.incrementDailyCounter("daily-counter", date);
       yield m.addDailyDiscreteNumeric("daily-discrete-numeric", i, date);
       yield m.addDailyDiscreteNumeric("daily-discrete-numeric", i + 100, date);
@@ -413,17 +394,17 @@ add_task(function test_json_payload_mult
     reporter._shutdown();
   }
 });
 
 add_task(function test_idle_daily() {
   let reporter = yield getReporter("idle_daily");
   try {
     let provider = new DummyProvider();
-    yield reporter.registerProvider(provider);
+    yield reporter._providerManager.registerProvider(provider);
 
     let now = new Date();
     let m = provider.getMeasurement("DummyMeasurement", 1);
     for (let i = 0; i < 200; i++) {
       let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY);
       yield m.incrementDailyCounter("daily-counter", date);
     }
 
@@ -454,18 +435,18 @@ add_task(function test_data_submission_t
   } finally {
     reporter._shutdown();
   }
 });
 
 add_task(function test_data_submission_success() {
   let [reporter, server] = yield getReporterAndServer("data_submission_success");
   try {
-    yield reporter.registerProviderFromType(DummyProvider);
-    yield reporter.registerProviderFromType(DummyConstantProvider);
+    yield reporter._providerManager.registerProviderFromType(DummyProvider);
+    yield reporter._providerManager.registerProviderFromType(DummyConstantProvider);
 
     do_check_eq(reporter.lastPingDate.getTime(), 0);
     do_check_false(reporter.haveRemoteData());
 
     let deferred = Promise.defer();
 
     let request = new DataSubmissionRequest(deferred, new Date());
     reporter.requestDataUpload(request);
@@ -483,17 +464,17 @@ add_task(function test_data_submission_s
   } finally {
     yield shutdownServer(server);
   }
 });
 
 add_task(function test_recurring_daily_pings() {
   let [reporter, server] = yield getReporterAndServer("recurring_daily_pings");
   try {
-    reporter.registerProvider(new DummyProvider());
+    reporter._providerManager.registerProvider(new DummyProvider());
 
     let policy = reporter._policy;
 
     defineNow(policy, policy._futureDate(-24 * 60 * 68 * 1000));
     policy.recordUserAcceptance();
     defineNow(policy, policy.nextDataSubmissionDate);
     let promise = policy.checkStateAndTrigger();
     do_check_neq(promise, null);
--- a/services/metrics/Metrics.jsm
+++ b/services/metrics/Metrics.jsm
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 #ifndef MERGED_COMPARTMENT
 
 this.EXPORTED_SYMBOLS = ["Metrics"];
 
-const {utils: Cu} = Components;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 
 #endif
 
 // We concatenate the JSMs together to eliminate compartment overhead.
 // This is a giant hack until compartment overhead is no longer an
 // issue.
--- a/services/metrics/modules-testing/mocks.jsm
+++ b/services/metrics/modules-testing/mocks.jsm
@@ -35,17 +35,19 @@ DummyMeasurement.prototype = {
     "daily-last-text": {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT},
     "last-numeric": {type: Metrics.Storage.FIELD_LAST_NUMERIC},
     "last-text": {type: Metrics.Storage.FIELD_LAST_TEXT},
   },
 };
 
 
 this.DummyProvider = function DummyProvider(name="DummyProvider") {
-  this.name = name;
+  Object.defineProperty(this, "name", {
+    value: name,
+  });
 
   this.measurementTypes = [DummyMeasurement];
 
   Metrics.Provider.call(this);
 
   this.constantMeasurementName = "DummyMeasurement";
   this.collectConstantCount = 0;
   this.throwDuringCollectConstantData = null;
@@ -54,16 +56,18 @@ this.DummyProvider = function DummyProvi
   this.collectDailyCount = 0;
 
   this.havePushedMeasurements = true;
 }
 
 DummyProvider.prototype = {
   __proto__: Metrics.Provider.prototype,
 
+  name: "DummyProvider",
+
   collectConstantData: function () {
     this.collectConstantCount++;
 
     if (this.throwDuringCollectConstantData) {
       throw new Error(this.throwDuringCollectConstantData);
     }
 
     return this.enqueueStorageOperation(function doStorage() {
@@ -95,11 +99,13 @@ DummyProvider.prototype = {
 
 this.DummyConstantProvider = function () {
   DummyProvider.call(this, "DummyConstantProvider");
 }
 
 DummyConstantProvider.prototype = {
   __proto__: DummyProvider.prototype,
 
+  name: "DummyConstantProvider",
+
   pullOnly: true,
 };
 
--- a/services/metrics/providermanager.jsm
+++ b/services/metrics/providermanager.jsm
@@ -2,17 +2,17 @@
  * 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/. */
 
 "use strict";
 
 #ifndef MERGED_COMPARTMENT
 this.EXPORTED_SYMBOLS = ["ProviderManager"];
 
-const {utils: Cu} = Components;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
 #endif
 
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-common/utils.js");
@@ -27,16 +27,23 @@ Cu.import("resource://services-common/ut
 this.ProviderManager = function (storage) {
   this._log = Log4Moz.repository.getLogger("Services.Metrics.ProviderManager");
 
   this._providers = new Map();
   this._storage = storage;
 
   this._providerInitQueue = [];
   this._providerInitializing = false;
+
+  this._pullOnlyProviders = {};
+  this._pullOnlyProvidersRegistered = false;
+
+  // Callback to allow customization of providers after they are constructed
+  // but before they call out into their initialization code.
+  this.onProviderInit = null;
 }
 
 this.ProviderManager.prototype = Object.freeze({
   get providers() {
     let providers = [];
     for (let [name, entry] of this._providers) {
       providers.push(entry.provider);
     }
@@ -53,16 +60,82 @@ this.ProviderManager.prototype = Object.
     if (!provider) {
       return null;
     }
 
     return provider.provider;
   },
 
   /**
+   * Registers providers from a category manager category.
+   *
+   * This examines the specified category entries and registers found
+   * providers.
+   *
+   * Category entries are essentially JS modules and the name of the symbol
+   * within that module that is a `Metrics.Provider` instance.
+   *
+   * The category entry name is the name of the JS type for the provider. The
+   * value is the resource:// URI to import which makes this type available.
+   *
+   * Example entry:
+   *
+   *   FooProvider resource://gre/modules/foo.jsm
+   *
+   * One can register entries in the application's .manifest file. e.g.
+   *
+   *   category healthreport-js-provider FooProvider resource://gre/modules/foo.jsm
+   *
+   * Then to load them:
+   *
+   *   let reporter = getHealthReporter("healthreport.");
+   *   reporter.registerProvidersFromCategoryManager("healthreport-js-provider");
+   *
+   * @param category
+   *        (string) Name of category to query and load from.
+   */
+  registerProvidersFromCategoryManager: function (category) {
+    this._log.info("Registering providers from category: " + category);
+    let cm = Cc["@mozilla.org/categorymanager;1"]
+               .getService(Ci.nsICategoryManager);
+
+    let promises = [];
+    let enumerator = cm.enumerateCategory(category);
+    while (enumerator.hasMoreElements()) {
+      let entry = enumerator.getNext()
+                            .QueryInterface(Ci.nsISupportsCString)
+                            .toString();
+
+      let uri = cm.getCategoryEntry(category, entry);
+      this._log.info("Attempting to load provider from category manager: " +
+                     entry + " from " + uri);
+
+      try {
+        let ns = {};
+        Cu.import(uri, ns);
+
+        let promise = this.registerProviderFromType(ns[entry]);
+        if (promise) {
+          promises.push(promise);
+        }
+      } catch (ex) {
+        this._recordError("Error registering provider from category manager : " +
+                          entry + ": ", ex);
+        continue;
+      }
+    }
+
+    return Task.spawn(function wait() {
+      for (let promise of promises) {
+        yield promise;
+      }
+    });
+  },
+
+  /**
    * Registers a `MetricsProvider` with this manager.
    *
    * Once a `MetricsProvider` is registered, data will be collected from it
    * whenever we collect data.
    *
    * The returned value is a promise that will be resolved once registration
    * is complete.
    *
@@ -89,25 +162,124 @@ this.ProviderManager.prototype = Object.
     if (this._providerInitQueue.length == 1) {
       this._popAndInitProvider();
     }
 
     return deferred.promise;
   },
 
   /**
+   * Registers a provider from its constructor function.
+   *
+   * If the provider is pull-only, it will be stashed away and
+   * initialized later. Null will be returned.
+   *
+   * If it is not pull-only, it will be initialized immediately and a
+   * promise will be returned. The promise will be resolved when the
+   * provider has finished initializing.
+   */
+  registerProviderFromType: function (type) {
+    let proto = type.prototype;
+    if (proto.pullOnly) {
+      this._log.info("Provider is pull-only. Deferring initialization: " +
+                     proto.name);
+      this._pullOnlyProviders[proto.name] = type;
+
+      return null;
+    }
+
+    let provider = this._initProviderFromType(type);
+    return this.registerProvider(provider);
+  },
+
+  /**
+   * Initializes a provider from its type.
+   *
+   * This is how a constructor function should be turned into a provider
+   * instance.
+   *
+   * A side-effect is the provider is registered with the manager.
+   */
+  _initProviderFromType: function (type) {
+    let provider = new type();
+    if (this.onProviderInit) {
+      this.onProviderInit(provider);
+    }
+
+    return provider;
+  },
+
+  /**
    * Remove a named provider from the manager.
    *
    * It is the caller's responsibility to shut down the provider
    * instance.
    */
   unregisterProvider: function (name) {
     this._providers.delete(name);
   },
 
+  /**
+   * Ensure that pull-only providers are registered.
+   */
+  ensurePullOnlyProvidersRegistered: function () {
+    if (this._pullOnlyProvidersRegistered) {
+      return Promise.resolve();
+    }
+
+    let onFinished = function () {
+      this._pullOnlyProvidersRegistered = true;
+
+      return Promise.resolve();
+    }.bind(this);
+
+    return Task.spawn(function registerPullProviders() {
+      for each (let providerType in this._pullOnlyProviders) {
+        try {
+          let provider = this._initProviderFromType(providerType);
+          yield this.registerProvider(provider);
+        } catch (ex) {
+          this._recordError("Error registering pull-only provider", ex);
+        }
+      }
+    }.bind(this)).then(onFinished, onFinished);
+  },
+
+  ensurePullOnlyProvidersUnregistered: function () {
+    if (!this._pullOnlyProvidersRegistered) {
+      return Promise.resolve();
+    }
+
+    let onFinished = function () {
+      this._pullOnlyProvidersRegistered = false;
+
+      return Promise.resolve();
+    }.bind(this);
+
+    return Task.spawn(function unregisterPullProviders() {
+      for (let provider of this.providers) {
+        if (!provider.pullOnly) {
+          continue;
+        }
+
+        this._log.info("Shutting down pull-only provider: " +
+                       provider.name);
+
+        try {
+          yield provider.shutdown();
+        } catch (ex) {
+          this._recordError("Error when shutting down provider: " +
+                            provider.name, ex);
+        } finally {
+          this.unregisterProvider(provider.name);
+        }
+      }
+    }.bind(this)).then(onFinished, onFinished);
+  },
+
   _popAndInitProvider: function () {
     if (!this._providerInitQueue.length || this._providerInitializing) {
       return;
     }
 
     let [provider, deferred] = this._providerInitQueue.shift();
     this._providerInitializing = true;
 
--- a/services/metrics/tests/xpcshell/test_metrics_provider_manager.js
+++ b/services/metrics/tests/xpcshell/test_metrics_provider_manager.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const {utils: Cu} = Components;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://testing-common/services/metrics/mocks.jsm");
 
 function run_test() {
   run_next_test();
 };
 
@@ -44,16 +44,36 @@ add_task(function test_register_provider
 
   manager.unregisterProvider(dummy.name);
   do_check_eq(manager._providers.size, 0);
   do_check_null(manager.getProvider(dummy.name));
 
   yield storage.close();
 });
 
+add_task(function test_register_providers_from_category_manager() {
+  const category = "metrics-providers-js-modules";
+
+  let cm = Cc["@mozilla.org/categorymanager;1"]
+             .getService(Ci.nsICategoryManager);
+  cm.addCategoryEntry(category, "DummyProvider",
+                      "resource://testing-common/services/metrics/mocks.jsm",
+                      false, true);
+
+  let storage = yield Metrics.Storage("register_providers_from_category_manager");
+  let manager = new Metrics.ProviderManager(storage);
+  try {
+    do_check_eq(manager._providers.size, 0);
+    yield manager.registerProvidersFromCategoryManager(category);
+    do_check_eq(manager._providers.size, 1);
+  } finally {
+    yield storage.close();
+  }
+});
+
 add_task(function test_collect_constant_data() {
   let storage = yield Metrics.Storage("collect_constant_data");
   let errorCount = 0;
   let manager= new Metrics.ProviderManager(storage);
   manager.onProviderError = function () { errorCount++; }
   let provider = new DummyProvider();
   yield manager.registerProvider(provider);