Bug 812608 - Part 2: Refactor FHR on top of new Metrics APIs; r=rnewman
authorGregory Szorc <gps@mozilla.com>
Sun, 06 Jan 2013 12:13:27 -0800
changeset 117843 ee9453c65e339986b43ecdb7ffb72c3eff5fafe6
parent 117842 f3f08413e8635fcb94f9365cb947a63ab92af796
child 117844 d4ce01fb62551508ec19742b8143db19de5d6d90
push id24116
push usergszorc@mozilla.com
push dateMon, 07 Jan 2013 08:22:48 +0000
treeherdermozilla-central@66d595814554 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs812608
milestone20.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 812608 - Part 2: Refactor FHR on top of new Metrics APIs; r=rnewman This also includes a lot of revamped Firefox Health Report features. The payload format has changed. There is now robust service shutdown logic. There are more prefs to control behavior. It's almost a rewritten service.
services/healthreport/HealthReportService.js
services/healthreport/healthreport-prefs.js
services/healthreport/healthreporter.jsm
services/healthreport/profile.jsm
services/healthreport/providers.jsm
services/healthreport/tests/xpcshell/test_healthreporter.js
services/healthreport/tests/xpcshell/test_profile.js
services/healthreport/tests/xpcshell/test_provider_appinfo.js
services/healthreport/tests/xpcshell/test_provider_sysinfo.js
--- a/services/healthreport/HealthReportService.js
+++ b/services/healthreport/HealthReportService.js
@@ -5,127 +5,146 @@
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-common/preferences.js");
 
 
-const INITIAL_STARTUP_DELAY_MSEC = 10 * 1000;
 const BRANCH = "healthreport.";
-const JS_PROVIDERS_CATEGORY = "healthreport-js-provider";
-
+const DEFAULT_LOAD_DELAY_MSEC = 10 * 1000;
 
 /**
  * The Firefox Health Report XPCOM service.
  *
- * This instantiates an instance of HealthReporter (assuming it is enabled)
- * and starts it upon application startup.
+ * External consumers will be interested in the "reporter" property of this
+ * service. This property is a `HealthReporter` instance that powers the
+ * service. The property may be null if the Health Report service is not
+ * enabled.
+ *
+ * EXAMPLE USAGE
+ * =============
+ *
+ * let reporter = Cc["@mozilla.org/healthreport/service;1"]
+ *                  .getService(Ci.nsISupports)
+ *                  .wrappedJSObject
+ *                  .reporter;
  *
- * One can obtain a reference to the underlying HealthReporter instance by
- * accessing .reporter. If this property is null, the reporter isn't running
- * yet or has been disabled.
+ * if (reporter.haveRemoteData) {
+ *   // ...
+ * }
+ *
+ * IMPLEMENTATION NOTES
+ * ====================
+ *
+ * In order to not adversely impact application start time, the `HealthReporter`
+ * instance is not initialized until a few seconds after "final-ui-startup."
+ * The exact delay is configurable via preferences so it can be adjusted with
+ * a hotfix extension if the default value is ever problematic.
+ *
+ * Shutdown of the `HealthReporter` instance is handled completely within the
+ * instance (it registers observers on initialization). See the notes on that
+ * type for more.
  */
 this.HealthReportService = function HealthReportService() {
   this.wrappedJSObject = this;
 
-  this.prefs = new Preferences(BRANCH);
+  this._prefs = new Preferences(BRANCH);
+
   this._reporter = null;
 }
 
 HealthReportService.prototype = {
   classID: Components.ID("{e354c59b-b252-4040-b6dd-b71864e3e35c}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
 
   observe: function observe(subject, topic, data) {
     // If the background service is disabled, don't do anything.
-    if (!this.prefs.get("serviceEnabled", true)) {
+    if (!this._prefs.get("service.enabled", true)) {
       return;
     }
 
     let os = Cc["@mozilla.org/observer-service;1"]
                .getService(Ci.nsIObserverService);
 
     switch (topic) {
       case "app-startup":
         os.addObserver(this, "final-ui-startup", true);
         break;
 
       case "final-ui-startup":
         os.removeObserver(this, "final-ui-startup");
-        os.addObserver(this, "quit-application", true);
+
+        let delayInterval = this._prefs.get("service.loadDelayMsec") ||
+                            DEFAULT_LOAD_DELAY_MSEC;
 
         // Delay service loading a little more so things have an opportunity
         // to cool down first.
         this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
         this.timer.initWithCallback({
           notify: function notify() {
             // Side effect: instantiates the reporter instance if not already
             // accessed.
             let reporter = this.reporter;
             delete this.timer;
           }.bind(this),
-        }, INITIAL_STARTUP_DELAY_MSEC, this.timer.TYPE_ONE_SHOT);
-
-        break;
+        }, delayInterval, this.timer.TYPE_ONE_SHOT);
 
-      case "quit-application-granted":
-        if (this.reporter) {
-          this.reporter.stop();
-        }
-
-        os.removeObserver(this, "quit-application");
         break;
     }
   },
 
   /**
    * The HealthReporter instance associated with this service.
+   *
+   * If the service is disabled, this will return null.
+   *
+   * The obtained instance may not be fully initialized.
    */
   get reporter() {
-    if (!this.prefs.get("serviceEnabled", true)) {
+    if (!this._prefs.get("service.enabled", true)) {
       return null;
     }
 
     if (this._reporter) {
       return this._reporter;
     }
 
+    let ns = {};
     // Lazy import so application startup isn't adversely affected.
-    let ns = {};
+    Cu.import("resource://gre/modules/Task.jsm", ns);
+    Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm", ns);
     Cu.import("resource://services-common/log4moz.js", ns);
-    Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm", ns);
 
     // How many times will we rewrite this code before rolling it up into a
     // generic module? See also bug 451283.
     const LOGGERS = [
-      "Metrics",
       "Services.HealthReport",
       "Services.Metrics",
       "Services.BagheeraClient",
+      "Sqlite.Connection.healthreport",
     ];
 
     let prefs = new Preferences(BRANCH + "logging.");
     if (prefs.get("consoleEnabled", true)) {
       let level = prefs.get("consoleLevel", "Warn");
       let appender = new ns.Log4Moz.ConsoleAppender();
       appender.level = ns.Log4Moz.Level[level] || ns.Log4Moz.Level.Warn;
 
       for (let name of LOGGERS) {
         let logger = ns.Log4Moz.repository.getLogger(name);
         logger.addAppender(appender);
       }
     }
 
+    // The reporter initializes in the background.
     this._reporter = new ns.HealthReporter(BRANCH);
-    this._reporter.registerProvidersFromCategoryManager(JS_PROVIDERS_CATEGORY);
-    this._reporter.start();
 
     return this._reporter;
   },
 };
 
 Object.freeze(HealthReportService.prototype);
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HealthReportService]);
--- a/services/healthreport/healthreport-prefs.js
+++ b/services/healthreport/healthreport-prefs.js
@@ -1,22 +1,24 @@
 /* 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/. */
 
 pref("healthreport.documentServerURI", "https://data.mozilla.com/");
 pref("healthreport.documentServerNamespace", "metrics");
-pref("healthreport.serviceEnabled", true);
 pref("healthreport.logging.consoleEnabled", true);
 pref("healthreport.logging.consoleLevel", "Warn");
 pref("healthreport.policy.currentDaySubmissionFailureCount", 0);
 pref("healthreport.policy.dataSubmissionEnabled", true);
 pref("healthreport.policy.dataSubmissionPolicyAccepted", false);
 pref("healthreport.policy.dataSubmissionPolicyBypassAcceptance", false);
 pref("healthreport.policy.dataSubmissionPolicyNotifiedTime", "0");
 pref("healthreport.policy.dataSubmissionPolicyResponseType", "");
 pref("healthreport.policy.dataSubmissionPolicyResponseTime", "0");
 pref("healthreport.policy.firstRunTime", "0");
 pref("healthreport.policy.lastDataSubmissionFailureTime", "0");
 pref("healthreport.policy.lastDataSubmissionRequestedTime", "0");
 pref("healthreport.policy.lastDataSubmissionSuccessfulTime", "0");
 pref("healthreport.policy.nextDataSubmissionTime", "0");
+pref("healthreport.service.enabled", true);
+pref("healthreport.service.loadDelayMsec", 10000);
+pref("healthreport.service.providerCategories", "healthreport-js-provider");
 
