Bug 841074 - Statically declare fields on FHR measurements; r=rnewman

This commit is contained in:
Gregory Szorc 2013-02-21 14:11:54 -08:00
parent 8a3fa24cd0
commit c8cb148cc8
6 changed files with 158 additions and 190 deletions

View File

@ -191,9 +191,9 @@ ProfileMetadataMeasurement.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},
},
};

View File

@ -49,6 +49,11 @@ 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.
*
@ -65,33 +70,22 @@ AppInfoMeasurement.prototype = Object.freeze({
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},
},
});
@ -106,9 +100,8 @@ AppVersionMeasurement.prototype = Object.freeze({
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},
},
});
@ -277,17 +270,15 @@ SysInfoMeasurement.prototype = Object.freeze({
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,
},
});
@ -382,9 +373,8 @@ CurrentSessionMeasurement.prototype = Object.freeze({
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.
@ -431,27 +421,19 @@ PreviousSessionsMeasurement.prototype = Object.freeze({
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,
},
});
@ -540,8 +522,8 @@ ActiveAddonsMeasurement.prototype = Object.freeze({
name: "active",
version: 1,
configureStorage: function () {
return this.registerStorageField("addons", this.storage.FIELD_LAST_TEXT);
fields: {
addons: LAST_TEXT_FIELD,
},
_serializeJSONSingular: function (data) {
@ -568,13 +550,11 @@ AddonCountsMeasurement.prototype = Object.freeze({
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,
},
});
@ -740,9 +720,9 @@ DailyCrashesMeasurement.prototype = Object.freeze({
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,
},
});
@ -907,11 +887,9 @@ PlacesMeasurement.prototype = Object.freeze({
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,
},
});
@ -969,6 +947,31 @@ SearchCountMeasurement.prototype = Object.freeze({
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: [
@ -984,25 +987,6 @@ SearchCountMeasurement.prototype = Object.freeze({
"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 () {

View File

@ -28,26 +28,33 @@ 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.
@ -65,11 +72,25 @@ this.Measurement = function () {
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] = {
@ -81,21 +102,6 @@ this.Measurement = function () {
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.
*
@ -144,7 +150,7 @@ Measurement.prototype = Object.freeze({
* @return bool
*/
hasField: function (name) {
return this._fieldsByName.has(name);
return name in this.fields;
},
/**
@ -156,7 +162,7 @@ Measurement.prototype = Object.freeze({
* (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);
@ -166,7 +172,7 @@ Measurement.prototype = Object.freeze({
},
fieldType: function (name) {
let entry = this._fieldsByName.get(name);
let entry = this._fields[name];
if (!entry) {
throw new Error("Unknown field: " + name);
@ -175,37 +181,15 @@ Measurement.prototype = Object.freeze({
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));
},
//---------------------------------------------------------------------------
@ -352,7 +336,7 @@ Measurement.prototype = Object.freeze({
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;
}
@ -383,7 +367,7 @@ Measurement.prototype = Object.freeze({
let result = {"_v": this.version};
for (let [field, data] of data) {
if (!this._fieldsByName.has(field)) {
if (!(field in this._fields)) {
continue;
}
@ -566,7 +550,7 @@ Provider.prototype = Object.freeze({
measurement.id = id;
yield measurement.configureStorage();
yield measurement._configureStorage();
self.measurements.set([measurement.name, measurement.version].join(":"),
measurement);

View File

@ -27,17 +27,14 @@ DummyMeasurement.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},
},
};

View File

@ -1071,26 +1071,24 @@ MetricsStorageSqliteBackend.prototype = Object.freeze({
}
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);
});
},
@ -2059,3 +2057,8 @@ MetricsStorageSqliteBackend.prototype = Object.freeze({
},
});
// Alias built-in field types to public API.
for (let property of MetricsStorageSqliteBackend.prototype._BUILTIN_TYPES) {
this.MetricsStorageBackend[property] = MetricsStorageSqliteBackend.prototype[property];
}

View File

@ -54,7 +54,7 @@ add_task(function test_init() {
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"));