1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 #ifndef MERGED_COMPARTMENT
9 this.EXPORTED_SYMBOLS = [
14 const {utils: Cu} = Components;
16 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
20 Cu.import("resource://gre/modules/Promise.jsm");
21 Cu.import("resource://gre/modules/Preferences.jsm");
22 Cu.import("resource://gre/modules/Task.jsm");
23 Cu.import("resource://gre/modules/Log.jsm");
24 Cu.import("resource://services-common/utils.js");
29 * Represents a collection of related pieces/fields of data.
31 * This is an abstract base type.
33 * This type provides the primary interface for storing, retrieving, and
36 * Each measurement consists of a set of named fields. Each field is primarily
37 * identified by a string name, which must be unique within the measurement.
39 * Each derived type must define the following properties:
41 * name -- String name of this measurement. This is the primary way
42 * measurements are distinguished within a provider.
44 * version -- Integer version of this measurement. This is a secondary
45 * identifier for a measurement within a provider. The version denotes
46 * the behavior of this measurement and the composition of its fields over
47 * time. When a new field is added or the behavior of an existing field
48 * changes, the version should be incremented. The initial version of a
49 * measurement is typically 1.
51 * fields -- Object defining the fields this measurement holds. Keys in the
52 * object are string field names. Values are objects describing how the
53 * field works. The following properties are recognized:
55 * type -- The string type of this field. This is typically one of the
56 * FIELD_* constants from the Metrics.Storage type.
59 * FUTURE: provide hook points for measurements to supplement with custom
62 this.Measurement = function () {
64 throw new Error("Measurement must have a name.");
68 throw new Error("Measurement must have a version.");
71 if (!Number.isInteger(this.version)) {
72 throw new Error("Measurement's version must be an integer: " + this.version);
76 throw new Error("Measurement must define fields.");
79 for (let [name, info] in Iterator(this.fields)) {
81 throw new Error("Field does not contain metadata: " + name);
85 throw new Error("Field is missing required type property: " + name);
89 this._log = Log.repository.getLogger("Services.Metrics.Measurement." + this.name);
95 this._serializers = {};
96 this._serializers[this.SERIALIZE_JSON] = {
97 singular: this._serializeJSONSingular.bind(this),
98 daily: this._serializeJSONDay.bind(this),
102 Measurement.prototype = Object.freeze({
103 SERIALIZE_JSON: "json",
106 * Obtain a serializer for this measurement.
108 * Implementations should return an object with the following keys:
110 * singular -- Serializer for singular data.
111 * daily -- Serializer for daily data.
113 * Each item is a function that takes a single argument: the data to
114 * serialize. The passed data is a subset of that returned from
115 * this.getValues(). For "singular," data.singular is passed. For "daily",
116 * data.days.get(<day>) is passed.
118 * This function receives a single argument: the serialization format we
119 * are requesting. This is one of the SERIALIZE_* constants on this base type.
121 * For SERIALIZE_JSON, the function should return an object that
122 * JSON.stringify() knows how to handle. This could be an anonymous object or
123 * array or any object with a property named `toJSON` whose value is a
124 * function. The returned object will be added to a larger document
125 * containing the results of all `serialize` calls.
127 * The default implementation knows how to serialize built-in types using
128 * very simple logic. If small encoding size is a goal, the default
129 * implementation may not be suitable. If an unknown field type is
130 * encountered, the default implementation will error.
133 * (string) A SERIALIZE_* constant defining what serialization format
136 serializer: function (format) {
137 if (!(format in this._serializers)) {
138 throw new Error("Don't know how to serialize format: " + format);
141 return this._serializers[format];
145 * Whether this measurement contains the named field.
148 * (string) Name of field.
152 hasField: function (name) {
153 return name in this.fields;
157 * The unique identifier for a named field.
159 * This will throw if the field is not known.
162 * (string) Name of field.
164 fieldID: function (name) {
165 let entry = this._fields[name];
168 throw new Error("Unknown field: " + name);
174 fieldType: function (name) {
175 let entry = this._fields[name];
178 throw new Error("Unknown field: " + name);
184 _configureStorage: function () {
186 for (let [name, info] in Iterator(this.fields)) {
187 if (this.storage.hasFieldFromMeasurement(this.id, name)) {
189 [this.storage.fieldIDFromMeasurement(this.id, name), info.type];
193 missing.push([name, info.type]);
196 if (!missing.length) {
197 return CommonUtils.laterTickResolvingPromise();
200 // We only perform a transaction if we have work to do (to avoid
201 // extra SQLite overhead).
202 return this.storage.enqueueTransaction(function registerFields() {
203 for (let [name, type] of missing) {
204 this._log.debug("Registering field: " + name + " " + type);
205 let id = yield this.storage.registerField(this.id, name, type);
206 this._fields[name] = [id, type];
211 //---------------------------------------------------------------------------
212 // Data Recording Functions
214 // Functions in this section are used to record new values against this
215 // measurement instance.
217 // Generally speaking, these functions will throw if the specified field does
218 // not exist or if the storage function requested is not appropriate for the
219 // type of that field. These functions will also return a promise that will
220 // be resolved when the underlying storage operation has completed.
221 //---------------------------------------------------------------------------
224 * Increment a daily counter field in this measurement by 1.
226 * By default, the counter for the current day will be incremented.
228 * If the field is not known or is not a daily counter, this will throw.
233 * (string) The name of the field whose value to increment.
235 * (Date) Day on which to increment the counter.
237 * (integer) How much to increment by.
240 incrementDailyCounter: function (field, date=new Date(), by=1) {
241 return this.storage.incrementDailyCounterFromFieldID(this.fieldID(field),
246 * Record a new numeric value for a daily discrete numeric field.
249 * (string) The name of the field to append a value to.
251 * (Number) Number to append.
253 * (Date) Day on which to append the value.
257 addDailyDiscreteNumeric: function (field, value, date=new Date()) {
258 return this.storage.addDailyDiscreteNumericFromFieldID(
259 this.fieldID(field), value, date);
263 * Record a new text value for a daily discrete text field.
265 * This is like `addDailyDiscreteNumeric` but for daily discrete text fields.
267 addDailyDiscreteText: function (field, value, date=new Date()) {
268 return this.storage.addDailyDiscreteTextFromFieldID(
269 this.fieldID(field), value, date);
273 * Record the last seen value for a last numeric field.
276 * (string) The name of the field to set the value of.
278 * (Number) The value to set.
280 * (Date) When this value was recorded.
284 setLastNumeric: function (field, value, date=new Date()) {
285 return this.storage.setLastNumericFromFieldID(this.fieldID(field), value,
290 * Record the last seen value for a last text field.
292 * This is like `setLastNumeric` except for last text fields.
294 setLastText: function (field, value, date=new Date()) {
295 return this.storage.setLastTextFromFieldID(this.fieldID(field), value,
300 * Record the most recent value for a daily last numeric field.
303 * (string) The name of a daily last numeric field.
305 * (Number) The value to set.
307 * (Date) Day on which to record the last value.
311 setDailyLastNumeric: function (field, value, date=new Date()) {
312 return this.storage.setDailyLastNumericFromFieldID(this.fieldID(field),
317 * Record the most recent value for a daily last text field.
319 * This is like `setDailyLastNumeric` except for a daily last text field.
321 setDailyLastText: function (field, value, date=new Date()) {
322 return this.storage.setDailyLastTextFromFieldID(this.fieldID(field),
326 //---------------------------------------------------------------------------
327 // End of data recording APIs.
328 //---------------------------------------------------------------------------
331 * Obtain all values stored for this measurement.
333 * The default implementation obtains all known types from storage. If the
334 * measurement provides custom types or stores values somewhere other than
335 * storage, it should define its own implementation.
337 * This returns a promise that resolves to a data structure which is
338 * understood by the measurement's serialize() function.
340 getValues: function () {
341 return this.storage.getMeasurementValues(this.id);
344 deleteLastNumeric: function (field) {
345 return this.storage.deleteLastNumericFromFieldID(this.fieldID(field));
348 deleteLastText: function (field) {
349 return this.storage.deleteLastTextFromFieldID(this.fieldID(field));
353 * This method is used by the default serializers to control whether a field
354 * is included in the output.
356 * There could be legacy fields in storage we no longer care about.
358 * This method is a hook to allow measurements to change this behavior, e.g.,
359 * to implement a dynamic fieldset.
361 * You will also need to override `fieldType`.
363 * @return (boolean) true if the specified field should be included in
366 shouldIncludeField: function (field) {
367 return field in this._fields;
370 _serializeJSONSingular: function (data) {
371 let result = {"_v": this.version};
373 for (let [field, data] of data) {
374 // There could be legacy fields in storage we no longer care about.
375 if (!this.shouldIncludeField(field)) {
379 let type = this.fieldType(field);
382 case this.storage.FIELD_LAST_NUMERIC:
383 case this.storage.FIELD_LAST_TEXT:
384 result[field] = data[1];
387 case this.storage.FIELD_DAILY_COUNTER:
388 case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
389 case this.storage.FIELD_DAILY_DISCRETE_TEXT:
390 case this.storage.FIELD_DAILY_LAST_NUMERIC:
391 case this.storage.FIELD_DAILY_LAST_TEXT:
395 throw new Error("Unknown field type: " + type);
402 _serializeJSONDay: function (data) {
403 let result = {"_v": this.version};
405 for (let [field, data] of data) {
406 if (!this.shouldIncludeField(field)) {
410 let type = this.fieldType(field);
413 case this.storage.FIELD_DAILY_COUNTER:
414 case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
415 case this.storage.FIELD_DAILY_DISCRETE_TEXT:
416 case this.storage.FIELD_DAILY_LAST_NUMERIC:
417 case this.storage.FIELD_DAILY_LAST_TEXT:
418 result[field] = data;
421 case this.storage.FIELD_LAST_NUMERIC:
422 case this.storage.FIELD_LAST_TEXT:
426 throw new Error("Unknown field type: " + type);
436 * An entity that emits data.
438 * A `Provider` consists of a string name (must be globally unique among all
439 * known providers) and a set of `Measurement` instances.
441 * The main role of a `Provider` is to produce metrics data and to store said
442 * data in the storage backend.
444 * Metrics data collection is initiated either by a manager calling a
445 * `collect*` function on `Provider` instances or by the `Provider` registering
446 * to some external event and then reacting whenever they occur.
448 * `Provider` implementations interface directly with a storage backend. For
449 * common stored values (daily counters, daily discrete values, etc),
450 * implementations should interface with storage via the various helper
451 * functions on the `Measurement` instances. For custom stored value types,
452 * implementations will interact directly with the low-level storage APIs.
454 * Because multiple providers exist and could be responding to separate
455 * external events simultaneously and because not all operations performed by
456 * storage can safely be performed in parallel, writing directly to storage at
457 * event time is dangerous. Therefore, interactions with storage must be
458 * deferred until it is safe to perform them.
460 * This typically looks something like:
462 * // This gets called when an external event worthy of recording metrics
463 * // occurs. The function receives a numeric value associated with the event.
464 * function onExternalEvent (value) {
465 * let now = new Date();
466 * let m = this.getMeasurement("foo", 1);
468 * this.enqueueStorageOperation(function storeExternalEvent() {
470 * // We interface with storage via the `Measurement` helper functions.
471 * // These each return a promise that will be resolved when the
472 * // operation finishes. We rely on behavior of storage where operations
473 * // are executed single threaded and sequentially. Therefore, we only
474 * // need to return the final promise.
475 * m.incrementDailyCounter("foo", now);
476 * return m.addDailyDiscreteNumericValue("my_value", value, now);
482 * `Provider` is an abstract base class. Implementations must define a few
486 * The `name` property should be a string defining the provider's name. The
487 * name must be globally unique for the application. The name is used as an
488 * identifier to distinguish providers from each other.
491 * This must be an array of `Measurement`-derived types. Note that elements
492 * in the array are the type functions, not instances. Instances of the
493 * `Measurement` are created at run-time by the `Provider` and are bound
494 * to the provider and to a specific storage backend.
496 this.Provider = function () {
498 throw new Error("Provider must define a name.");
501 if (!Array.isArray(this.measurementTypes)) {
502 throw new Error("Provider must define measurement types.");
505 this._log = Log.repository.getLogger("Services.Metrics.Provider." + this.name);
507 this.measurements = null;
511 Provider.prototype = Object.freeze({
513 * Whether the provider only pulls data from other sources.
515 * If this is true, the provider pulls data from other sources. By contrast,
516 * "push-based" providers subscribe to foreign sources and record/react to
517 * external events as they happen.
519 * Pull-only providers likely aren't instantiated until a data collection
520 * is performed. Thus, implementations cannot rely on a provider instance
521 * always being alive. This is an optimization so provider instances aren't
522 * dead weight while the application is running.
524 * This must be set on the prototype to have an effect.
529 * Obtain a `Measurement` from its name and version.
531 * If the measurement is not found, an Error is thrown.
533 getMeasurement: function (name, version) {
534 if (!Number.isInteger(version)) {
535 throw new Error("getMeasurement expects an integer version. Got: " + version);
538 let m = this.measurements.get([name, version].join(":"));
541 throw new Error("Unknown measurement: " + name + " v" + version);
547 init: function (storage) {
548 if (this.storage !== null) {
549 throw new Error("Provider() not called. Did the sub-type forget to call it?");
553 throw new Error("Provider has already been initialized.");
556 this.measurements = new Map();
557 this.storage = storage;
560 return Task.spawn(function init() {
561 let pre = self.preInit();
562 if (!pre || typeof(pre.then) != "function") {
563 throw new Error("preInit() does not return a promise.");
567 for (let measurementType of self.measurementTypes) {
568 let measurement = new measurementType();
570 measurement.provider = self;
571 measurement.storage = self.storage;
573 let id = yield storage.registerMeasurement(self.name, measurement.name,
574 measurement.version);
578 yield measurement._configureStorage();
580 self.measurements.set([measurement.name, measurement.version].join(":"),
584 let post = self.postInit();
585 if (!post || typeof(post.then) != "function") {
586 throw new Error("postInit() does not return a promise.");
592 shutdown: function () {
593 let promise = this.onShutdown();
595 if (!promise || typeof(promise.then) != "function") {
596 throw new Error("onShutdown implementation does not return a promise.");
603 * Hook point for implementations to perform pre-initialization activity.
605 * This method will be called before measurement registration.
607 * Implementations should return a promise which is resolved when
608 * initialization activities have completed.
610 preInit: function () {
611 return CommonUtils.laterTickResolvingPromise();
615 * Hook point for implementations to perform post-initialization activity.
617 * This method will be called after `preInit` and measurement registration,
618 * but before initialization is finished.
620 * If a `Provider` instance needs to register observers, etc, it should
621 * implement this function.
623 * Implementations should return a promise which is resolved when
624 * initialization activities have completed.
626 postInit: function () {
627 return CommonUtils.laterTickResolvingPromise();
631 * Hook point for shutdown of instances.
633 * This is the opposite of `onInit`. If a `Provider` needs to unregister
634 * observers, etc, this is where it should do it.
636 * Implementations should return a promise which is resolved when
637 * shutdown activities have completed.
639 onShutdown: function () {
640 return CommonUtils.laterTickResolvingPromise();
644 * Collects data that doesn't change during the application's lifetime.
646 * Implementations should return a promise that resolves when all data has
647 * been collected and storage operations have been finished.
651 collectConstantData: function () {
652 return CommonUtils.laterTickResolvingPromise();
656 * Collects data approximately every day.
658 * For long-running applications, this is called approximately every day.
659 * It may or may not be called every time the application is run. It also may
660 * be called more than once per day.
662 * Implementations should return a promise that resolves when all data has
663 * been collected and storage operations have completed.
667 collectDailyData: function () {
668 return CommonUtils.laterTickResolvingPromise();
672 * Queue a deferred storage operation.
674 * Deferred storage operations are the preferred method for providers to
675 * interact with storage. When collected data is to be added to storage,
676 * the provider creates a function that performs the necessary storage
677 * interactions and then passes that function to this function. Pending
678 * storage operations will be executed sequentially by a coordinator.
680 * The passed function should return a promise which will be resolved upon
681 * completion of storage interaction.
683 enqueueStorageOperation: function (func) {
684 return this.storage.enqueueOperation(func);
688 * Obtain persisted provider state.
690 * Provider state consists of key-value pairs of string names and values.
691 * Providers can stuff whatever they want into state. They are encouraged to
692 * store as little as possible for performance reasons.
694 * State is backed by storage and is robust.
696 * These functions do not enqueue on storage automatically, so they should
697 * be guarded by `enqueueStorageOperation` or some other mutex.
700 * (string) The property to retrieve.
702 * @return Promise<string|null> String value on success. null if no state
703 * is available under this key.
705 getState: function (key) {
706 return this.storage.getProviderState(this.name, key);
710 * Set state for this provider.
712 * This is the complementary API for `getState` and obeys the same
713 * storage restrictions.
715 setState: function (key, value) {
716 return this.storage.setProviderState(this.name, key, value);
719 _dateToDays: function (date) {
720 return Math.floor(date.getTime() / MILLISECONDS_PER_DAY);
723 _daysToDate: function (days) {
724 return new Date(days * MILLISECONDS_PER_DAY);