--- a/services/healthreport/healthreporter.jsm
+++ b/services/healthreport/healthreporter.jsm
@@ -3,74 +3,142 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["HealthReporter"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-common/bagheeraclient.js");
 Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/services/healthreport/policy.jsm");
-Cu.import("resource://gre/modules/services/metrics/collector.jsm");
 
 
 // Oldest year to allow in date preferences. This module was implemented in
 // 2012 and no dates older than that should be encountered.
 const OLDEST_ALLOWED_YEAR = 2012;
 
+const DAYS_IN_PAYLOAD = 180;
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
+
+const DEFAULT_DATABASE_NAME = "healthreport.sqlite";
+
 
 /**
- * Coordinates collection and submission of metrics.
+ * Coordinates collection and submission of health report metrics.
  *
  * This is the main type for Firefox Health Report. It glues all the
  * lower-level components (such as collection and submission) together.
  *
  * An instance of this type is created as an XPCOM service. See
  * HealthReportService.js and HealthReportComponents.manifest.
  *
  * It is theoretically possible to have multiple instances of this running
  * in the application. For example, this type may one day handle submission
  * of telemetry data as well. However, there is some moderate coupling between
  * this type and *the* Firefox Health Report (e.g. the policy). This could
  * be abstracted if needed.
  *
+ * IMPLEMENTATION NOTES
+ * ====================
+ *
+ * Initialization and shutdown are somewhat complicated and worth explaining
+ * in extra detail.
+ *
+ * The complexity is driven by the requirements of SQLite connection management.
+ * Once you have a SQLite connection, it isn't enough to just let the
+ * application shut down. If there is an open connection or if there are
+ * outstanding SQL statements come XPCOM shutdown time, Storage will assert.
+ * On debug builds you will crash. On release builds you will get a shutdown
+ * hang. This must be avoided!
+ *
+ * During initialization, the second we create a SQLite connection (via
+ * Metrics.Storage) we register observers for application shutdown. The
+ * "quit-application" notification initiates our shutdown procedure. The
+ * subsequent "profile-do-change" notification ensures it has completed.
+ *
+ * The handler for "profile-do-change" may result in event loop spinning. This
+ * is because of race conditions between our shutdown code and application
+ * shutdown.
+ *
+ * All of our shutdown routines are async. There is the potential that these
+ * async functions will not complete before XPCOM shutdown. If they don't
+ * finish in time, we could get assertions in Storage. Our solution is to
+ * initiate storage early in the shutdown cycle ("quit-application").
+ * Hopefully all the async operations have completed by the time we reach
+ * "profile-do-change." If so, great. If not, we spin the event loop until
+ * they have completed, avoiding potential race conditions.
+ *
  * @param branch
  *        (string) The preferences branch to use for state storage. The value
  *        must end with a period (.).
  */
-this.HealthReporter = function HealthReporter(branch) {
+function HealthReporter(branch) {
   if (!branch.endsWith(".")) {
-    throw new Error("Branch argument must end with a period (.): " + branch);
+    throw new Error("Branch must end with a period (.): " + branch);
   }
 
   this._log = Log4Moz.repository.getLogger("Services.HealthReport.HealthReporter");
+  this._log.info("Initializing health reporter instance against " + branch);
 
   this._prefs = new Preferences(branch);
 
-  let policyBranch = new Preferences(branch + "policy.");
-  this._policy = new HealthReportPolicy(policyBranch, this);
-  this._collector = new MetricsCollector();
-
   if (!this.serverURI) {
     throw new Error("No server URI defined. Did you forget to define the pref?");
   }
 
   if (!this.serverNamespace) {
     throw new Error("No server namespace defined. Did you forget a pref?");
   }
+
+  this._dbName = this._prefs.get("dbName") || DEFAULT_DATABASE_NAME;
+
+  let policyBranch = new Preferences(branch + "policy.");
+  this._policy = new HealthReportPolicy(policyBranch, this);
+
+  this._storage = null;
+  this._storageInProgress = false;
+  this._collector = null;
+  this._initialized = false;
+  this._initializeHadError = false;
+  this._initializedDeferred = Promise.defer();
+  this._shutdownRequested = false;
+  this._shutdownInitiated = false;
+  this._shutdownComplete = false;
+  this._shutdownCompleteCallback = null;
+
+  this._ensureDirectoryExists(this._stateDir)
+      .then(this._onStateDirCreated.bind(this),
+            this._onInitError.bind(this));
+
 }
 
-HealthReporter.prototype = {
+HealthReporter.prototype = Object.freeze({
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+  /**
+   * Whether the service is fully initialized and running.
+   *
+   * If this is false, it is not safe to call most functions.
+   */
+  get initialized() {
+    return this._initialized;
+  },
+
   /**
    * When we last successfully submitted data to the server.
    *
    * This is sent as part of the upload. This is redundant with similar data
    * in the policy because we like the modules to be loosely coupled and the
    * similar data in the policy is only used for forensic purposes.
    */
   get lastPingDate() {
@@ -141,147 +209,347 @@ HealthReporter.prototype = {
     this._prefs.set("lastSubmitID", value || "");
   },
 
   /**
    * Whether remote data is currently stored.
    *
    * @return bool
    */
-  haveRemoteData: function haveRemoteData() {
+  haveRemoteData: function () {
     return !!this.lastSubmitID;
   },
 
+  //----------------------------------------------------
+  // SERVICE CONTROL FUNCTIONS
+  //
+  // You shouldn't need to call any of these externally.
+  //----------------------------------------------------
+
+  _onInitError: function (error) {
+    this._log.error("Error during initialization: " +
+                    CommonUtils.exceptionStr(error));
+    this._initializeHadError = true;
+    this._initiateShutdown();
+    this._initializedDeferred.reject(error);
+
+    // FUTURE consider poisoning prototype's functions so calls fail with a
+    // useful error message.
+  },
+
+  _onStateDirCreated: function () {
+    // As soon as we have could storage, we need to register cleanup or
+    // else bad things happen on shutdown.
+    Services.obs.addObserver(this, "quit-application", false);
+    Services.obs.addObserver(this, "profile-before-change", false);
+
+    this._storageInProgress = true;
+    Metrics.Storage(this._dbName).then(this._onStorageCreated.bind(this),
+                                       this._onInitError.bind(this));
+  },
+
+  // Called when storage has been opened.
+  _onStorageCreated: function (storage) {
+    this._log.info("Storage initialized.");
+    this._storage = storage;
+    this._storageInProgress = false;
+
+    if (this._shutdownRequested) {
+      this._initiateShutdown();
+      return;
+    }
+
+    Task.spawn(this._initializeCollector.bind(this))
+        .then(this._onCollectorInitialized.bind(this),
+              this._onInitError.bind(this));
+  },
+
+  _initializeCollector: function () {
+    if (this._collector) {
+      throw new Error("Collector has already been initialized.");
+    }
+
+    this._log.info("Initializing collector.");
+    this._collector = new Metrics.Collector(this._storage);
+
+    let catString = this._prefs.get("service.providerCategories") || "";
+    if (catString.length) {
+      for (let category of catString.split(",")) {
+        yield this.registerProvidersFromCategoryManager(category);
+      }
+    }
+  },
+
+  _onCollectorInitialized: function () {
+    if (this._shutdownRequested) {
+      this._initiateShutdown();
+      return;
+    }
+
+    this._policy.startPolling();
+    this._log.info("HealthReporter started.");
+    this._initialized = true;
+    Services.obs.addObserver(this, "idle-daily", false);
+    this._initializedDeferred.resolve(this);
+  },
+
+  // nsIObserver to handle shutdown.
+  observe: function (subject, topic, data) {
+    switch (topic) {
+      case "quit-application":
+        Services.obs.removeObserver(this, "quit-application");
+        this._initiateShutdown();
+        break;
+
+      case "profile-before-change":
+        Services.obs.removeObserver(this, "profile-before-change");
+        this._waitForShutdown();
+        break;
+
+      case "idle-daily":
+        this._performDailyMaintenance();
+        break;
+    }
+  },
+
+  _initiateShutdown: function () {
+    // Ensure we only begin the main shutdown sequence once.
+    if (this._shutdownInitiated) {
+      this._log.warn("Shutdown has already been initiated. No-op.");
+      return;
+    }
+
+    this._log.info("Request to shut down.");
+
+    this._initialized = false;
+    this._shutdownRequested = true;
+
+    // Safe to call multiple times.
+    this._policy.stopPolling();
+
+    // If storage is in the process of initializing, we need to wait for it
+    // to finish before continuing. The initialization process will call us
+    // again once storage has initialized.
+    if (this._storageInProgress) {
+      this._log.warn("Storage is in progress of initializing. Waiting to finish.");
+      return;
+    }
+
+    // Everything from here must only be performed once or else race conditions
+    // could occur.
+    this._shutdownInitiated = true;
+
+    Services.obs.removeObserver(this, "idle-daily");
+
+    // If we have collectors, we need to shut down providers.
+    if (this._collector) {
+      let onShutdown = this._onCollectorShutdown.bind(this);
+      Task.spawn(this._shutdownCollector.bind(this))
+          .then(onShutdown, onShutdown);
+      return;
+    }
+
+    this._onCollectorShutdown();
+  },
+
+  _shutdownCollector: function () {
+    for (let provider of this._collector.providers) {
+      try {
+        yield provider.shutdown();
+      } catch (ex) {
+        this._log.warn("Error when shutting down provider: " +
+                       CommonUtils.exceptionStr(ex));
+      }
+    }
+  },
+
+  _onCollectorShutdown: function () {
+    this._collector = null;
+
+    if (this._storage) {
+      let onClose = this._onStorageClose.bind(this);
+      this._storage.close().then(onClose, onClose);
+      return;
+    }
+
+    this._onStorageClose();
+  },
+
+  _onStorageClose: function (error) {
+    if (error) {
+      this._log.warn("Error when closing storage: " +
+                     CommonUtils.exceptionStr(error));
+    }
+
+    this._storage = null;
+    this._shutdownComplete = true;
+
+    if (this._shutdownCompleteCallback) {
+      this._shutdownCompleteCallback();
+    }
+  },
+
+  _waitForShutdown: function () {
+    if (this._shutdownComplete) {
+      return;
+    }
+
+    this._shutdownCompleteCallback = Async.makeSpinningCallback();
+    this._shutdownCompleteCallback.wait();
+    this._shutdownCompleteCallback = null;
+  },
+
   /**
-   * Perform post-construction initialization and start background activity.
-   *
-   * If this isn't called, no data upload will occur.
+   * Convenience method to shut down the instance.
    *
-   * This returns a promise that will be fulfilled when all initialization
-   * activity is completed. It is not safe for this instance to perform
-   * additional actions until this promise has been resolved.
+   * This should *not* be called outside of tests.
    */
-  start: function start() {
-    let onExists = function onExists() {
-      this._policy.startPolling();
-      this._log.info("HealthReporter started.");
-
-      return Promise.resolve();
-    }.bind(this);
-
-    return this._ensureDirectoryExists(this._stateDir)
-               .then(onExists);
+  _shutdown: function () {
+    this._initiateShutdown();
+    this._waitForShutdown();
   },
 
   /**
-   * Stop background functionality.
+   * Return a promise that is resolved once the service has been initialized.
    */
-  stop: function stop() {
-    this._policy.stopPolling();
+  onInit: function () {
+    if (this._initializeHadError) {
+      throw new Error("Service failed to initialize.");
+    }
+
+    if (this._initialized) {
+      return Promise.resolve(this);
+    }
+
+    return this._initializedDeferred.promise;
   },
 
+  _performDailyMaintenance: function () {
+    this._log.info("Request to perform daily maintenance.");
+
+    if (!this._initialized) {
+      return;
+    }
+
+    let now = new Date();
+    let cutoff = new Date(now.getTime() - MILLISECONDS_PER_DAY * (DAYS_IN_PAYLOAD - 1));
+
+    // The operation is enqueued and put in a transaction by the storage module.
+    this._storage.pruneDataBefore(cutoff);
+  },
+
+  //--------------------
+  // Provider Management
+  //--------------------
+
   /**
-   * Register a `MetricsProvider` with this instance.
+   * Register a `Metrics.Provider` with this instance.
    *
    * This needs to be called or no data will be collected. See also
    * registerProvidersFromCategoryManager`.
    *
    * @param provider
-   *        (MetricsProvider) The provider to register for collection.
+   *        (Metrics.Provider) The provider to register for collection.
    */
-  registerProvider: function registerProvider(provider) {
+  registerProvider: function (provider) {
     return this._collector.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 `MetricsProvider` instance.
+   * 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 = new HealthReporter("healthreport.");
+   *   let reporter = getHealthReporter("healthreport.");
    *   reporter.registerProvidersFromCategoryManager("healthreport-js-provider");
    *
    * @param category
    *        (string) Name of category to query and load from.
    */
-  registerProvidersFromCategoryManager:
-    function registerProvidersFromCategoryManager(category) {
-
+  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 provider = new ns[entry]();
-        this.registerProvider(provider);
+        promises.push(this.registerProvider(provider));
       } catch (ex) {
         this._log.warn("Error registering provider from category manager: " +
                        entry + "; " + CommonUtils.exceptionStr(ex));
         continue;
       }
     }
+
+    return Task.spawn(function wait() {
+      for (let promise of promises) {
+        yield promise;
+      }
+    });
   },
 
   /**
    * Collect all measurements for all registered providers.
    */
-  collectMeasurements: function collectMeasurements() {
-    return this._collector.collectConstantMeasurements();
+  collectMeasurements: function () {
+    return this._collector.collectConstantData();
   },
 
   /**
    * Record the user's rejection of the data submission policy.
    *
    * This should be what everything uses to disable data submission.
    *
    * @param reason
    *        (string) Why data submission is being disabled.
    */
-  recordPolicyRejection: function recordPolicyRejection(reason) {
+  recordPolicyRejection: function (reason) {
     this._policy.recordUserRejection(reason);
   },
 
   /**
    * Record the user's acceptance of the data submission policy.
    *
    * This should be what everything uses to enable data submission.
    *
    * @param reason
    *        (string) Why data submission is being enabled.
    */
-  recordPolicyAcceptance: function recordPolicyAcceptance(reason) {
+  recordPolicyAcceptance: function (reason) {
     this._policy.recordUserAcceptance(reason);
   },
 
   /**
    * Whether the data submission policy has been accepted.
    *
    * If this is true, health data will be submitted unless one of the kill
    * switches is active.
@@ -301,44 +569,128 @@ HealthReporter.prototype = {
   /**
    * Request that server data be deleted.
    *
    * If deletion is scheduled to occur immediately, a promise will be returned
    * that will be fulfilled when the deletion attempt finishes. Otherwise,
    * callers should poll haveRemoteData() to determine when remote data is
    * deleted.
    */
-  requestDeleteRemoteData: function requestDeleteRemoteData(reason) {
+  requestDeleteRemoteData: function (reason) {
     if (!this.lastSubmitID) {
       return;
     }
 
     return this._policy.deleteRemoteData(reason);
   },
 
-  getJSONPayload: function getJSONPayload() {
+  getJSONPayload: function () {
+    return Task.spawn(this._getJSONPayload.bind(this, this._now()));
+  },
+
+  _getJSONPayload: function (now) {
+    let pingDateString = this._formatDate(now);
+    this._log.info("Producing JSON payload for " + pingDateString);
+
     let o = {
       version: 1,
-      thisPingDate: this._formatDate(this._now()),
-      providers: {},
+      thisPingDate: pingDateString,
+      data: {last: {}, days: {}},
     };
 
+    let outputDataDays = o.data.days;
+
+    // We need to be careful that data in errors does not leak potentially
+    // private information.
+    // FUTURE ask Privacy if we can put exception stacks in here.
+    let errors = [];
+
     let lastPingDate = this.lastPingDate;
     if (lastPingDate.getTime() > 0) {
       o.lastPingDate = this._formatDate(lastPingDate);
     }
 
-    for (let [name, provider] of this._collector.collectionResults) {
-      o.providers[name] = provider;
+    for (let provider of this._collector.providers) {
+      let providerName = provider.name;
+
+      let providerEntry = {
+        measurements: {},
+      };
+
+      for (let [measurementKey, measurement] of provider.measurements) {
+        let name = providerName + "." + measurement.name + "." + measurement.version;
+
+        let serializer;
+        try {
+          serializer = measurement.serializer(measurement.SERIALIZE_JSON);
+        } catch (ex) {
+          this._log.warn("Error obtaining serializer for measurement: " + name +
+                         ": " + CommonUtils.exceptionStr(ex));
+          errors.push("Could not obtain serializer: " + name);
+          continue;
+        }
+
+        let data;
+        try {
+          data = yield this._storage.getMeasurementValues(measurement.id);
+        } catch (ex) {
+          this._log.warn("Error obtaining data for measurement: " +
+                         name + ": " + CommonUtils.exceptionStr(ex));
+          errors.push("Could not obtain data: " + name);
+          continue;
+        }
+
+        if (data.singular.size) {
+          try {
+            o.data.last[name] = serializer.singular(data.singular);
+          } catch (ex) {
+            this._log.warn("Error serializing data: " + CommonUtils.exceptionStr(ex));
+            errors.push("Error serializing singular: " + name);
+            continue;
+          }
+        }
+
+        let dataDays = data.days;
+        for (let i = 0; i < DAYS_IN_PAYLOAD; i++) {
+          let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY);
+          if (!dataDays.hasDay(date)) {
+            continue;
+          }
+          let dateFormatted = this._formatDate(date);
+
+          try {
+            let serialized = serializer.daily(dataDays.getDay(date));
+            if (!serialized) {
+              continue;
+            }
+
+            if (!(dateFormatted in outputDataDays)) {
+              outputDataDays[dateFormatted] = {};
+            }
+
+            outputDataDays[dateFormatted][name] = serialized;
+          } catch (ex) {
+            this._log.warn("Error populating data for day: " +
+                           CommonUtils.exceptionStr(ex));
+            errors.push("Could not serialize day: " + name +
+                        " ( " + dateFormatted + ")");
+            continue;
+          }
+        }
+      }
     }
 
-    return JSON.stringify(o);
+    if (errors.length) {
+      o.errors = errors;
+    }
+
+    throw new Task.Result(JSON.stringify(o));
   },
 
-  _onBagheeraResult: function _onBagheeraResult(request, isDelete, result) {
+  _onBagheeraResult: function (request, isDelete, result) {
     this._log.debug("Received Bagheera result.");
 
     let promise = Promise.resolve(null);
 
     if (!result.transportSuccess) {
       request.onSubmissionFailureSoft("Network transport error.");
       return promise;
     }
@@ -357,46 +709,44 @@ HealthReporter.prototype = {
       this.lastPingDate = now;
     }
 
     request.onSubmissionSuccess(now);
 
     return promise;
   },
 
-  _onSubmitDataRequestFailure: function _onSubmitDataRequestFailure(error) {
+  _onSubmitDataRequestFailure: function (error) {
     this._log.error("Error processing request to submit data: " +
                     CommonUtils.exceptionStr(error));
   },
 
-  _formatDate: function _formatDate(date) {
+  _formatDate: function (date) {
     // Why, oh, why doesn't JS have a strftime() equivalent?
     return date.toISOString().substr(0, 10);
   },
 
 
-  _uploadData: function _uploadData(request) {
+  _uploadData: function (request) {
     let id = CommonUtils.generateUUID();
 
     this._log.info("Uploading data to server: " + this.serverURI + " " +
                    this.serverNamespace + ":" + id);
     let client = new BagheeraClient(this.serverURI);
 
-    let payload = this.getJSONPayload();
-
-    return this._saveLastPayload(payload)
-               .then(client.uploadJSON.bind(client,
-                                            this.serverNamespace,
-                                            id,
-                                            payload,
-                                            this.lastSubmitID))
-               .then(this._onBagheeraResult.bind(this, request, false));
+    return Task.spawn(function doUpload() {
+      let payload = yield this.getJSONPayload();
+      yield this._saveLastPayload(payload);
+      let result = yield client.uploadJSON(this.serverNamespace, id, payload,
+                                           this.lastSubmitID);
+      yield this._onBagheeraResult(request, false, result);
+    }.bind(this));
   },
 
-  _deleteRemoteData: function _deleteRemoteData(request) {
+  _deleteRemoteData: function (request) {
     if (!this.lastSubmitID) {
       this._log.info("Received request to delete remote data but no data stored.");
       request.onNoDataAvailable();
       return;
     }
 
     this._log.warn("Deleting remote data.");
     let client = new BagheeraClient(this.serverURI);
@@ -414,17 +764,17 @@ HealthReporter.prototype = {
     if (!profD || !profD.length) {
       throw new Error("Could not obtain profile directory. OS.File not " +
                       "initialized properly?");
     }
 
     return OS.Path.join(profD, "healthreport");
   },
 
-  _ensureDirectoryExists: function _ensureDirectoryExists(path) {
+  _ensureDirectoryExists: function (path) {
     let deferred = Promise.defer();
 
     OS.File.makeDir(path).then(
       function onResult() {
         deferred.resolve(true);
       },
       function onError(error) {
         if (error.becauseExists) {
@@ -438,17 +788,17 @@ HealthReporter.prototype = {
 
     return deferred.promise;
   },
 
   get _lastPayloadPath() {
     return OS.Path.join(this._stateDir, "lastpayload.json");
   },
 
-  _saveLastPayload: function _saveLastPayload(payload) {
+  _saveLastPayload: function (payload) {
     let path = this._lastPayloadPath;
     let pathTmp = path + ".tmp";
 
     let encoder = new TextEncoder();
     let buffer = encoder.encode(payload);
 
     return OS.File.writeAtomic(path, buffer, {tmpPath: pathTmp});
   },
@@ -457,17 +807,17 @@ HealthReporter.prototype = {
    * Obtain the last uploaded payload.
    *
    * The promise is resolved to a JSON-decoded object on success. The promise
    * is rejected if the last uploaded payload could not be found or there was
    * an error reading or parsing it.
    *
    * @return Promise<object>
    */
-  getLastPayload: function getLoadPayload() {
+  getLastPayload: function () {
     let path = this._lastPayloadPath;
 
     return OS.File.read(path).then(
       function onData(buffer) {
         let decoder = new TextDecoder();
         let json = JSON.parse(decoder.decode(buffer));
 
         return Promise.resolve(json);
@@ -481,31 +831,29 @@ HealthReporter.prototype = {
   _now: function _now() {
     return new Date();
   },
 
   //-----------------------------
   // HealthReportPolicy listeners
   //-----------------------------
 
-  onRequestDataUpload: function onRequestDataSubmission(request) {
+  onRequestDataUpload: function (request) {
     this.collectMeasurements()
         .then(this._uploadData.bind(this, request),
               this._onSubmitDataRequestFailure.bind(this));
   },
 
-  onNotifyDataPolicy: function onNotifyDataPolicy(request) {
+  onNotifyDataPolicy: function (request) {
     // This isn't very loosely coupled. We may want to have this call
     // registered listeners instead.
     Observers.notify("healthreport:notify-data-policy:request", request);
   },
 
-  onRequestRemoteDelete: function onRequestRemoteDelete(request) {
+  onRequestRemoteDelete: function (request) {
     this._deleteRemoteData(request);
   },
 
   //------------------------------------
   // End of HealthReportPolicy listeners
   //------------------------------------
-};
+});
 
-Object.freeze(HealthReporter.prototype);
-
--- a/services/healthreport/profile.jsm
+++ b/services/healthreport/profile.jsm
@@ -6,34 +6,36 @@
 
 this.EXPORTED_SYMBOLS = [
   "ProfileCreationTimeAccessor",
   "ProfileMetadataProvider",
 ];
 
 const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
 
-const DEFAULT_PROFILE_MEASUREMENT_NAME = "org.mozilla.profile";
+const DEFAULT_PROFILE_MEASUREMENT_NAME = "age";
 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"};
 
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/osfile.jsm")
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-common/utils.js");
 
 // Profile creation time access.
 // This is separate from the provider to simplify testing and enable extraction
 // to a shared location in the future.
-function ProfileCreationTimeAccessor(profile) {
+function ProfileCreationTimeAccessor(profile, log) {
   this.profilePath = profile || OS.Constants.Path.profileDir;
   if (!this.profilePath) {
     throw new Error("No profile directory.");
   }
+  this._log = log || {"debug": function (s) { dump(s + "\n"); }};
 }
 ProfileCreationTimeAccessor.prototype = {
   /**
    * There are three ways we can get our creation time:
    *
    * 1. From our own saved value (to avoid redundant work).
    * 2. From the on-disk JSON file.
    * 3. By calculating it from the filesystem.
@@ -110,41 +112,57 @@ ProfileCreationTimeAccessor.prototype = 
                .then(onOldest.bind(this));
   },
 
   /**
    * Traverse the contents of the profile directory, finding the oldest file
    * and returning its creation timestamp.
    */
   getOldestProfileTimestamp: function () {
+    let self = this;
     let oldest = Date.now() + 1000;
     let iterator = new OS.File.DirectoryIterator(this.profilePath);
-dump("Iterating over profile " + this.profilePath);
+    self._log.debug("Iterating over profile " + this.profilePath);
     if (!iterator) {
       throw new Error("Unable to fetch oldest profile entry: no profile iterator.");
     }
 
     function onEntry(entry) {
-      if ("winLastWriteDate" in entry) {
-        // Under Windows, additional information allow us to sort files immediately
-        // without having to perform additional I/O.
-        let timestamp = entry.winCreationDate.getTime();
-        if (timestamp < oldest) {
-          oldest = timestamp;
+      function onStatSuccess(info) {
+        // OS.File doesn't seem to be behaving. See Bug 827148.
+        // Let's do the best we can. This whole function is defensive.
+        let date;
+        if ("winBirthDate" in info) {
+          date = info.winBirthDate;
+        } else if ("macBirthDate" in info) {
+          date = info.macBirthDate;
         }
-        return;
-      }
 
-      // Under other OSes, we need to call OS.File.stat.
-      function onStatSuccess(info) {
-        let date = info.creationDate;
-        let timestamp = date.getTime();
-        dump("CREATION DATE: " + entry.path + " = " + date);
-        if (timestamp < oldest) {
-          oldest = timestamp;
+        if (!date || !date.getTime()) {
+          // Hack: as of this writing, OS.File will only return file
+          // creation times of any kind of Mac and Windows, where birthTime
+          // is defined. That means we're unable to function on Linux.
+          // Use ctime, fall back to mtime.
+          // Oh, and info.macBirthDate doesn't work.
+          self._log.debug("No birth date: using ctime/mtime.");
+          try {
+            date = info.creationDate ||
+                   info.lastModificationDate ||
+                   info.unixLastStatusChangeDate;
+          } catch (ex) {
+            self._log.debug("Exception fetching creation date: " + ex);
+          }
+        }
+
+        if (date) {
+          let timestamp = date.getTime();
+          self._log.debug("Using date: " + entry.path + " = " + date);
+          if (timestamp < oldest) {
+            oldest = timestamp;
+          }
         }
       }
       return OS.File.stat(entry.path)
                     .then(onStatSuccess);
     }
 
     let promise = iterator.forEach(onEntry);
 
@@ -160,69 +178,66 @@ dump("Iterating over profile " + this.pr
 
     return promise.then(onSuccess, onFailure);
   },
 }
 
 /**
  * Measurements pertaining to the user's profile.
  */
-function ProfileMetadataMeasurement(name=DEFAULT_PROFILE_MEASUREMENT_NAME) {
-  MetricsMeasurement.call(this, name, 1);
+function ProfileMetadataMeasurement() {
+  Metrics.Measurement.call(this);
 }
 ProfileMetadataMeasurement.prototype = {
-  __proto__: MetricsMeasurement.prototype,
+  __proto__: Metrics.Measurement.prototype,
 
-  fields: {
+  name: DEFAULT_PROFILE_MEASUREMENT_NAME,
+  version: 1,
+
+  configureStorage: function () {
     // Profile creation date. Number of days since Unix epoch.
-    "profileCreation": REQUIRED_UINT32_TYPE,
+    return this.registerStorageField("profileCreation", this.storage.FIELD_LAST_NUMERIC);
   },
 };
 
 /**
  * Turn a millisecond timestamp into a day timestamp.
  *
  * @param msec a number of milliseconds since epoch.
  * @return the number of whole days denoted by the input.
  */
 function truncate(msec) {
   return Math.floor(msec / MILLISECONDS_PER_DAY);
 }
 
 /**
- * A MetricsProvider for profile metadata, such as profile creation time.
+ * A Metrics.Provider for profile metadata, such as profile creation time.
  */
-function ProfileMetadataProvider(name="ProfileMetadataProvider") {
-  MetricsProvider.call(this, name);
+function ProfileMetadataProvider() {
+  Metrics.Provider.call(this);
 }
 ProfileMetadataProvider.prototype = {
-  __proto__: MetricsProvider.prototype,
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.profile",
+
+  measurementTypes: [ProfileMetadataMeasurement],
 
   getProfileCreationDays: function () {
-    let accessor = new ProfileCreationTimeAccessor();
+    let accessor = new ProfileCreationTimeAccessor(null, this._log);
 
     return accessor.created
                    .then(truncate);
   },
 
-  collectConstantMeasurements: function () {
-    let result = this.createResult();
-    result.expectMeasurement("org.mozilla.profile");
-    result.populate = this._populateConstants.bind(this);
-    return result;
-  },
+  collectConstantData: function () {
+    let m = this.getMeasurement(DEFAULT_PROFILE_MEASUREMENT_NAME, 1);
 
-  _populateConstants: function (result) {
-    let name = DEFAULT_PROFILE_MEASUREMENT_NAME;
-    result.addMeasurement(new ProfileMetadataMeasurement(name));
-    function onSuccess(days) {
-      result.setValue(name, "profileCreation", days);
-      result.finish();
-    }
-    function onFailure(ex) {
-      result.addError(ex);
-      result.finish();
-    }
-    return this.getProfileCreationDays()
-               .then(onSuccess, onFailure);
+    return Task.spawn(function collectConstant() {
+      let createdDays = yield this.getProfileCreationDays();
+
+      yield this.enqueueStorageOperation(function storeDays() {
+        return m.setLastNumeric("profileCreation", createdDays);
+      });
+    }.bind(this));
   },
 };
 
--- a/services/healthreport/providers.jsm
+++ b/services/healthreport/providers.jsm
@@ -16,97 +16,107 @@
 
 this.EXPORTED_SYMBOLS = [
   "AppInfoProvider",
   "SysInfoProvider",
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-common/utils.js");
 
 
-const REQUIRED_STRING_TYPE = {type: "TYPE_STRING"};
-const OPTIONAL_STRING_TYPE = {type: "TYPE_STRING", optional: true};
-const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"};
-
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
                                   "resource://gre/modules/UpdateChannel.jsm");
 
 /**
  * Represents basic application state.
  *
  * This is roughly a union of nsIXULAppInfo, nsIXULRuntime, with a few extra
  * pieces thrown in.
  */
 function AppInfoMeasurement() {
-  MetricsMeasurement.call(this, "appinfo", 1);
+  Metrics.Measurement.call(this);
 }
 
-AppInfoMeasurement.prototype = {
-  __proto__: MetricsMeasurement.prototype,
+AppInfoMeasurement.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "appinfo",
+  version: 1,
 
-  fields: {
-    vendor: REQUIRED_STRING_TYPE,
-    name: REQUIRED_STRING_TYPE,
-    id: REQUIRED_STRING_TYPE,
-    version: REQUIRED_STRING_TYPE,
-    appBuildID: REQUIRED_STRING_TYPE,
-    platformVersion: REQUIRED_STRING_TYPE,
-    platformBuildID: REQUIRED_STRING_TYPE,
-    os: REQUIRED_STRING_TYPE,
-    xpcomabi: REQUIRED_STRING_TYPE,
-    updateChannel: REQUIRED_STRING_TYPE,
-    distributionID: REQUIRED_STRING_TYPE,
-    distributionVersion: REQUIRED_STRING_TYPE,
-    hotfixVersion: REQUIRED_STRING_TYPE,
-    locale: REQUIRED_STRING_TYPE,
+  LAST_TEXT_FIELDS: [
+    "vendor",
+    "name",
+    "id",
+    "version",
+    "appBuildID",
+    "platformVersion",
+    "platformBuildID",
+    "os",
+    "xpcomabi",
+    "updateChannel",
+    "distributionID",
+    "distributionVersion",
+    "hotfixVersion",
+    "locale",
+  ],
+
+  configureStorage: function () {
+    let self = this;
+    return Task.spawn(function configureStorage() {
+      for (let field of self.LAST_TEXT_FIELDS) {
+        yield self.registerStorageField(field, self.storage.FIELD_LAST_TEXT);
+      }
+    });
   },
-};
-
-Object.freeze(AppInfoMeasurement.prototype);
+});
 
 
 this.AppInfoProvider = function AppInfoProvider() {
-  MetricsProvider.call(this, "app-info");
+  Metrics.Provider.call(this);
 
   this._prefs = new Preferences({defaultBranch: null});
 }
-AppInfoProvider.prototype = {
-  __proto__: MetricsProvider.prototype,
+AppInfoProvider.prototype = Object.freeze({
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.appInfo",
+
+  measurementTypes: [AppInfoMeasurement],
 
   appInfoFields: {
     // From nsIXULAppInfo.
     vendor: "vendor",
     name: "name",
     id: "ID",
     version: "version",
     appBuildID: "appBuildID",
     platformVersion: "platformVersion",
     platformBuildID: "platformBuildID",
 
     // From nsIXULRuntime.
     os: "OS",
     xpcomabi: "XPCOMABI",
   },
 
-  collectConstantMeasurements: function collectConstantMeasurements() {
-    let result = this.createResult();
-    result.expectMeasurement("appinfo");
-
-    result.populate = this._populateConstants.bind(this);
-    return result;
+  collectConstantData: function () {
+    return this.enqueueStorageOperation(function collect() {
+      return Task.spawn(this._populateConstants.bind(this));
+    }.bind(this));
   },
 
-  _populateConstants: function _populateConstants(result) {
-    result.addMeasurement(new AppInfoMeasurement());
+  _populateConstants: function () {
+    let m = this.getMeasurement(AppInfoMeasurement.prototype.name,
+                                AppInfoMeasurement.prototype.version);
 
     let ai;
     try {
       ai = Services.appinfo;
     } catch (ex) {
       this._log.warn("Could not obtain Services.appinfo: " +
                      CommonUtils.exceptionStr(ex));
       throw ex;
@@ -114,141 +124,132 @@ AppInfoProvider.prototype = {
 
     if (!ai) {
       this._log.warn("Services.appinfo is unavailable.");
       throw ex;
     }
 
     for (let [k, v] in Iterator(this.appInfoFields)) {
       try {
-        result.setValue("appinfo", k, ai[v]);
+        yield m.setLastText(k, ai[v]);
       } catch (ex) {
         this._log.warn("Error obtaining Services.appinfo." + v);
-        result.addError(ex);
       }
     }
 
     try {
-      result.setValue("appinfo", "updateChannel", UpdateChannel.get());
+      yield m.setLastText("updateChannel", UpdateChannel.get());
     } catch (ex) {
       this._log.warn("Could not obtain update channel: " +
                      CommonUtils.exceptionStr(ex));
-      result.addError(ex);
     }
 
-    result.setValue("appinfo", "distributionID", this._prefs.get("distribution.id", ""));
-    result.setValue("appinfo", "distributionVersion", this._prefs.get("distribution.version", ""));
-    result.setValue("appinfo", "hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", ""));
+    yield m.setLastText("distributionID", this._prefs.get("distribution.id", ""));
+    yield m.setLastText("distributionVersion", this._prefs.get("distribution.version", ""));
+    yield m.setLastText("hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", ""));
 
     try {
       let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
                      .getService(Ci.nsIXULChromeRegistry)
                      .getSelectedLocale("global");
-      result.setValue("appinfo", "locale", locale);
+      yield m.setLastText("locale", locale);
     } catch (ex) {
       this._log.warn("Could not obtain application locale: " +
                      CommonUtils.exceptionStr(ex));
-      result.addError(ex);
     }
-
-    result.finish();
   },
-};
-
-Object.freeze(AppInfoProvider.prototype);
+});
 
 
 function SysInfoMeasurement() {
-  MetricsMeasurement.call(this, "sysinfo", 1);
+  Metrics.Measurement.call(this);
 }
 
-SysInfoMeasurement.prototype = {
-  __proto__: MetricsMeasurement.prototype,
+SysInfoMeasurement.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "sysinfo",
+  version: 1,
 
-  fields: {
-    cpuCount: REQUIRED_UINT32_TYPE,
-    memoryMB: REQUIRED_UINT32_TYPE,
-    manufacturer: OPTIONAL_STRING_TYPE,
-    device: OPTIONAL_STRING_TYPE,
-    hardware: OPTIONAL_STRING_TYPE,
-    name: OPTIONAL_STRING_TYPE,
-    version: OPTIONAL_STRING_TYPE,
-    architecture: OPTIONAL_STRING_TYPE,
+  configureStorage: function () {
+    return Task.spawn(function configureStorage() {
+      yield this.registerStorageField("cpuCount", this.storage.FIELD_LAST_NUMERIC);
+      yield this.registerStorageField("memoryMB", this.storage.FIELD_LAST_NUMERIC);
+      yield this.registerStorageField("manufacturer", this.storage.FIELD_LAST_TEXT);
+      yield this.registerStorageField("device", this.storage.FIELD_LAST_TEXT);
+      yield this.registerStorageField("hardware", this.storage.FIELD_LAST_TEXT);
+      yield this.registerStorageField("name", this.storage.FIELD_LAST_TEXT);
+      yield this.registerStorageField("version", this.storage.FIELD_LAST_TEXT);
+      yield this.registerStorageField("architecture", this.storage.FIELD_LAST_TEXT);
+    }.bind(this));
   },
-},
-
-Object.freeze(SysInfoMeasurement.prototype);
+});
 
 
 this.SysInfoProvider = function SysInfoProvider() {
-  MetricsProvider.call(this, "sys-info");
+  Metrics.Provider.call(this);
 };
 
-SysInfoProvider.prototype = {
-  __proto__: MetricsProvider.prototype,
+SysInfoProvider.prototype = Object.freeze({
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.sysinfo",
+
+  measurementTypes: [SysInfoMeasurement],
 
   sysInfoFields: {
     cpucount: "cpuCount",
     memsize: "memoryMB",
     manufacturer: "manufacturer",
     device: "device",
     hardware: "hardware",
     name: "name",
     version: "version",
     arch: "architecture",
   },
 
-  INT_FIELDS: new Set("cpucount", "memsize"),
-
-  collectConstantMeasurements: function collectConstantMeasurements() {
-    let result = this.createResult();
-    result.expectMeasurement("sysinfo");
-
-    result.populate = this._populateConstants.bind(this);
-
-    return result;
+  collectConstantData: function () {
+    return this.enqueueStorageOperation(function collection() {
+      return Task.spawn(this._populateConstants.bind(this));
+    }.bind(this));
   },
 
-  _populateConstants: function _populateConstants(result) {
-    result.addMeasurement(new SysInfoMeasurement());
+  _populateConstants: function () {
+    let m = this.getMeasurement(SysInfoMeasurement.prototype.name,
+                                SysInfoMeasurement.prototype.version);
 
     let si = Cc["@mozilla.org/system-info;1"]
                .getService(Ci.nsIPropertyBag2);
 
     for (let [k, v] in Iterator(this.sysInfoFields)) {
       try {
         if (!si.hasKey(k)) {
           this._log.debug("Property not available: " + k);
           continue;
         }
 
         let value = si.getProperty(k);
+        let method = "setLastText";
 
-        if (this.INT_FIELDS.has(k)) {
+        if (["cpucount", "memsize"].indexOf(k) != -1) {
           let converted = parseInt(value, 10);
           if (Number.isNaN(converted)) {
-            result.addError(new Error("Value is not an integer: " + k + "=" +
-                                      value));
             continue;
           }
 
           value = converted;
+          method = "setLastNumeric";
         }
 
         // Round memory to mebibytes.
         if (k == "memsize") {
           value = Math.round(value / 1048576);
         }
 
-        result.setValue("sysinfo", v, value);
+        yield m[method](v, value);
       } catch (ex) {
         this._log.warn("Error obtaining system info field: " + k + " " +
                        CommonUtils.exceptionStr(ex));
-        result.addError(ex);
       }
     }
+  },
+});
 
-    result.finish();
-  },
-};
-
-Object.freeze(SysInfoProvider.prototype);
-
--- a/services/healthreport/tests/xpcshell/test_healthreporter.js
+++ b/services/healthreport/tests/xpcshell/test_healthreporter.js
@@ -5,23 +5,26 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
 Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm");
 Cu.import("resource://gre/modules/services/healthreport/policy.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://testing-common/services-common/bagheeraserver.js");
 Cu.import("resource://testing-common/services/metrics/mocks.jsm");
 
 
 const SERVER_HOSTNAME = "localhost";
 const SERVER_PORT = 8080;
 const SERVER_URI = "http://" + SERVER_HOSTNAME + ":" + SERVER_PORT;
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 
 
 function defineNow(policy, now) {
   print("Adjusting fake system clock to " + now);
   Object.defineProperty(policy, "now", {
     value: function customNow() {
       return now;
     },
@@ -29,257 +32,293 @@ function defineNow(policy, now) {
   });
 }
 
 function getReporter(name, uri=SERVER_URI) {
   let branch = "healthreport.testing. " + name + ".";
 
   let prefs = new Preferences(branch);
   prefs.set("documentServerURI", uri);
+  prefs.set("dbName", name);
 
-  return new HealthReporter(branch);
+  let reporter = new HealthReporter(branch);
+  return reporter.onInit();
 }
 
 function getReporterAndServer(name, namespace="test") {
-  let reporter = getReporter(name, SERVER_URI);
-  reporter.serverNamespace = namespace;
+  return Task.spawn(function get() {
+    let reporter = yield getReporter(name, SERVER_URI);
+    reporter.serverNamespace = namespace;
+
+    let server = new BagheeraServer(SERVER_URI);
+    server.createNamespace(namespace);
+
+    server.start(SERVER_PORT);
 
-  let server = new BagheeraServer(SERVER_URI);
-  server.createNamespace(namespace);
+    throw new Task.Result([reporter, server]);
+  });
+}
 
-  server.start(SERVER_PORT);
+function shutdownServer(server) {
+  let deferred = Promise.defer();
+  server.stop(deferred.resolve.bind(deferred));
 
-  return [reporter, server];
+  return deferred.promise;
 }
 
 function run_test() {
   run_next_test();
 }
 
-add_test(function test_constructor() {
-  let reporter = getReporter("constructor");
+add_task(function test_constructor() {
+  let reporter = yield getReporter("constructor");
 
   do_check_eq(reporter.lastPingDate.getTime(), 0);
   do_check_null(reporter.lastSubmitID);
 
   reporter.lastSubmitID = "foo";
   do_check_eq(reporter.lastSubmitID, "foo");
   reporter.lastSubmitID = null;
   do_check_null(reporter.lastSubmitID);
 
   let failed = false;
   try {
     new HealthReporter("foo.bar");
   } catch (ex) {
     failed = true;
-    do_check_true(ex.message.startsWith("Branch argument must end"));
+    do_check_true(ex.message.startsWith("Branch must end"));
   } finally {
     do_check_true(failed);
     failed = false;
   }
 
-  run_next_test();
+  reporter._shutdown();
 });
 
-add_test(function test_register_providers_from_category_manager() {
+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 = getReporter("category_manager");
-  do_check_eq(reporter._collector._providers.length, 0);
-  reporter.registerProvidersFromCategoryManager(category);
-  do_check_eq(reporter._collector._providers.length, 1);
+  let reporter = yield getReporter("category_manager");
+  do_check_eq(reporter._collector._providers.size, 0);
+  yield reporter.registerProvidersFromCategoryManager(category);
+  do_check_eq(reporter._collector._providers.size, 1);
 
-  run_next_test();
+  reporter._shutdown();
 });
 
-add_test(function test_start() {
-  let reporter = getReporter("start");
-  reporter.start().then(function onStarted() {
-    reporter.stop();
-    run_next_test();
-  });
-});
-
-add_test(function test_json_payload_simple() {
-  let reporter = getReporter("json_payload_simple");
+add_task(function test_json_payload_simple() {
+  let reporter = yield getReporter("json_payload_simple");
 
   let now = new Date();
-  let payload = reporter.getJSONPayload();
+  let payload = yield reporter.getJSONPayload();
   let original = JSON.parse(payload);
 
   do_check_eq(original.version, 1);
   do_check_eq(original.thisPingDate, reporter._formatDate(now));
-  do_check_eq(Object.keys(original.providers).length, 0);
+  do_check_eq(Object.keys(original.data.last).length, 0);
+  do_check_eq(Object.keys(original.data.days).length, 0);
 
   reporter.lastPingDate = new Date(now.getTime() - 24 * 60 * 60 * 1000 - 10);
 
-  original = JSON.parse(reporter.getJSONPayload());
+  original = JSON.parse(yield reporter.getJSONPayload());
   do_check_eq(original.lastPingDate, reporter._formatDate(reporter.lastPingDate));
 
   // This could fail if we cross UTC day boundaries at the exact instance the
   // test is executed. Let's tempt fate.
   do_check_eq(original.thisPingDate, reporter._formatDate(now));
 
-  run_next_test();
+  reporter._shutdown();
+});
+
+add_task(function test_json_payload_dummy_provider() {
+  let reporter = yield getReporter("json_payload_dummy_provider");
+
+  yield reporter.registerProvider(new DummyProvider());
+  yield reporter.collectMeasurements();
+  let payload = yield reporter.getJSONPayload();
+  print(payload);
+  let o = JSON.parse(payload);
+
+  do_check_eq(Object.keys(o.data.last).length, 1);
+  do_check_true("DummyProvider.DummyMeasurement.1" in o.data.last);
+
+  reporter._shutdown();
 });
 
-add_test(function test_json_payload_dummy_provider() {
-  let reporter = getReporter("json_payload_dummy_provider");
-
-  reporter.registerProvider(new DummyProvider());
-  reporter.collectMeasurements().then(function onResult() {
-    let o = JSON.parse(reporter.getJSONPayload());
+add_task(function test_json_payload_multiple_days() {
+  let reporter = yield getReporter("json_payload_multiple_days");
+  let provider = new DummyProvider();
+  yield reporter.registerProvider(provider);
 
-    do_check_eq(Object.keys(o.providers).length, 1);
-    do_check_true("DummyProvider" in o.providers);
-    do_check_true("measurements" in o.providers.DummyProvider);
-    do_check_true("DummyMeasurement" in o.providers.DummyProvider.measurements);
+  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);
+    yield m.addDailyDiscreteText("daily-discrete-text", "" + i, date);
+    yield m.addDailyDiscreteText("daily-discrete-text", "" + (i + 50), date);
+    yield m.setDailyLastNumeric("daily-last-numeric", date.getTime(), date);
+  }
 
-    run_next_test();
-  });
+  let payload = yield reporter.getJSONPayload();
+  print(payload);
+  let o = JSON.parse(payload);
+
+  do_check_eq(Object.keys(o.data.days).length, 180);
+  let today = reporter._formatDate(now);
+  do_check_true(today in o.data.days);
+
+  reporter._shutdown();
 });
 
-add_test(function test_notify_policy_observers() {
-  let reporter = getReporter("notify_policy_observers");
-
-  Observers.add("healthreport:notify-data-policy:request",
-                function onObserver(subject, data) {
-    Observers.remove("healthreport:notify-data-policy:request", onObserver);
+add_task(function test_idle_daily() {
+  let reporter = yield getReporter("idle_daily");
+  let provider = new DummyProvider();
+  yield reporter.registerProvider(provider);
 
-    do_check_true("foo" in subject);
+  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);
+  }
 
-    run_next_test();
-  });
+  let values = yield m.getValues();
+  do_check_eq(values.days.size, 200);
+
+  Services.obs.notifyObservers(null, "idle-daily", null);
 
-  reporter.onNotifyDataPolicy({foo: "bar"});
+  let values = yield m.getValues();
+  do_check_eq(values.days.size, 180);
+
+  reporter._shutdown();
 });
 
-add_test(function test_data_submission_transport_failure() {
-  let reporter = getReporter("data_submission_transport_failure");
+add_task(function test_data_submission_transport_failure() {
+  let reporter = yield getReporter("data_submission_transport_failure");
   reporter.serverURI = "http://localhost:8080/";
   reporter.serverNamespace = "test00";
 
   let deferred = Promise.defer();
-  deferred.promise.then(function onResult(request) {
-    do_check_eq(request.state, request.SUBMISSION_FAILURE_SOFT);
-
-    run_next_test();
-  });
-
   let request = new DataSubmissionRequest(deferred, new Date(Date.now + 30000));
   reporter.onRequestDataUpload(request);
+
+  yield deferred.promise;
+  do_check_eq(request.state, request.SUBMISSION_FAILURE_SOFT);
+
+  reporter._shutdown();
 });
 
-add_test(function test_data_submission_success() {
-  let [reporter, server] = getReporterAndServer("data_submission_success");
+add_task(function test_data_submission_success() {
+  let [reporter, server] = yield getReporterAndServer("data_submission_success");
 
   do_check_eq(reporter.lastPingDate.getTime(), 0);
   do_check_false(reporter.haveRemoteData());
 
   let deferred = Promise.defer();
-  deferred.promise.then(function onResult(request) {
-    do_check_eq(request.state, request.SUBMISSION_SUCCESS);
-    do_check_neq(reporter.lastPingDate.getTime(), 0);
-    do_check_true(reporter.haveRemoteData());
-
-    server.stop(run_next_test);
-  });
 
   let request = new DataSubmissionRequest(deferred, new Date());
   reporter.onRequestDataUpload(request);
+  yield deferred.promise;
+  do_check_eq(request.state, request.SUBMISSION_SUCCESS);
+  do_check_true(reporter.lastPingDate.getTime() > 0);
+  do_check_true(reporter.haveRemoteData());
+
+  reporter._shutdown();
+  yield shutdownServer(server);
 });
 
-add_test(function test_recurring_daily_pings() {
-  let [reporter, server] = getReporterAndServer("recurring_daily_pings");
+add_task(function test_recurring_daily_pings() {
+  let [reporter, server] = yield getReporterAndServer("recurring_daily_pings");
   reporter.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);
+  yield promise;
 
-  promise.then(function onUploadComplete() {
-    let lastID = reporter.lastSubmitID;
-
-    do_check_neq(lastID, null);
-    do_check_true(server.hasDocument(reporter.serverNamespace, lastID));
+  let lastID = reporter.lastSubmitID;
+  do_check_neq(lastID, null);
+  do_check_true(server.hasDocument(reporter.serverNamespace, lastID));
 
-    // Skip forward to next scheduled submission time.
-    defineNow(policy, policy.nextDataSubmissionDate);
-    let promise = policy.checkStateAndTrigger();
-    do_check_neq(promise, null);
-    promise.then(function onSecondUploadCOmplete() {
-      do_check_neq(reporter.lastSubmitID, lastID);
-      do_check_true(server.hasDocument(reporter.serverNamespace, reporter.lastSubmitID));
-      do_check_false(server.hasDocument(reporter.serverNamespace, lastID));
+  // Skip forward to next scheduled submission time.
+  defineNow(policy, policy.nextDataSubmissionDate);
+  promise = policy.checkStateAndTrigger();
+  do_check_neq(promise, null);
+  yield promise;
+  do_check_neq(reporter.lastSubmitID, lastID);
+  do_check_true(server.hasDocument(reporter.serverNamespace, reporter.lastSubmitID));
+  do_check_false(server.hasDocument(reporter.serverNamespace, lastID));
 
-      server.stop(run_next_test);
-    });
-  });
+  reporter._shutdown();
+  yield shutdownServer(server);
 });
 
-add_test(function test_request_remote_data_deletion() {
-  let [reporter, server] = getReporterAndServer("request_remote_data_deletion");
+add_task(function test_request_remote_data_deletion() {
+  let [reporter, server] = yield getReporterAndServer("request_remote_data_deletion");
 
   let policy = reporter._policy;
   defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
   policy.recordUserAcceptance();
   defineNow(policy, policy.nextDataSubmissionDate);
-  policy.checkStateAndTrigger().then(function onUploadComplete() {
-    let id = reporter.lastSubmitID;
-    do_check_neq(id, null);
-    do_check_true(server.hasDocument(reporter.serverNamespace, id));
+  yield policy.checkStateAndTrigger();
+  let id = reporter.lastSubmitID;
+  do_check_neq(id, null);
+  do_check_true(server.hasDocument(reporter.serverNamespace, id));
 
-    defineNow(policy, policy._futureDate(10 * 1000));
+  defineNow(policy, policy._futureDate(10 * 1000));
 
-    let promise = reporter.requestDeleteRemoteData();
-    do_check_neq(promise, null);
-    promise.then(function onDeleteComplete() {
-      do_check_null(reporter.lastSubmitID);
-      do_check_false(reporter.haveRemoteData());
-      do_check_false(server.hasDocument(reporter.serverNamespace, id));
+  let promise = reporter.requestDeleteRemoteData();
+  do_check_neq(promise, null);
+  yield promise;
+  do_check_null(reporter.lastSubmitID);
+  do_check_false(reporter.haveRemoteData());
+  do_check_false(server.hasDocument(reporter.serverNamespace, id));
 
-      server.stop(run_next_test);
-    });
-  });
+  reporter._shutdown();
+  yield shutdownServer(server);
 });
 
-add_test(function test_policy_accept_reject() {
-  let [reporter, server] = getReporterAndServer("policy_accept_reject");
+add_task(function test_policy_accept_reject() {
+  let [reporter, server] = yield getReporterAndServer("policy_accept_reject");
 
   do_check_false(reporter.dataSubmissionPolicyAccepted);
   do_check_false(reporter.willUploadData);
 
   reporter.recordPolicyAcceptance();
   do_check_true(reporter.dataSubmissionPolicyAccepted);
   do_check_true(reporter.willUploadData);
 
   reporter.recordPolicyRejection();
   do_check_false(reporter.dataSubmissionPolicyAccepted);
   do_check_false(reporter.willUploadData);
 
-  server.stop(run_next_test);
+  reporter._shutdown();
+  yield shutdownServer(server);
 });
 
 
-add_test(function test_upload_save_payload() {
-  let [reporter, server] = getReporterAndServer("upload_save_payload");
+add_task(function test_upload_save_payload() {
+  let [reporter, server] = yield getReporterAndServer("upload_save_payload");
 
   let deferred = Promise.defer();
   let request = new DataSubmissionRequest(deferred, new Date(), false);
 
-  reporter._uploadData(request).then(function onUpload() {
-    reporter.getLastPayload().then(function onJSON(json) {
-      do_check_true("thisPingDate" in json);
-      server.stop(run_next_test);
-    });
-  });
+  yield reporter._uploadData(request);
+  let json = yield reporter.getLastPayload();
+  do_check_true("thisPingDate" in json);
+
+  reporter._shutdown();
+  yield shutdownServer(server);
 });
 
--- a/services/healthreport/tests/xpcshell/test_profile.js
+++ b/services/healthreport/tests/xpcshell/test_profile.js
@@ -8,46 +8,39 @@ const {utils: Cu} = Components;
 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 
 // Create profile directory before use.
 // It can be no older than a day ago….
 let profile_creation_lower = Date.now() - MILLISECONDS_PER_DAY;
 do_get_profile();
 
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
+Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/services/healthreport/profile.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
+
 function MockProfileMetadataProvider(name="MockProfileMetadataProvider") {
-  ProfileMetadataProvider.call(this, name);
+  this.name = name;
+  ProfileMetadataProvider.call(this);
 }
 MockProfileMetadataProvider.prototype = {
   __proto__: ProfileMetadataProvider.prototype,
 
   getProfileCreationDays: function getProfileCreationDays() {
     return Promise.resolve(1234);
   },
 };
 
 
 function run_test() {
   run_next_test();
 }
 
 /**
- * Treat the provided function as a generator of promises,
- * suitable for use with Task.spawn. Success runs next test;
- * failure throws.
- */
-function testTask(promiseFunction) {
-  Task.spawn(promiseFunction).then(run_next_test, do_throw);
-}
-
-/**
  * Ensure that OS.File works in our environment.
  * This test can go once there are xpcshell tests for OS.File.
  */
 add_test(function use_os_file() {
   Cu.import("resource://gre/modules/osfile.jsm")
 
   // Ensure that we get constants, too.
   do_check_neq(OS.Constants.Path.profileDir, null);
@@ -79,77 +72,62 @@ add_test(function test_time_accessor_no_
      .then(function onSuccess(json) {
        do_throw("File existed!");
      },
      function onFailure() {
        run_next_test();
      });
 });
 
-add_test(function test_time_accessor_named_file() {
+add_task(function test_time_accessor_named_file() {
   let acc = getAccessor();
 
-  testTask(function () {
-    // There should be no file yet.
-    yield acc.writeTimes({created: 12345}, "test.json");
-    yield acc.readTimes("test.json")
-             .then(function onSuccess(json) {
-               print("Read: " + JSON.stringify(json));
-               do_check_eq(12345, json.created);
-               run_next_test();
-             });
-  });
+  // There should be no file yet.
+  yield acc.writeTimes({created: 12345}, "test.json");
+  let json = yield acc.readTimes("test.json")
+  print("Read: " + JSON.stringify(json));
+  do_check_eq(12345, json.created);
 });
 
-add_test(function test_time_accessor_creates_file() {
+add_task(function test_time_accessor_creates_file() {
   let lower = profile_creation_lower;
 
   // Ensure that provided contents are merged, and existing
   // files can be overwritten. These two things occur if we
   // read and then decide that we have to write.
   let acc = getAccessor();
   let existing = {abc: "123", easy: "abc"};
   let expected;
 
-  testTask(function () {
-    yield acc.computeAndPersistTimes(existing, "test2.json")
-             .then(function onSuccess(created) {
-               let upper = Date.now() + 1000;
-               print(lower + " < " + created + " <= " + upper);
-               do_check_true(lower < created);
-               do_check_true(upper >= created);
-               expected = created;
-             });
-    yield acc.readTimes("test2.json")
-             .then(function onSuccess(json) {
-               print("Read: " + JSON.stringify(json));
-               do_check_eq("123", json.abc);
-               do_check_eq("abc", json.easy);
-               do_check_eq(expected, json.created);
-             });
-  });
+  let created = yield acc.computeAndPersistTimes(existing, "test2.json")
+  let upper = Date.now() + 1000;
+  print(lower + " < " + created + " <= " + upper);
+  do_check_true(lower < created);
+  do_check_true(upper >= created);
+  expected = created;
+
+  let json = yield acc.readTimes("test2.json")
+  print("Read: " + JSON.stringify(json));
+  do_check_eq("123", json.abc);
+  do_check_eq("abc", json.easy);
+  do_check_eq(expected, json.created);
 });
 
-add_test(function test_time_accessor_all() {
+add_task(function test_time_accessor_all() {
   let lower = profile_creation_lower;
   let acc = getAccessor();
   let expected;
-  testTask(function () {
-    yield acc.created
-             .then(function onSuccess(created) {
-               let upper = Date.now() + 1000;
-               do_check_true(lower < created);
-               do_check_true(upper >= created);
-               expected = created;
-             });
-    yield acc.created
-             .then(function onSuccess(again) {
-               do_check_eq(expected, again);
-             });
-  });
+  let created = yield acc.created
+  let upper = Date.now() + 1000;
+  do_check_true(lower < created);
+  do_check_true(upper >= created);
+  expected = created;
+
+  let again = yield acc.created
+  do_check_eq(expected, again);
 });
 
 add_test(function test_constructor() {
   let provider = new ProfileMetadataProvider("named");
   run_next_test();
 });
 
 add_test(function test_profile_files() {
@@ -166,48 +144,53 @@ add_test(function test_profile_files() {
     do_throw("Directory iteration failed: " + ex);
   }
 
   provider.getProfileCreationDays().then(onSuccess, onFailure);
 });
 
 // A generic test helper. We use this with both real
 // and mock providers in these tests.
-function test_collect_constant(provider, valueTest) {
-  let result = provider.collectConstantMeasurements();
-  do_check_true(result instanceof MetricsCollectionResult);
+function test_collect_constant(provider) {
+  return Task.spawn(function () {
+    yield provider.collectConstantData();
 
-  result.onFinished(function onFinished() {
-    do_check_eq(result.expectedMeasurements.size, 1);
-    do_check_true(result.expectedMeasurements.has("org.mozilla.profile"));
-    let m = result.measurements.get("org.mozilla.profile");
-    do_check_true(!!m);
-    valueTest(m.getValue("profileCreation"));
+    let m = provider.getMeasurement("age", 1);
+    do_check_neq(m, null);
+    let values = yield m.getValues();
+    do_check_eq(values.singular.size, 1);
+    do_check_true(values.singular.has("profileCreation"));
 
-    run_next_test();
+    throw new Task.Result(values.singular.get("profileCreation")[1]);
   });
-
-  result.populate(result);
 }
 
-add_test(function test_collect_constant_mock() {
+add_task(function test_collect_constant_mock() {
+  let storage = yield Metrics.Storage("collect_constant_mock");
   let provider = new MockProfileMetadataProvider();
-  function valueTest(v) {
-    do_check_eq(v, 1234);
-  }
-  test_collect_constant(provider, valueTest);
+  yield provider.init(storage);
+
+  let v = yield test_collect_constant(provider);
+  do_check_eq(v, 1234);
+
+  yield storage.close();
 });
 
-add_test(function test_collect_constant_real() {
+add_task(function test_collect_constant_real() {
   let provider = new ProfileMetadataProvider();
-  function valueTest(v) {
-    let ms = v * MILLISECONDS_PER_DAY;
-    let lower = profile_creation_lower;
-    let upper = Date.now() + 1000;
-    print("Day:   " + v);
-    print("msec:  " + ms);
-    print("Lower: " + lower);
-    print("Upper: " + upper);
-    do_check_true(lower <= ms);
-    do_check_true(upper >= ms);
-  }
-  test_collect_constant(provider, valueTest);
+  let storage = yield Metrics.Storage("collect_constant_real");
+  yield provider.init(storage);
+
+  let v = yield test_collect_constant(provider);
+
+  let ms = v * MILLISECONDS_PER_DAY;
+  let lower = profile_creation_lower;
+  let upper = Date.now() + 1000;
+  print("Day:   " + v);
+  print("msec:  " + ms);
+  print("Lower: " + lower);
+  print("Upper: " + upper);
+  do_check_true(lower <= ms);
+  do_check_true(upper >= ms);
+
+  yield storage.close();
 });
+
--- a/services/healthreport/tests/xpcshell/test_provider_appinfo.js
+++ b/services/healthreport/tests/xpcshell/test_provider_appinfo.js
@@ -1,50 +1,46 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const {interfaces: Ci, results: Cr, utils: Cu} = Components;
 
+Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
 
 function run_test() {
   run_next_test();
 }
 
 add_test(function test_constructor() {
   let provider = new AppInfoProvider();
 
   run_next_test();
 });
 
-add_test(function test_collect_smoketest() {
+add_task(function test_collect_smoketest() {
+  let storage = yield Metrics.Storage("collect_smoketest");
   let provider = new AppInfoProvider();
-
-  let result = provider.collectConstantMeasurements();
-  do_check_true(result instanceof MetricsCollectionResult);
+  yield provider.init(storage);
 
-  result.onFinished(function onFinished() {
-    do_check_eq(result.expectedMeasurements.size, 1);
-    do_check_true(result.expectedMeasurements.has("appinfo"));
-    do_check_eq(result.measurements.size, 1);
-    do_check_true(result.measurements.has("appinfo"));
-    do_check_eq(result.errors.length, 0);
+  yield provider.collectConstantData();
+
+  let m = provider.getMeasurement("appinfo", 1);
+  let data = yield storage.getMeasurementValues(m.id);
+  let serializer = m.serializer(m.SERIALIZE_JSON);
+  let d = serializer.singular(data.singular);
 
-    let ai = result.measurements.get("appinfo");
-    do_check_eq(ai.getValue("vendor"), "Mozilla");
-    do_check_eq(ai.getValue("name"), "xpcshell");
-    do_check_eq(ai.getValue("id"), "xpcshell@tests.mozilla.org");
-    do_check_eq(ai.getValue("version"), "1");
-    do_check_eq(ai.getValue("appBuildID"), "20121107");
-    do_check_eq(ai.getValue("platformVersion"), "p-ver");
-    do_check_eq(ai.getValue("platformBuildID"), "20121106");
-    do_check_eq(ai.getValue("os"), "XPCShell");
-    do_check_eq(ai.getValue("xpcomabi"), "noarch-spidermonkey");
+  do_check_eq(d.vendor, "Mozilla");
+  do_check_eq(d.name, "xpcshell");
+  do_check_eq(d.id, "xpcshell@tests.mozilla.org");
+  do_check_eq(d.version, "1");
+  do_check_eq(d.appBuildID, "20121107");
+  do_check_eq(d.platformVersion, "p-ver");
+  do_check_eq(d.platformBuildID, "20121106");
+  do_check_eq(d.os, "XPCShell");
+  do_check_eq(d.xpcomabi, "noarch-spidermonkey");
 
-    run_next_test();
-  });
+  yield storage.close();
+});
 
-  result.populate(result);
-});
--- a/services/healthreport/tests/xpcshell/test_provider_sysinfo.js
+++ b/services/healthreport/tests/xpcshell/test_provider_sysinfo.js
@@ -1,45 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const {interfaces: Ci, results: Cr, utils: Cu} = Components;
 
+Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
 
 
 function run_test() {
   run_next_test();
 }
 
 add_test(function test_constructor() {
   let provider = new SysInfoProvider();
 
   run_next_test();
 });
 
-add_test(function test_collect_smoketest() {
+add_task(function test_collect_smoketest() {
+  let storage = yield Metrics.Storage("collect_smoketest");
   let provider = new SysInfoProvider();
+  yield provider.init(storage);
 
-  let result = provider.collectConstantMeasurements();
-  do_check_true(result instanceof MetricsCollectionResult);
+  yield provider.collectConstantData();
 
-  result.onFinished(function onFinished() {
-    do_check_eq(result.expectedMeasurements.size, 1);
-    do_check_true(result.expectedMeasurements.has("sysinfo"));
-    do_check_eq(result.measurements.size, 1);
-    do_check_true(result.measurements.has("sysinfo"));
-    do_check_eq(result.errors.length, 0);
+  let m = provider.getMeasurement("sysinfo", 1);
+  let data = yield storage.getMeasurementValues(m.id);
+  let serializer = m.serializer(m.SERIALIZE_JSON);
+  let d = serializer.singular(data.singular);
 
-    let si = result.measurements.get("sysinfo");
-    do_check_true(si.getValue("cpuCount") > 0);
-    do_check_neq(si.getValue("name"), null);
+  do_check_true(d.cpuCount > 0);
+  do_check_neq(d.name, null);
 
-    run_next_test();
-  });
-
-  result.populate(result);
+  yield storage.close();
 });