Bug 812608 - Part 1: Persistent storage backend for metrics data; r=rnewman
authorGregory Szorc <gps@mozilla.com>
Sun, 06 Jan 2013 12:13:19 -0800
changeset 117842 f3f08413e8635fcb94f9365cb947a63ab92af796
parent 117841 8c1c09376c4afdc49f30e3396d5b9cabe17388d9
child 117843 ee9453c65e339986b43ecdb7ffb72c3eff5fafe6
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 1: Persistent storage backend for metrics data; r=rnewman
services/metrics/Makefile.in
services/metrics/Metrics.jsm
services/metrics/collector.jsm
services/metrics/dataprovider.jsm
services/metrics/modules-testing/mocks.jsm
services/metrics/storage.jsm
services/metrics/tests/xpcshell/head.js
services/metrics/tests/xpcshell/test_load_modules.js
services/metrics/tests/xpcshell/test_metrics_collection_result.js
services/metrics/tests/xpcshell/test_metrics_collector.js
services/metrics/tests/xpcshell/test_metrics_measurement.js
services/metrics/tests/xpcshell/test_metrics_provider.js
services/metrics/tests/xpcshell/test_metrics_storage.js
services/metrics/tests/xpcshell/xpcshell.ini
--- a/services/metrics/Makefile.in
+++ b/services/metrics/Makefile.in
@@ -7,22 +7,27 @@ topsrcdir = @top_srcdir@
 srcdir    = @srcdir@
 VPATH     = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 modules := \
   collector.jsm \
   dataprovider.jsm \
+  storage.jsm \
   $(NULL)
 
 testing_modules := \
   mocks.jsm \
   $(NULL)
 
+# We install Metrics.jsm into the "main" JSM repository and the rest in
+# services. External consumers should only go through Metrics.jsm.
+EXTRA_JS_MODULES := Metrics.jsm
+
 TEST_DIRS += tests
 
 MODULES_FILES := $(modules)
 MODULES_DEST = $(FINAL_TARGET)/modules/services/metrics
 INSTALL_TARGETS += MODULES
 
 TESTING_JS_MODULES := $(addprefix modules-testing/,$(testing_modules))
 TESTING_JS_MODULE_DIR := services/metrics
new file mode 100644
--- /dev/null
+++ b/services/metrics/Metrics.jsm
@@ -0,0 +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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["Metrics"];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/services/metrics/collector.jsm");
+Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
+Cu.import("resource://gre/modules/services/metrics/storage.jsm");
+
+
+this.Metrics = {
+  Collector: Collector,
+  Measurement: Measurement,
+  Provider: Provider,
+  Storage: MetricsStorageBackend,
+  dateToDays: dateToDays,
+  daysToDate: daysToDate,
+};
+
--- a/services/metrics/collector.jsm
+++ b/services/metrics/collector.jsm
@@ -1,166 +1,161 @@
 /* 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/. */
 
 "use strict";
 
