Bug 847662 - Part 3: Move provider management code into provider manager; r=rnewman
authorGregory Szorc <gps@mozilla.com>
Mon, 11 Mar 2013 14:12:24 -0700
changeset 124413 97d6871f02e7c671fdcb23b5e9ae7ee2b4c9b410
parent 124412 5e2536a86e7f66acf6426f2f2815b9b2d8dc6d98
child 124414 9e353ac8efaa8dfff5ffb11dc11d0e52b62df16d
push id24420
push userryanvm@gmail.com
push dateTue, 12 Mar 2013 19:35:31 +0000
treeherdermozilla-central@79b8e0a0bdb7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs847662
milestone22.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 847662 - Part 3: Move provider management code into provider manager; r=rnewman
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.");
@@ -395,183 +394,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
@@ -677,30 +512,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.
@@ -1093,26 +928,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);