Bug 812608 - Part 1: Persistent storage backend for metrics data; r=rnewman

This commit is contained in:
Gregory Szorc 2013-01-06 12:13:19 -08:00
Родитель 739e2663f3
Коммит 9a4c5c6720
14 изменённых файлов: 3656 добавлений и 1005 удалений

Просмотреть файл

@ -12,12 +12,17 @@ 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)

Просмотреть файл

@ -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,
};

Просмотреть файл

@ -4,11 +4,12 @@
"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");
@ -17,44 +18,61 @@ 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 = new Map();
this._storage = storage;
this._providers = [];
this.collectionResults = new Map();
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));
},
/**
@ -63,55 +81,41 @@ MetricsCollector.prototype = {
* 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);
@ -122,8 +126,11 @@ MetricsCollector.prototype = {
*
* 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);
}
@ -131,36 +138,24 @@ MetricsCollector.prototype = {
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);
});

Просмотреть файл

@ -5,489 +5,531 @@
"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.
* Represents a collection of related pieces/fields of data.
*
* This is how data is recorded and represented. Each instance of this type
* represents a related set of data.
* This is an abstract base type. Providers implement child types that
* implement core functions such as `registerStorage`.
*
* Each data set has some basic metadata associated with it. This includes a
* name and version.
* This type provides the primary interface for storing, retrieving, and
* serializing 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.
* 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.
*
* Data is added to instances by calling `setValue()`. Values are validated
* against the schema at add time.
* 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.
*
* Field Specification
* ===================
* 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_*`.
*
* 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:
*
* 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.
*
* @param name
* (string) Name of this data set.
* @param version
* (Number) Integer version of the data in this set.
* 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.");
this.Measurement = function () {
if (!this.name) {
throw new Error("Measurement must have a name.");
}
if (!name) {
throw new Error("Must define a name for this measurement.");
if (!this.version) {
throw new Error("Measurement must have a version.");
}
if (!version) {
throw new Error("Must define a version for this measurement.");
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.name = name;
this.version = version;
this.id = null;
this.storage = null;
this._fieldsByName = new Map();
this.values = new Map();
}
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);
}
},
},
/**
* A string field.
*
* Values must be valid UTF-8 strings.
*/
TYPE_STRING: {
validate: function validate(value) {
if (typeof(value) != "string") {
throw new Error("STRING field expects a string. Got " + typeof(value));
}
},
},
/**
* Set the value of a field.
*
* This is ultimately how fields are set. All field sets should go through
* this function.
*
* Values are validated when they are set. If the value passed does not
* validate against the field's specification, an Error will be thrown.
*
* @param name
* (string) The name of the field whose value to set.
* @param value
* The value to set the field to.
*/
setValue: function setValue(name, value) {
if (!this.fields[name]) {
throw new Error("Attempting to set unknown field: " + name);
}
let type = this.fields[name].type;
if (!(type in this)) {
throw new Error("Unknown field type: " + type);
}
this[type].validate(value);
this.values.set(name, value);
},
/**
* 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);
},
/**
* Validate that this instance is in conformance with the specification.
*
* This ensures all required fields are present. Field value validation
* occurs when individual fields are set.
*/
validate: function validate() {
for (let field in this.fields) {
let spec = this.fields[field];
if (!spec.optional && !(field in this.values)) {
throw new Error("Required field not defined: " + field);
}
}
},
toJSON: function toJSON() {
let fields = {};
for (let [k, v] of this.values) {
fields[k] = v;
}
return {
name: this.name,
version: this.version,
fields: fields,
};
},
};
Object.freeze(MetricsMeasurement.prototype);
/**
* 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.");
}
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;
}
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;
},
/**
* Create a new `MetricsCollectionResult` tied to this provider.
*/
createResult: function createResult() {
return new MetricsCollectionResult(this.name);
},
};
Object.freeze(MetricsProvider.prototype);
/**
* Holds the result of metrics collection.
*
* This is the type eventually returned by the MetricsProvider.collect*
* functions. It holds all results and any state/errors that occurred while
* collecting.
*
* This type is essentially a container for `MetricsMeasurement` instances that
* provides some smarts useful for capturing state.
*
* 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.
*
* Next, they should define the `populate` property to a function that
* populates the instance.
*
* The `populate` function implementation should add empty `MetricsMeasurement`
* instances to the result via `addMeasurement`. Then, it should populate these
* measurements via `setValue`.
*
* It is preferred to populate via this type instead of directly on
* `MetricsMeasurement` instances so errors with data population can be
* captured and reported.
*
* Once population has finished, `finish()` must be called.
*
* @param name
* (string) The name of the provider this result came from.
*/
this.MetricsCollectionResult = function MetricsCollectionResult(name) {
if (!name || typeof(name) != "string") {
throw new Error("Must provide name argument to MetricsCollectionResult.");
}
this._log = Log4Moz.repository.getLogger("Services.Metrics.MetricsCollectionResult");
this.name = 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._serializers = {};
this._serializers[this.SERIALIZE_JSON] = {
singular: this._serializeJSONSingular.bind(this),
daily: this._serializeJSONDay.bind(this),
};
this._deferred = Promise.defer();
}
MetricsCollectionResult.prototype = {
/**
* The Set of `MetricsMeasurement` names currently missing from this result.
*/
get missingMeasurements() {
let missing = new Set();
Measurement.prototype = Object.freeze({
SERIALIZE_JSON: "json",
for (let name of this.expectedMeasurements) {
if (this.measurements.has(name)) {
/**
* 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
* 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.
*
* This function receives a single argument: the serialization format we
* are requesting. This is one of the SERIALIZE_* constants on this base type.
*
* 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.
*/
serializer: function (format) {
if (!(format in this._serializers)) {
throw new Error("Don't know how to serialize format: " + format);
}
return this._serializers[format];
},
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);
}
return entry[0];
},
fieldType: function (name) {
let entry = this._fieldsByName.get(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);
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;
},
incrementDailyCounter: function (field, date=new Date()) {
return this.storage.incrementDailyCounterFromFieldID(this.fieldID(field),
date);
},
addDailyDiscreteNumeric: function (field, value, date=new Date()) {
return this.storage.addDailyDiscreteNumericFromFieldID(
this.fieldID(field), value, date);
},
addDailyDiscreteText: function (field, value, date=new Date()) {
return this.storage.addDailyDiscreteTextFromFieldID(
this.fieldID(field), value, date);
},
setLastNumeric: function (field, value, date=new Date()) {
return this.storage.setLastNumericFromFieldID(this.fieldID(field), value,
date);
},
setLastText: function (field, value, date=new Date()) {
return this.storage.setLastTextFromFieldID(this.fieldID(field), value,
date);
},
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);
},
/**
* 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.
*/
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;
}
missing.add(name);
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);
}
}
return missing;
return result;
},
/**
* 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);
_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;
},
});
/**
* 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.
*
* 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.
*
* `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.
*
* 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:
*
* // 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() {
*
* // 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));
*
* }
*
*
* `Provider` is an abstract base class. Implementations must define a few
* properties:
*
* 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.
*
* 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.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.Provider." + this.name);
this.measurements = null;
this.storage = null;
}
Provider.prototype = Object.freeze({
/**
* Add a `MetricsMeasurement` to this result.
* Obtain a `Measurement` from its name and version.
*
* If the measurement is not found, an Error is thrown.
*/
addMeasurement: function addMeasurement(data) {
if (!(data instanceof MetricsMeasurement)) {
throw new Error("addMeasurement expects a MetricsMeasurement instance.");
getMeasurement: function (name, version) {
if (!Number.isInteger(version)) {
throw new Error("getMeasurement expects an integer version. Got: " + version);
}
if (!this.expectedMeasurements.has(data.name)) {
throw new Error("Not expecting this measurement: " + data.name);
}
let m = this.measurements.get([name, version].join(":"));
if (this.measurements.has(data.name)) {
throw new Error("Measurement of this name already present: " + data.name);
}
this.measurements.set(data.name, data);
},
/**
* 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.
*
* @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.
*
* @return bool
* Whether the assignment was successful.
*/
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);
throw new Error("Unknown measurement: " + name + " v" + version);
}
try {
m.setValue(field, value);
return true;
} catch (ex) {
this.addError(ex);
return m;
},
if (rethrow) {
throw ex;
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.");
}
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);
measurement.id = id;
yield measurement.configureStorage();
self.measurements.set([measurement.name, measurement.version].join(":"),
measurement);
}
return false;
}
},
let promise = self.onInit();
/**
* Record an error that was encountered when populating this result.
*/
addError: function addError(error) {
this.errors.push(error);
},
/**
* Aggregate another MetricsCollectionResult into this one.
*
* Instances can only be aggregated together if they belong to the same
* provider (they have the same name).
*/
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);
if (!promise || typeof(promise.then) != "function") {
throw new Error("onInit() does not return a promise.");
}
this.measurements.set(name, m);
}
this.errors = this.errors.concat(other.errors);
yield promise;
});
},
toJSON: function toJSON() {
let o = {
measurements: {},
missing: [],
errors: [],
};
shutdown: function () {
let promise = this.onShutdown();
for (let [name, value] of this.measurements) {
o.measurements[name] = value;
if (!promise || typeof(promise.then) != "function") {
throw new Error("onShutdown implementation does not return a promise.");
}
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;
return promise;
},
/**
* Signal that population of the result has finished.
* Hook point for implementations to perform initialization activity.
*
* This will resolve the internal promise.
* If a `Provider` instance needs to register observers, etc, it should
* implement this function.
*
* Implementations should return a promise which is resolved when
* initialization activities have completed.
*/
finish: function finish() {
this._deferred.resolve(this);
onInit: function () {
return Promise.resolve();
},
/**
* Chain deferred behavior until after the result has finished population.
* Hook point for shutdown of instances.
*
* This is a wrapped around the internal promise's `then`.
* This is the opposite of `onInit`. If a `Provider` needs to unregister
* observers, etc, this is where it should do it.
*
* We can't call this "then" because the core promise library will get
* confused.
* Implementations should return a promise which is resolved when
* shutdown activities have completed.
*/
onFinished: function onFinished(onFulfill, onError) {
return this._deferred.promise.then(onFulfill, onError);
onShutdown: function () {
return Promise.resolve();
},
};
Object.freeze(MetricsCollectionResult.prototype);
/**
* 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();
},
/**
* Queue a deferred storage operation.
*
* 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.
*/
enqueueStorageOperation: function (func) {
return this.storage.enqueueOperation(func);
},
getState: function (key) {
let name = this.name;
let storage = this.storage;
return storage.enqueueOperation(function get() {
return storage.getProviderState(name, key);
});
},
setState: function (key, value) {
let name = this.name;
let storage = this.storage;
return storage.enqueueOperation(function set() {
return storage.setProviderState(name, key, value);
});
},
_dateToDays: function (date) {
return Math.floor(date.getTime() / MILLISECONDS_PER_DAY);
},
_daysToDate: function (days) {
return new Date(days * MILLISECONDS_PER_DAY);
},
});

Просмотреть файл

@ -11,64 +11,79 @@ this.EXPORTED_SYMBOLS = [
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,
fields: {
"string": {
type: "TYPE_STRING",
},
version: 1,
"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;
}
DummyProvider.prototype = {
__proto__: MetricsProvider.prototype,
collectConstantMeasurements: function collectConstantMeasurements() {
this.havePushedMeasurements = true;
}
DummyProvider.prototype = {
__proto__: Metrics.Provider.prototype,
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();
},
};

2028
services/metrics/storage.jsm Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -4,10 +4,12 @@
"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);

Просмотреть файл

@ -6,6 +6,7 @@
const modules = [
"collector.jsm",
"dataprovider.jsm",
"storage.jsm",
];
const test_modules = [
@ -22,5 +23,7 @@ function run_test() {
let resource = "resource://testing-common/services/metrics/" + m;
Components.utils.import(resource, {});
}
Components.utils.import("resource://gre/modules/Metrics.jsm", {});
}

Просмотреть файл

@ -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();
});

