Bug 841074 - Statically declare fields on FHR measurements; r=rnewman
authorGregory Szorc <gps@mozilla.com>
Thu, 21 Feb 2013 14:11:54 -0800
changeset 122586 5054f997ef77367e94a3cc8f920ba390702a81f2
parent 122585 437c955ff06d87ef8205b04c283aaa939559ab1a
child 122587 6c126d076b0dfec40521a53c6f31579e3911958b
push id24349
push userryanvm@gmail.com
push dateFri, 22 Feb 2013 17:43:12 +0000
treeherdermozilla-central@e36f42046452 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs841074
milestone22.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 841074 - Statically declare fields on FHR measurements; r=rnewman
services/healthreport/profile.jsm
services/healthreport/providers.jsm
services/metrics/dataprovider.jsm
services/metrics/modules-testing/mocks.jsm
services/metrics/storage.jsm
services/metrics/tests/xpcshell/test_metrics_provider.js
--- a/services/healthreport/profile.jsm
+++ b/services/healthreport/profile.jsm
@@ -186,19 +186,19 @@ function ProfileMetadataMeasurement() {
   Metrics.Measurement.call(this);
 }
 ProfileMetadataMeasurement.prototype = {
   __proto__: Metrics.Measurement.prototype,
 
   name: DEFAULT_PROFILE_MEASUREMENT_NAME,
   version: 1,
 
-  configureStorage: function () {
+  fields: {
     // Profile creation date. Number of days since Unix epoch.
-    return this.registerStorageField("profileCreation", this.storage.FIELD_LAST_NUMERIC);
+    profileCreation: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
   },
 };
 
 /**
  * Turn a millisecond timestamp into a day timestamp.
  *
  * @param msec a number of milliseconds since epoch.
  * @return the number of whole days denoted by the input.
--- a/services/healthreport/providers.jsm
+++ b/services/healthreport/providers.jsm
@@ -44,76 +44,69 @@ Cu.import("resource://services-common/ut
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
                                   "resource://gre/modules/UpdateChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesDBUtils",
                                   "resource://gre/modules/PlacesDBUtils.jsm");
 
 
+const LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_LAST_TEXT};
+const DAILY_DISCRETE_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_DISCRETE_NUMERIC};
+const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC};
+const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
+
 /**
  * Represents basic application state.
  *
  * This is roughly a union of nsIXULAppInfo, nsIXULRuntime, with a few extra
  * pieces thrown in.
  */
 function AppInfoMeasurement() {
   Metrics.Measurement.call(this);
 }
 
 AppInfoMeasurement.prototype = Object.freeze({
   __proto__: Metrics.Measurement.prototype,
 
   name: "appinfo",
   version: 1,
 
-  LAST_TEXT_FIELDS: [
-    "vendor",
-    "name",
-    "id",
-    "version",
-    "appBuildID",
-    "platformVersion",
-    "platformBuildID",
-    "os",
-    "xpcomabi",
-    "updateChannel",
-    "distributionID",
-    "distributionVersion",
-    "hotfixVersion",
-    "locale",
-  ],
-
-  configureStorage: function () {
-    let self = this;
-    return Task.spawn(function configureStorage() {
-      for (let field of self.LAST_TEXT_FIELDS) {
-        yield self.registerStorageField(field, self.storage.FIELD_LAST_TEXT);
-      }
-
-      yield self.registerStorageField("isDefaultBrowser",
-                                      self.storage.FIELD_DAILY_LAST_NUMERIC);
-    });
+  fields: {
+    vendor: LAST_TEXT_FIELD,
+    name: LAST_TEXT_FIELD,
+    id: LAST_TEXT_FIELD,
+    version: LAST_TEXT_FIELD,
+    appBuildID: LAST_TEXT_FIELD,
+    platformVersion: LAST_TEXT_FIELD,
+    platformBuildID: LAST_TEXT_FIELD,
+    os: LAST_TEXT_FIELD,
+    xpcomabi: LAST_TEXT_FIELD,
+    updateChannel: LAST_TEXT_FIELD,
+    distributionID: LAST_TEXT_FIELD,
+    distributionVersion: LAST_TEXT_FIELD,
+    hotfixVersion: LAST_TEXT_FIELD,
+    locale: LAST_TEXT_FIELD,
+    isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
   },
 });
 
 
 function AppVersionMeasurement() {
   Metrics.Measurement.call(this);
 }
 
 AppVersionMeasurement.prototype = Object.freeze({
   __proto__: Metrics.Measurement.prototype,
 
   name: "versions",
   version: 1,
 
-  configureStorage: function () {
-    return this.registerStorageField("version",
-                                     this.storage.FIELD_DAILY_DISCRETE_TEXT);
+  fields: {
+    version: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
   },
 });
 
 
 
 this.AppInfoProvider = function AppInfoProvider() {
   Metrics.Provider.call(this);
 
@@ -272,27 +265,25 @@ function SysInfoMeasurement() {
 }
 
 SysInfoMeasurement.prototype = Object.freeze({
   __proto__: Metrics.Measurement.prototype,
 
   name: "sysinfo",
   version: 1,
 
-  configureStorage: function () {
-    return Task.spawn(function configureStorage() {
-      yield this.registerStorageField("cpuCount", this.storage.FIELD_LAST_NUMERIC);
-      yield this.registerStorageField("memoryMB", this.storage.FIELD_LAST_NUMERIC);
-      yield this.registerStorageField("manufacturer", this.storage.FIELD_LAST_TEXT);
-      yield this.registerStorageField("device", this.storage.FIELD_LAST_TEXT);
-      yield this.registerStorageField("hardware", this.storage.FIELD_LAST_TEXT);
-      yield this.registerStorageField("name", this.storage.FIELD_LAST_TEXT);
-      yield this.registerStorageField("version", this.storage.FIELD_LAST_TEXT);
-      yield this.registerStorageField("architecture", this.storage.FIELD_LAST_TEXT);
-    }.bind(this));
+  fields: {
+    cpuCount: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
+    memoryMB: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
+    manufacturer: LAST_TEXT_FIELD,
+    device: LAST_TEXT_FIELD,
+    hardware: LAST_TEXT_FIELD,
+    name: LAST_TEXT_FIELD,
+    version: LAST_TEXT_FIELD,
+    architecture: LAST_TEXT_FIELD,
   },
 });
 
 
 this.SysInfoProvider = function SysInfoProvider() {
   Metrics.Provider.call(this);
 };
 
@@ -377,19 +368,18 @@ function CurrentSessionMeasurement() {
 }
 
 CurrentSessionMeasurement.prototype = Object.freeze({
   __proto__: Metrics.Measurement.prototype,
 
   name: "current",
   version: 3,
 
-  configureStorage: function () {
-    return Promise.resolve();
-  },
+  // Storage is in preferences.
+  fields: {},
 
   /**
    * All data is stored in prefs, so we have a custom implementation.
    */
   getValues: function () {
     let sessions = this.provider.healthReporter.sessionRecorder;
 
     let fields = new Map();
@@ -426,37 +416,29 @@ function PreviousSessionsMeasurement() {
 }
 
 PreviousSessionsMeasurement.prototype = Object.freeze({
   __proto__: Metrics.Measurement.prototype,
 
   name: "previous",
   version: 3,
 
-  DAILY_DISCRETE_NUMERIC_FIELDS: [
+  fields: {
     // Milliseconds of sessions that were properly shut down.
-    "cleanActiveTicks",
-    "cleanTotalTime",
+    cleanActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
+    cleanTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
 
     // Milliseconds of sessions that were not properly shut down.
-    "abortedActiveTicks",
-    "abortedTotalTime",
+    abortedActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
+    abortedTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
 
     // Startup times in milliseconds.
-    "main",
-    "firstPaint",
-    "sessionRestored",
-  ],
-
-  configureStorage: function () {
-    return Task.spawn(function configureStorage() {
-      for (let field of this.DAILY_DISCRETE_NUMERIC_FIELDS) {
-        yield this.registerStorageField(field, this.storage.FIELD_DAILY_DISCRETE_NUMERIC);
-      }
-    }.bind(this));
+    main: DAILY_DISCRETE_NUMERIC_FIELD,
+    firstPaint: DAILY_DISCRETE_NUMERIC_FIELD,
+    sessionRestored: DAILY_DISCRETE_NUMERIC_FIELD,
   },
 });
 
 
 /**
  * Records information about the current browser session.
  *
  * A browser session is defined as an application/process lifetime. We
@@ -535,18 +517,18 @@ function ActiveAddonsMeasurement() {
 }
 
 ActiveAddonsMeasurement.prototype = Object.freeze({
   __proto__: Metrics.Measurement.prototype,
 
   name: "active",
   version: 1,
 
-  configureStorage: function () {
-    return this.registerStorageField("addons", this.storage.FIELD_LAST_TEXT);
+  fields: {
+    addons: LAST_TEXT_FIELD,
   },
 
   _serializeJSONSingular: function (data) {
     if (!data.has("addons")) {
       this._log.warn("Don't have active addons info. Weird.");
       return null;
     }
 
@@ -563,23 +545,21 @@ function AddonCountsMeasurement() {
 }
 
 AddonCountsMeasurement.prototype = Object.freeze({
   __proto__: Metrics.Measurement.prototype,
 
   name: "counts",
   version: 1,
 
-  configureStorage: function () {
-    return Task.spawn(function registerFields() {
-      yield this.registerStorageField("theme", this.storage.FIELD_DAILY_LAST_NUMERIC);
-      yield this.registerStorageField("lwtheme", this.storage.FIELD_DAILY_LAST_NUMERIC);
-      yield this.registerStorageField("plugin", this.storage.FIELD_DAILY_LAST_NUMERIC);
-      yield this.registerStorageField("extension", this.storage.FIELD_DAILY_LAST_NUMERIC);
-    }.bind(this));
+  fields: {
+    theme: DAILY_LAST_NUMERIC_FIELD,
+    lwtheme: DAILY_LAST_NUMERIC_FIELD,
+    plugin: DAILY_LAST_NUMERIC_FIELD,
+    extension: DAILY_LAST_NUMERIC_FIELD,
   },
 });
 
 
 this.AddonsProvider = function () {
   Metrics.Provider.call(this);
 
   this._prefs = new Preferences({defaultBranch: null});
@@ -735,19 +715,19 @@ function DailyCrashesMeasurement() {
 }
 
 DailyCrashesMeasurement.prototype = Object.freeze({
   __proto__: Metrics.Measurement.prototype,
 
   name: "crashes",
   version: 1,
 
-  configureStorage: function () {
-    this.registerStorageField("pending", this.storage.FIELD_DAILY_COUNTER);
-    this.registerStorageField("submitted", this.storage.FIELD_DAILY_COUNTER);
+  fields: {
+    pending: DAILY_COUNTER_FIELD,
+    submitted: DAILY_COUNTER_FIELD,
   },
 });
 
 this.CrashesProvider = function () {
   Metrics.Provider.call(this);
 };
 
 CrashesProvider.prototype = Object.freeze({
@@ -902,21 +882,19 @@ function PlacesMeasurement() {
 }
 
 PlacesMeasurement.prototype = Object.freeze({
   __proto__: Metrics.Measurement.prototype,
 
   name: "places",
   version: 1,
 
-  configureStorage: function () {
-    return Task.spawn(function registerFields() {
-      yield this.registerStorageField("pages", this.storage.FIELD_DAILY_LAST_NUMERIC);
-      yield this.registerStorageField("bookmarks", this.storage.FIELD_DAILY_LAST_NUMERIC);
-    }.bind(this));
+  fields: {
+    pages: DAILY_LAST_NUMERIC_FIELD,
+    bookmarks: DAILY_LAST_NUMERIC_FIELD,
   },
 });
 
 
 /**
  * Collects information about Places.
  */
 this.PlacesProvider = function () {
@@ -964,50 +942,56 @@ function SearchCountMeasurement() {
 }
 
 SearchCountMeasurement.prototype = Object.freeze({
   __proto__: Metrics.Measurement.prototype,
 
   name: "counts",
   version: 1,
 
+  // We only record searches for search engines that have partner agreements
+  // with Mozilla.
+  fields: {
+    "amazon.com.abouthome": DAILY_COUNTER_FIELD,
+    "amazon.com.contextmenu": DAILY_COUNTER_FIELD,
+    "amazon.com.searchbar": DAILY_COUNTER_FIELD,
+    "amazon.com.urlbar": DAILY_COUNTER_FIELD,
+    "bing.abouthome": DAILY_COUNTER_FIELD,
+    "bing.contextmenu": DAILY_COUNTER_FIELD,
+    "bing.searchbar": DAILY_COUNTER_FIELD,
+    "bing.urlbar": DAILY_COUNTER_FIELD,
+    "google.abouthome": DAILY_COUNTER_FIELD,
+    "google.contextmenu": DAILY_COUNTER_FIELD,
+    "google.searchbar": DAILY_COUNTER_FIELD,
+    "google.urlbar": DAILY_COUNTER_FIELD,
+    "yahoo.abouthome": DAILY_COUNTER_FIELD,
+    "yahoo.contextmenu": DAILY_COUNTER_FIELD,
+    "yahoo.searchbar": DAILY_COUNTER_FIELD,
+    "yahoo.urlbar": DAILY_COUNTER_FIELD,
+    "other.abouthome": DAILY_COUNTER_FIELD,
+    "other.contextmenu": DAILY_COUNTER_FIELD,
+    "other.searchbar": DAILY_COUNTER_FIELD,
+    "other.urlbar": DAILY_COUNTER_FIELD,
+  },
+
   // If an engine is removed from this list, it may not be reported any more.
   // Verify side-effects are sane before removing an entry.
   PARTNER_ENGINES: [
     "amazon.com",
     "bing",
     "google",
     "yahoo",
   ],
 
   SOURCES: [
     "abouthome",
     "contextmenu",
     "searchbar",
     "urlbar",
   ],
-
-  configureStorage: function () {
-    // We only record searches for search engines that have partner
-    // agreements with Mozilla.
-    let engines = this.PARTNER_ENGINES.concat("other");
-
-    let promise;
-
-    // While this creates a large number of fields, storage is sparse and there
-    // will be no overhead for fields that aren't used in a given day.
-    for (let engine of engines) {
-      for (let source of this.SOURCES) {
-        promise = this.registerStorageField(engine + "." + source,
-                                            this.storage.FIELD_DAILY_COUNTER);
-      }
-    }
-
-    return promise;
-  },
 });
 
 this.SearchesProvider = function () {
   Metrics.Provider.call(this);
 };
 
 this.SearchesProvider.prototype = Object.freeze({
   __proto__: Metrics.Provider.prototype,
--- a/services/metrics/dataprovider.jsm
+++ b/services/metrics/dataprovider.jsm
@@ -23,36 +23,43 @@ Cu.import("resource://services-common/lo
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-common/utils.js");
 
 
 
 /**
  * Represents a collection of related pieces/fields of data.
  *
- * This is an abstract base type. Providers implement child types that
- * implement core functions such as `registerStorage`.
+ * This is an abstract base type.
  *
  * This type provides the primary interface for storing, retrieving, and
  * serializing data.
  *
- * 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.
- *
  * 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.
  *
- * 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_*`.
+ * Each derived type must define the following properties:
+ *
+ *   name -- String name of this measurement. This is the primary way
+ *     measurements are distinguished within a provider.
+ *
+ *   version -- Integer version of this measurement. This is a secondary
+ *     identifier for a measurement within a provider. The version denotes
+ *     the behavior of this measurement and the composition of its fields over
+ *     time. When a new field is added or the behavior of an existing field
+ *     changes, the version should be incremented. The initial version of a
+ *     measurement is typically 1.
+ *
+ *   fields -- Object defining the fields this measurement holds. Keys in the
+ *     object are string field names. Values are objects describing how the
+ *     field works. The following properties are recognized:
+ *
+ *       type -- The string type of this field. This is typically one of the
+ *         FIELD_* constants from the Metrics.Storage type.
+ *
  *
  * FUTURE: provide hook points for measurements to supplement with custom
  * storage needs.
  */
 this.Measurement = function () {
   if (!this.name) {
     throw new Error("Measurement must have a name.");
   }
@@ -60,48 +67,47 @@ this.Measurement = function () {
   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 (!this.fields) {
+    throw new Error("Measurement must define fields.");
+  }
+
+  for (let [name, info] in Iterator(this.fields)) {
+    if (!info) {
+      throw new Error("Field does not contain metadata: " + name);
+    }
+
+    if (!info.type) {
+      throw new Error("Field is missing required type property: " + name);
+    }
+  }
+
   this._log = Log4Moz.repository.getLogger("Services.Metrics.Measurement." + this.name);
 
   this.id = null;
   this.storage = null;
-  this._fieldsByName = new Map();
+  this._fields = {};
 
   this._serializers = {};
   this._serializers[this.SERIALIZE_JSON] = {
     singular: this._serializeJSONSingular.bind(this),
     daily: this._serializeJSONDay.bind(this),
   };
 }
 
 Measurement.prototype = Object.freeze({
   SERIALIZE_JSON: "json",
 
   /**
-   * Configures the storage backend so that it can store this measurement.
-   *
-   * Implementations must return a promise which is resolved when storage has
-   * been configured.
-   *
-   * Most implementations will typically call into this.registerStorageField()
-   * to configure fields in storage.
-   *
-   * FUTURE: Provide method for upgrading from older measurement versions.
-   */
-  configureStorage: function () {
-    throw new Error("configureStorage() must be implemented.");
-  },
-
-  /**
    * 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.
    *
    * Each item is a function that takes a single argument: the data to
@@ -139,78 +145,56 @@ Measurement.prototype = Object.freeze({
    * Whether this measurement contains the named field.
    *
    * @param name
    *        (string) Name of field.
    *
    * @return bool
    */
   hasField: function (name) {
-    return this._fieldsByName.has(name);
+    return name in this.fields;
   },
 
   /**
    * The unique identifier for a named field.
    *
    * This will throw if the field is not known.
    *
    * @param name
    *        (string) Name of field.
    */
   fieldID: function (name) {
-    let entry = this._fieldsByName.get(name);
+    let entry = this._fields[name];
 
     if (!entry) {
       throw new Error("Unknown field: " + name);
     }
 
     return entry[0];
   },
 
   fieldType: function (name) {
-    let entry = this._fieldsByName.get(name);
+    let entry = this._fields[name];
 
     if (!entry) {
       throw new Error("Unknown field: " + name);
     }
 
     return entry[1];
   },
 
-  /**
-   * Register a named field with storage that's attached to this measurement.
-   *
-   * 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).
-   */
-  registerStorageField: function (name, type) {
-    this._log.debug("Registering field: " + name + " " + type);
+  _configureStorage: function () {
+    return Task.spawn(function configureFields() {
+      for (let [name, info] in Iterator(this.fields)) {
+        this._log.debug("Registering field: " + name + " " + info.type);
 
-    let deferred = Promise.defer();
-
-    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;
+        let id = yield this.storage.registerField(this.id, name, info.type);
+        this._fields[name] = [id, info.type];
+      }
+    }.bind(this));
   },
 
   //---------------------------------------------------------------------------
   // Data Recording Functions
   //
   // Functions in this section are used to record new values against this
   // measurement instance.
   //
@@ -347,17 +331,17 @@ Measurement.prototype = Object.freeze({
     return this.storage.deleteLastTextFromFieldID(this.fieldID(field));
   },
 
   _serializeJSONSingular: function (data) {
     let result = {"_v": this.version};
 
     for (let [field, data] of data) {
       // There could be legacy fields in storage we no longer care about.
-      if (!this._fieldsByName.has(field)) {
+      if (!(field in this._fields)) {
         continue;
       }
 
       let type = this.fieldType(field);
 
       switch (type) {
         case this.storage.FIELD_LAST_NUMERIC:
         case this.storage.FIELD_LAST_TEXT:
@@ -378,17 +362,17 @@ Measurement.prototype = Object.freeze({
 
     return result;
   },
 
   _serializeJSONDay: function (data) {
     let result = {"_v": this.version};
 
     for (let [field, data] of data) {
-      if (!this._fieldsByName.has(field)) {
+      if (!(field in this._fields)) {
         continue;
       }
 
       let type = this.fieldType(field);
 
       switch (type) {
         case this.storage.FIELD_DAILY_COUNTER:
         case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
@@ -561,17 +545,17 @@ Provider.prototype = Object.freeze({
         measurement.provider = self;
         measurement.storage = self.storage;
 
         let id = yield storage.registerMeasurement(self.name, measurement.name,
                                                    measurement.version);
 
         measurement.id = id;
 
-        yield measurement.configureStorage();
+        yield measurement._configureStorage();
 
         self.measurements.set([measurement.name, measurement.version].join(":"),
                               measurement);
       }
 
       let promise = self.onInit();
 
       if (!promise || typeof(promise.then) != "function") {
--- a/services/metrics/modules-testing/mocks.jsm
+++ b/services/metrics/modules-testing/mocks.jsm
@@ -22,27 +22,24 @@ this.DummyMeasurement = function DummyMe
   Metrics.Measurement.call(this);
 }
 
 DummyMeasurement.prototype = {
   __proto__: Metrics.Measurement.prototype,
 
   version: 1,
 
-  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);
-    });
+  fields: {
+    "daily-counter": {type: Metrics.Storage.FIELD_DAILY_COUNTER},
+    "daily-discrete-numeric": {type: Metrics.Storage.FIELD_DAILY_DISCRETE_NUMERIC},
+    "daily-discrete-text": {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
+    "daily-last-numeric": {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
+    "daily-last-text": {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT},
+    "last-numeric": {type: Metrics.Storage.FIELD_LAST_NUMERIC},
+    "last-text": {type: Metrics.Storage.FIELD_LAST_TEXT},
   },
 };
 
 
 this.DummyProvider = function DummyProvider(name="DummyProvider") {
   this.name = name;
 
   this.measurementTypes = [DummyMeasurement];
--- a/services/metrics/storage.jsm
+++ b/services/metrics/storage.jsm
@@ -1066,36 +1066,34 @@ MetricsStorageSqliteBackend.prototype = 
       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,
-        };
+    return Task.spawn(function createField() {
+      let params = {
+        measurement_id: measurementID,
+        field: field,
+        value_type: typeID,
+      };
 
-        yield self._connection.executeCached(SQL.addField, params);
+      yield self._connection.executeCached(SQL.addField, params);
 
-        let rows = yield self._connection.executeCached(SQL.getFieldID, params);
+      let rows = yield self._connection.executeCached(SQL.getFieldID, params);
 
-        let fieldID = rows[0].getResultByIndex(0);
+      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);
+      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);
-      });
+      throw new Task.Result(fieldID);
     });
   },
 
   /**
    * Initializes this instance with the database.
    *
    * This performs 2 major roles:
    *
@@ -2054,8 +2052,13 @@ MetricsStorageSqliteBackend.prototype = 
     }, function onError(error) {
       deferred.reject(error);
     });
 
     return deferred.promise;
   },
 });
 
+// Alias built-in field types to public API.
+for (let property of MetricsStorageSqliteBackend.prototype._BUILTIN_TYPES) {
+  this.MetricsStorageBackend[property] = MetricsStorageSqliteBackend.prototype[property];
+}
+
--- a/services/metrics/tests/xpcshell/test_metrics_provider.js
+++ b/services/metrics/tests/xpcshell/test_metrics_provider.js
@@ -49,17 +49,17 @@ add_task(function test_init() {
   let provider = new DummyProvider();
   let storage = yield Metrics.Storage("init");
 
   yield provider.init(storage);
 
   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_eq(Object.keys(m._fields).length, 7);
   do_check_true(m.hasField("daily-counter"));
   do_check_false(m.hasField("does-not-exist"));
 
   yield storage.close();
 });
 
 add_test(function test_prefs_integration() {
   let branch = "testing.prefs_integration.";