-this.EXPORTED_SYMBOLS = ["MetricsCollector"];
+this.EXPORTED_SYMBOLS = ["Collector"];
 
 const {utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
 
 
 /**
  * Handles and coordinates the collection of metrics data from providers.
  *
- * This provides an interface for managing `MetricsProvider` instances. It
+ * This provides an interface for managing `Metrics.Provider` instances. It
  * provides APIs for bulk collection of data.
  */
-this.MetricsCollector = function MetricsCollector() {
-  this._log = Log4Moz.repository.getLogger("Metrics.MetricsCollector");
+this.Collector = function (storage) {
+  this._log = Log4Moz.repository.getLogger("Services.Metrics.Collector");
 
-  this._providers = [];
-  this.collectionResults = new Map();
+  this._providers = new Map();
+  this._storage = storage;
+
   this.providerErrors = new Map();
 }
 
-MetricsCollector.prototype = {
+Collector.prototype = Object.freeze({
+  get providers() {
+    let providers = [];
+    for (let [name, entry] of this._providers) {
+      providers.push(entry.provider);
+    }
+
+    return providers;
+  },
+
   /**
    * Registers a `MetricsProvider` with this collector.
    *
    * 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.
+   *
    * @param provider
-   *        (MetricsProvider) The provider instance to register.
+   *        (Metrics.Provider) The provider instance to register.
+   *
+   * @return Promise<null>
    */
-  registerProvider: function registerProvider(provider) {
-    if (!(provider instanceof MetricsProvider)) {
-      throw new Error("argument must be a MetricsProvider instance.");
+  registerProvider: function (provider) {
+    if (!(provider instanceof Provider)) {
+      throw new Error("Argument must be a Provider instance.");
     }
 
-    for (let p of this._providers) {
-      if (p.provider == provider) {
-        return;
-      }
+    if (this._providers.has(provider.name)) {
+      return Promise.resolve();
     }
 
-    this._providers.push({
-      provider: provider,
-      constantsCollected: false,
-    });
+    return provider.init(this._storage).then(function afterInit() {
+      this._providers.set(provider.name, {
+        provider: provider,
+        constantsCollected: false,
+      });
 
-    this.providerErrors.set(provider.name, []);
+      this.providerErrors.set(provider.name, []);
+
+      return Promise.resolve();
+    }.bind(this));
   },
 
   /**
    * Collects all constant measurements from all providers.
    *
    * Returns a Promise that will be fulfilled once all data providers have
    * provided their constant data. A side-effect of this promise fulfillment
    * is that the collector is populated with the obtained collection results.
-   * The resolved value to the promise is this `MetricsCollector` instance.
+   * The resolved value to the promise is this `Collector` instance.
    */
-  collectConstantMeasurements: function collectConstantMeasurements() {
+  collectConstantData: function () {
     let promises = [];
 
-    for (let provider of this._providers) {
-      let name = provider.provider.name;
-
-      if (provider.constantsCollected) {
+    for (let [name, entry] of this._providers) {
+      if (entry.constantsCollected) {
         this._log.trace("Provider has already provided constant data: " +
                         name);
         continue;
       }
 
-      let result;
+      let collectPromise;
       try {
-        result = provider.provider.collectConstantMeasurements();
+        collectPromise = entry.provider.collectConstantData();
       } catch (ex) {
         this._log.warn("Exception when calling " + name +
-                       ".collectConstantMeasurements: " +
+                       ".collectConstantData: " +
                        CommonUtils.exceptionStr(ex));
         this.providerErrors.get(name).push(ex);
         continue;
       }
 
-      if (!result) {
-        this._log.trace("Provider does not provide constant data: " + name);
-        continue;
+      if (!collectPromise) {
+        throw new Error("Provider does not return a promise from " +
+                        "collectConstantData():" + name);
       }
 
-      try {
-        this._log.debug("Populating constant measurements: " + name);
-        result.populate(result);
-      } catch (ex) {
-        this._log.warn("Exception when calling " + name + ".populate(): " +
-                       CommonUtils.exceptionStr(ex));
-        result.addError(ex);
-        promises.push(Promise.resolve(result));
-        continue;
-      }
-
-      // Chain an invisible promise that updates state.
-      let promise = result.onFinished(function onFinished(result) {
-        provider.constantsCollected = true;
+      let promise = collectPromise.then(function onCollected(result) {
+        entry.constantsCollected = true;
 
         return Promise.resolve(result);
       });
 
-      promises.push(promise);
+      promises.push([name, promise]);
     }
 
     return this._handleCollectionPromises(promises);
   },
 
   /**
    * Handles promises returned by the collect* functions.
    *
    * This consumes the data resolved by the promises and returns a new promise
    * that will be resolved once all promises have been resolved.
+   *
+   * The promise is resolved even if one of the underlying collection
+   * promises is rejected.
    */
-  _handleCollectionPromises: function _handleCollectionPromises(promises) {
+  _handleCollectionPromises: function (promises) {
     if (!promises.length) {
       return Promise.resolve(this);
     }
 
     let deferred = Promise.defer();
     let finishedCount = 0;
 
-    let onResult = function onResult(result) {
-      try {
-        this._log.debug("Got result for " + result.name);
-
-        if (this.collectionResults.has(result.name)) {
-          this.collectionResults.get(result.name).aggregate(result);
-        } else {
-          this.collectionResults.set(result.name, result);
-        }
-      } finally {
-        finishedCount++;
-        if (finishedCount >= promises.length) {
-          deferred.resolve(this);
-        }
+    let onComplete = function () {
+      finishedCount++;
+      if (finishedCount >= promises.length) {
+        deferred.resolve(this);
       }
     }.bind(this);
 
-    let onError = function onError(error) {
-      this._log.warn("Error when handling result: " +
-                     CommonUtils.exceptionStr(error));
-      deferred.reject(error);
-    }.bind(this);
-
-    for (let promise of promises) {
-      promise.then(onResult, onError);
+    for (let [name, promise] of promises) {
+      let onError = function (error) {
+        this._log.warn("Collection promise was rejected: " +
+                       CommonUtils.exceptionStr(error));
+        this.providerErrors.get(name).push(error);
+        onComplete();
+      }.bind(this);
+      promise.then(onComplete, onError);
     }
 
     return deferred.promise;
   },
-};
+});
 
-Object.freeze(MetricsCollector.prototype);
-
--- a/services/metrics/dataprovider.jsm
+++ b/services/metrics/dataprovider.jsm
@@ -1,493 +1,535 @@
 /* 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/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
-  "MetricsCollectionResult",
-  "MetricsMeasurement",
-  "MetricsProvider",
+  "Measurement",
+  "Provider",
 ];
 
 const {utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/utils.js");
+
+
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 
 
 /**
- * Represents a measurement of data.
- *
- * This is how data is recorded and represented. Each instance of this type
- * represents a related set of data.
- *
- * Each data set has some basic metadata associated with it. This includes a
- * name and version.
+ * Represents a collection of related pieces/fields of data.
  *
- * This type is meant to be an abstract base type. Child types should define
- * a `fields` property which is a mapping of field names to metadata describing
- * that field. This field constitutes the "schema" of the measurement/type.
+ * This is an abstract base type. Providers implement child types that
+ * implement core functions such as `registerStorage`.
  *
- * Data is added to instances by calling `setValue()`. Values are validated
- * against the schema at add time.
- *
- * Field Specification
- * ===================
+ * This type provides the primary interface for storing, retrieving, and
+ * serializing data.
  *
- * The `fields` property is a mapping of string field names to a mapping of
- * metadata describing the field. This mapping can have the following
- * properties:
+ * Each derived type must define a `name` and `version` property. These must be
+ * a string name and integer version, respectively. The `name` is used to
+ * identify the measurement within a `Provider`. The version is to denote the
+ * behavior of the `Measurement` and the composition of its fields over time.
+ * When a new field is added or the behavior of an existing field changes
+ * (perhaps the method for storing it has changed), the version should be
+ * incremented.
  *
- *   type -- A string corresponding to the TYPE_* property name describing a
- *           field type. The TYPE_* properties are defined on this type. e.g.
- *           "TYPE_STRING".
- *
- *   optional -- If true, this field is optional. If omitted, the field is
- *               required.
+ * Each measurement consists of a set of named fields. Each field is primarily
+ * identified by a string name, which must be unique within the measurement.
  *
- * @param name
- *        (string) Name of this data set.
- * @param version
- *        (Number) Integer version of the data in this set.
+ * For fields backed by the SQLite metrics storage backend, fields must have a
+ * strongly defined type. Valid types include daily counters, daily discrete
+ * text values, etc. See `MetricsStorageSqliteBackend.FIELD_*`.
+ *
+ * FUTURE: provide hook points for measurements to supplement with custom
+ * storage needs.
  */
-this.MetricsMeasurement = function MetricsMeasurement(name, version) {
-  if (!this.fields) {
-    throw new Error("fields not defined on instance. You are likely using " +
-                    "this type incorrectly.");
-  }
-
-  if (!name) {
-    throw new Error("Must define a name for this measurement.");
+this.Measurement = function () {
+  if (!this.name) {
+    throw new Error("Measurement must have a name.");
   }
 
-  if (!version) {
-    throw new Error("Must define a version for this measurement.");
+  if (!this.version) {
+    throw new Error("Measurement must have a version.");
+  }
+
+  if (!Number.isInteger(this.version)) {
+    throw new Error("Measurement's version must be an integer: " + this.version);
   }
 
-  if (!Number.isInteger(version)) {
-    throw new Error("version must be an integer: " + version);
-  }
+  this._log = Log4Moz.repository.getLogger("Services.Metrics.Measurement." + this.name);
+
+  this.id = null;
+  this.storage = null;
+  this._fieldsByName = new Map();
 
-  this.name = name;
-  this.version = version;
-
-  this.values = new Map();
+  this._serializers = {};
+  this._serializers[this.SERIALIZE_JSON] = {
+    singular: this._serializeJSONSingular.bind(this),
+    daily: this._serializeJSONDay.bind(this),
+  };
 }
 
-MetricsMeasurement.prototype = {
-  /**
-   * An unsigned integer field stored in 32 bits.
-   *
-   * This holds values from 0 to 2^32 - 1.
-   */
-  TYPE_UINT32: {
-    validate: function validate(value) {
-      if (!Number.isInteger(value)) {
-        throw new Error("UINT32 field expects an integer. Got " + value);
-      }
-
-      if (value < 0) {
-        throw new Error("UINT32 field expects a positive integer. Got " + value);
-      }
-
-      if (value >= 0xffffffff) {
-        throw new Error("Value is too large to fit within 32 bits: " + value);
-      }
-    },
-  },
+Measurement.prototype = Object.freeze({
+  SERIALIZE_JSON: "json",
 
   /**
-   * A string field.
+   * Configures the storage backend so that it can store this measurement.
+   *
+   * Implementations must return a promise which is resolved when storage has
+   * been configured.
    *
-   * Values must be valid UTF-8 strings.
+   * Most implementations will typically call into this.registerStorageField()
+   * to configure fields in storage.
+   *
+   * FUTURE: Provide method for upgrading from older measurement versions.
    */
-  TYPE_STRING: {
-    validate: function validate(value) {
-      if (typeof(value) != "string") {
-        throw new Error("STRING field expects a string. Got " + typeof(value));
-      }
-    },
+  configureStorage: function () {
+    throw new Error("configureStorage() must be implemented.");
   },
 
   /**
-   * Set the value of a field.
+   * Obtain a serializer for this measurement.
+   *
+   * Implementations should return an object with the following keys:
+   *
+   *   singular -- Serializer for singular data.
+   *   daily -- Serializer for daily data.
    *
-   * This is ultimately how fields are set. All field sets should go through
-   * this function.
+   * Each item is a function that takes a single argument: the data to
+   * serialize. The passed data is a subset of that returned from
+   * this.getValues(). For "singular," data.singular is passed. For "daily",
+   * data.days.get(<day>) is passed.
    *
-   * Values are validated when they are set. If the value passed does not
-   * validate against the field's specification, an Error will be thrown.
+   * This function receives a single argument: the serialization format we
+   * are requesting. This is one of the SERIALIZE_* constants on this base type.
    *
-   * @param name
-   *        (string) The name of the field whose value to set.
-   * @param value
-   *        The value to set the field to.
+   * For SERIALIZE_JSON, the function should return an object that
+   * JSON.stringify() knows how to handle. This could be an anonymous object or
+   * array or any object with a property named `toJSON` whose value is a
+   * function. The returned object will be added to a larger document
+   * containing the results of all `serialize` calls.
+   *
+   * The default implementation knows how to serialize built-in types using
+   * very simple logic. If small encoding size is a goal, the default
+   * implementation may not be suitable. If an unknown field type is
+   * encountered, the default implementation will error.
+   *
+   * @param format
+   *        (string) A SERIALIZE_* constant defining what serialization format
+   *        to use.
    */
-  setValue: function setValue(name, value) {
-    if (!this.fields[name]) {
-      throw new Error("Attempting to set unknown field: " + name);
+  serializer: function (format) {
+    if (!(format in this._serializers)) {
+      throw new Error("Don't know how to serialize format: " + format);
     }
 
-    let type = this.fields[name].type;
+    return this._serializers[format];
+  },
 
-    if (!(type in this)) {
-      throw new Error("Unknown field type: " + type);
+  hasField: function (name) {
+    return this._fieldsByName.has(name);
+  },
+
+  fieldID: function (name) {
+    let entry = this._fieldsByName.get(name);
+
+    if (!entry) {
+      throw new Error("Unknown field: " + name);
     }
 
-    this[type].validate(value);
-    this.values.set(name, value);
+    return entry[0];
   },
 
-  /**
-   * Obtain the value of a named field.
-   *
-   * @param name
-   *        (string) The name of the field to retrieve.
-   */
-  getValue: function getValue(name) {
-    return this.values.get(name);
+  fieldType: function (name) {
+    let entry = this._fieldsByName.get(name);
+
+    if (!entry) {
+      throw new Error("Unknown field: " + name);
+    }
+
+    return entry[1];
   },
 
   /**
-   * Validate that this instance is in conformance with the specification.
+   * Register a named field with storage that's attached to this measurement.
    *
-   * This ensures all required fields are present. Field value validation
-   * occurs when individual fields are set.
+   * This is typically called during `configureStorage`. The `Measurement`
+   * implementation passes the field name and its type (one of the
+   * storage.FIELD_* constants). The storage backend then allocates space
+   * for this named field. A side-effect of calling this is that the field's
+   * storage ID is stored in this._fieldsByName and subsequent calls to the
+   * storage modifiers below will know how to reference this field in the
+   * storage backend.
+   *
+   * @param name
+   *        (string) The name of the field being registered.
+   * @param type
+   *        (string) A field type name. This is typically one of the
+   *        storage.FIELD_* constants. It could also be a custom type
+   *        (presumably registered by this measurement or provider).
    */
-  validate: function validate() {
-    for (let field in this.fields) {
-      let spec = this.fields[field];
+  registerStorageField: function (name, type) {
+    this._log.debug("Registering field: " + name + " " + type);
+
+    let deferred = Promise.defer();
 
-      if (!spec.optional && !(field in this.values)) {
-        throw new Error("Required field not defined: " + field);
-      }
-    }
+    let self = this;
+    this.storage.registerField(this.id, name, type).then(
+      function onSuccess(id) {
+        self._fieldsByName.set(name, [id, type]);
+        deferred.resolve();
+      }, deferred.reject);
+
+    return deferred.promise;
   },
 
-  toJSON: function toJSON() {
-    let fields = {};
-    for (let [k, v] of this.values) {
-      fields[k] = v;
-    }
+  incrementDailyCounter: function (field, date=new Date()) {
+    return this.storage.incrementDailyCounterFromFieldID(this.fieldID(field),
+                                                         date);
+  },
 
-    return {
-      name: this.name,
-      version: this.version,
-      fields: fields,
-    };
+  addDailyDiscreteNumeric: function (field, value, date=new Date()) {
+    return this.storage.addDailyDiscreteNumericFromFieldID(
+                          this.fieldID(field), value, date);
   },
-};
 
-Object.freeze(MetricsMeasurement.prototype);
-
+  addDailyDiscreteText: function (field, value, date=new Date()) {
+    return this.storage.addDailyDiscreteTextFromFieldID(
+                          this.fieldID(field), value, date);
+  },
 
-/**
- * Entity which provides metrics data for recording.
- *
- * This essentially provides an interface that different systems must implement
- * to provide collected metrics data.
- *
- * This type consists of various collect* functions. These functions are called
- * by the metrics collector at different points during the application's
- * lifetime. These functions return a `MetricsCollectionResult` instance.
- * This type behaves a lot like a promise. It has a `onFinished()` that can chain
- * deferred events until after the result is populated.
- *
- * Implementations of collect* functions should call `createResult()` to create
- * a new `MetricsCollectionResult` instance. They should then register
- * expected measurements with this instance, define a `populate` function on
- * it, then return the instance.
- *
- * It is important for the collect* functions to just create the empty
- * `MetricsCollectionResult` and nothing more. This is to enable the callee
- * to handle errors gracefully. If the collect* function were to raise, the
- * callee may not receive a `MetricsCollectionResult` instance and it would not
- * know what data is missing.
- *
- * See the documentation for `MetricsCollectionResult` for details on how
- * to perform population.
- *
- * Receivers of created `MetricsCollectionResult` instances should wait
- * until population has finished. They can do this by chaining on to the
- * promise inside that instance by calling `onFinished()`.
- *
- * The collect* functions can return null to signify that they will never
- * provide any data. This is the default implementation. An implemented
- * collect* function should *never* return null. Instead, it should return
- * a `MetricsCollectionResult` with expected measurements that has finished
- * populating (i.e. an empty result).
- *
- * @param name
- *        (string) The name of this provider.
- */
-this.MetricsProvider = function MetricsProvider(name) {
-  if (!name) {
-    throw new Error("MetricsProvider must have a name.");
-  }
+  setLastNumeric: function (field, value, date=new Date()) {
+    return this.storage.setLastNumericFromFieldID(this.fieldID(field), value,
+                                                  date);
+  },
 
-  if (typeof(name) != "string") {
-    throw new Error("name must be a string. Got: " + typeof(name));
-  }
-
-  this._log = Log4Moz.repository.getLogger("Services.Metrics.MetricsProvider");
-
-  this.name = name;
-}
+  setLastText: function (field, value, date=new Date()) {
+    return this.storage.setLastTextFromFieldID(this.fieldID(field), value,
+                                               date);
+  },
 
-MetricsProvider.prototype = {
-  /**
-   * Collects constant measurements.
-   *
-   * Constant measurements are data that doesn't change during the lifetime of
-   * the application/process. The metrics collector only needs to call this
-   * once per `MetricsProvider` instance per process lifetime.
-   */
-  collectConstantMeasurements: function collectConstantMeasurements() {
-    return null;
+  setDailyLastNumeric: function (field, value, date=new Date()) {
+    return this.storage.setDailyLastNumericFromFieldID(this.fieldID(field),
+                                                       value, date);
+  },
+
+  setDailyLastText: function (field, value, date=new Date()) {
+    return this.storage.setDailyLastTextFromFieldID(this.fieldID(field),
+                                                    value, date);
   },
 
   /**
-   * Create a new `MetricsCollectionResult` tied to this provider.
+   * Obtain all values stored for this measurement.
+   *
+   * The default implementation obtains all known types from storage. If the
+   * measurement provides custom types or stores values somewhere other than
+   * storage, it should define its own implementation.
+   *
+   * This returns a promise that resolves to a data structure which is
+   * understood by the measurement's serialize() function.
    */
-  createResult: function createResult() {
-    return new MetricsCollectionResult(this.name);
+  getValues: function () {
+    return this.storage.getMeasurementValues(this.id);
+  },
+
+  deleteLastNumeric: function (field) {
+    return this.storage.deleteLastNumericFromFieldID(this.fieldID(field));
+  },
+
+  deleteLastText: function (field) {
+    return this.storage.deleteLastTextFromFieldID(this.fieldID(field));
   },
-};
+
+  _serializeJSONSingular: function (data) {
+    let result = {};
+
+    for (let [field, data] of data) {
+      // There could be legacy fields in storage we no longer care about.
+      if (!this._fieldsByName.has(field)) {
+        continue;
+      }
+
+      let type = this.fieldType(field);
+
+      switch (type) {
+        case this.storage.FIELD_LAST_NUMERIC:
+        case this.storage.FIELD_LAST_TEXT:
+          result[field] = data[1];
+          break;
+
+        case this.storage.FIELD_DAILY_COUNTER:
+        case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
+        case this.storage.FIELD_DAILY_DISCRETE_TEXT:
+        case this.storage.FIELD_DAILY_LAST_NUMERIC:
+        case this.storage.FIELD_DAILY_LAST_TEXT:
+          continue;
+
+        default:
+          throw new Error("Unknown field type: " + type);
+      }
+    }
 
-Object.freeze(MetricsProvider.prototype);
+    return result;
+  },
+
+  _serializeJSONDay: function (data) {
+    let result = {};
+
+    for (let [field, data] of data) {
+      if (!this._fieldsByName.has(field)) {
+        continue;
+      }
+
+      let type = this.fieldType(field);
+
+      switch (type) {
+        case this.storage.FIELD_DAILY_COUNTER:
+        case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
+        case this.storage.FIELD_DAILY_DISCRETE_TEXT:
+        case this.storage.FIELD_DAILY_LAST_NUMERIC:
+        case this.storage.FIELD_DAILY_LAST_TEXT:
+          result[field] = data;
+          break;
+
+        case this.storage.FIELD_LAST_NUMERIC:
+        case this.storage.FIELD_LAST_TEXT:
+          continue;
+
+        default:
+          throw new Error("Unknown field type: " + type);
+      }
+    }
+
+    return result;
+  },
+});
 
 
 /**
- * Holds the result of metrics collection.
+ * An entity that emits data.
+ *
+ * A `Provider` consists of a string name (must be globally unique among all
+ * known providers) and a set of `Measurement` instances.
  *
- * This is the type eventually returned by the MetricsProvider.collect*
- * functions. It holds all results and any state/errors that occurred while
- * collecting.
+ * The main role of a `Provider` is to produce metrics data and to store said
+ * data in the storage backend.
+ *
+ * Metrics data collection is initiated either by a collector calling a
+ * `collect*` function on `Provider` instances or by the `Provider` registering
+ * to some external event and then reacting whenever they occur.
  *
- * This type is essentially a container for `MetricsMeasurement` instances that
- * provides some smarts useful for capturing state.
+ * `Provider` implementations interface directly with a storage backend. For
+ * common stored values (daily counters, daily discrete values, etc),
+ * implementations should interface with storage via the various helper
+ * functions on the `Measurement` instances. For custom stored value types,
+ * implementations will interact directly with the low-level storage APIs.
  *
- * The first things consumers of new instances should do is define the set of
- * expected measurements this result will contain via `expectMeasurement`. If
- * population of this instance is aborted or times out, downstream consumers
- * will know there is missing data.
+ * Because multiple providers exist and could be responding to separate
+ * external events simultaneously and because not all operations performed by
+ * storage can safely be performed in parallel, writing directly to storage at
+ * event time is dangerous. Therefore, interactions with storage must be
+ * deferred until it is safe to perform them.
+ *
+ * This typically looks something like:
  *
- * Next, they should define the `populate` property to a function that
- * populates the instance.
+ *   // This gets called when an external event worthy of recording metrics
+ *   // occurs. The function receives a numeric value associated with the event.
+ *   function onExternalEvent (value) {
+ *     let now = new Date();
+ *     let m = this.getMeasurement("foo", 1);
+ *
+ *     this.enqueueStorageOperation(function storeExternalEvent() {
  *
- * The `populate` function implementation should add empty `MetricsMeasurement`
- * instances to the result via `addMeasurement`. Then, it should populate these
- * measurements via `setValue`.
+ *       // We interface with storage via the `Measurement` helper functions.
+ *       // These each return a promise that will be resolved when the
+ *       // operation finishes. We rely on behavior of storage where operations
+ *       // are executed single threaded and sequentially. Therefore, we only
+ *       // need to return the final promise.
+ *       m.incrementDailyCounter("foo", now);
+ *       return m.addDailyDiscreteNumericValue("my_value", value, now);
+ *     }.bind(this));
  *
- * It is preferred to populate via this type instead of directly on
- * `MetricsMeasurement` instances so errors with data population can be
- * captured and reported.
+ *   }
+ *
+ *
+ * `Provider` is an abstract base class. Implementations must define a few
+ * properties:
  *
- * Once population has finished, `finish()` must be called.
+ *   name
+ *     The `name` property should be a string defining the provider's name. The
+ *     name must be globally unique for the application. The name is used as an
+ *     identifier to distinguish providers from each other.
  *
- * @param name
- *        (string) The name of the provider this result came from.
+ *   measurementTypes
+ *     This must be an array of `Measurement`-derived types. Note that elements
+ *     in the array are the type functions, not instances. Instances of the
+ *     `Measurement` are created at run-time by the `Provider` and are bound
+ *     to the provider and to a specific storage backend.
  */
-this.MetricsCollectionResult = function MetricsCollectionResult(name) {
-  if (!name || typeof(name) != "string") {
-    throw new Error("Must provide name argument to MetricsCollectionResult.");
+this.Provider = function () {
+  if (!this.name) {
+    throw new Error("Provider must define a name.");
+  }
+
+  if (!Array.isArray(this.measurementTypes)) {
+    throw new Error("Provider must define measurement types.");
   }
 
-  this._log = Log4Moz.repository.getLogger("Services.Metrics.MetricsCollectionResult");
-
-  this.name = name;
+  this._log = Log4Moz.repository.getLogger("Services.Metrics.Provider." + this.name);
 
-  this.measurements = new Map();
-  this.expectedMeasurements = new Set();
-  this.errors = [];
-
-  this.populate = function populate() {
-    throw new Error("populate() must be defined on MetricsCollectionResult " +
-                    "instance.");
-  };
-
-  this._deferred = Promise.defer();
+  this.measurements = null;
+  this.storage = null;
 }
 
-MetricsCollectionResult.prototype = {
+Provider.prototype = Object.freeze({
   /**
-   * The Set of `MetricsMeasurement` names currently missing from this result.
+   * Obtain a `Measurement` from its name and version.
+   *
+   * If the measurement is not found, an Error is thrown.
    */
-  get missingMeasurements() {
-    let missing = new Set();
+  getMeasurement: function (name, version) {
+    if (!Number.isInteger(version)) {
+      throw new Error("getMeasurement expects an integer version. Got: " + version);
+    }
+
+    let m = this.measurements.get([name, version].join(":"));
 
-    for (let name of this.expectedMeasurements) {
-      if (this.measurements.has(name)) {
-        continue;
-      }
+    if (!m) {
+      throw new Error("Unknown measurement: " + name + " v" + version);
+    }
+
+    return m;
+  },
 
-      missing.add(name);
+  init: function (storage) {
+    if (this.storage !== null) {
+      throw new Error("Provider() not called. Did the sub-type forget to call it?");
+    }
+
+    if (this.storage) {
+      throw new Error("Provider has already been initialized.");
     }
 
-    return missing;
-  },
+    this.measurements = new Map();
+    this.storage = storage;
+
+    let self = this;
+    return Task.spawn(function init() {
+      for (let measurementType of self.measurementTypes) {
+        let measurement = new measurementType();
+
+        measurement.provider = self;
+        measurement.storage = self.storage;
+
+        let id = yield storage.registerMeasurement(self.name, measurement.name,
+                                                   measurement.version);
 
-  /**
-   * Record that this result is expected to provide a named measurement.
-   *
-   * This function should be called ASAP on new `MetricsCollectionResult`
-   * instances. It defines expectations about what data should be present.
-   *
-   * @param name
-   *        (string) The name of the measurement this result should contain.
-   */
-  expectMeasurement: function expectMeasurement(name) {
-    this.expectedMeasurements.add(name);
+        measurement.id = id;
+
+        yield measurement.configureStorage();
+
+        self.measurements.set([measurement.name, measurement.version].join(":"),
+                              measurement);
+      }
+
+      let promise = self.onInit();
+
+      if (!promise || typeof(promise.then) != "function") {
+        throw new Error("onInit() does not return a promise.");
+      }
+
+      yield promise;
+    });
   },
 
-  /**
-   * Add a `MetricsMeasurement` to this result.
-   */
-  addMeasurement: function addMeasurement(data) {
-    if (!(data instanceof MetricsMeasurement)) {
-      throw new Error("addMeasurement expects a MetricsMeasurement instance.");
+  shutdown: function () {
+    let promise = this.onShutdown();
+
+    if (!promise || typeof(promise.then) != "function") {
+      throw new Error("onShutdown implementation does not return a promise.");
     }
 
-    if (!this.expectedMeasurements.has(data.name)) {
-      throw new Error("Not expecting this measurement: " + data.name);
-    }
-
-    if (this.measurements.has(data.name)) {
-      throw new Error("Measurement of this name already present: " + data.name);
-    }
-
-    this.measurements.set(data.name, data);
+    return promise;
   },
 
   /**
-   * Sets the value of a field in a registered measurement instance.
-   *
-   * This is a convenience function to set a field on a measurement. If an
-   * error occurs, it will record that error in the errors container.
-   *
-   * Attempting to set a value on a measurement that does not exist results
-   * in an Error being thrown. Attempting a bad assignment on an existing
-   * measurement will not throw unless `rethrow` is true.
+   * Hook point for implementations to perform initialization activity.
    *
-   * @param name
-   *        (string) The `MetricsMeasurement` on which to set the value.
-   * @param field
-   *        (string) The field we are setting.
-   * @param value
-   *        The value being set.
-   * @param rethrow
-   *        (bool) Whether to rethrow any errors encountered.
+   * If a `Provider` instance needs to register observers, etc, it should
+   * implement this function.
    *
-   * @return bool
-   *         Whether the assignment was successful.
+   * Implementations should return a promise which is resolved when
+   * initialization activities have completed.
    */
-  setValue: function setValue(name, field, value, rethrow=false) {
-    let m = this.measurements.get(name);
-    if (!m) {
-      throw new Error("Attempting to operate on an undefined measurement: " +
-                      name);
-    }
-
-    try {
-      m.setValue(field, value);
-      return true;
-    } catch (ex) {
-      this.addError(ex);
-
-      if (rethrow) {
-        throw ex;
-      }
-
-      return false;
-    }
+  onInit: function () {
+    return Promise.resolve();
   },
 
   /**
-   * Record an error that was encountered when populating this result.
+   * Hook point for shutdown of instances.
+   *
+   * This is the opposite of `onInit`. If a `Provider` needs to unregister
+   * observers, etc, this is where it should do it.
+   *
+   * Implementations should return a promise which is resolved when
+   * shutdown activities have completed.
    */
-  addError: function addError(error) {
-    this.errors.push(error);
+  onShutdown: function () {
+    return Promise.resolve();
+  },
+
+  /**
+   * Collects data that doesn't change during the application's lifetime.
+   *
+   * Implementations should return a promise that resolves when all data has
+   * been collected and storage operations have been finished.
+   */
+  collectConstantData: function () {
+    return Promise.resolve();
   },
 
   /**
-   * Aggregate another MetricsCollectionResult into this one.
+   * Queue a deferred storage operation.
    *
-   * Instances can only be aggregated together if they belong to the same
-   * provider (they have the same name).
+   * Deferred storage operations are the preferred method for providers to
+   * interact with storage. When collected data is to be added to storage,
+   * the provider creates a function that performs the necessary storage
+   * interactions and then passes that function to this function. Pending
+   * storage operations will be executed sequentially by a coordinator.
+   *
+   * The passed function should return a promise which will be resolved upon
+   * completion of storage interaction.
    */
-  aggregate: function aggregate(other) {
-    if (!(other instanceof MetricsCollectionResult)) {
-      throw new Error("aggregate expects a MetricsCollectionResult instance.");
-    }
-
-    if (this.name != other.name) {
-      throw new Error("Can only aggregate MetricsCollectionResult from " +
-                      "the same provider. " + this.name + " != " + other.name);
-    }
-
-    for (let name of other.expectedMeasurements) {
-      this.expectedMeasurements.add(name);
-    }
-
-    for (let [name, m] of other.measurements) {
-      if (this.measurements.has(name)) {
-        throw new Error("Incoming result has same measurement as us: " + name);
-      }
-
-      this.measurements.set(name, m);
-    }
-
-    this.errors = this.errors.concat(other.errors);
+  enqueueStorageOperation: function (func) {
+    return this.storage.enqueueOperation(func);
   },
 
-  toJSON: function toJSON() {
-    let o = {
-      measurements: {},
-      missing: [],
-      errors: [],
-    };
-
-    for (let [name, value] of this.measurements) {
-      o.measurements[name] = value;
-    }
-
-    for (let missing of this.missingMeasurements) {
-      o.missing.push(missing);
-    }
-
-    for (let error of this.errors) {
-      if (error.message) {
-        o.errors.push(error.message);
-      } else {
-        o.errors.push(error);
-      }
-    }
-
-    return o;
+  getState: function (key) {
+    let name = this.name;
+    let storage = this.storage;
+    return storage.enqueueOperation(function get() {
+      return storage.getProviderState(name, key);
+    });
   },
 
-  /**
-   * Signal that population of the result has finished.
-   *
-   * This will resolve the internal promise.
-   */
-  finish: function finish() {
-    this._deferred.resolve(this);
+  setState: function (key, value) {
+    let name = this.name;
+    let storage = this.storage;
+    return storage.enqueueOperation(function set() {
+      return storage.setProviderState(name, key, value);
+    });
   },
 
-  /**
-   * Chain deferred behavior until after the result has finished population.
-   *
-   * This is a wrapped around the internal promise's `then`.
-   *
-   * We can't call this "then" because the core promise library will get
-   * confused.
-   */
-  onFinished: function onFinished(onFulfill, onError) {
-    return this._deferred.promise.then(onFulfill, onError);
+  _dateToDays: function (date) {
+    return Math.floor(date.getTime() / MILLISECONDS_PER_DAY);
   },
-};
 
-Object.freeze(MetricsCollectionResult.prototype);
+  _daysToDate: function (days) {
+    return new Date(days * MILLISECONDS_PER_DAY);
+  },
+});
 
--- a/services/metrics/modules-testing/mocks.jsm
+++ b/services/metrics/modules-testing/mocks.jsm
@@ -6,69 +6,84 @@
 
 this.EXPORTED_SYMBOLS = [
   "DummyMeasurement",
   "DummyProvider",
 ];
 
 const {utils: Cu} = Components;
 
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
+Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Metrics.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 
 this.DummyMeasurement = function DummyMeasurement(name="DummyMeasurement") {
-  MetricsMeasurement.call(this, name, 2);
+  this.name = name;
+
+  Metrics.Measurement.call(this);
 }
+
 DummyMeasurement.prototype = {
-  __proto__: MetricsMeasurement.prototype,
+  __proto__: Metrics.Measurement.prototype,
+
+  version: 1,
 
-  fields: {
-    "string": {
-      type: "TYPE_STRING",
-    },
-
-    "uint32": {
-      type: "TYPE_UINT32",
-      optional: true,
-    },
+  configureStorage: function () {
+    let self = this;
+    return Task.spawn(function configureStorage() {
+      yield self.registerStorageField("daily-counter", self.storage.FIELD_DAILY_COUNTER);
+      yield self.registerStorageField("daily-discrete-numeric", self.storage.FIELD_DAILY_DISCRETE_NUMERIC);
+      yield self.registerStorageField("daily-discrete-text", self.storage.FIELD_DAILY_DISCRETE_TEXT);
+      yield self.registerStorageField("daily-last-numeric", self.storage.FIELD_DAILY_LAST_NUMERIC);
+      yield self.registerStorageField("daily-last-text", self.storage.FIELD_DAILY_LAST_TEXT);
+      yield self.registerStorageField("last-numeric", self.storage.FIELD_LAST_NUMERIC);
+      yield self.registerStorageField("last-text", self.storage.FIELD_LAST_TEXT);
+    });
   },
 };
 
 
 this.DummyProvider = function DummyProvider(name="DummyProvider") {
-  MetricsProvider.call(this, name);
+  this.name = name;
+
+  this.measurementTypes = [DummyMeasurement];
+
+  Metrics.Provider.call(this);
 
   this.constantMeasurementName = "DummyMeasurement";
   this.collectConstantCount = 0;
-  this.throwDuringCollectConstantMeasurements = null;
+  this.throwDuringCollectConstantData = null;
   this.throwDuringConstantPopulate = null;
+
+  this.havePushedMeasurements = true;
 }
+
 DummyProvider.prototype = {
-  __proto__: MetricsProvider.prototype,
+  __proto__: Metrics.Provider.prototype,
 
-  collectConstantMeasurements: function collectConstantMeasurements() {
+  collectConstantData: function () {
     this.collectConstantCount++;
 
-    let result = this.createResult();
-    result.expectMeasurement(this.constantMeasurementName);
-
-    result.populate = this._populateConstantResult.bind(this);
-
-    if (this.throwDuringCollectConstantMeasurements) {
-      throw new Error(this.throwDuringCollectConstantMeasurements);
+    if (this.throwDuringCollectConstantData) {
+      throw new Error(this.throwDuringCollectConstantData);
     }
 
-    return result;
+    return this.enqueueStorageOperation(function doStorage() {
+      if (this.throwDuringConstantPopulate) {
+        throw new Error(this.throwDuringConstantPopulate);
+      }
+
+      let m = this.getMeasurement("DummyMeasurement", 1);
+      let now = new Date();
+      m.incrementDailyCounter("daily-counter", now);
+      m.addDailyDiscreteNumeric("daily-discrete-numeric", 1, now);
+      m.addDailyDiscreteNumeric("daily-discrete-numeric", 2, now);
+      m.addDailyDiscreteText("daily-discrete-text", "foo", now);
+      m.addDailyDiscreteText("daily-discrete-text", "bar", now);
+      m.setDailyLastNumeric("daily-last-numeric", 3, now);
+      m.setDailyLastText("daily-last-text", "biz", now);
+      m.setLastNumeric("last-numeric", 4, now);
+      return m.setLastText("last-text", "bazfoo", now);
+    }.bind(this));
   },
 
-  _populateConstantResult: function _populateConstantResult(result) {
-    if (this.throwDuringConstantPopulate) {
-      throw new Error(this.throwDuringConstantPopulate);
-    }
+};
 
-    this._log.debug("Populating constant measurement in DummyProvider.");
-    result.addMeasurement(new DummyMeasurement(this.constantMeasurementName));
-
-    result.setValue(this.constantMeasurementName, "string", "foo");
-    result.setValue(this.constantMeasurementName, "uint32", 24);
-
-    result.finish();
-  },
-};
new file mode 100644
--- /dev/null
+++ b/services/metrics/storage.jsm
@@ -0,0 +1,2028 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "MetricsStorageBackend",
+  "dateToDays",
+  "daysToDate",
+];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Sqlite.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/utils.js");
+
+
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
+
+// These do not account for leap seconds. Meh.
+function dateToDays(date) {
+  return Math.floor(date.getTime() / MILLISECONDS_PER_DAY);
+}
+
+function daysToDate(days) {
+  return new Date(days * MILLISECONDS_PER_DAY);
+}
+
+/**
+ * Represents a collection of per-day values.
+ *
+ * This is a proxy around a Map which can transparently round Date instances to
+ * their appropriate key.
+ *
+ * This emulates Map by providing .size and iterator support. Note that keys
+ * from the iterator are Date instances corresponding to midnight of the start
+ * of the day. get(), has(), and set() are modeled as getDay(), hasDay(), and
+ * setDay(), respectively.
+ *
+ * All days are defined in terms of UTC (as opposed to local time).
+ */
+function DailyValues() {
+  this._days = new Map();
+}
+
+DailyValues.prototype = Object.freeze({
+  __iterator__: function () {
+    for (let [k, v] of this._days) {
+      yield [daysToDate(k), v];
+    }
+  },
+
+  get size() {
+    return this._days.size;
+  },
+
+  hasDay: function (date) {
+    return this._days.has(dateToDays(date));
+  },
+
+  getDay: function (date) {
+    return this._days.get(dateToDays(date));
+  },
+
+  setDay: function (date, value) {
+    this._days.set(dateToDays(date), value);
+  },
+
+  appendValue: function (date, value) {
+    let key = dateToDays(date);
+
+    if (this._days.has(key)) {
+      return this._days.get(key).push(value);
+    }
+
+    this._days.set(key, [value]);
+  },
+});
+
+
+/**
+ * DATABASE INFO
+ * =============
+ *
+ * We use a SQLite database as the backend for persistent storage of metrics
+ * data.
+ *
+ * Every piece of recorded data is associated with a measurement. A measurement
+ * is an entity with a name and version. Each measurement is associated with a
+ * named provider.
+ *
+ * When the metrics system is initialized, we ask providers (the entities that
+ * emit data) to configure the database for storage of their data. They tell
+ * storage what their requirements are. For example, they'll register
+ * named daily counters associated with specific measurements.
+ *
+ * Recorded data is stored differently depending on the requirements for
+ * storing it. We have facilities for storing the following classes of data:
+ *
+ *  1) Counts of event/field occurrences aggregated by day.
+ *  2) Discrete values of fields aggregated by day.
+ *  3) Discrete values of fields aggregated by day max 1 per day (last write
+ *     wins).
+ *  4) Discrete values of fields max 1 (last write wins).
+ *
+ * Most data is aggregated per day mainly for privacy reasons. This does throw
+ * away potentially useful data. But, it's not currently used, so there is no
+ * need to keep the granular information.
+ *
+ * Database Schema
+ * ---------------
+ *
+ * This database contains the following tables:
+ *
+ *   providers -- Maps provider string name to an internal ID.
+ *   provider_state -- Holds opaque persisted state for providers.
+ *   measurements -- Holds the set of known measurements (name, version,
+ *     provider tuples).
+ *   types -- The data types that can be stored in measurements/fields.
+ *   fields -- Describes entities that occur within measurements.
+ *   daily_counters -- Holds daily-aggregated counts of events. Each row is
+ *     associated with a field and a day.
+ *   daily_discrete_numeric -- Holds numeric values for fields grouped by day.
+ *     Each row contains a discrete value associated with a field that occurred
+ *     on a specific day. There can be multiple rows per field per day.
+ *   daily_discrete_text -- Holds text values for fields grouped by day. Each
+ *     row contains a discrete value associated with a field that occurred on a
+ *     specific day.
+ *   daily_last_numeric -- Holds numeric values where the last encountered
+ *     value for a given day is retained.
+ *   daily_last_text -- Like daily_last_numeric except for text values.
+ *   last_numeric -- Holds the most recent value for a numeric field.
+ *   last_text -- Like last_numeric except for text fields.
+ *
+ * Notes
+ * -----
+ *
+ * It is tempting to use SQLite's julianday() function to store days that
+ * things happened. However, a Julian Day begins at *noon* in 4714 B.C. This
+ * results in weird half day offsets from UNIX time. So, we instead store
+ * number of days since UNIX epoch, not Julian.
+ */
+
+/**
+ * All of our SQL statements are stored in a central mapping so they can easily
+ * be audited for security, perf, etc.
+ */
+const SQL = {
+  // Create the providers table.
+  createProvidersTable:
+    "CREATE TABLE providers (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "name TEXT, " +
+      "UNIQUE (name) " +
+    ")",
+
+  createProviderStateTable:
+    "CREATE TABLE provider_state (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "provider_id INTEGER, " +
+      "name TEXT, " +
+      "VALUE TEXT, " +
+      "UNIQUE (provider_id, name), " +
+      "FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE" +
+    ")",
+
+  createProviderStateProviderIndex:
+    "CREATE INDEX i_provider_state_provider_id ON provider_state (provider_id)",
+
+  createMeasurementsTable:
+    "CREATE TABLE measurements (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "provider_id INTEGER, " +
+      "name TEXT, " +
+      "version INTEGER, " +
+      "UNIQUE (provider_id, name, version), " +
+      "FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE" +
+    ")",
+
+  createMeasurementsProviderIndex:
+    "CREATE INDEX i_measurements_provider_id ON measurements (provider_id)",
+
+  createMeasurementsView:
+    "CREATE VIEW v_measurements AS " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version " +
+      "FROM providers, measurements " +
+      "WHERE " +
+        "measurements.provider_id = providers.id",
+
+  createTypesTable:
+    "CREATE TABLE types (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "name TEXT, " +
+      "UNIQUE (name)" +
+    ")",
+
+  createFieldsTable:
+    "CREATE TABLE fields (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "measurement_id INTEGER, " +
+      "name TEXT, " +
+      "value_type INTEGER , " +
+      "UNIQUE (measurement_id, name), " +
+      "FOREIGN KEY (measurement_id) REFERENCES measurements(id) ON DELETE CASCADE " +
+      "FOREIGN KEY (value_type) REFERENCES types(id) ON DELETE CASCADE " +
+    ")",
+
+  createFieldsMeasurementIndex:
+    "CREATE INDEX i_fields_measurement_id ON fields (measurement_id)",
+
+  createFieldsView:
+    "CREATE VIEW v_fields AS " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "types.id AS type_id, " +
+        "types.name AS type_name " +
+      "FROM providers, measurements, fields, types " +
+      "WHERE " +
+        "fields.measurement_id = measurements.id " +
+        "AND measurements.provider_id = providers.id " +
+        "AND fields.value_type = types.id",
+
+  createDailyCountersTable:
+    "CREATE TABLE daily_counters (" +
+      "field_id INTEGER, " +
+      "day INTEGER, " +
+      "value INTEGER, " +
+      "UNIQUE(field_id, day), " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createDailyCountersFieldIndex:
+    "CREATE INDEX i_daily_counters_field_id ON daily_counters (field_id)",
+
+  createDailyCountersDayIndex:
+    "CREATE INDEX i_daily_counters_day ON daily_counters (day)",
+
+  createDailyCountersView:
+    "CREATE VIEW v_daily_counters AS SELECT " +
+      "providers.id AS provider_id, " +
+      "providers.name AS provider_name, " +
+      "measurements.id AS measurement_id, " +
+      "measurements.name AS measurement_name, " +
+      "measurements.version AS measurement_version, " +
+      "fields.id AS field_id, " +
+      "fields.name AS field_name, " +
+      "daily_counters.day AS day, " +
+      "daily_counters.value AS value " +
+    "FROM providers, measurements, fields, daily_counters " +
+    "WHERE " +
+      "daily_counters.field_id = fields.id " +
+      "AND fields.measurement_id = measurements.id " +
+      "AND measurements.provider_id = providers.id",
+
+  createDailyDiscreteNumericsTable:
+    "CREATE TABLE daily_discrete_numeric (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "field_id INTEGER, " +
+      "day INTEGER, " +
+      "value INTEGER, " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createDailyDiscreteNumericsFieldIndex:
+    "CREATE INDEX i_daily_discrete_numeric_field_id " +
+    "ON daily_discrete_numeric (field_id)",
+
+  createDailyDiscreteNumericsDayIndex:
+    "CREATE INDEX i_daily_discrete_numeric_day " +
+    "ON daily_discrete_numeric (day)",
+
+  createDailyDiscreteTextTable:
+    "CREATE TABLE daily_discrete_text (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "field_id INTEGER, " +
+      "day INTEGER, " +
+      "value TEXT, " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createDailyDiscreteTextFieldIndex:
+    "CREATE INDEX i_daily_discrete_text_field_id " +
+    "ON daily_discrete_text (field_id)",
+
+  createDailyDiscreteTextDayIndex:
+    "CREATE INDEX i_daily_discrete_text_day " +
+    "ON daily_discrete_text (day)",
+
+  createDailyDiscreteView:
+    "CREATE VIEW v_daily_discrete AS " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "daily_discrete_numeric.id AS value_id, " +
+        "daily_discrete_numeric.day AS day, " +
+        "daily_discrete_numeric.value AS value, " +
+        '"numeric" AS value_type ' +
+        "FROM providers, measurements, fields, daily_discrete_numeric " +
+        "WHERE " +
+          "daily_discrete_numeric.field_id = fields.id " +
+          "AND fields.measurement_id = measurements.id " +
+          "AND measurements.provider_id = providers.id " +
+      "UNION ALL " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "daily_discrete_text.id AS value_id, " +
+        "daily_discrete_text.day AS day, " +
+        "daily_discrete_text.value AS value, " +
+        '"text" AS value_type ' +
+        "FROM providers, measurements, fields, daily_discrete_text " +
+        "WHERE " +
+          "daily_discrete_text.field_id = fields.id " +
+          "AND fields.measurement_id = measurements.id " +
+          "AND measurements.provider_id = providers.id " +
+      "ORDER BY day ASC, value_id ASC",
+
+  createLastNumericTable:
+    "CREATE TABLE last_numeric (" +
+      "field_id INTEGER PRIMARY KEY, " +
+      "day INTEGER, " +
+      "value NUMERIC, " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createLastTextTable:
+    "CREATE TABLE last_text (" +
+      "field_id INTEGER PRIMARY KEY, " +
+      "day INTEGER, " +
+      "value TEXT, " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createLastView:
+    "CREATE VIEW v_last AS " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "last_numeric.day AS day, " +
+        "last_numeric.value AS value, " +
+        '"numeric" AS value_type ' +
+        "FROM providers, measurements, fields, last_numeric " +
+        "WHERE " +
+          "last_numeric.field_id = fields.id " +
+          "AND fields.measurement_id = measurements.id " +
+          "AND measurements.provider_id = providers.id " +
+      "UNION ALL " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "last_text.day AS day, " +
+        "last_text.value AS value, " +
+        '"text" AS value_type ' +
+        "FROM providers, measurements, fields, last_text " +
+        "WHERE " +
+          "last_text.field_id = fields.id " +
+          "AND fields.measurement_id = measurements.id " +
+          "AND measurements.provider_id = providers.id",
+
+  createDailyLastNumericTable:
+    "CREATE TABLE daily_last_numeric (" +
+      "field_id INTEGER, " +
+      "day INTEGER, " +
+      "value NUMERIC, " +
+      "UNIQUE (field_id, day) " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createDailyLastNumericFieldIndex:
+    "CREATE INDEX i_daily_last_numeric_field_id ON daily_last_numeric (field_id)",
+
+  createDailyLastNumericDayIndex:
+    "CREATE INDEX i_daily_last_numeric_day ON daily_last_numeric (day)",
+
+  createDailyLastTextTable:
+    "CREATE TABLE daily_last_text (" +
+      "field_id INTEGER, " +
+      "day INTEGER, " +
+      "value TEXT, " +
+      "UNIQUE (field_id, day) " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createDailyLastTextFieldIndex:
+    "CREATE INDEX i_daily_last_text_field_id ON daily_last_text (field_id)",
+
+  createDailyLastTextDayIndex:
+    "CREATE INDEX i_daily_last_text_day ON daily_last_text (day)",
+
+  createDailyLastView:
+    "CREATE VIEW v_daily_last AS " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "daily_last_numeric.day AS day, " +
+        "daily_last_numeric.value AS value, " +
+        '"numeric" as value_type ' +
+        "FROM providers, measurements, fields, daily_last_numeric " +
+        "WHERE " +
+          "daily_last_numeric.field_id = fields.id " +
+          "AND fields.measurement_id = measurements.id " +
+          "AND measurements.provider_id = providers.id " +
+      "UNION ALL " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "daily_last_text.day AS day, " +
+        "daily_last_text.value AS value, " +
+        '"text" as value_type ' +
+        "FROM providers, measurements, fields, daily_last_text " +
+        "WHERE " +
+          "daily_last_text.field_id = fields.id " +
+          "AND fields.measurement_id = measurements.id " +
+          "AND measurements.provider_id = providers.id",
+
+  // Mutation.
+
+  addProvider: "INSERT INTO providers (name) VALUES (:provider)",
+
+  setProviderState:
+    "INSERT OR REPLACE INTO provider_state " +
+      "(provider_id, name, value) " +
+      "VALUES (:provider_id, :name, :value)",
+
+  addMeasurement:
+    "INSERT INTO measurements (provider_id, name, version) " +
+      "VALUES (:provider_id, :measurement, :version)",
+
+  addType: "INSERT INTO types (name) VALUES (:name)",
+
+  addField:
+    "INSERT INTO fields (measurement_id, name, value_type) " +
+      "VALUES (:measurement_id, :field, :value_type)",
+
+  incrementDailyCounterFromFieldID:
+    "INSERT OR REPLACE INTO daily_counters VALUES (" +
+      ":field_id, " +
+      ":days, " +
+      "COALESCE(" +
+        "(SELECT value FROM daily_counters WHERE " +
+          "field_id = :field_id AND day = :days " +
+        "), " +
+        "0" +
+      ") + 1)",
+
+  deleteLastNumericFromFieldID:
+    "DELETE FROM last_numeric WHERE field_id = :field_id",
+
+  deleteLastTextFromFieldID:
+    "DELETE FROM last_text WHERE field_id = :field_id",
+
+  setLastNumeric:
+    "INSERT OR REPLACE INTO last_numeric VALUES (:field_id, :days, :value)",
+
+  setLastText:
+    "INSERT OR REPLACE INTO last_text VALUES (:field_id, :days, :value)",
+
+  setDailyLastNumeric:
+    "INSERT OR REPLACE INTO daily_last_numeric VALUES (:field_id, :days, :value)",
+
+  setDailyLastText:
+    "INSERT OR REPLACE INTO daily_last_text VALUES (:field_id, :days, :value)",
+
+  addDailyDiscreteNumeric:
+    "INSERT INTO daily_discrete_numeric " +
+    "(field_id, day, value) VALUES (:field_id, :days, :value)",
+
+  addDailyDiscreteText:
+    "INSERT INTO daily_discrete_text " +
+    "(field_id, day, value) VALUES (:field_id, :days, :value)",
+
+  pruneOldDailyCounters: "DELETE FROM daily_counters WHERE day < :days",
+  pruneOldDailyDiscreteNumeric: "DELETE FROM daily_discrete_numeric WHERE day < :days",
+  pruneOldDailyDiscreteText: "DELETE FROM daily_discrete_text WHERE day < :days",
+  pruneOldDailyLastNumeric: "DELETE FROM daily_last_numeric WHERE day < :days",
+  pruneOldDailyLastText: "DELETE FROM daily_last_text WHERE day < :days",
+  pruneOldLastNumeric: "DELETE FROM last_numeric WHERE day < :days",
+  pruneOldLastText: "DELETE FROM last_text WHERE day < :days",
+
+  // Retrieval.
+
+  getProviderID: "SELECT id FROM providers WHERE name = :provider",
+
+  getProviders: "SELECT id, name FROM providers",
+
+  getProviderStateWithName:
+    "SELECT value FROM provider_state " +
+      "WHERE provider_id = :provider_id " +
+      "AND name = :name",
+
+  getMeasurements: "SELECT * FROM v_measurements",
+
+  getMeasurementID:
+    "SELECT id FROM measurements " +
+      "WHERE provider_id = :provider_id " +
+        "AND name = :measurement " +
+        "AND version = :version",
+
+  getFieldID:
+    "SELECT id FROM fields " +
+      "WHERE measurement_id = :measurement_id " +
+        "AND name = :field " +
+        "AND value_type = :value_type " +
+    "",
+
+  getTypes: "SELECT * FROM types",
+
+  getTypeID: "SELECT id FROM types WHERE name = :name",
+
+  getDailyCounterCountsFromFieldID:
+    "SELECT day, value FROM daily_counters " +
+      "WHERE field_id = :field_id " +
+      "ORDER BY day ASC",
+
+  getDailyCounterCountFromFieldID:
+    "SELECT value FROM daily_counters " +
+      "WHERE field_id = :field_id " +
+        "AND day = :days",
+
+  getMeasurementDailyCounters:
+    "SELECT field_name, day, value FROM v_daily_counters " +
+    "WHERE measurement_id = :measurement_id",
+
+  getFieldInfo: "SELECT * FROM v_fields",
+
+  getLastNumericFromFieldID:
+    "SELECT day, value FROM last_numeric WHERE field_id = :field_id",
+
+  getLastTextFromFieldID:
+    "SELECT day, value FROM last_text WHERE field_id = :field_id",
+
+  getMeasurementLastValues:
+    "SELECT field_name, day, value FROM v_last " +
+    "WHERE measurement_id = :measurement_id",
+
+  getDailyDiscreteNumericFromFieldID:
+    "SELECT day, value FROM daily_discrete_numeric " +
+      "WHERE field_id = :field_id " +
+      "ORDER BY day ASC, id ASC",
+
+  getDailyDiscreteNumericFromFieldIDAndDay:
+    "SELECT day, value FROM daily_discrete_numeric " +
+      "WHERE field_id = :field_id AND day = :days " +
+      "ORDER BY id ASC",
+
+  getDailyDiscreteTextFromFieldID:
+    "SELECT day, value FROM daily_discrete_text " +
+      "WHERE field_id = :field_id " +
+      "ORDER BY day ASC, id ASC",
+
+  getDailyDiscreteTextFromFieldIDAndDay:
+    "SELECT day, value FROM daily_discrete_text " +
+      "WHERE field_id = :field_id AND day = :days " +
+      "ORDER BY id ASC",
+
+  getMeasurementDailyDiscreteValues:
+    "SELECT field_name, day, value_id, value FROM v_daily_discrete " +
+    "WHERE measurement_id = :measurement_id " +
+    "ORDER BY day ASC, value_id ASC",
+
+  getDailyLastNumericFromFieldID:
+    "SELECT day, value FROM daily_last_numeric " +
+      "WHERE field_id = :field_id " +
+      "ORDER BY day ASC",
+
+  getDailyLastNumericFromFieldIDAndDay:
+    "SELECT day, value FROM daily_last_numeric " +
+      "WHERE field_id = :field_id AND day = :days",
+
+  getDailyLastTextFromFieldID:
+    "SELECT day, value FROM daily_last_text " +
+      "WHERE field_id = :field_id " +
+      "ORDER BY day ASC",
+
+  getDailyLastTextFromFieldIDAndDay:
+    "SELECT day, value FROM daily_last_text " +
+      "WHERE field_id = :field_id AND day = :days",
+
+  getMeasurementDailyLastValues:
+    "SELECT field_name, day, value FROM v_daily_last " +
+    "WHERE measurement_id = :measurement_id",
+};
+
+
+function dailyKeyFromDate(date) {
+  let year = String(date.getUTCFullYear());
+  let month = String(date.getUTCMonth() + 1);
+  let day = String(date.getUTCDate());
+
+  if (month.length < 2) {
+    month = "0" + month;
+  }
+
+  if (day.length < 2) {
+    day = "0" + day;
+  }
+
+  return year + "-" + month + "-" + day;
+}
+
+
+/**
+ * Create a new backend instance bound to a SQLite database at the given path.
+ *
+ * This returns a promise that will resolve to a `MetricsStorageSqliteBackend`
+ * instance. The resolved instance will be initialized and ready for use.
+ *
+ * Very few consumers have a need to call this. Instead, a higher-level entity
+ * likely calls this and sets up the database connection for a service or
+ * singleton.
+ */
+this.MetricsStorageBackend = function (path) {
+  return Task.spawn(function initTask() {
+    let connection = yield Sqlite.openConnection({
+      path: path,
+
+      // There should only be one connection per database, so we disable this
+      // for perf reasons.
+      sharedMemoryCache: false,
+    });
+
+    // If we fail initializing the storage object, we need to close the
+    // database connection or else Storage will assert on shutdown.
+    let storage;
+    try {
+      storage = new MetricsStorageSqliteBackend(connection);
+      yield storage._init();
+    } catch (ex) {
+      yield connection.close();
+      throw ex;
+    }
+
+    throw new Task.Result(storage);
+  });
+};
+
+
+/**
+ * Manages storage of metrics data in a SQLite database.
+ *
+ * This is the main type used for interfacing with the database.
+ *
+ * Instances of this should be obtained by calling MetricsStorageConnection().
+ *
+ * The current implementation will not work if the database is mutated by
+ * multiple connections because of the way we cache primary keys.
+ *
+ * FUTURE enforce 1 read/write connection per database limit.
+ */
+function MetricsStorageSqliteBackend(connection) {
+  this._log = Log4Moz.repository.getLogger("Services.Metrics.MetricsStorage");
+
+  this._connection = connection;
+
+  // Integer IDs to string name.
+  this._typesByID = new Map();
+
+  // String name to integer IDs.
+  this._typesByName = new Map();
+
+  // Maps provider names to integer IDs.
+  this._providerIDs = new Map();
+
+  // Maps :-delimited strings of [provider name, name, version] to integer IDs.
+  this._measurementsByInfo = new Map();
+
+  // Integer IDs to Arrays of [provider name, name, version].
+  this._measurementsByID = new Map();
+
+  // Integer IDs to Arrays of [measurement id, field name, value name]
+  this._fieldsByID = new Map();
+
+  // Maps :-delimited strings of [measurement id, field name] to integer ID.
+  this._fieldsByInfo = new Map();
+
+  // Maps measurement ID to sets of field IDs.
+  this._fieldsByMeasurement = new Map();
+
+  this._queuedOperations = [];
+  this._queuedInProgress = false;
+}
+
+MetricsStorageSqliteBackend.prototype = Object.freeze({
+  FIELD_DAILY_COUNTER: "daily-counter",
+  FIELD_DAILY_DISCRETE_NUMERIC: "daily-discrete-numeric",
+  FIELD_DAILY_DISCRETE_TEXT: "daily-discrete-text",
+  FIELD_DAILY_LAST_NUMERIC: "daily-last-numeric",
+  FIELD_DAILY_LAST_TEXT: "daily-last-text",
+  FIELD_LAST_NUMERIC: "last-numeric",
+  FIELD_LAST_TEXT: "last-text",
+
+  _BUILTIN_TYPES: [
+    "FIELD_DAILY_COUNTER",
+    "FIELD_DAILY_DISCRETE_NUMERIC",
+    "FIELD_DAILY_DISCRETE_TEXT",
+    "FIELD_DAILY_LAST_NUMERIC",
+    "FIELD_DAILY_LAST_TEXT",
+    "FIELD_LAST_NUMERIC",
+    "FIELD_LAST_TEXT",
+  ],
+
+  // Statements that are used to create the initial DB schema.
+  _SCHEMA_STATEMENTS: [
+    "createProvidersTable",
+    "createProviderStateTable",
+    "createProviderStateProviderIndex",
+    "createMeasurementsTable",
+    "createMeasurementsProviderIndex",
+    "createMeasurementsView",
+    "createTypesTable",
+    "createFieldsTable",
+    "createFieldsMeasurementIndex",
+    "createFieldsView",
+    "createDailyCountersTable",
+    "createDailyCountersFieldIndex",
+    "createDailyCountersDayIndex",
+    "createDailyCountersView",
+    "createDailyDiscreteNumericsTable",
+    "createDailyDiscreteNumericsFieldIndex",
+    "createDailyDiscreteNumericsDayIndex",
+    "createDailyDiscreteTextTable",
+    "createDailyDiscreteTextFieldIndex",
+    "createDailyDiscreteTextDayIndex",
+    "createDailyDiscreteView",
+    "createDailyLastNumericTable",
+    "createDailyLastNumericFieldIndex",
+    "createDailyLastNumericDayIndex",
+    "createDailyLastTextTable",
+    "createDailyLastTextFieldIndex",
+    "createDailyLastTextDayIndex",
+    "createDailyLastView",
+    "createLastNumericTable",
+    "createLastTextTable",
+    "createLastView",
+  ],
+
+  // Statements that are used to prune old data.
+  _PRUNE_STATEMENTS: [
+    "pruneOldDailyCounters",
+    "pruneOldDailyDiscreteNumeric",
+    "pruneOldDailyDiscreteText",
+    "pruneOldDailyLastNumeric",
+    "pruneOldDailyLastText",
+    "pruneOldLastNumeric",
+    "pruneOldLastText",
+  ],
+
+  /**
+   * Close the database connection.
+   *
+   * This should be called on all instances or the SQLite layer may complain
+   * loudly. After this has been called, the connection cannot be used.
+   *
+   * @return Promise<>
+   */
+  close: function () {
+    return Task.spawn(function doClose() {
+      // There is some light magic involved here. First, we enqueue an
+      // operation to ensure that all pending operations have the opportunity
+      // to execute. We additionally execute a SQL operation. Due to the FIFO
+      // execution order of issued statements, this will cause us to wait on
+      // any outstanding statements before closing.
+      try {
+        yield this.enqueueOperation(function dummyOperation() {
+          return this._connection.execute("SELECT 1");
+        }.bind(this));
+      } catch (ex) {}
+
+      try {
+        yield this._connection.close();
+      } finally {
+        this._connection = null;
+      }
+    }.bind(this));
+  },
+
+  /**
+   * Whether a provider is known to exist.
+   *
+   * @param provider
+   *        (string) Name of the provider.
+   */
+  hasProvider: function (provider) {
+    return this._providerIDs.has(provider);
+  },
+
+  /**
+   * Whether a measurement is known to exist.
+   *
+   * @param provider
+   *        (string) Name of the provider.
+   * @param name
+   *        (string) Name of the measurement.
+   * @param version
+   *        (Number) Integer measurement version.
+   */
+  hasMeasurement: function (provider, name, version) {
+    return this._measurementsByInfo.has([provider, name, version].join(":"));
+  },
+
+  /**
+   * Whether a named field exists in a measurement.
+   *
+   * @param measurementID
+   *        (Number) The integer primary key of the measurement.
+   * @param field
+   *        (string) The name of the field to look for.
+   */
+  hasFieldFromMeasurement: function (measurementID, field) {
+    return this._fieldsByInfo.has([measurementID, field].join(":"));
+  },
+
+  /**
+   * Whether a field is known.
+   *
+   * @param provider
+   *        (string) Name of the provider having the field.
+   * @param measurement
+   *        (string) Name of the measurement in the provider having the field.
+   * @param field
+   *        (string) Name of the field in the measurement.
+   */
+  hasField: function (provider, measurement, version, field) {
+    let key = [provider, measurement, version].join(":");
+    let measurementID = this._measurementsByInfo.get(key);
+    if (!measurementID) {
+      return false;
+    }
+
+    return this.hasFieldFromMeasurement(measurementID, field);
+  },
+
+  /**
+   * Look up the integer primary key of a provider.
+   *
+   * @param provider
+   *        (string) Name of the provider.
+   */
+  providerID: function (provider) {
+    return this._providerIDs.get(provider);
+  },
+
+  /**
+   * Look up the integer primary key of a measurement.
+   *
+   * @param provider
+   *        (string) Name of the provider.
+   * @param measurement
+   *        (string) Name of the measurement.
+   * @param version
+   *        (Number) Integer version of the measurement.
+   */
+  measurementID: function (provider, measurement, version) {
+    return this._measurementsByInfo.get([provider, measurement, version].join(":"));
+  },
+
+  fieldIDFromMeasurement: function (measurementID, field) {
+    return this._fieldsByInfo.get([measurementID, field].join(":"));
+  },
+
+  fieldID: function (provider, measurement, version, field) {
+    let measurementID = this.measurementID(provider, measurement, version);
+    if (!measurementID) {
+      return null;
+    }
+
+    return this.fieldIDFromMeasurement(measurementID, field);
+  },
+
+  measurementHasAnyDailyCounterFields: function (measurementID) {
+    return this.measurementHasAnyFieldsOfTypes(measurementID,
+                                               [this.FIELD_DAILY_COUNTER]);
+  },
+
+  measurementHasAnyLastFields: function (measurementID) {
+    return this.measurementHasAnyFieldsOfTypes(measurementID,
+                                               [this.FIELD_LAST_NUMERIC,
+                                                this.FIELD_LAST_TEXT]);
+  },
+
+  measurementHasAnyDailyLastFields: function (measurementID) {
+    return this.measurementHasAnyFieldsOfTypes(measurementID,
+                                               [this.FIELD_DAILY_LAST_NUMERIC,
+                                                this.FIELD_DAILY_LAST_TEXT]);
+  },
+
+  measurementHasAnyDailyDiscreteFields: function (measurementID) {
+    return this.measurementHasAnyFieldsOfTypes(measurementID,
+                                               [this.FIELD_DAILY_DISCRETE_NUMERIC,
+                                                this.FIELD_DAILY_DISCRETE_TEXT]);
+  },
+
+  measurementHasAnyFieldsOfTypes: function (measurementID, types) {
+    if (!this._fieldsByMeasurement.has(measurementID)) {
+      return false;
+    }
+
+    let fieldIDs = this._fieldsByMeasurement.get(measurementID);
+    for (let fieldID of fieldIDs) {
+      let fieldType = this._fieldsByID.get(fieldID)[2];
+      if (types.indexOf(fieldType) != -1) {
+        return true;
+      }
+    }
+
+    return false;
+  },
+
+  /**
+   * Register a measurement with the backend.
+   *
+   * Measurements must be registered before storage can be allocated to them.
+   *
+   * A measurement consists of a string name and integer version attached
+   * to a named provider.
+   *
+   * This returns a promise that resolves to the storage ID for this
+   * measurement.
+   *
+   * If the measurement is not known to exist, it is registered with storage.
+   * If the measurement has already been registered, this is effectively a
+   * no-op (that still returns a promise resolving to the storage ID).
+   *
+   * @param provider
+   *        (string) Name of the provider this measurement belongs to.
+   * @param name
+   *        (string) Name of this measurement.
+   * @param version
+   *        (Number) Integer version of this measurement.
+   */
+  registerMeasurement: function (provider, name, version) {
+    if (this.hasMeasurement(provider, name, version)) {
+      return Promise.resolve(this.measurementID(provider, name, version));
+    }
+
+    // Registrations might not be safe to perform in parallel with provider
+    // operations. So, we queue them.
+    let self = this;
+    return this.enqueueOperation(function createMeasurementOperation() {
+      return Task.spawn(function createMeasurement() {
+        let providerID = self._providerIDs.get(provider);
+
+        if (!providerID) {
+          yield self._connection.executeCached(SQL.addProvider, {provider: provider});
+          let rows = yield self._connection.executeCached(SQL.getProviderID,
+                                                          {provider: provider});
+
+          providerID = rows[0].getResultByIndex(0);
+
+          self._providerIDs.set(provider, providerID);
+        }
+
+        let params = {
+          provider_id: providerID,
+          measurement: name,
+          version: version,
+        };
+
+        yield self._connection.executeCached(SQL.addMeasurement, params);
+        let rows = yield self._connection.executeCached(SQL.getMeasurementID, params);
+
+        let measurementID = rows[0].getResultByIndex(0);
+
+        self._measurementsByInfo.set([provider, name, version].join(":"), measurementID);
+        self._measurementsByID.set(measurementID, [provider, name, version]);
+        self._fieldsByMeasurement.set(measurementID, new Set());
+
+        throw new Task.Result(measurementID);
+      });
+    });
+  },
+
+  /**
+   * Register a field with the backend.
+   *
+   * Fields are what recorded pieces of data are primarily associated with.
+   *
+   * Fields are associated with measurements. Measurements must be registered
+   * via `registerMeasurement` before fields can be registered. This is
+   * enforced by this function requiring the database primary key of the
+   * measurement as an argument.
+   *
+   * @param measurementID
+   *        (Number) Integer primary key of measurement this field belongs to.
+   * @param field
+   *        (string) Name of this field.
+   * @param valueType
+   *        (string) Type name of this field. Must be a registered type. Is
+   *        likely one of the FIELD_ constants on this type.
+   *
+   * @return Promise<integer>
+   */
+  registerField: function (measurementID, field, valueType) {
+    if (!valueType) {
+      throw new Error("Value type must be defined.");
+    }
+
+    if (!this._measurementsByID.has(measurementID)) {
+      throw new Error("Measurement not known: " + measurementID);
+    }
+
+    if (!this._typesByName.has(valueType)) {
+      throw new Error("Unknown value type: " + valueType);
+    }
+
+    let typeID = this._typesByName.get(valueType);
+
+    if (!typeID) {
+      throw new Error("Undefined type: " + valueType);
+    }
+
+    if (this.hasFieldFromMeasurement(measurementID, field)) {
+      let id = this.fieldIDFromMeasurement(measurementID, field);
+      let existingType = this._fieldsByID.get(id)[2];
+
+      if (valueType != existingType) {
+        throw new Error("Field already defined with different type: " + existingType);
+      }
+
+      return Promise.resolve(this.fieldIDFromMeasurement(measurementID, field));
+    }
+
+    let self = this;
+    return this.enqueueOperation(function addFieldOperation() {
+      return Task.spawn(function createField() {
+        let params = {
+          measurement_id: measurementID,
+          field: field,
+          value_type: typeID,
+        };
+
+        yield self._connection.executeCached(SQL.addField, params);
+
+        let rows = yield self._connection.executeCached(SQL.getFieldID, params);
+
+        let fieldID = rows[0].getResultByIndex(0);
+
+        self._fieldsByID.set(fieldID, [measurementID, field, valueType]);
+        self._fieldsByInfo.set([measurementID, field].join(":"), fieldID);
+        self._fieldsByMeasurement.get(measurementID).add(fieldID);
+
+        throw new Task.Result(fieldID);
+      });
+    });
+  },
+
+  /**
+   * Initializes this instance with the database.
+   *
+   * This performs 2 major roles:
+   *
+   *   1) Set up database schema (creates tables).
+   *   2) Synchronize database with local instance.
+   */
+  _init: function() {
+    let self = this;
+    return Task.spawn(function initTask() {
+      // 1. Create the schema.
+      yield self._connection.executeTransaction(function ensureSchema(conn) {
+        let schema = conn.schemaVersion;
+
+        if (schema == 0) {
+          self._log.info("Creating database schema.");
+
+          for (let k of self._SCHEMA_STATEMENTS) {
+            yield self._connection.execute(SQL[k]);
+          }
+
+          self._connection.schemaVersion = 1;
+        } else if (schema != 1) {
+          throw new Error("Unknown database schema: " + schema);
+        } else {
+          self._log.debug("Database schema up to date.");
+        }
+      });
+
+      // 2. Retrieve existing types.
+      yield self._connection.execute(SQL.getTypes, null, function onRow(row) {
+        let id = row.getResultByName("id");
+        let name = row.getResultByName("name");
+
+        self._typesByID.set(id, name);
+        self._typesByName.set(name, id);
+      });
+
+      // 3. Populate built-in types with database.
+      for (let type of self._BUILTIN_TYPES) {
+        type = self[type];
+        if (self._typesByName.has(type)) {
+          continue;
+        }
+
+        let params = {name: type};
+        yield self._connection.executeCached(SQL.addType, params);
+        let rows = yield self._connection.executeCached(SQL.getTypeID, params);
+        let id = rows[0].getResultByIndex(0);
+
+        self._typesByID.set(id, type);
+        self._typesByName.set(type, id);
+      }
+
+      // 4. Obtain measurement info.
+      yield self._connection.execute(SQL.getMeasurements, null, function onRow(row) {
+        let providerID = row.getResultByName("provider_id");
+        let providerName = row.getResultByName("provider_name");
+        let measurementID = row.getResultByName("measurement_id");
+        let measurementName = row.getResultByName("measurement_name");
+        let measurementVersion = row.getResultByName("measurement_version");
+
+        self._providerIDs.set(providerName, providerID);
+
+        let info = [providerName, measurementName, measurementVersion].join(":");
+
+        self._measurementsByInfo.set(info, measurementID);
+        self._measurementsByID.set(measurementID, info);
+        self._fieldsByMeasurement.set(measurementID, new Set());
+      });
+
+      // 5. Obtain field info.
+      yield self._connection.execute(SQL.getFieldInfo, null, function onRow(row) {
+        let measurementID = row.getResultByName("measurement_id");
+        let fieldID = row.getResultByName("field_id");
+        let fieldName = row.getResultByName("field_name");
+        let typeName = row.getResultByName("type_name");
+
+        self._fieldsByID.set(fieldID, [measurementID, fieldName, typeName]);
+        self._fieldsByInfo.set([measurementID, fieldName].join(":"), fieldID);
+        self._fieldsByMeasurement.get(measurementID).add(fieldID);
+      });
+    });
+  },
+
+  /**
+   * Prune all data from earlier than the specified date.
+   *
+   * Data stored on days before the specified Date will be permanently
+   * deleted.
+   *
+   * This returns a promise that will be resolved when data has been deleted.
+   *
+   * @param date
+   *        (Date) Old data threshold.
+   * @return Promise<>
+   */
+  pruneDataBefore: function (date) {
+    let statements = this._PRUNE_STATEMENTS;
+
+    let self = this;
+    return this.enqueueOperation(function doPrune() {
+      return self._connection.executeTransaction(function prune(conn) {
+        let days = dateToDays(date);
+
+        let params = {days: days};
+        for (let name of statements) {
+          yield conn.execute(SQL[name], params);
+        }
+      });
+    });
+  },
+
+  /**
+   * Ensure a field ID matches a specified type.
+   *
+   * This is called internally as part of adding values to ensure that
+   * the type of a field matches the operation being performed.
+   */
+  _ensureFieldType: function (id, type) {
+    let info = this._fieldsByID.get(id);
+
+    if (!info || !Array.isArray(info)) {
+      throw new Error("Unknown field ID: " + id);
+    }
+
+    if (type != info[2]) {
+      throw new Error("Field type does not match the expected for this " +
+                      "operation. Actual: " + info[2] + "; Expected: " +
+                      type);
+    }
+  },
+
+  /**
+   * Enqueue a storage operation to be performed when the database is ready.
+   *
+   * The primary use case of this function is to prevent potentially
+   * conflicting storage operations from being performed in parallel. By
+   * calling this function, passed storage operations will be serially
+   * executed, avoiding potential order of operation issues.
+   *
+   * The passed argument is a function that will perform storage operations.
+   * The function should return a promise that will be resolved when all
+   * storage operations have been completed.
+   *
+   * The passed function may be executed immediately. If there are already
+   * queued operations, it will be appended to the queue and executed after all
+   * before it have finished.
+   *
+   * This function returns a promise that will be resolved or rejected with
+   * the same value that the function's promise was resolved or rejected with.
+   *
+   * @param func
+   *        (function) Function performing storage interactions.
+   * @return Promise<>
+   */
+  enqueueOperation: function (func) {
+    if (typeof(func) != "function") {
+      throw new Error("enqueueOperation expects a function. Got: " + typeof(func));
+    }
+
+    let deferred = Promise.defer();
+
+    this._queuedOperations.push([func, deferred]);
+
+    if (this._queuedOperations.length == 1) {
+      this._popAndPerformQueuedOperation();
+    }
+
+    return deferred.promise;
+  },
+
+  _popAndPerformQueuedOperation: function () {
+    if (!this._queuedOperations.length || this._queuedInProgress) {
+      return;
+    }
+
+    this._log.trace("Performing queued operation.");
+    let [func, deferred] = this._queuedOperations.pop();
+    let promise;
+
+    try {
+      this._queuedInProgress = true;
+      promise = func();
+    } catch (ex) {
+      this._log.warn("Queued operation threw during execution: " +
+                     CommonUtils.exceptionStr(ex));
+      this._queuedInProgress = false;
+      deferred.reject(ex);
+      this._popAndPerformQueuedOperation();
+      return;
+    }
+
+    if (!promise || typeof(promise.then) != "function") {
+      let msg = "Queued operation did not return a promise: " + func;
+      this._log.warn(msg);
+
+      this._queuedInProgress = false;
+      deferred.reject(new Error(msg));
+      this._popAndPerformQueuedOperation();
+      return;
+    }
+
+    promise.then(
+      function onSuccess(result) {
+        this._log.trace("Queued operation completed.");
+        this._queuedInProgress = false;
+        deferred.resolve(result);
+        this._popAndPerformQueuedOperation();
+      }.bind(this),
+      function onError(error) {
+        this._log.warn("Failure when performing queued operation: " +
+                       CommonUtils.exceptionStr(error));
+        this._queuedInProgress = false;
+        deferred.reject(error);
+        this._popAndPerformQueuedOperation();
+      }.bind(this)
+    );
+  },
+
+  /**
+   * Obtain all values associated with a measurement.
+   *
+   * This returns a promise that resolves to an object. The keys of the object
+   * are:
+   *
+   *   days -- DailyValues where the values are Maps of field name to data
+   *     structures. The data structures could be simple (string or number) or
+   *     Arrays if the field type allows multiple values per day.
+   *
+   *   singular -- Map of field names to values. This holds all fields that
+   *     don't have a temporal component.
+   *
+   * @param id
+   *        (Number) Primary key of measurement whose values to retrieve.
+   */
+  getMeasurementValues: function (id) {
+    let deferred = Promise.defer();
+    let days = new DailyValues();
+    let singular = new Map();
+
+    let self = this;
+    this.enqueueOperation(function enqueuedGetMeasurementValues() {
+      return Task.spawn(function fetchMeasurementValues() {
+        function handleResult(data) {
+          for (let [field, values] of data) {
+            for (let [day, value] of Iterator(values)) {
+              if (!days.hasDay(day)) {
+                days.setDay(day, new Map());
+              }
+
+              days.getDay(day).set(field, value);
+            }
+          }
+        }
+
+        if (self.measurementHasAnyDailyCounterFields(id)) {
+          let counters = yield self.getMeasurementDailyCountersFromMeasurementID(id);
+          handleResult(counters);
+        }
+
+        if (self.measurementHasAnyDailyLastFields(id)) {
+          let dailyLast = yield self.getMeasurementDailyLastValuesFromMeasurementID(id);
+          handleResult(dailyLast);
+        }
+
+        if (self.measurementHasAnyDailyDiscreteFields(id)) {
+          let dailyDiscrete = yield self.getMeasurementDailyDiscreteValuesFromMeasurementID(id);
+          handleResult(dailyDiscrete);
+        }
+
+        if (self.measurementHasAnyLastFields(id)) {
+          let last = yield self.getMeasurementLastValuesFromMeasurementID(id);
+
+          for (let [field, value] of last) {
+            singular.set(field, value);
+          }
+        }
+
+      });
+    }).then(function onSuccess() {
+      deferred.resolve({singular: singular, days: days});
+    }, function onError(error) {
+      deferred.reject(error);
+    });
+
+    return deferred.promise;
+  },
+
+  //---------------------------------------------------------------------------
+  // Low-level storage operations
+  //
+  // These will be performed immediately (or at least as soon as the underlying
+  // connection allows them to be.) It is recommended to call these from within
+  // a function added via `enqueueOperation()` or they may inadvertently be
+  // performed during another enqueued operation, which may be a transaction
+  // that is rolled back.
+  // ---------------------------------------------------------------------------
+
+  /**
+   * Set state for a provider.
+   *
+   * Providers have the ability to register persistent state with the backend.
+   * Persistent state doesn't expire. The format of the data is completely up
+   * to the provider beyond the requirement that values be UTF-8 strings.
+   *
+   * This returns a promise that will be resolved when the underlying database
+   * operation has completed.
+   *
+   * @param provider
+   *        (string) Name of the provider.
+   * @param key
+   *        (string) Key under which to store this state.
+   * @param value
+   *        (string) Value for this state.
+   * @return Promise<>
+   */
+  setProviderState: function (provider, key, value) {
+    if (typeof(key) != "string") {
+      throw new Error("State key must be a string. Got: " + key);
+    }
+
+    if (typeof(value) != "string") {
+      throw new Error("State value must be a string. Got: " + value);
+    }
+
+    let id = this.providerID(provider);
+    if (!id) {
+      throw new Error("Unknown provider: " + provider);
+    }
+
+    return this._connection.executeCached(SQL.setProviderState, {
+      provider_id: id,
+      name: key,
+      value: value,
+    });
+  },
+
+  /**
+   * Obtain named state for a provider.
+   *
+   *
+   * The returned promise will resolve to the state from the database or null
+   * if the key is not stored.
+   *
+   * @param provider
+   *        (string) The name of the provider whose state to obtain.
+   * @param key
+   *        (string) The state's key to retrieve.
+   *
+   * @return Promise<data>
+   */
+  getProviderState: function (provider, key) {
+    let id = this.providerID(provider);
+    if (!id) {
+      throw new Error("Unknown provider: " + provider);
+    }
+
+    let conn = this._connection;
+    return Task.spawn(function queryDB() {
+      let rows = yield conn.executeCached(SQL.getProviderStateWithName, {
+        provider_id: id,
+        name: key,
+      });
+
+      if (!rows.length) {
+        throw new Task.Result(null);
+      }
+
+      throw new Task.Result(rows[0].getResultByIndex(0));
+    });
+  },
+
+  /**
+   * Increment a daily counter from a numeric field id.
+   *
+   * @param id
+   *        (integer) Primary key of field to increment.
+   * @param date
+   *        (Date) When the increment occurred. This is typically "now" but can
+   *        be explicitly defined for events that occurred in the past.
+   */
+  incrementDailyCounterFromFieldID: function (id, date=new Date()) {
+    this._ensureFieldType(id, this.FIELD_DAILY_COUNTER);
+
+    let params = {
+      field_id: id,
+      days: dateToDays(date),
+    };
+
+    return this._connection.executeCached(SQL.incrementDailyCounterFromFieldID,
+                                          params);
+  },
+
+  /**
+   * Obtain all counts for a specific daily counter.
+   *
+   * @param id
+   *        (integer) The ID of the field being retrieved.
+   */
+  getDailyCounterCountsFromFieldID: function (id) {
+    this._ensureFieldType(id, this.FIELD_DAILY_COUNTER);
+
+    let self = this;
+    return Task.spawn(function fetchCounterDays() {
+      let rows = yield self._connection.executeCached(SQL.getDailyCounterCountsFromFieldID,
+                                                      {field_id: id});
+
+      let result = new DailyValues();
+      for (let row of rows) {
+        let days = row.getResultByIndex(0);
+        let counter = row.getResultByIndex(1);
+
+        let date = daysToDate(days);
+        result.setDay(date, counter);
+      }
+
+      throw new Task.Result(result);
+    });
+  },
+
+  /**
+   * Get the value of a daily counter for a given day.
+   *
+   * @param field
+   *        (integer) Field ID to retrieve.
+   * @param date
+   *        (Date) Date for day from which to obtain data.
+   */
+  getDailyCounterCountFromFieldID: function (field, date) {
+    this._ensureFieldType(field, this.FIELD_DAILY_COUNTER);
+
+    let params = {
+      field_id: field,
+      days: dateToDays(date),
+    };
+
+    let self = this;
+    return Task.spawn(function fetchCounter() {
+      let rows = yield self._connection.executeCached(SQL.getDailyCounterCountFromFieldID,
+                                                      params);
+      if (!rows.length) {
+        throw new Task.Result(null);
+      }
+
+      throw new Task.Result(rows[0].getResultByIndex(0));
+    });
+  },
+
+  /**
+   * Define the value for a "last numeric" field.
+   *
+   * The previous value (if any) will be replaced by the value passed, even if
+   * the date of the incoming value is older than what's recorded in the
+   * database.
+   *
+   * @param fieldID
+   *        (Number) Integer primary key of field to update.
+   * @param value
+   *        (Number) Value to record.
+   * @param date
+   *        (Date) When this value was produced.
+   */
+  setLastNumericFromFieldID: function (fieldID, value, date=new Date()) {
+    this._ensureFieldType(fieldID, this.FIELD_LAST_NUMERIC);
+
+    if (typeof(value) != "number") {
+      throw new Error("Value is not a number: " + value);
+    }
+
+    let params = {
+      field_id: fieldID,
+      days: dateToDays(date),
+      value: value,
+    };
+
+    return this._connection.executeCached(SQL.setLastNumeric, params);
+  },
+
+  /**
+   * Define the value of a "last text" field.
+   *
+   * See `setLastNumericFromFieldID` for behavior.
+   */
+  setLastTextFromFieldID: function (fieldID, value, date=new Date()) {
+    this._ensureFieldType(fieldID, this.FIELD_LAST_TEXT);
+
+    if (typeof(value) != "string") {
+      throw new Error("Value is not a string: " + value);
+    }
+
+    let params = {
+      field_id: fieldID,
+      days: dateToDays(date),
+      value: value,
+    };
+
+    return this._connection.executeCached(SQL.setLastText, params);
+  },
+
+  /**
+   * Obtain the value of a "last numeric" field.
+   *
+   * This returns a promise that will be resolved with an Array of [date, value]
+   * if a value is known or null if no last value is present.
+   *
+   * @param fieldID
+   *        (Number) Integer primary key of field to retrieve.
+   */
+  getLastNumericFromFieldID: function (fieldID) {
+    this._ensureFieldType(fieldID, this.FIELD_LAST_NUMERIC);
+
+    let self = this;
+    return Task.spawn(function fetchLastField() {
+      let rows = yield self._connection.executeCached(SQL.getLastNumericFromFieldID,
+                                                      {field_id: fieldID});
+
+      if (!rows.length) {
+        throw new Task.Result(null);
+      }
+
+      let row = rows[0];
+      let days = row.getResultByIndex(0);
+      let value = row.getResultByIndex(1);
+
+      throw new Task.Result([daysToDate(days), value]);
+    });
+  },
+
+  /**
+   * Obtain the value of a "last text" field.
+   *
+   * See `getLastNumericFromFieldID` for behavior.
+   */
+  getLastTextFromFieldID: function (fieldID) {
+    this._ensureFieldType(fieldID, this.FIELD_LAST_TEXT);
+
+    let self = this;
+    return Task.spawn(function fetchLastField() {
+      let rows = yield self._connection.executeCached(SQL.getLastTextFromFieldID,
+                                                      {field_id: fieldID});
+
+      if (!rows.length) {
+        throw new Task.Result(null);
+      }
+
+      let row = rows[0];
+      let days = row.getResultByIndex(0);
+      let value = row.getResultByIndex(1);
+
+      throw new Task.Result([daysToDate(days), value]);
+    });
+  },
+
+  /**
+   * Delete the value (if any) in a "last numeric" field.
+   */
+  deleteLastNumericFromFieldID: function (fieldID) {
+    this._ensureFieldType(fieldID, this.FIELD_LAST_NUMERIC);
+
+    return this._connection.executeCached(SQL.deleteLastNumericFromFieldID,
+                                          {field_id: fieldID});
+  },
+
+  /**
+   * Delete the value (if any) in a "last text" field.
+   */
+  deleteLastTextFromFieldID: function (fieldID) {
+    this._ensureFieldType(fieldID, this.FIELD_LAST_TEXT);
+
+    return this._connection.executeCached(SQL.deleteLastTextFromFieldID,
+                                          {field_id: fieldID});
+  },
+
+  /**
+   * Record a value for a "daily last numeric" field.
+   *
+   * The field can hold 1 value per calendar day. If the field already has a
+   * value for the day specified (defaults to now), that value will be
+   * replaced, even if the date specified is older (within the day) than the
+   * previously recorded value.
+   *
+   * @param fieldID
+   *        (Number) Integer primary key of field.
+   * @param value
+   *        (Number) Value to record.
+   * @param date
+   *        (Date) When the value was produced. Defaults to now.
+   */
+  setDailyLastNumericFromFieldID: function (fieldID, value, date=new Date()) {
+    this._ensureFieldType(fieldID, this.FIELD_DAILY_LAST_NUMERIC);
+
+    let params = {
+      field_id: fieldID,
+      days: dateToDays(date),
+      value: value,
+    };
+
+    return this._connection.executeCached(SQL.setDailyLastNumeric, params);
+  },
+
+  /**
+   * Record a value for a "daily last text" field.
+   *
+   * See `setDailyLastNumericFromFieldID` for behavior.
+   */
+  setDailyLastTextFromFieldID: function (fieldID, value, date=new Date()) {
+    this._ensureFieldType(fieldID, this.FIELD_DAILY_LAST_TEXT);
+
+    let params = {
+      field_id: fieldID,
+      days: dateToDays(date),
+      value: value,
+    };
+
+    return this._connection.executeCached(SQL.setDailyLastText, params);
+  },
+
+  /**
+   * Obtain value(s) from a "daily last numeric" field.
+   *
+   * This returns a promise that resolves to a DailyValues instance. If `date`
+   * is specified, that instance will have at most 1 entry. If there is no
+   * `date` constraint, then all stored values will be retrieved.
+   *
+   * @param fieldID
+   *        (Number) Integer primary key of field to retrieve.
+   * @param date optional
+   *        (Date) If specified, only return data for this day.
+   *
+   * @return Promise<DailyValues>
+   */
+  getDailyLastNumericFromFieldID: function (fieldID, date=null) {
+    this._ensureFieldType(fieldID, this.FIELD_DAILY_LAST_NUMERIC);
+
+    let params = {field_id: fieldID};
+    let name = "getDailyLastNumericFromFieldID";
+
+    if (date) {
+      params.days = dateToDays(date);
+      name = "getDailyLastNumericFromFieldIDAndDay";
+    }
+
+    return this._getDailyLastFromFieldID(name, params);
+  },
+
+  /**
+   * Obtain value(s) from a "daily last text" field.
+   *
+   * See `getDailyLastNumericFromFieldID` for behavior.
+   */
+  getDailyLastTextFromFieldID: function (fieldID, date=null) {
+    this._ensureFieldType(fieldID, this.FIELD_DAILY_LAST_TEXT);
+
+    let params = {field_id: fieldID};
+    let name = "getDailyLastTextFromFieldID";
+
+    if (date) {
+      params.days = dateToDays(date);
+      name = "getDailyLastTextFromFieldIDAndDay";
+    }
+
+    return this._getDailyLastFromFieldID(name, params);
+  },
+
+  _getDailyLastFromFieldID: function (name, params) {
+    let self = this;
+    return Task.spawn(function fetchDailyLastForField() {
+      let rows = yield self._connection.executeCached(SQL[name], params);
+
+      let result = new DailyValues();
+      for (let row of rows) {
+        let d = daysToDate(row.getResultByIndex(0));
+        let value = row.getResultByIndex(1);
+
+        result.setDay(d, value);
+      }
+
+      throw new Task.Result(result);
+    });
+  },
+
+  /**
+   * Add a new value for a "daily discrete numeric" field.
+   *
+   * This appends a new value to the list of values for a specific field. All
+   * values are retained. Duplicate values are allowed.
+   *
+   * @param fieldID
+   *        (Number) Integer primary key of field.
+   * @param value
+   *        (Number) Value to record.
+   * @param date optional
+   *        (Date) When this value occurred. Values are bucketed by day.
+   */
+  addDailyDiscreteNumericFromFieldID: function (fieldID, value, date=new Date()) {
+    this._ensureFieldType(fieldID, this.FIELD_DAILY_DISCRETE_NUMERIC);
+
+    if (typeof(value) != "number") {
+      throw new Error("Number expected. Got: " + value);
+    }
+
+    let params = {
+      field_id: fieldID,
+      days: dateToDays(date),
+      value: value,
+    };
+
+    return this._connection.executeCached(SQL.addDailyDiscreteNumeric, params);
+  },
+
+  /**
+   * Add a new value for a "daily discrete text" field.
+   *
+   * See `addDailyDiscreteNumericFromFieldID` for behavior.
+   */
+  addDailyDiscreteTextFromFieldID: function (fieldID, value, date=new Date()) {
+    this._ensureFieldType(fieldID, this.FIELD_DAILY_DISCRETE_TEXT);
+
+    if (typeof(value) != "string") {
+      throw new Error("String expected. Got: " + value);
+    }
+
+    let params = {
+      field_id: fieldID,
+      days: dateToDays(date),
+      value: value,
+    };
+
+    return this._connection.executeCached(SQL.addDailyDiscreteText, params);
+  },
+
+  /**
+   * Obtain values for a "daily discrete numeric" field.
+   *
+   * This returns a promise that resolves to a `DailyValues` instance. If
+   * `date` is specified, there will be at most 1 key in that instance. If
+   * not, all data from the database will be retrieved.
+   *
+   * Values in that instance will be arrays of the raw values.
+   *
+   * @param fieldID
+   *        (Number) Integer primary key of field to retrieve.
+   * @param date optional
+   *        (Date) Day to obtain data for. Date can be any time in the day.
+   */
+  getDailyDiscreteNumericFromFieldID: function (fieldID, date=null) {
+    this._ensureFieldType(fieldID, this.FIELD_DAILY_DISCRETE_NUMERIC);
+
+    let params = {field_id: fieldID};
+
+    let name = "getDailyDiscreteNumericFromFieldID";
+
+    if (date) {
+      params.days = dateToDays(date);
+      name = "getDailyDiscreteNumericFromFieldIDAndDay";
+    }
+
+    return this._getDailyDiscreteFromFieldID(name, params);
+  },
+
+  /**
+   * Obtain values for a "daily discrete text" field.
+   *
+   * See `getDailyDiscreteNumericFromFieldID` for behavior.
+   */
+  getDailyDiscreteTextFromFieldID: function (fieldID, date=null) {
+    this._ensureFieldType(fieldID, this.FIELD_DAILY_DISCRETE_TEXT);
+
+    let params = {field_id: fieldID};
+
+    let name = "getDailyDiscreteTextFromFieldID";
+
+    if (date) {
+      params.days = dateToDays(date);
+      name = "getDailyDiscreteTextFromFieldIDAndDay";
+    }
+
+    return this._getDailyDiscreteFromFieldID(name, params);
+  },
+
+  _getDailyDiscreteFromFieldID: function (name, params) {
+    let self = this;
+    return Task.spawn(function fetchDailyDiscreteValuesForField() {
+      let rows = yield self._connection.executeCached(SQL[name], params);
+
+      let result = new DailyValues();
+      for (let row of rows) {
+        let d = daysToDate(row.getResultByIndex(0));
+        let value = row.getResultByIndex(1);
+
+        result.appendValue(d, value);
+      }
+
+      throw new Task.Result(result);
+    });
+  },
+
+  /**
+   * Obtain the counts of daily counters in a measurement.
+   *
+   * This returns a promise that resolves to a Map of field name strings to
+   * DailyValues that hold per-day counts.
+   *
+   * @param id
+   *        (Number) Integer primary key of measurement.
+   *
+   * @return Promise<Map>
+   */
+  getMeasurementDailyCountersFromMeasurementID: function (id) {
+    let self = this;
+    return Task.spawn(function fetchDailyCounters() {
+      let rows = yield self._connection.execute(SQL.getMeasurementDailyCounters,
+                                                {measurement_id: id});
+
+      let result = new Map();
+      for (let row of rows) {
+        let field = row.getResultByName("field_name");
+        let date = daysToDate(row.getResultByName("day"));
+        let value = row.getResultByName("value");
+
+        if (!result.has(field)) {
+          result.set(field, new DailyValues());
+        }
+
+        result.get(field).setDay(date, value);
+      }
+
+      throw new Task.Result(result);
+    });
+  },
+
+  /**
+   * Obtain the values of "last" fields from a measurement.
+   *
+   * This returns a promise that resolves to a Map of field name to an array
+   * of [date, value].
+   *
+   * @param id
+   *        (Number) Integer primary key of measurement whose data to retrieve.
+   *
+   * @return Promise<Map>
+   */
+  getMeasurementLastValuesFromMeasurementID: function (id) {
+    let self = this;
+    return Task.spawn(function fetchMeasurementLastValues() {
+      let rows = yield self._connection.execute(SQL.getMeasurementLastValues,
+                                                {measurement_id: id});
+
+      let result = new Map();
+      for (let row of rows) {
+        let date = daysToDate(row.getResultByIndex(1));
+        let value = row.getResultByIndex(2);
+        result.set(row.getResultByIndex(0), [date, value]);
+      }
+
+      throw new Task.Result(result);
+    });
+  },
+
+  /**
+   * Obtain the values of "last daily" fields from a measurement.
+   *
+   * This returns a promise that resolves to a Map of field name to DailyValues
+   * instances. Each DailyValues instance has days for which a daily last value
+   * is defined. The values in each DailyValues are the raw last value for that
+   * day.
+   *
+   * @param id
+   *        (Number) Integer primary key of measurement whose data to retrieve.
+   *
+   * @return Promise<Map>
+   */
+  getMeasurementDailyLastValuesFromMeasurementID: function (id) {
+    let self = this;
+    return Task.spawn(function fetchMeasurementDailyLastValues() {
+      let rows = yield self._connection.execute(SQL.getMeasurementDailyLastValues,
+                                                {measurement_id: id});
+
+      let result = new Map();
+      for (let row of rows) {
+        let field = row.getResultByName("field_name");
+        let date = daysToDate(row.getResultByName("day"));
+        let value = row.getResultByName("value");
+
+        if (!result.has(field)) {
+          result.set(field, new DailyValues());
+        }
+
+        result.get(field).setDay(date, value);
+      }
+
+      throw new Task.Result(result);
+    });
+  },
+
+  /**
+   * Obtain the values of "daily discrete" fields from a measurement.
+   *
+   * This obtains all discrete values for all "daily discrete" fields in a
+   * measurement.
+   *
+   * This returns a promise that resolves to a Map. The Map's keys are field
+   * string names. Values are `DailyValues` instances. The values inside
+   * the `DailyValues` are arrays of the raw discrete values.
+   *
+   * @param id
+   *        (Number) Integer primary key of measurement.
+   *
+   * @return Promise<Map>
+   */
+  getMeasurementDailyDiscreteValuesFromMeasurementID: function (id) {
+    let deferred = Promise.defer();
+    let result = new Map();
+
+    this._connection.execute(SQL.getMeasurementDailyDiscreteValues,
+                             {measurement_id: id}, function onRow(row) {
+      let field = row.getResultByName("field_name");
+      let date = daysToDate(row.getResultByName("day"));
+      let value = row.getResultByName("value");
+
+      if (!result.has(field)) {
+        result.set(field, new DailyValues());
+      }
+
+      result.get(field).appendValue(date, value);
+    }).then(function onComplete() {
+      deferred.resolve(result);
+    }, function onError(error) {
+      deferred.reject(error);
+    });
+
+    return deferred.promise;
+  },
+});
+
--- a/services/metrics/tests/xpcshell/head.js
+++ b/services/metrics/tests/xpcshell/head.js
@@ -1,13 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 (function initMetricsTestingInfrastructure() {
+  do_get_profile();
+
   let ns = {};
   Components.utils.import("resource://testing-common/services-common/logging.js",
                           ns);
 
-  ns.initTestLogging();
+  ns.initTestLogging("Trace");
 }).call(this);
 
--- a/services/metrics/tests/xpcshell/test_load_modules.js
+++ b/services/metrics/tests/xpcshell/test_load_modules.js
@@ -1,26 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const modules = [
   "collector.jsm",
   "dataprovider.jsm",
+  "storage.jsm",
 ];
 
 const test_modules = [
   "mocks.jsm",
 ];
 
 function run_test() {
   for (let m of modules) {
     let resource = "resource://gre/modules/services/metrics/" + m;
     Components.utils.import(resource, {});
   }
 
   for (let m of test_modules) {
     let resource = "resource://testing-common/services/metrics/" + m;
     Components.utils.import(resource, {});
   }
+
+  Components.utils.import("resource://gre/modules/Metrics.jsm", {});
 }
 
deleted file mode 100644
--- a/services/metrics/tests/xpcshell/test_metrics_collection_result.js
+++ /dev/null
@@ -1,243 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-const {utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
-Cu.import("resource://testing-common/services/metrics/mocks.jsm");
-
-
-function run_test() {
-  run_next_test();
-};
-
-add_test(function test_constructor() {
-  let result = new MetricsCollectionResult("foo");
-  do_check_eq(result.name, "foo");
-
-  let failed = false;
-  try {
-    new MetricsCollectionResult();
-  } catch (ex) {
-    do_check_true(ex.message.startsWith("Must provide name argument to Metrics"));
-    failed = true;
-  } finally {
-    do_check_true(failed);
-    failed = false;
-  }
-
-  try {
-    result.populate();
-  } catch (ex) {
-    do_check_true(ex.message.startsWith("populate() must be defined"));
-    failed = true;
-  } finally {
-    do_check_true(failed);
-    failed = false;
-  }
-
-  run_next_test();
-});
-
-add_test(function test_expected_measurements() {
-  let result = new MetricsCollectionResult("foo");
-  do_check_eq(result.missingMeasurements.size0);
-
-  result.expectMeasurement("foo");
-  result.expectMeasurement("bar");
-  do_check_eq(result.missingMeasurements.size, 2);
-  do_check_true(result.missingMeasurements.has("foo"));
-  do_check_true(result.missingMeasurements.has("bar"));
-
-  run_next_test();
-});
-
-add_test(function test_missing_measurements() {
-  let result = new MetricsCollectionResult("foo");
-
-  let missing = result.missingMeasurements;
-  do_check_eq(missing.size, 0);
-
-  result.expectMeasurement("DummyMeasurement");
-  result.expectMeasurement("b");
-
-  missing = result.missingMeasurements;
-  do_check_eq(missing.size, 2);
-  do_check_true(missing.has("DummyMeasurement"));
-  do_check_true(missing.has("b"));
-
-  result.addMeasurement(new DummyMeasurement());
-  missing = result.missingMeasurements;
-  do_check_eq(missing.size, 1);
-  do_check_true(missing.has("b"));
-
-  run_next_test();
-});
-
-add_test(function test_add_measurement() {
-  let result = new MetricsCollectionResult("add_measurement");
-
-  let failed = false;
-  try {
-    result.addMeasurement(new DummyMeasurement());
-  } catch (ex) {
-    do_check_true(ex.message.startsWith("Not expecting this measurement"));
-    failed = true;
-  } finally {
-    do_check_true(failed);
-    failed = false;
-  }
-
-  result.expectMeasurement("foo");
-  result.addMeasurement(new DummyMeasurement("foo"));
-
-  do_check_eq(result.measurements.size, 1);
-  do_check_true(result.measurements.has("foo"));
-
-  run_next_test();
-});
-
-add_test(function test_set_value() {
-  let result = new MetricsCollectionResult("set_value");
-  result.expectMeasurement("DummyMeasurement");
-  result.addMeasurement(new DummyMeasurement());
-
-  do_check_true(result.setValue("DummyMeasurement", "string", "hello world"));
-
-  let failed = false;
-  try {
-    result.setValue("unknown", "irrelevant", "irrelevant");
-  } catch (ex) {
-    do_check_true(ex.message.startsWith("Attempting to operate on an undefined measurement"));
-    failed = true;
-  } finally {
-    do_check_true(failed);
-    failed = false;
-  }
-
-  do_check_eq(result.errors.length, 0);
-  do_check_false(result.setValue("DummyMeasurement", "string", 42));
-  do_check_eq(result.errors.length, 1);
-
-  try {
-    result.setValue("DummyMeasurement", "string", 42, true);
-  } catch (ex) {
-    failed = true;
-  } finally {
-    do_check_true(failed);
-    failed = false;
-  }
-
-  run_next_test();
-});
-
-add_test(function test_aggregate_bad_argument() {
-  let result = new MetricsCollectionResult("bad_argument");
-
-  let failed = false;
-  try {
-    result.aggregate(null);
-  } catch (ex) {
-    do_check_true(ex.message.startsWith("aggregate expects a MetricsCollection"));
-    failed = true;
-  } finally {
-    do_check_true(failed);
-    failed = false;
-  }
-
-  try {
-    let result2 = new MetricsCollectionResult("bad_argument2");
-    result.aggregate(result2);
-  } catch (ex) {
-    do_check_true(ex.message.startsWith("Can only aggregate"));
-    failed = true;
-  } finally {
-    do_check_true(failed);
-    failed = false;
-  }
-
-  run_next_test();
-});
-
-add_test(function test_aggregate_side_effects() {
-  let result1 = new MetricsCollectionResult("aggregate");
-  let result2 = new MetricsCollectionResult("aggregate");
-
-  result1.expectMeasurement("dummy1");
-  result1.expectMeasurement("foo");
-
-  result2.expectMeasurement("dummy2");
-  result2.expectMeasurement("bar");
-
-  result1.addMeasurement(new DummyMeasurement("dummy1"));
-  result1.setValue("dummy1", "invalid", "invalid");
-
-  result2.addMeasurement(new DummyMeasurement("dummy2"));
-  result2.setValue("dummy2", "another", "invalid");
-
-  result1.aggregate(result2);
-
-  do_check_eq(result1.expectedMeasurements.size, 4);
-  do_check_true(result1.expectedMeasurements.has("bar"));
-
-  do_check_eq(result1.measurements.size, 2);
-  do_check_true(result1.measurements.has("dummy1"));
-  do_check_true(result1.measurements.has("dummy2"));
-
-  do_check_eq(result1.missingMeasurements.size, 2);
-  do_check_true(result1.missingMeasurements.has("bar"));
-
-  do_check_eq(result1.errors.length, 2);
-
-  run_next_test();
-});
-
-add_test(function test_json() {
-  let result = new MetricsCollectionResult("json");
-  result.expectMeasurement("dummy1");
-  result.expectMeasurement("dummy2");
-  result.expectMeasurement("missing1");
-  result.expectMeasurement("missing2");
-
-  result.addMeasurement(new DummyMeasurement("dummy1"));
-  result.addMeasurement(new DummyMeasurement("dummy2"));
-
-  result.setValue("dummy1", "string", "hello world");
-  result.setValue("dummy2", "uint32", 42);
-  result.setValue("dummy1", "invalid", "irrelevant");
-
-  let json = JSON.parse(JSON.stringify(result));
-
-  do_check_eq(Object.keys(json).length, 3);
-  do_check_true("measurements" in json);
-  do_check_true("missing" in json);
-  do_check_true("errors" in json);
-
-  do_check_eq(Object.keys(json.measurements).length, 2);
-  do_check_true("dummy1" in json.measurements);
-  do_check_true("dummy2" in json.measurements);
-
-  do_check_eq(json.missing.length, 2);
-  let missing = new Set(json.missing);
-  do_check_true(missing.has("missing1"));
-  do_check_true(missing.has("missing2"));
-
-  do_check_eq(json.errors.length, 1);
-
-  run_next_test();
-});
-
-add_test(function test_finish() {
-  let result = new MetricsCollectionResult("finish");
-
-  result.onFinished(function onFinished(result2) {
-    do_check_eq(result, result2);
-
-    run_next_test();
-  });
-
-  result.finish();
-});
-
--- a/services/metrics/tests/xpcshell/test_metrics_collector.js
+++ b/services/metrics/tests/xpcshell/test_metrics_collector.js
@@ -1,162 +1,127 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const {utils: Cu} = Components;
 
-Cu.import("resource://gre/modules/services/metrics/collector.jsm");
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
+Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://testing-common/services/metrics/mocks.jsm");
 
-
 function run_test() {
   run_next_test();
 };
 
-add_test(function test_constructor() {
-  let collector = new MetricsCollector();
+add_task(function test_constructor() {
+  let storage = yield Metrics.Storage("constructor");
+  let collector = new Metrics.Collector(storage);
 
-  run_next_test();
+  yield storage.close();
 });
 
-add_test(function test_register_provider() {
-  let collector = new MetricsCollector();
+add_task(function test_register_provider() {
+  let storage = yield Metrics.Storage("register_provider");
+
+  let collector = new Metrics.Collector(storage);
   let dummy = new DummyProvider();
 
-  collector.registerProvider(dummy);
-  do_check_eq(collector._providers.length, 1);
-  collector.registerProvider(dummy);
-  do_check_eq(collector._providers.length, 1);
+  yield collector.registerProvider(dummy);
+  do_check_eq(collector._providers.size, 1);
+  yield collector.registerProvider(dummy);
+  do_check_eq(collector._providers.size, 1);
   do_check_eq(collector.providerErrors.size, 1);
 
   let failed = false;
   try {
     collector.registerProvider({});
   } catch (ex) {
-    do_check_true(ex.message.startsWith("argument must be a MetricsProvider"));
+    do_check_true(ex.message.startsWith("Argument must be a Provider"));
     failed = true;
   } finally {
     do_check_true(failed);
     failed = false;
   }
 
-  run_next_test();
+  yield storage.close();
 });
 
-add_test(function test_collect_constant_measurements() {
-  let collector = new MetricsCollector();
+add_task(function test_collect_constant_data() {
+  let storage = yield Metrics.Storage("collect_constant_data");
+  let collector = new Metrics.Collector(storage);
   let provider = new DummyProvider();
-  collector.registerProvider(provider);
+  yield collector.registerProvider(provider);
 
   do_check_eq(provider.collectConstantCount, 0);
 
-  collector.collectConstantMeasurements().then(function onResult() {
-    do_check_eq(provider.collectConstantCount, 1);
-    do_check_eq(collector.collectionResults.size, 1);
-    do_check_true(collector.collectionResults.has("DummyProvider"));
+  yield collector.collectConstantData();
+  do_check_eq(provider.collectConstantCount, 1);
 
-    let result = collector.collectionResults.get("DummyProvider");
-    do_check_true(result instanceof MetricsCollectionResult);
+  do_check_true(collector._providers.get("DummyProvider").constantsCollected);
+  do_check_eq(collector.providerErrors.get("DummyProvider").length, 0);
 
-    do_check_true(collector._providers[0].constantsCollected);
-    do_check_eq(collector.providerErrors.get("DummyProvider").length, 0);
-
-    run_next_test();
-  });
+  yield storage.close();
 });
 
-add_test(function test_collect_constant_throws() {
-  let collector = new MetricsCollector();
+add_task(function test_collect_constant_throws() {
+  let storage = yield Metrics.Storage("collect_constant_throws");
+  let collector = new Metrics.Collector(storage);
   let provider = new DummyProvider();
-  provider.throwDuringCollectConstantMeasurements = "Fake error during collect";
-  collector.registerProvider(provider);
-
-  collector.collectConstantMeasurements().then(function onResult() {
-    do_check_eq(collector.providerErrors.get("DummyProvider").length, 1);
-    do_check_eq(collector.providerErrors.get("DummyProvider")[0].message,
-                provider.throwDuringCollectConstantMeasurements);
-
-    run_next_test();
-  });
-});
+  provider.throwDuringCollectConstantData = "Fake error during collect";
+  yield collector.registerProvider(provider);
 
-add_test(function test_collect_constant_populate_throws() {
-  let collector = new MetricsCollector();
-  let provider = new DummyProvider();
-  provider.throwDuringConstantPopulate = "Fake error during constant populate";
-  collector.registerProvider(provider);
-
-  collector.collectConstantMeasurements().then(function onResult() {
-    do_check_eq(collector.collectionResults.size, 1);
-    do_check_true(collector.collectionResults.has("DummyProvider"));
+  yield collector.collectConstantData();
+  do_check_true(collector.providerErrors.has(provider.name));
+  let errors = collector.providerErrors.get(provider.name);
+  do_check_eq(errors.length, 1);
+  do_check_eq(errors[0].message, provider.throwDuringCollectConstantData);
 
-    let result = collector.collectionResults.get("DummyProvider");
-    do_check_eq(result.errors.length, 1);
-    do_check_eq(result.errors[0].message, provider.throwDuringConstantPopulate);
-
-    do_check_false(collector._providers[0].constantsCollected);
-    do_check_eq(collector.providerErrors.get("DummyProvider").length, 0);
-
-    run_next_test();
-  });
+  yield storage.close();
 });
 
-add_test(function test_collect_constant_onetime() {
-  let collector = new MetricsCollector();
+add_task(function test_collect_constant_populate_throws() {
+  let storage = yield Metrics.Storage("collect_constant_populate_throws");
+  let collector = new Metrics.Collector(storage);
   let provider = new DummyProvider();
-  collector.registerProvider(provider);
+  provider.throwDuringConstantPopulate = "Fake error during constant populate";
+  yield collector.registerProvider(provider);
 
-  collector.collectConstantMeasurements().then(function onResult() {
-    do_check_eq(provider.collectConstantCount, 1);
+  yield collector.collectConstantData();
 
-    collector.collectConstantMeasurements().then(function onResult() {
-      do_check_eq(provider.collectConstantCount, 1);
+  let errors = collector.providerErrors.get(provider.name);
+  do_check_eq(errors.length, 1);
+  do_check_eq(errors[0].message, provider.throwDuringConstantPopulate);
+  do_check_false(collector._providers.get(provider.name).constantsCollected);
 
-      run_next_test();
-    });
-  });
+  yield storage.close();
 });
 
-add_test(function test_collect_multiple() {
-  let collector = new MetricsCollector();
+add_task(function test_collect_constant_onetime() {
+  let storage = yield Metrics.Storage("collect_constant_onetime");
+  let collector = new Metrics.Collector(storage);
+  let provider = new DummyProvider();
+  yield collector.registerProvider(provider);
+
+  yield collector.collectConstantData();
+  do_check_eq(provider.collectConstantCount, 1);
+
+  yield collector.collectConstantData();
+  do_check_eq(provider.collectConstantCount, 1);
+
+  yield storage.close();
+});
+
+add_task(function test_collect_multiple() {
+  let storage = yield Metrics.Storage("collect_multiple");
+  let collector = new Metrics.Collector(storage);
 
   for (let i = 0; i < 10; i++) {
-    collector.registerProvider(new DummyProvider("provider" + i));
+    yield collector.registerProvider(new DummyProvider("provider" + i));
   }
 
-  do_check_eq(collector._providers.length, 10);
+  do_check_eq(collector._providers.size, 10);
 
-  collector.collectConstantMeasurements().then(function onResult(innerCollector) {
-    do_check_eq(collector, innerCollector);
-    do_check_eq(collector.collectionResults.size, 10);
+  yield collector.collectConstantData();
 
-    run_next_test();
-  });
+  yield storage.close();
 });
 
-add_test(function test_collect_aggregate() {
-  let collector = new MetricsCollector();
-
-  let dummy1 = new DummyProvider();
-  dummy1.constantMeasurementName = "measurement1";
-
-  let dummy2 = new DummyProvider();
-  dummy2.constantMeasurementName = "measurement2";
-
-  collector.registerProvider(dummy1);
-  collector.registerProvider(dummy2);
-  do_check_eq(collector._providers.length, 2);
-
-  collector.collectConstantMeasurements().then(function onResult() {
-    do_check_eq(collector.collectionResults.size, 1);
-
-    let measurements = collector.collectionResults.get("DummyProvider").measurements;
-    do_check_eq(measurements.size, 2);
-    do_check_true(measurements.has("measurement1"));
-    do_check_true(measurements.has("measurement2"));
-
-    run_next_test();
-  });
-});
-
deleted file mode 100644
--- a/services/metrics/tests/xpcshell/test_metrics_measurement.js
+++ /dev/null
@@ -1,115 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-const {utils: Cu} = Components;
-
-Cu.import("resource://testing-common/services/metrics/mocks.jsm");
-
-
-function run_test() {
-  run_next_test();
-};
-
-add_test(function test_constructor() {
-  let m = new DummyMeasurement();
-  do_check_eq(m.name, "DummyMeasurement");
-  do_check_eq(m.version, 2);
-
-  run_next_test();
-});
-
-add_test(function test_add_string() {
-  let m = new DummyMeasurement();
-
-  m.setValue("string", "hello world");
-  do_check_eq(m.getValue("string"), "hello world");
-
-  let failed = false;
-  try {
-    m.setValue("string", 46);
-  } catch (ex) {
-    do_check_true(ex.message.startsWith("STRING field expects a string"));
-    failed = true;
-  } finally {
-    do_check_true(failed);
-  }
-
-  run_next_test();
-});
-
-add_test(function test_add_uint32() {
-  let m = new DummyMeasurement();
-
-  m.setValue("uint32", 52342);
-  do_check_eq(m.getValue("uint32"), 52342);
-
-  let failed = false;
-  try {
-    m.setValue("uint32", -1);
-  } catch (ex) {
-    failed = true;
-    do_check_true(ex.message.startsWith("UINT32 field expects a positive"));
-  } finally {
-    do_check_true(failed);
-    failed = false;
-  }
-
-  try {
-    m.setValue("uint32", "foo");
-  } catch (ex) {
-    failed = true;
-    do_check_true(ex.message.startsWith("UINT32 field expects an integer"));
-  } finally {
-    do_check_true(failed);
-    failed = false;
-  }
-
-  try {
-    m.setValue("uint32", Math.pow(2, 32));
-  } catch (ex) {
-    failed = true;
-    do_check_true(ex.message.startsWith("Value is too large"));
-  } finally {
-    do_check_true(failed);
-    failed = false;
-  }
-
-  run_next_test();
-});
-
-add_test(function test_validate() {
-  let m = new DummyMeasurement();
-
-  let failed = false;
-  try {
-    m.validate();
-  } catch (ex) {
-    failed = true;
-    do_check_true(ex.message.startsWith("Required field not defined"));
-  } finally {
-    do_check_true(failed);
-    failed = false;
-  }
-
-  run_next_test();
-});
-
-add_test(function test_toJSON() {
-  let m = new DummyMeasurement();
-
-  m.setValue("string", "foo bar");
-
-  let json = JSON.parse(JSON.stringify(m));
-  do_check_eq(Object.keys(json).length, 3);
-  do_check_eq(json.name, "DummyMeasurement");
-  do_check_eq(json.version, 2);
-  do_check_true("fields" in json);
-
-  do_check_eq(Object.keys(json.fields).length, 1);
-  do_check_eq(json.fields.string, "foo bar");
-
-  run_next_test();
-});
-
--- a/services/metrics/tests/xpcshell/test_metrics_provider.js
+++ b/services/metrics/tests/xpcshell/test_metrics_provider.js
@@ -1,64 +1,274 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const {utils: Cu} = Components;
 
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
+Cu.import("resource://gre/modules/Metrics.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://testing-common/services/metrics/mocks.jsm");
 
 
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
+
+
+function getProvider(storageName) {
+  return Task.spawn(function () {
+    let provider = new DummyProvider();
+    let storage = yield Metrics.Storage(storageName);
+
+    yield provider.init(storage);
+
+    throw new Task.Result(provider);
+  });
+}
+
+
 function run_test() {
   run_next_test();
 };
 
 add_test(function test_constructor() {
-  let provider = new MetricsProvider("foo");
-
   let failed = false;
   try {
-    new MetricsProvider();
+    new Metrics.Provider();
   } catch(ex) {
-    do_check_true(ex.message.startsWith("MetricsProvider must have a name"));
+    do_check_true(ex.message.startsWith("Provider must define a name"));
     failed = true;
   }
   finally {
     do_check_true(failed);
   }
 
   run_next_test();
 });
 
-add_test(function test_default_collectors() {
-  let provider = new MetricsProvider("foo");
+add_task(function test_init() {
+  let provider = new DummyProvider();
+  let storage = yield Metrics.Storage("init");
+
+  yield provider.init(storage);
 
-  for (let property in MetricsProvider.prototype) {
+  let m = provider.getMeasurement("DummyMeasurement", 1);
+  do_check_true(m instanceof Metrics.Measurement);
+  do_check_eq(m.id, 1);
+  do_check_eq(m._fieldsByName.size, 7);
+  do_check_true(m.hasField("daily-counter"));
+  do_check_false(m.hasField("does-not-exist"));
+
+  yield storage.close();
+});
+
+add_task(function test_default_collectors() {
+  let provider = new DummyProvider();
+  let storage = yield Metrics.Storage("default_collectors");
+  yield provider.init(storage);
+
+  for (let property in Metrics.Provider.prototype) {
     if (!property.startsWith("collect")) {
       continue;
     }
 
     let result = provider[property]();
-    do_check_null(result);
+    do_check_neq(result, null);
+    do_check_eq(typeof(result.then), "function");
   }
 
-  run_next_test();
+  yield storage.close();
 });
 
-add_test(function test_collect_constant_synchronous() {
-  let provider = new DummyProvider();
+add_task(function test_measurement_storage_basic() {
+  let provider = yield getProvider("measurement_storage_basic");
+  let m = provider.getMeasurement("DummyMeasurement", 1);
+
+  let now = new Date();
+  let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
+
+  // Daily counter.
+  let counterID = m.fieldID("daily-counter");
+  yield m.incrementDailyCounter("daily-counter", now);
+  yield m.incrementDailyCounter("daily-counter", now);
+  yield m.incrementDailyCounter("daily-counter", yesterday);
+  let count = yield provider.storage.getDailyCounterCountFromFieldID(counterID, now);
+  do_check_eq(count, 2);
+
+  count = yield provider.storage.getDailyCounterCountFromFieldID(counterID, yesterday);
+  do_check_eq(count, 1);
+
+  // Daily discrete numeric.
+  let dailyDiscreteNumericID = m.fieldID("daily-discrete-numeric");
+  yield m.addDailyDiscreteNumeric("daily-discrete-numeric", 5, now);
+  yield m.addDailyDiscreteNumeric("daily-discrete-numeric", 6, now);
+  yield m.addDailyDiscreteNumeric("daily-discrete-numeric", 7, yesterday);
+
+  let values = yield provider.storage.getDailyDiscreteNumericFromFieldID(
+    dailyDiscreteNumericID, now);
 
-  let result = provider.collectConstantMeasurements();
-  do_check_true(result instanceof MetricsCollectionResult);
-  result.populate(result);
+  do_check_eq(values.size, 1);
+  do_check_true(values.hasDay(now));
+  let actual = values.getDay(now);
+  do_check_eq(actual.length, 2);
+  do_check_eq(actual[0], 5);
+  do_check_eq(actual[1], 6);
+
+  values = yield provider.storage.getDailyDiscreteNumericFromFieldID(
+    dailyDiscreteNumericID, yesterday);
+
+  do_check_eq(values.size, 1);
+  do_check_true(values.hasDay(yesterday));
+  do_check_eq(values.getDay(yesterday)[0], 7);
+
+  // Daily discrete text.
+  let dailyDiscreteTextID = m.fieldID("daily-discrete-text");
+  yield m.addDailyDiscreteText("daily-discrete-text", "foo", now);
+  yield m.addDailyDiscreteText("daily-discrete-text", "bar", now);
+  yield m.addDailyDiscreteText("daily-discrete-text", "biz", yesterday);
+
+  values = yield provider.storage.getDailyDiscreteTextFromFieldID(
+    dailyDiscreteTextID, now);
+
+  do_check_eq(values.size, 1);
+  do_check_true(values.hasDay(now));
+  actual = values.getDay(now);
+  do_check_eq(actual.length, 2);
+  do_check_eq(actual[0], "foo");
+  do_check_eq(actual[1], "bar");
+
+  values = yield provider.storage.getDailyDiscreteTextFromFieldID(
+    dailyDiscreteTextID, yesterday);
+  do_check_true(values.hasDay(yesterday));
+  do_check_eq(values.getDay(yesterday)[0], "biz");
 
-  result.onFinished(function onResult(res2) {
-    do_check_eq(result, res2);
+  // Daily last numeric.
+  let lastDailyNumericID = m.fieldID("daily-last-numeric");
+  yield m.setDailyLastNumeric("daily-last-numeric", 5, now);
+  yield m.setDailyLastNumeric("daily-last-numeric", 6, yesterday);
+
+  let result = yield provider.storage.getDailyLastNumericFromFieldID(
+    lastDailyNumericID, now);
+  do_check_eq(result.size, 1);
+  do_check_true(result.hasDay(now));
+  do_check_eq(result.getDay(now), 5);
+
+  result = yield provider.storage.getDailyLastNumericFromFieldID(
+    lastDailyNumericID, yesterday);
+  do_check_true(result.hasDay(yesterday));
+  do_check_eq(result.getDay(yesterday), 6);
+
+  yield m.setDailyLastNumeric("daily-last-numeric", 7, now);
+  result = yield provider.storage.getDailyLastNumericFromFieldID(
+    lastDailyNumericID, now);
+  do_check_eq(result.getDay(now), 7);
+
+  // Daily last text.
+  let lastDailyTextID = m.fieldID("daily-last-text");
+  yield m.setDailyLastText("daily-last-text", "foo", now);
+  yield m.setDailyLastText("daily-last-text", "bar", yesterday);
+
+  result = yield provider.storage.getDailyLastTextFromFieldID(
+    lastDailyTextID, now);
+  do_check_eq(result.size, 1);
+  do_check_true(result.hasDay(now));
+  do_check_eq(result.getDay(now), "foo");
 
-    let m = result.measurements.get("DummyMeasurement");
-    do_check_eq(m.getValue("uint32"), 24);
+  result = yield provider.storage.getDailyLastTextFromFieldID(
+    lastDailyTextID, yesterday);
+  do_check_true(result.hasDay(yesterday));
+  do_check_eq(result.getDay(yesterday), "bar");
+
+  yield m.setDailyLastText("daily-last-text", "biz", now);
+  result = yield provider.storage.getDailyLastTextFromFieldID(
+    lastDailyTextID, now);
+  do_check_eq(result.getDay(now), "biz");
+
+  // Last numeric.
+  let lastNumericID = m.fieldID("last-numeric");
+  yield m.setLastNumeric("last-numeric", 1, now);
+  result = yield provider.storage.getLastNumericFromFieldID(lastNumericID);
+  do_check_eq(result[1], 1);
+  do_check_true(result[0].getTime() < now.getTime());
+  do_check_true(result[0].getTime() > yesterday.getTime());
 
-    run_next_test();
-  });
+  yield m.setLastNumeric("last-numeric", 2, now);
+  result = yield provider.storage.getLastNumericFromFieldID(lastNumericID);
+  do_check_eq(result[1], 2);
+
+  // Last text.
+  let lastTextID = m.fieldID("last-text");
+  yield m.setLastText("last-text", "foo", now);
+  result = yield provider.storage.getLastTextFromFieldID(lastTextID);
+  do_check_eq(result[1], "foo");
+  do_check_true(result[0].getTime() < now.getTime());
+  do_check_true(result[0].getTime() > yesterday.getTime());
+
+  yield m.setLastText("last-text", "bar", now);
+  result = yield provider.storage.getLastTextFromFieldID(lastTextID);
+  do_check_eq(result[1], "bar");
+
+  yield provider.storage.close();
 });
 
+add_task(function test_serialize_json_default() {
+  let provider = yield getProvider("serialize_json_default");
+
+  let now = new Date();
+  let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
+
+  let m = provider.getMeasurement("DummyMeasurement", 1);
+
+  m.incrementDailyCounter("daily-counter", now);
+  m.incrementDailyCounter("daily-counter", now);
+  m.incrementDailyCounter("daily-counter", yesterday);
+
+  m.addDailyDiscreteNumeric("daily-discrete-numeric", 1, now);
+  m.addDailyDiscreteNumeric("daily-discrete-numeric", 2, now);
+  m.addDailyDiscreteNumeric("daily-discrete-numeric", 3, yesterday);
+
+  m.addDailyDiscreteText("daily-discrete-text", "foo", now);
+  m.addDailyDiscreteText("daily-discrete-text", "bar", now);
+  m.addDailyDiscreteText("daily-discrete-text", "baz", yesterday);
+
+  m.setDailyLastNumeric("daily-last-numeric", 4, now);
+  m.setDailyLastNumeric("daily-last-numeric", 5, yesterday);
+
+  m.setDailyLastText("daily-last-text", "apple", now);
+  m.setDailyLastText("daily-last-text", "orange", yesterday);
+
+  m.setLastNumeric("last-numeric", 6, now);
+  yield m.setLastText("last-text", "hello", now);
+
+  let data = yield provider.storage.getMeasurementValues(m.id);
+
+  let serializer = m.serializer(m.SERIALIZE_JSON);
+  let formatted = serializer.singular(data.singular);
+
+  do_check_eq(Object.keys(formatted).length, 2);
+  do_check_true("last-numeric" in formatted);
+  do_check_true("last-text" in formatted);
+  do_check_eq(formatted["last-numeric"], 6);
+  do_check_eq(formatted["last-text"], "hello");
+
+  formatted = serializer.daily(data.days.getDay(now));
+  do_check_eq(Object.keys(formatted).length, 5);
+  do_check_eq(formatted["daily-counter"], 2);
+
+  do_check_true(Array.isArray(formatted["daily-discrete-numeric"]));
+  do_check_eq(formatted["daily-discrete-numeric"].length, 2);
+  do_check_eq(formatted["daily-discrete-numeric"][0], 1);
+  do_check_eq(formatted["daily-discrete-numeric"][1], 2);
+
+  do_check_true(Array.isArray(formatted["daily-discrete-text"]));
+  do_check_eq(formatted["daily-discrete-text"].length, 2);
+  do_check_eq(formatted["daily-discrete-text"][0], "foo");
+  do_check_eq(formatted["daily-discrete-text"][1], "bar");
+
+  do_check_eq(formatted["daily-last-numeric"], 4);
+  do_check_eq(formatted["daily-last-text"], "apple");
+
+  formatted = serializer.daily(data.days.getDay(yesterday));
+  do_check_eq(formatted["daily-last-numeric"], 5);
+  do_check_eq(formatted["daily-last-text"], "orange");
+
+  yield provider.storage.close();
+});
new file mode 100644
--- /dev/null
+++ b/services/metrics/tests/xpcshell/test_metrics_storage.js
@@ -0,0 +1,721 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Metrics.jsm");
+Cu.import("resource://services-common/utils.js");
+
+
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
+
+
+function run_test() {
+  run_next_test();
+}
+
+add_test(function test_days_date_conversion() {
+  let toDays = Metrics.dateToDays;
+  let toDate = Metrics.daysToDate;
+
+  let d = new Date(0);
+  do_check_eq(toDays(d), 0);
+
+  d = new Date(MILLISECONDS_PER_DAY);
+  do_check_eq(toDays(d), 1);
+
+  d = new Date(MILLISECONDS_PER_DAY - 1);
+  do_check_eq(toDays(d), 0);
+
+  d = new Date("1970-12-31T23:59:59.999Z");
+  do_check_eq(toDays(d), 364);
+
+  d = new Date("1971-01-01T00:00:00Z");
+  do_check_eq(toDays(d), 365);
+
+  d = toDate(0);
+  do_check_eq(d.getTime(), 0);
+
+  d = toDate(1);
+  do_check_eq(d.getTime(), MILLISECONDS_PER_DAY);
+
+  d = toDate(365);
+  do_check_eq(d.getUTCFullYear(), 1971);
+  do_check_eq(d.getUTCMonth(), 0);
+  do_check_eq(d.getUTCDate(), 1);
+  do_check_eq(d.getUTCHours(), 0);
+  do_check_eq(d.getUTCMinutes(), 0);
+  do_check_eq(d.getUTCSeconds(), 0);
+  do_check_eq(d.getUTCMilliseconds(), 0);
+
+  run_next_test();
+});
+
+add_task(function test_get_sqlite_backend() {
+  let backend = yield Metrics.Storage("get_sqlite_backend.sqlite");
+
+  do_check_neq(backend._connection, null);
+
+  yield backend.close();
+  do_check_null(backend._connection);
+});
+
+add_task(function test_reconnect() {
+  let backend = yield Metrics.Storage("reconnect");
+  yield backend.close();
+
+  let backend2 = yield Metrics.Storage("reconnect");
+  yield backend2.close();
+});
+
+add_task(function test_future_schema_errors() {
+  let backend = yield Metrics.Storage("future_schema_errors");
+  backend._connection.schemaVersion = 2;
+  yield backend.close();
+
+  let backend2;
+  let failed = false;
+  try {
+    backend2 = yield Metrics.Storage("future_schema_errors");
+  } catch (ex) {
+    failed = true;
+    do_check_true(ex.message.startsWith("Unknown database schema"));
+  }
+
+  do_check_null(backend2);
+  do_check_true(failed);
+});
+
+add_task(function test_measurement_registration() {
+  let backend = yield Metrics.Storage("measurement_registration");
+
+  do_check_false(backend.hasProvider("foo"));
+  do_check_false(backend.hasMeasurement("foo", "bar", 1));
+
+  let id = yield backend.registerMeasurement("foo", "bar", 1);
+  do_check_eq(id, 1);
+
+  do_check_true(backend.hasProvider("foo"));
+  do_check_true(backend.hasMeasurement("foo", "bar", 1));
+  do_check_eq(backend.measurementID("foo", "bar", 1), id);
+  do_check_false(backend.hasMeasurement("foo", "bar", 2));
+
+  let id2 = yield backend.registerMeasurement("foo", "bar", 2);
+  do_check_eq(id2, 2);
+  do_check_true(backend.hasMeasurement("foo", "bar", 2));
+  do_check_eq(backend.measurementID("foo", "bar", 2), id2);
+
+  yield backend.close();
+});
+
+add_task(function test_field_registration_basic() {
+  let backend = yield Metrics.Storage("field_registration_basic");
+
+  do_check_false(backend.hasField("foo", "bar", 1, "baz"));
+
+  let mID = yield backend.registerMeasurement("foo", "bar", 1);
+  do_check_false(backend.hasField("foo", "bar", 1, "baz"));
+  do_check_false(backend.hasFieldFromMeasurement(mID, "baz"));
+
+  let bazID = yield backend.registerField(mID, "baz",
+                                          backend.FIELD_DAILY_COUNTER);
+  do_check_true(backend.hasField("foo", "bar", 1, "baz"));
+  do_check_true(backend.hasFieldFromMeasurement(mID, "baz"));
+
+  let bar2ID = yield backend.registerMeasurement("foo", "bar2", 1);
+
+  yield backend.registerField(bar2ID, "baz",
+                              backend.FIELD_DAILY_DISCRETE_NUMERIC);
+
+  do_check_true(backend.hasField("foo", "bar2", 1, "baz"));
+
+  yield backend.close();
+});
+
+// Ensure changes types of fields results in fatal error.
+add_task(function test_field_registration_changed_type() {
+  let backend = yield Metrics.Storage("field_registration_changed_type");
+
+  let mID = yield backend.registerMeasurement("bar", "bar", 1);
+
+  let id = yield backend.registerField(mID, "baz",
+                                       backend.FIELD_DAILY_COUNTER);
+
+  let caught = false;
+  try {
+    yield backend.registerField(mID, "baz",
+                                backend.FIELD_DAILY_DISCRETE_NUMERIC);
+  } catch (ex) {
+    caught = true;
+    do_check_true(ex.message.startsWith("Field already defined with different type"));
+  }
+
+  do_check_true(caught);
+
+  yield backend.close();
+});
+
+add_task(function test_field_registration_repopulation() {
+  let backend = yield Metrics.Storage("field_registration_repopulation");
+
+  let mID1 = yield backend.registerMeasurement("foo", "bar", 1);
+  let mID2 = yield backend.registerMeasurement("foo", "bar", 2);
+  let mID3 = yield backend.registerMeasurement("foo", "biz", 1);
+  let mID4 = yield backend.registerMeasurement("baz", "foo", 1);
+
+  let fID1 = yield backend.registerField(mID1, "foo", backend.FIELD_DAILY_COUNTER);
+  let fID2 = yield backend.registerField(mID1, "bar", backend.FIELD_DAILY_DISCRETE_NUMERIC);
+  let fID3 = yield backend.registerField(mID4, "foo", backend.FIELD_LAST_TEXT);
+
+  yield backend.close();
+
+  backend = yield Metrics.Storage("field_registration_repopulation");
+
+  do_check_true(backend.hasProvider("foo"));
+  do_check_true(backend.hasProvider("baz"));
+  do_check_true(backend.hasMeasurement("foo", "bar", 1));
+  do_check_eq(backend.measurementID("foo", "bar", 1), mID1);
+  do_check_true(backend.hasMeasurement("foo", "bar", 2));
+  do_check_eq(backend.measurementID("foo", "bar", 2), mID2);
+  do_check_true(backend.hasMeasurement("foo", "biz", 1));
+  do_check_eq(backend.measurementID("foo", "biz", 1), mID3);
+  do_check_true(backend.hasMeasurement("baz", "foo", 1));
+  do_check_eq(backend.measurementID("baz", "foo", 1), mID4);
+
+  do_check_true(backend.hasField("foo", "bar", 1, "foo"));
+  do_check_eq(backend.fieldID("foo", "bar", 1, "foo"), fID1);
+  do_check_true(backend.hasField("foo", "bar", 1, "bar"));
+  do_check_eq(backend.fieldID("foo", "bar", 1, "bar"), fID2);
+  do_check_true(backend.hasField("baz", "foo", 1, "foo"));
+  do_check_eq(backend.fieldID("baz", "foo", 1, "foo"), fID3);
+
+  yield backend.close();
+});
+
+add_task(function test_enqueue_operation_many() {
+  let backend = yield Metrics.Storage("enqueue_operation_many");
+
+  let promises = [];
+  for (let i = 0; i < 100; i++) {
+    promises.push(backend.registerMeasurement("foo", "bar" + i, 1));
+  }
+
+  for (let promise of promises) {
+    yield promise;
+  }
+
+  yield backend.close();
+});
+
+// If the operation did not return a promise, everything should still execute.
+add_task(function test_enqueue_operation_no_return_promise() {
+  let backend = yield Metrics.Storage("enqueue_operation_no_return_promise");
+
+  let mID = yield backend.registerMeasurement("foo", "bar", 1);
+  let fID = yield backend.registerField(mID, "baz", backend.FIELD_DAILY_COUNTER);
+  let now = new Date();
+
+  let promises = [];
+  for (let i = 0; i < 10; i++) {
+    promises.push(backend.enqueueOperation(function op() {
+      backend.incrementDailyCounterFromFieldID(fID, now);
+    }));
+  }
+
+  let deferred = Promise.defer();
+
+  let finished = 0;
+  for (let promise of promises) {
+    promise.then(
+      do_throw.bind(this, "Unexpected resolve."),
+      function onError() {
+        finished++;
+
+        if (finished == promises.length) {
+          backend.getDailyCounterCountFromFieldID(fID, now).then(function onCount(count) {
+            // There should not be a race condition here because storage
+            // serializes all statements. So, for the getDailyCounterCount
+            // query to finish means that all counter update statements must
+            // have completed.
+            do_check_eq(count, promises.length);
+            deferred.resolve();
+          });
+        }
+      }
+    );
+  }
+
+  yield deferred.promise;
+  yield backend.close();
+});
+
+// If an operation throws, subsequent operations should still execute.
+add_task(function test_enqueue_operation_throw_exception() {
+  let backend = yield Metrics.Storage("enqueue_operation_rejected_promise");
+
+  let mID = yield backend.registerMeasurement("foo", "bar", 1);
+  let fID = yield backend.registerField(mID, "baz", backend.FIELD_DAILY_COUNTER);
+  let now = new Date();
+
+  let deferred = Promise.defer();
+  backend.enqueueOperation(function bad() {
+    throw new Error("I failed.");
+  }).then(do_throw, function onError(error) {
+    do_check_true(error.message.contains("I failed."));
+    deferred.resolve();
+  });
+
+  let promise = backend.enqueueOperation(function () {
+    return backend.incrementDailyCounterFromFieldID(fID, now);
+  });
+
+  yield deferred.promise;
+  yield promise;
+
+  let count = yield backend.getDailyCounterCountFromFieldID(fID, now);
+  do_check_eq(count, 1);
+  yield backend.close();
+});
+
+// If an operation rejects, subsequent operations should still execute.
+add_task(function test_enqueue_operation_reject_promise() {
+  let backend = yield Metrics.Storage("enqueue_operation_reject_promise");
+
+  let mID = yield backend.registerMeasurement("foo", "bar", 1);
+  let fID = yield backend.registerField(mID, "baz", backend.FIELD_DAILY_COUNTER);
+  let now = new Date();
+
+  let deferred = Promise.defer();
+  backend.enqueueOperation(function reject() {
+    let d = Promise.defer();
+
+    CommonUtils.nextTick(function nextTick() {
+      d.reject("I failed.");
+    });
+
+    return d.promise;
+  }).then(do_throw, function onError(error) {
+    deferred.resolve();
+  });
+
+  let promise = backend.enqueueOperation(function () {
+    return backend.incrementDailyCounterFromFieldID(fID, now);
+  });
+
+  yield deferred.promise;
+  yield promise;
+
+  let count = yield backend.getDailyCounterCountFromFieldID(fID, now);
+  do_check_eq(count, 1);
+  yield backend.close();
+});
+
+add_task(function test_increment_daily_counter_basic() {
+  let backend = yield Metrics.Storage("increment_daily_counter_basic");
+
+  let mID = yield backend.registerMeasurement("foo", "bar", 1);
+
+  let fieldID = yield backend.registerField(mID, "baz",
+                                            backend.FIELD_DAILY_COUNTER);
+
+  let now = new Date();
+  yield backend.incrementDailyCounterFromFieldID(fieldID, now);
+
+  let count = yield backend.getDailyCounterCountFromFieldID(fieldID, now);
+  do_check_eq(count, 1);
+
+  yield backend.incrementDailyCounterFromFieldID(fieldID, now);
+  count = yield backend.getDailyCounterCountFromFieldID(fieldID, now);
+  do_check_eq(count, 2);
+
+  yield backend.close();
+});
+
+add_task(function test_increment_daily_counter_multiple_days() {
+  let backend = yield Metrics.Storage("increment_daily_counter_multiple_days");
+
+  let mID = yield backend.registerMeasurement("foo", "bar", 1);
+  let fieldID = yield backend.registerField(mID, "baz",
+                                            backend.FIELD_DAILY_COUNTER);
+
+  let days = [];
+  let now = Date.now();
+  for (let i = 0; i < 100; i++) {
+    days.push(new Date(now - i * MILLISECONDS_PER_DAY));
+  }
+
+  for (let day of days) {
+    yield backend.incrementDailyCounterFromFieldID(fieldID, day);
+  }
+
+  let result = yield backend.getDailyCounterCountsFromFieldID(fieldID);
+  do_check_eq(result.size, 100);
+  for (let day of days) {
+    do_check_true(result.hasDay(day));
+    do_check_eq(result.getDay(day), 1);
+  }
+
+  let fields = yield backend.getMeasurementDailyCountersFromMeasurementID(mID);
+  do_check_eq(fields.size, 1);
+  do_check_true(fields.has("baz"));
+  do_check_eq(fields.get("baz").size, 100);
+
+  for (let day of days) {
+    do_check_true(fields.get("baz").hasDay(day));
+    do_check_eq(fields.get("baz").getDay(day), 1);
+  }
+
+  yield backend.close();
+});
+
+add_task(function test_last_values() {
+  let backend = yield Metrics.Storage("set_last");
+
+  let mID = yield backend.registerMeasurement("foo", "bar", 1);
+  let numberID = yield backend.registerField(mID, "number",
+                                             backend.FIELD_LAST_NUMERIC);
+  let textID = yield backend.registerField(mID, "text",
+                                             backend.FIELD_LAST_TEXT);
+  let now = new Date();
+  let nowDay = new Date(Math.floor(now.getTime() / MILLISECONDS_PER_DAY) * MILLISECONDS_PER_DAY);
+
+  yield backend.setLastNumericFromFieldID(numberID, 42, now);
+  yield backend.setLastTextFromFieldID(textID, "hello world", now);
+
+  let result = yield backend.getLastNumericFromFieldID(numberID);
+  do_check_true(Array.isArray(result));
+  do_check_eq(result[0].getTime(), nowDay.getTime());
+  do_check_eq(typeof(result[1]), "number");
+  do_check_eq(result[1], 42);
+
+  result = yield backend.getLastTextFromFieldID(textID);
+  do_check_true(Array.isArray(result));
+  do_check_eq(result[0].getTime(), nowDay.getTime());
+  do_check_eq(typeof(result[1]), "string");
+  do_check_eq(result[1], "hello world");
+
+  let missingID = yield backend.registerField(mID, "missing",
+                                              backend.FIELD_LAST_NUMERIC);
+  do_check_null(yield backend.getLastNumericFromFieldID(missingID));
+
+  let fields = yield backend.getMeasurementLastValuesFromMeasurementID(mID);
+  do_check_eq(fields.size, 2);
+  do_check_true(fields.has("number"));
+  do_check_true(fields.has("text"));
+  do_check_eq(fields.get("number")[1], 42);
+  do_check_eq(fields.get("text")[1], "hello world");
+
+  yield backend.close();
+});
+
+add_task(function test_discrete_values_basic() {
+  let backend = yield Metrics.Storage("discrete_values_basic");
+
+  let mID = yield backend.registerMeasurement("foo", "bar", 1);
+  let numericID = yield backend.registerField(mID, "numeric",
+                                              backend.FIELD_DAILY_DISCRETE_NUMERIC);
+  let textID = yield backend.registerField(mID, "text",
+                                           backend.FIELD_DAILY_DISCRETE_TEXT);
+
+  let now = new Date();
+  let expectedNumeric = [];
+  let expectedText = [];
+  for (let i = 0; i < 100; i++) {
+    expectedNumeric.push(i);
+    expectedText.push("value" + i);
+    yield backend.addDailyDiscreteNumericFromFieldID(numericID, i, now);
+    yield backend.addDailyDiscreteTextFromFieldID(textID, "value" + i, now);
+  }
+
+  let values = yield backend.getDailyDiscreteNumericFromFieldID(numericID);
+  do_check_eq(values.size, 1);
+  do_check_true(values.hasDay(now));
+  do_check_true(Array.isArray(values.getDay(now)));
+  do_check_eq(values.getDay(now).length, expectedNumeric.length);
+
+  for (let i = 0; i < expectedNumeric.length; i++) {
+    do_check_eq(values.getDay(now)[i], expectedNumeric[i]);
+  }
+
+  values = yield backend.getDailyDiscreteTextFromFieldID(textID);
+  do_check_eq(values.size, 1);
+  do_check_true(values.hasDay(now));
+  do_check_true(Array.isArray(values.getDay(now)));
+  do_check_eq(values.getDay(now).length, expectedText.length);
+
+  for (let i = 0; i < expectedText.length; i++) {
+    do_check_eq(values.getDay(now)[i], expectedText[i]);
+  }
+
+  let fields = yield backend.getMeasurementDailyDiscreteValuesFromMeasurementID(mID);
+  do_check_eq(fields.size, 2);
+  do_check_true(fields.has("numeric"));
+  do_check_true(fields.has("text"));
+
+  let numeric = fields.get("numeric");
+  let text = fields.get("text");
+  do_check_true(numeric.hasDay(now));
+  do_check_true(text.hasDay(now));
+  do_check_eq(numeric.getDay(now).length, expectedNumeric.length);
+  do_check_eq(text.getDay(now).length, expectedText.length);
+
+  for (let i = 0; i < expectedNumeric.length; i++) {
+    do_check_eq(numeric.getDay(now)[i], expectedNumeric[i]);
+  }
+
+  for (let i = 0; i < expectedText.length; i++) {
+    do_check_eq(text.getDay(now)[i], expectedText[i]);
+  }
+
+  yield backend.close();
+});
+
+add_task(function test_discrete_values_multiple_days() {
+  let backend = yield Metrics.Storage("discrete_values_multiple_days");
+
+  let mID = yield backend.registerMeasurement("foo", "bar", 1);
+  let id = yield backend.registerField(mID, "baz",
+                                       backend.FIELD_DAILY_DISCRETE_NUMERIC);
+
+  let now = new Date();
+  let dates = [];
+  for (let i = 0; i < 50; i++) {
+    let date = new Date(now.getTime() + i * MILLISECONDS_PER_DAY);
+    dates.push(date);
+
+    yield backend.addDailyDiscreteNumericFromFieldID(id, i, date);
+  }
+
+  let values = yield backend.getDailyDiscreteNumericFromFieldID(id);
+  do_check_eq(values.size, 50);
+
+  let i = 0;
+  for (let date of dates) {
+    do_check_true(values.hasDay(date));
+    do_check_eq(values.getDay(date)[0], i);
+    i++;
+  }
+
+  let fields = yield backend.getMeasurementDailyDiscreteValuesFromMeasurementID(mID);
+  do_check_eq(fields.size, 1);
+  do_check_true(fields.has("baz"));
+  let baz = fields.get("baz");
+  do_check_eq(baz.size, 50);
+  i = 0;
+  for (let date of dates) {
+    do_check_true(baz.hasDay(date));
+    do_check_eq(baz.getDay(date).length, 1);
+    do_check_eq(baz.getDay(date)[0], i);
+    i++;
+  }
+
+  yield backend.close();
+});
+
+add_task(function test_daily_last_values() {
+  let backend = yield Metrics.Storage("daily_last_values");
+
+  let mID = yield backend.registerMeasurement("foo", "bar", 1);
+  let numericID = yield backend.registerField(mID, "numeric",
+                                              backend.FIELD_DAILY_LAST_NUMERIC);
+  let textID = yield backend.registerField(mID, "text",
+                                           backend.FIELD_DAILY_LAST_TEXT);
+
+  let now = new Date();
+  let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
+  let dayBefore = new Date(yesterday.getTime() - MILLISECONDS_PER_DAY);
+
+  yield backend.setDailyLastNumericFromFieldID(numericID, 1, yesterday);
+  yield backend.setDailyLastNumericFromFieldID(numericID, 2, now);
+  yield backend.setDailyLastNumericFromFieldID(numericID, 3, dayBefore);
+  yield backend.setDailyLastTextFromFieldID(textID, "foo", now);
+  yield backend.setDailyLastTextFromFieldID(textID, "bar", yesterday);
+  yield backend.setDailyLastTextFromFieldID(textID, "baz", dayBefore);
+
+  let days = yield backend.getDailyLastNumericFromFieldID(numericID);
+  do_check_eq(days.size, 3);
+  do_check_eq(days.getDay(yesterday), 1);
+  do_check_eq(days.getDay(now), 2);
+  do_check_eq(days.getDay(dayBefore), 3);
+
+  days = yield backend.getDailyLastTextFromFieldID(textID);
+  do_check_eq(days.size, 3);
+  do_check_eq(days.getDay(now), "foo");
+  do_check_eq(days.getDay(yesterday), "bar");
+  do_check_eq(days.getDay(dayBefore), "baz");
+
+  yield backend.setDailyLastNumericFromFieldID(numericID, 4, yesterday);
+  days = yield backend.getDailyLastNumericFromFieldID(numericID);
+  do_check_eq(days.getDay(yesterday), 4);
+
+  yield backend.setDailyLastTextFromFieldID(textID, "biz", yesterday);
+  days = yield backend.getDailyLastTextFromFieldID(textID);
+  do_check_eq(days.getDay(yesterday), "biz");
+
+  days = yield backend.getDailyLastNumericFromFieldID(numericID, yesterday);
+  do_check_eq(days.size, 1);
+  do_check_eq(days.getDay(yesterday), 4);
+
+  days = yield backend.getDailyLastTextFromFieldID(textID, yesterday);
+  do_check_eq(days.size, 1);
+  do_check_eq(days.getDay(yesterday), "biz");
+
+  let fields = yield backend.getMeasurementDailyLastValuesFromMeasurementID(mID);
+  do_check_eq(fields.size, 2);
+  do_check_true(fields.has("numeric"));
+  do_check_true(fields.has("text"));
+  let numeric = fields.get("numeric");
+  let text = fields.get("text");
+  do_check_true(numeric.hasDay(yesterday));
+  do_check_true(numeric.hasDay(dayBefore));
+  do_check_true(numeric.hasDay(now));
+  do_check_true(text.hasDay(yesterday));
+  do_check_true(text.hasDay(dayBefore));
+  do_check_true(text.hasDay(now));
+  do_check_eq(numeric.getDay(yesterday), 4);
+  do_check_eq(text.getDay(yesterday), "biz");
+
+  yield backend.close();
+});
+
+add_task(function test_prune_data_before() {
+  let backend = yield Metrics.Storage("prune_data_before");
+
+  let mID = yield backend.registerMeasurement("foo", "bar", 1);
+
+  let counterID = yield backend.registerField(mID, "baz",
+                                              backend.FIELD_DAILY_COUNTER);
+  let text1ID = yield backend.registerField(mID, "one_text_1",
+                                            backend.FIELD_LAST_TEXT);
+  let text2ID = yield backend.registerField(mID, "one_text_2",
+                                            backend.FIELD_LAST_TEXT);
+  let numeric1ID = yield backend.registerField(mID, "one_numeric_1",
+                                               backend.FIELD_LAST_NUMERIC);
+  let numeric2ID = yield backend.registerField(mID, "one_numeric_2",
+                                                backend.FIELD_LAST_NUMERIC);
+  let text3ID = yield backend.registerField(mID, "daily_last_text_1",
+                                            backend.FIELD_DAILY_LAST_TEXT);
+  let text4ID = yield backend.registerField(mID, "daily_last_text_2",
+                                            backend.FIELD_DAILY_LAST_TEXT);
+  let numeric3ID = yield backend.registerField(mID, "daily_last_numeric_1",
+                                               backend.FIELD_DAILY_LAST_NUMERIC);
+  let numeric4ID = yield backend.registerField(mID, "daily_last_numeric_2",
+                                               backend.FIELD_DAILY_LAST_NUMERIC);
+
+  let now = new Date();
+  let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
+  let dayBefore = new Date(yesterday.getTime() - MILLISECONDS_PER_DAY);
+
+  yield backend.incrementDailyCounterFromFieldID(counterID, now);
+  yield backend.incrementDailyCounterFromFieldID(counterID, yesterday);
+  yield backend.incrementDailyCounterFromFieldID(counterID, dayBefore);
+  yield backend.setLastTextFromFieldID(text1ID, "hello", dayBefore);
+  yield backend.setLastTextFromFieldID(text2ID, "world", yesterday);
+  yield backend.setLastNumericFromFieldID(numeric1ID, 42, dayBefore);
+  yield backend.setLastNumericFromFieldID(numeric2ID, 43, yesterday);
+  yield backend.setDailyLastTextFromFieldID(text3ID, "foo", dayBefore);
+  yield backend.setDailyLastTextFromFieldID(text3ID, "bar", yesterday);
+  yield backend.setDailyLastTextFromFieldID(text4ID, "hello", dayBefore);
+  yield backend.setDailyLastTextFromFieldID(text4ID, "world", yesterday);
+  yield backend.setDailyLastNumericFromFieldID(numeric3ID, 40, dayBefore);
+  yield backend.setDailyLastNumericFromFieldID(numeric3ID, 41, yesterday);
+  yield backend.setDailyLastNumericFromFieldID(numeric4ID, 42, dayBefore);
+  yield backend.setDailyLastNumericFromFieldID(numeric4ID, 43, yesterday);
+
+  let days = yield backend.getDailyCounterCountsFromFieldID(counterID);
+  do_check_eq(days.size, 3);
+
+  yield backend.pruneDataBefore(yesterday);
+  days = yield backend.getDailyCounterCountsFromFieldID(counterID);
+  do_check_eq(days.size, 2);
+  do_check_false(days.hasDay(dayBefore));
+
+  do_check_null(yield backend.getLastTextFromFieldID(text1ID));
+  do_check_null(yield backend.getLastNumericFromFieldID(numeric1ID));
+
+  let result = yield backend.getLastTextFromFieldID(text2ID);
+  do_check_true(Array.isArray(result));
+  do_check_eq(result[1], "world");
+
+  result = yield backend.getLastNumericFromFieldID(numeric2ID);
+  do_check_true(Array.isArray(result));
+  do_check_eq(result[1], 43);
+
+  result = yield backend.getDailyLastNumericFromFieldID(numeric3ID);
+  do_check_eq(result.size, 1);
+  do_check_true(result.hasDay(yesterday));
+
+  result = yield backend.getDailyLastTextFromFieldID(text3ID);
+  do_check_eq(result.size, 1);
+  do_check_true(result.hasDay(yesterday));
+
+  yield backend.close();
+});
+
+add_task(function test_provider_state() {
+  let backend = yield Metrics.Storage("provider_state");
+
+  yield backend.registerMeasurement("foo", "bar", 1);
+  yield backend.setProviderState("foo", "apple", "orange");
+  let value = yield backend.getProviderState("foo", "apple");
+  do_check_eq(value, "orange");
+
+  yield backend.setProviderState("foo", "apple", "pear");
+  value = yield backend.getProviderState("foo", "apple");
+  do_check_eq(value, "pear");
+
+  yield backend.close();
+});
+
+add_task(function test_get_measurement_values() {
+  let backend = yield Metrics.Storage("get_measurement_values");
+
+  let mID = yield backend.registerMeasurement("foo", "bar", 1);
+  let id1 = yield backend.registerField(mID, "id1", backend.FIELD_DAILY_COUNTER);
+  let id2 = yield backend.registerField(mID, "id2", backend.FIELD_DAILY_DISCRETE_NUMERIC);
+  let id3 = yield backend.registerField(mID, "id3", backend.FIELD_DAILY_DISCRETE_TEXT);
+  let id4 = yield backend.registerField(mID, "id4", backend.FIELD_DAILY_LAST_NUMERIC);
+  let id5 = yield backend.registerField(mID, "id5", backend.FIELD_DAILY_LAST_TEXT);
+  let id6 = yield backend.registerField(mID, "id6", backend.FIELD_LAST_NUMERIC);
+  let id7 = yield backend.registerField(mID, "id7", backend.FIELD_LAST_TEXT);
+
+  let now = new Date();
+  let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
+
+  yield backend.incrementDailyCounterFromFieldID(id1, now);
+  yield backend.addDailyDiscreteNumericFromFieldID(id2, 3, now);
+  yield backend.addDailyDiscreteNumericFromFieldID(id2, 4, now);
+  yield backend.addDailyDiscreteNumericFromFieldID(id2, 5, yesterday);
+  yield backend.addDailyDiscreteNumericFromFieldID(id2, 6, yesterday);
+  yield backend.addDailyDiscreteTextFromFieldID(id3, "1", now);
+  yield backend.addDailyDiscreteTextFromFieldID(id3, "2", now);
+  yield backend.addDailyDiscreteTextFromFieldID(id3, "3", yesterday);
+  yield backend.addDailyDiscreteTextFromFieldID(id3, "4", yesterday);
+  yield backend.setDailyLastNumericFromFieldID(id4, 1, now);
+  yield backend.setDailyLastNumericFromFieldID(id4, 2, yesterday);
+  yield backend.setDailyLastTextFromFieldID(id5, "foo", now);
+  yield backend.setDailyLastTextFromFieldID(id5, "bar", yesterday);
+  yield backend.setLastNumericFromFieldID(id6, 42, now);
+  yield backend.setLastTextFromFieldID(id7, "foo", now);
+
+  let fields = yield backend.getMeasurementValues(mID);
+  do_check_eq(Object.keys(fields).length, 2);
+  do_check_true("days" in fields);
+  do_check_true("singular" in fields);
+  do_check_eq(fields.days.size, 2);
+  do_check_true(fields.days.hasDay(now));
+  do_check_true(fields.days.hasDay(yesterday));
+  do_check_eq(fields.days.getDay(now).size, 5);
+  do_check_eq(fields.days.getDay(yesterday).size, 4);
+  do_check_eq(fields.days.getDay(now).get("id3")[0], 1);
+  do_check_eq(fields.days.getDay(yesterday).get("id4"), 2);
+  do_check_eq(fields.singular.size, 2);
+  do_check_eq(fields.singular.get("id6")[1], 42);
+  do_check_eq(fields.singular.get("id7")[1], "foo");
+
+  yield backend.close();
+});
+
--- a/services/metrics/tests/xpcshell/xpcshell.ini
+++ b/services/metrics/tests/xpcshell/xpcshell.ini
@@ -1,9 +1,8 @@
 [DEFAULT]
 head = head.js
 tail =
 
 [test_load_modules.js]
-[test_metrics_collection_result.js]
-[test_metrics_measurement.js]
 [test_metrics_provider.js]
 [test_metrics_collector.js]
+[test_metrics_storage.js]