Просмотреть файл

@ -5,158 +5,123 @@
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);
provider.throwDuringCollectConstantData = "Fake error during collect";
yield 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);
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);
run_next_test();
});
yield storage.close();
});
add_test(function test_collect_constant_populate_throws() {
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();
provider.throwDuringConstantPopulate = "Fake error during constant populate";
collector.registerProvider(provider);
yield 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();
let result = collector.collectionResults.get("DummyProvider");
do_check_eq(result.errors.length, 1);
do_check_eq(result.errors[0].message, provider.throwDuringConstantPopulate);
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);
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_onetime() {
let storage = yield Metrics.Storage("collect_constant_onetime");
let collector = new Metrics.Collector(storage);
let provider = new DummyProvider();
collector.registerProvider(provider);
yield collector.registerProvider(provider);
collector.collectConstantMeasurements().then(function onResult() {
do_check_eq(provider.collectConstantCount, 1);
yield collector.collectConstantData();
do_check_eq(provider.collectConstantCount, 1);
collector.collectConstantMeasurements().then(function onResult() {
do_check_eq(provider.collectConstantCount, 1);
yield collector.collectConstantData();
do_check_eq(provider.collectConstantCount, 1);
run_next_test();
});
});
yield storage.close();
});
add_test(function test_collect_multiple() {
let collector = new MetricsCollector();
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();
});
});
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();
});
yield storage.close();
});

Просмотреть файл

@ -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();
});

Просмотреть файл

@ -5,22 +5,36 @@
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 {
@ -30,35 +44,231 @@ add_test(function test_constructor() {
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");
for (let property in MetricsProvider.prototype) {
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_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 result = provider.collectConstantMeasurements();
do_check_true(result instanceof MetricsCollectionResult);
result.populate(result);
let now = new Date();
let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
result.onFinished(function onResult(res2) {
do_check_eq(result, res2);
// 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);
let m = result.measurements.get("DummyMeasurement");
do_check_eq(m.getValue("uint32"), 24);
count = yield provider.storage.getDailyCounterCountFromFieldID(counterID, yesterday);
do_check_eq(count, 1);
run_next_test();
});
// 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);
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");
// 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");
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());
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();
});

Просмотреть файл

@ -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();
});

Просмотреть файл

@ -3,7 +3,6 @@ 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]