services/metrics/dataprovider.jsm

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 #ifndef MERGED_COMPARTMENT
michael@0 8
michael@0 9 this.EXPORTED_SYMBOLS = [
michael@0 10 "Measurement",
michael@0 11 "Provider",
michael@0 12 ];
michael@0 13
michael@0 14 const {utils: Cu} = Components;
michael@0 15
michael@0 16 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
michael@0 17
michael@0 18 #endif
michael@0 19
michael@0 20 Cu.import("resource://gre/modules/Promise.jsm");
michael@0 21 Cu.import("resource://gre/modules/Preferences.jsm");
michael@0 22 Cu.import("resource://gre/modules/Task.jsm");
michael@0 23 Cu.import("resource://gre/modules/Log.jsm");
michael@0 24 Cu.import("resource://services-common/utils.js");
michael@0 25
michael@0 26
michael@0 27
michael@0 28 /**
michael@0 29 * Represents a collection of related pieces/fields of data.
michael@0 30 *
michael@0 31 * This is an abstract base type.
michael@0 32 *
michael@0 33 * This type provides the primary interface for storing, retrieving, and
michael@0 34 * serializing data.
michael@0 35 *
michael@0 36 * Each measurement consists of a set of named fields. Each field is primarily
michael@0 37 * identified by a string name, which must be unique within the measurement.
michael@0 38 *
michael@0 39 * Each derived type must define the following properties:
michael@0 40 *
michael@0 41 * name -- String name of this measurement. This is the primary way
michael@0 42 * measurements are distinguished within a provider.
michael@0 43 *
michael@0 44 * version -- Integer version of this measurement. This is a secondary
michael@0 45 * identifier for a measurement within a provider. The version denotes
michael@0 46 * the behavior of this measurement and the composition of its fields over
michael@0 47 * time. When a new field is added or the behavior of an existing field
michael@0 48 * changes, the version should be incremented. The initial version of a
michael@0 49 * measurement is typically 1.
michael@0 50 *
michael@0 51 * fields -- Object defining the fields this measurement holds. Keys in the
michael@0 52 * object are string field names. Values are objects describing how the
michael@0 53 * field works. The following properties are recognized:
michael@0 54 *
michael@0 55 * type -- The string type of this field. This is typically one of the
michael@0 56 * FIELD_* constants from the Metrics.Storage type.
michael@0 57 *
michael@0 58 *
michael@0 59 * FUTURE: provide hook points for measurements to supplement with custom
michael@0 60 * storage needs.
michael@0 61 */
michael@0 62 this.Measurement = function () {
michael@0 63 if (!this.name) {
michael@0 64 throw new Error("Measurement must have a name.");
michael@0 65 }
michael@0 66
michael@0 67 if (!this.version) {
michael@0 68 throw new Error("Measurement must have a version.");
michael@0 69 }
michael@0 70
michael@0 71 if (!Number.isInteger(this.version)) {
michael@0 72 throw new Error("Measurement's version must be an integer: " + this.version);
michael@0 73 }
michael@0 74
michael@0 75 if (!this.fields) {
michael@0 76 throw new Error("Measurement must define fields.");
michael@0 77 }
michael@0 78
michael@0 79 for (let [name, info] in Iterator(this.fields)) {
michael@0 80 if (!info) {
michael@0 81 throw new Error("Field does not contain metadata: " + name);
michael@0 82 }
michael@0 83
michael@0 84 if (!info.type) {
michael@0 85 throw new Error("Field is missing required type property: " + name);
michael@0 86 }
michael@0 87 }
michael@0 88
michael@0 89 this._log = Log.repository.getLogger("Services.Metrics.Measurement." + this.name);
michael@0 90
michael@0 91 this.id = null;
michael@0 92 this.storage = null;
michael@0 93 this._fields = {};
michael@0 94
michael@0 95 this._serializers = {};
michael@0 96 this._serializers[this.SERIALIZE_JSON] = {
michael@0 97 singular: this._serializeJSONSingular.bind(this),
michael@0 98 daily: this._serializeJSONDay.bind(this),
michael@0 99 };
michael@0 100 }
michael@0 101
michael@0 102 Measurement.prototype = Object.freeze({
michael@0 103 SERIALIZE_JSON: "json",
michael@0 104
michael@0 105 /**
michael@0 106 * Obtain a serializer for this measurement.
michael@0 107 *
michael@0 108 * Implementations should return an object with the following keys:
michael@0 109 *
michael@0 110 * singular -- Serializer for singular data.
michael@0 111 * daily -- Serializer for daily data.
michael@0 112 *
michael@0 113 * Each item is a function that takes a single argument: the data to
michael@0 114 * serialize. The passed data is a subset of that returned from
michael@0 115 * this.getValues(). For "singular," data.singular is passed. For "daily",
michael@0 116 * data.days.get(<day>) is passed.
michael@0 117 *
michael@0 118 * This function receives a single argument: the serialization format we
michael@0 119 * are requesting. This is one of the SERIALIZE_* constants on this base type.
michael@0 120 *
michael@0 121 * For SERIALIZE_JSON, the function should return an object that
michael@0 122 * JSON.stringify() knows how to handle. This could be an anonymous object or
michael@0 123 * array or any object with a property named `toJSON` whose value is a
michael@0 124 * function. The returned object will be added to a larger document
michael@0 125 * containing the results of all `serialize` calls.
michael@0 126 *
michael@0 127 * The default implementation knows how to serialize built-in types using
michael@0 128 * very simple logic. If small encoding size is a goal, the default
michael@0 129 * implementation may not be suitable. If an unknown field type is
michael@0 130 * encountered, the default implementation will error.
michael@0 131 *
michael@0 132 * @param format
michael@0 133 * (string) A SERIALIZE_* constant defining what serialization format
michael@0 134 * to use.
michael@0 135 */
michael@0 136 serializer: function (format) {
michael@0 137 if (!(format in this._serializers)) {
michael@0 138 throw new Error("Don't know how to serialize format: " + format);
michael@0 139 }
michael@0 140
michael@0 141 return this._serializers[format];
michael@0 142 },
michael@0 143
michael@0 144 /**
michael@0 145 * Whether this measurement contains the named field.
michael@0 146 *
michael@0 147 * @param name
michael@0 148 * (string) Name of field.
michael@0 149 *
michael@0 150 * @return bool
michael@0 151 */
michael@0 152 hasField: function (name) {
michael@0 153 return name in this.fields;
michael@0 154 },
michael@0 155
michael@0 156 /**
michael@0 157 * The unique identifier for a named field.
michael@0 158 *
michael@0 159 * This will throw if the field is not known.
michael@0 160 *
michael@0 161 * @param name
michael@0 162 * (string) Name of field.
michael@0 163 */
michael@0 164 fieldID: function (name) {
michael@0 165 let entry = this._fields[name];
michael@0 166
michael@0 167 if (!entry) {
michael@0 168 throw new Error("Unknown field: " + name);
michael@0 169 }
michael@0 170
michael@0 171 return entry[0];
michael@0 172 },
michael@0 173
michael@0 174 fieldType: function (name) {
michael@0 175 let entry = this._fields[name];
michael@0 176
michael@0 177 if (!entry) {
michael@0 178 throw new Error("Unknown field: " + name);
michael@0 179 }
michael@0 180
michael@0 181 return entry[1];
michael@0 182 },
michael@0 183
michael@0 184 _configureStorage: function () {
michael@0 185 let missing = [];
michael@0 186 for (let [name, info] in Iterator(this.fields)) {
michael@0 187 if (this.storage.hasFieldFromMeasurement(this.id, name)) {
michael@0 188 this._fields[name] =
michael@0 189 [this.storage.fieldIDFromMeasurement(this.id, name), info.type];
michael@0 190 continue;
michael@0 191 }
michael@0 192
michael@0 193 missing.push([name, info.type]);
michael@0 194 }
michael@0 195
michael@0 196 if (!missing.length) {
michael@0 197 return CommonUtils.laterTickResolvingPromise();
michael@0 198 }
michael@0 199
michael@0 200 // We only perform a transaction if we have work to do (to avoid
michael@0 201 // extra SQLite overhead).
michael@0 202 return this.storage.enqueueTransaction(function registerFields() {
michael@0 203 for (let [name, type] of missing) {
michael@0 204 this._log.debug("Registering field: " + name + " " + type);
michael@0 205 let id = yield this.storage.registerField(this.id, name, type);
michael@0 206 this._fields[name] = [id, type];
michael@0 207 }
michael@0 208 }.bind(this));
michael@0 209 },
michael@0 210
michael@0 211 //---------------------------------------------------------------------------
michael@0 212 // Data Recording Functions
michael@0 213 //
michael@0 214 // Functions in this section are used to record new values against this
michael@0 215 // measurement instance.
michael@0 216 //
michael@0 217 // Generally speaking, these functions will throw if the specified field does
michael@0 218 // not exist or if the storage function requested is not appropriate for the
michael@0 219 // type of that field. These functions will also return a promise that will
michael@0 220 // be resolved when the underlying storage operation has completed.
michael@0 221 //---------------------------------------------------------------------------
michael@0 222
michael@0 223 /**
michael@0 224 * Increment a daily counter field in this measurement by 1.
michael@0 225 *
michael@0 226 * By default, the counter for the current day will be incremented.
michael@0 227 *
michael@0 228 * If the field is not known or is not a daily counter, this will throw.
michael@0 229 *
michael@0 230 *
michael@0 231 *
michael@0 232 * @param field
michael@0 233 * (string) The name of the field whose value to increment.
michael@0 234 * @param date
michael@0 235 * (Date) Day on which to increment the counter.
michael@0 236 * @param by
michael@0 237 * (integer) How much to increment by.
michael@0 238 * @return Promise<>
michael@0 239 */
michael@0 240 incrementDailyCounter: function (field, date=new Date(), by=1) {
michael@0 241 return this.storage.incrementDailyCounterFromFieldID(this.fieldID(field),
michael@0 242 date, by);
michael@0 243 },
michael@0 244
michael@0 245 /**
michael@0 246 * Record a new numeric value for a daily discrete numeric field.
michael@0 247 *
michael@0 248 * @param field
michael@0 249 * (string) The name of the field to append a value to.
michael@0 250 * @param value
michael@0 251 * (Number) Number to append.
michael@0 252 * @param date
michael@0 253 * (Date) Day on which to append the value.
michael@0 254 *
michael@0 255 * @return Promise<>
michael@0 256 */
michael@0 257 addDailyDiscreteNumeric: function (field, value, date=new Date()) {
michael@0 258 return this.storage.addDailyDiscreteNumericFromFieldID(
michael@0 259 this.fieldID(field), value, date);
michael@0 260 },
michael@0 261
michael@0 262 /**
michael@0 263 * Record a new text value for a daily discrete text field.
michael@0 264 *
michael@0 265 * This is like `addDailyDiscreteNumeric` but for daily discrete text fields.
michael@0 266 */
michael@0 267 addDailyDiscreteText: function (field, value, date=new Date()) {
michael@0 268 return this.storage.addDailyDiscreteTextFromFieldID(
michael@0 269 this.fieldID(field), value, date);
michael@0 270 },
michael@0 271
michael@0 272 /**
michael@0 273 * Record the last seen value for a last numeric field.
michael@0 274 *
michael@0 275 * @param field
michael@0 276 * (string) The name of the field to set the value of.
michael@0 277 * @param value
michael@0 278 * (Number) The value to set.
michael@0 279 * @param date
michael@0 280 * (Date) When this value was recorded.
michael@0 281 *
michael@0 282 * @return Promise<>
michael@0 283 */
michael@0 284 setLastNumeric: function (field, value, date=new Date()) {
michael@0 285 return this.storage.setLastNumericFromFieldID(this.fieldID(field), value,
michael@0 286 date);
michael@0 287 },
michael@0 288
michael@0 289 /**
michael@0 290 * Record the last seen value for a last text field.
michael@0 291 *
michael@0 292 * This is like `setLastNumeric` except for last text fields.
michael@0 293 */
michael@0 294 setLastText: function (field, value, date=new Date()) {
michael@0 295 return this.storage.setLastTextFromFieldID(this.fieldID(field), value,
michael@0 296 date);
michael@0 297 },
michael@0 298
michael@0 299 /**
michael@0 300 * Record the most recent value for a daily last numeric field.
michael@0 301 *
michael@0 302 * @param field
michael@0 303 * (string) The name of a daily last numeric field.
michael@0 304 * @param value
michael@0 305 * (Number) The value to set.
michael@0 306 * @param date
michael@0 307 * (Date) Day on which to record the last value.
michael@0 308 *
michael@0 309 * @return Promise<>
michael@0 310 */
michael@0 311 setDailyLastNumeric: function (field, value, date=new Date()) {
michael@0 312 return this.storage.setDailyLastNumericFromFieldID(this.fieldID(field),
michael@0 313 value, date);
michael@0 314 },
michael@0 315
michael@0 316 /**
michael@0 317 * Record the most recent value for a daily last text field.
michael@0 318 *
michael@0 319 * This is like `setDailyLastNumeric` except for a daily last text field.
michael@0 320 */
michael@0 321 setDailyLastText: function (field, value, date=new Date()) {
michael@0 322 return this.storage.setDailyLastTextFromFieldID(this.fieldID(field),
michael@0 323 value, date);
michael@0 324 },
michael@0 325
michael@0 326 //---------------------------------------------------------------------------
michael@0 327 // End of data recording APIs.
michael@0 328 //---------------------------------------------------------------------------
michael@0 329
michael@0 330 /**
michael@0 331 * Obtain all values stored for this measurement.
michael@0 332 *
michael@0 333 * The default implementation obtains all known types from storage. If the
michael@0 334 * measurement provides custom types or stores values somewhere other than
michael@0 335 * storage, it should define its own implementation.
michael@0 336 *
michael@0 337 * This returns a promise that resolves to a data structure which is
michael@0 338 * understood by the measurement's serialize() function.
michael@0 339 */
michael@0 340 getValues: function () {
michael@0 341 return this.storage.getMeasurementValues(this.id);
michael@0 342 },
michael@0 343
michael@0 344 deleteLastNumeric: function (field) {
michael@0 345 return this.storage.deleteLastNumericFromFieldID(this.fieldID(field));
michael@0 346 },
michael@0 347
michael@0 348 deleteLastText: function (field) {
michael@0 349 return this.storage.deleteLastTextFromFieldID(this.fieldID(field));
michael@0 350 },
michael@0 351
michael@0 352 /**
michael@0 353 * This method is used by the default serializers to control whether a field
michael@0 354 * is included in the output.
michael@0 355 *
michael@0 356 * There could be legacy fields in storage we no longer care about.
michael@0 357 *
michael@0 358 * This method is a hook to allow measurements to change this behavior, e.g.,
michael@0 359 * to implement a dynamic fieldset.
michael@0 360 *
michael@0 361 * You will also need to override `fieldType`.
michael@0 362 *
michael@0 363 * @return (boolean) true if the specified field should be included in
michael@0 364 * payload output.
michael@0 365 */
michael@0 366 shouldIncludeField: function (field) {
michael@0 367 return field in this._fields;
michael@0 368 },
michael@0 369
michael@0 370 _serializeJSONSingular: function (data) {
michael@0 371 let result = {"_v": this.version};
michael@0 372
michael@0 373 for (let [field, data] of data) {
michael@0 374 // There could be legacy fields in storage we no longer care about.
michael@0 375 if (!this.shouldIncludeField(field)) {
michael@0 376 continue;
michael@0 377 }
michael@0 378
michael@0 379 let type = this.fieldType(field);
michael@0 380
michael@0 381 switch (type) {
michael@0 382 case this.storage.FIELD_LAST_NUMERIC:
michael@0 383 case this.storage.FIELD_LAST_TEXT:
michael@0 384 result[field] = data[1];
michael@0 385 break;
michael@0 386
michael@0 387 case this.storage.FIELD_DAILY_COUNTER:
michael@0 388 case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
michael@0 389 case this.storage.FIELD_DAILY_DISCRETE_TEXT:
michael@0 390 case this.storage.FIELD_DAILY_LAST_NUMERIC:
michael@0 391 case this.storage.FIELD_DAILY_LAST_TEXT:
michael@0 392 continue;
michael@0 393
michael@0 394 default:
michael@0 395 throw new Error("Unknown field type: " + type);
michael@0 396 }
michael@0 397 }
michael@0 398
michael@0 399 return result;
michael@0 400 },
michael@0 401
michael@0 402 _serializeJSONDay: function (data) {
michael@0 403 let result = {"_v": this.version};
michael@0 404
michael@0 405 for (let [field, data] of data) {
michael@0 406 if (!this.shouldIncludeField(field)) {
michael@0 407 continue;
michael@0 408 }
michael@0 409
michael@0 410 let type = this.fieldType(field);
michael@0 411
michael@0 412 switch (type) {
michael@0 413 case this.storage.FIELD_DAILY_COUNTER:
michael@0 414 case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
michael@0 415 case this.storage.FIELD_DAILY_DISCRETE_TEXT:
michael@0 416 case this.storage.FIELD_DAILY_LAST_NUMERIC:
michael@0 417 case this.storage.FIELD_DAILY_LAST_TEXT:
michael@0 418 result[field] = data;
michael@0 419 break;
michael@0 420
michael@0 421 case this.storage.FIELD_LAST_NUMERIC:
michael@0 422 case this.storage.FIELD_LAST_TEXT:
michael@0 423 continue;
michael@0 424
michael@0 425 default:
michael@0 426 throw new Error("Unknown field type: " + type);
michael@0 427 }
michael@0 428 }
michael@0 429
michael@0 430 return result;
michael@0 431 },
michael@0 432 });
michael@0 433
michael@0 434
michael@0 435 /**
michael@0 436 * An entity that emits data.
michael@0 437 *
michael@0 438 * A `Provider` consists of a string name (must be globally unique among all
michael@0 439 * known providers) and a set of `Measurement` instances.
michael@0 440 *
michael@0 441 * The main role of a `Provider` is to produce metrics data and to store said
michael@0 442 * data in the storage backend.
michael@0 443 *
michael@0 444 * Metrics data collection is initiated either by a manager calling a
michael@0 445 * `collect*` function on `Provider` instances or by the `Provider` registering
michael@0 446 * to some external event and then reacting whenever they occur.
michael@0 447 *
michael@0 448 * `Provider` implementations interface directly with a storage backend. For
michael@0 449 * common stored values (daily counters, daily discrete values, etc),
michael@0 450 * implementations should interface with storage via the various helper
michael@0 451 * functions on the `Measurement` instances. For custom stored value types,
michael@0 452 * implementations will interact directly with the low-level storage APIs.
michael@0 453 *
michael@0 454 * Because multiple providers exist and could be responding to separate
michael@0 455 * external events simultaneously and because not all operations performed by
michael@0 456 * storage can safely be performed in parallel, writing directly to storage at
michael@0 457 * event time is dangerous. Therefore, interactions with storage must be
michael@0 458 * deferred until it is safe to perform them.
michael@0 459 *
michael@0 460 * This typically looks something like:
michael@0 461 *
michael@0 462 * // This gets called when an external event worthy of recording metrics
michael@0 463 * // occurs. The function receives a numeric value associated with the event.
michael@0 464 * function onExternalEvent (value) {
michael@0 465 * let now = new Date();
michael@0 466 * let m = this.getMeasurement("foo", 1);
michael@0 467 *
michael@0 468 * this.enqueueStorageOperation(function storeExternalEvent() {
michael@0 469 *
michael@0 470 * // We interface with storage via the `Measurement` helper functions.
michael@0 471 * // These each return a promise that will be resolved when the
michael@0 472 * // operation finishes. We rely on behavior of storage where operations
michael@0 473 * // are executed single threaded and sequentially. Therefore, we only
michael@0 474 * // need to return the final promise.
michael@0 475 * m.incrementDailyCounter("foo", now);
michael@0 476 * return m.addDailyDiscreteNumericValue("my_value", value, now);
michael@0 477 * }.bind(this));
michael@0 478 *
michael@0 479 * }
michael@0 480 *
michael@0 481 *
michael@0 482 * `Provider` is an abstract base class. Implementations must define a few
michael@0 483 * properties:
michael@0 484 *
michael@0 485 * name
michael@0 486 * The `name` property should be a string defining the provider's name. The
michael@0 487 * name must be globally unique for the application. The name is used as an
michael@0 488 * identifier to distinguish providers from each other.
michael@0 489 *
michael@0 490 * measurementTypes
michael@0 491 * This must be an array of `Measurement`-derived types. Note that elements
michael@0 492 * in the array are the type functions, not instances. Instances of the
michael@0 493 * `Measurement` are created at run-time by the `Provider` and are bound
michael@0 494 * to the provider and to a specific storage backend.
michael@0 495 */
michael@0 496 this.Provider = function () {
michael@0 497 if (!this.name) {
michael@0 498 throw new Error("Provider must define a name.");
michael@0 499 }
michael@0 500
michael@0 501 if (!Array.isArray(this.measurementTypes)) {
michael@0 502 throw new Error("Provider must define measurement types.");
michael@0 503 }
michael@0 504
michael@0 505 this._log = Log.repository.getLogger("Services.Metrics.Provider." + this.name);
michael@0 506
michael@0 507 this.measurements = null;
michael@0 508 this.storage = null;
michael@0 509 }
michael@0 510
michael@0 511 Provider.prototype = Object.freeze({
michael@0 512 /**
michael@0 513 * Whether the provider only pulls data from other sources.
michael@0 514 *
michael@0 515 * If this is true, the provider pulls data from other sources. By contrast,
michael@0 516 * "push-based" providers subscribe to foreign sources and record/react to
michael@0 517 * external events as they happen.
michael@0 518 *
michael@0 519 * Pull-only providers likely aren't instantiated until a data collection
michael@0 520 * is performed. Thus, implementations cannot rely on a provider instance
michael@0 521 * always being alive. This is an optimization so provider instances aren't
michael@0 522 * dead weight while the application is running.
michael@0 523 *
michael@0 524 * This must be set on the prototype to have an effect.
michael@0 525 */
michael@0 526 pullOnly: false,
michael@0 527
michael@0 528 /**
michael@0 529 * Obtain a `Measurement` from its name and version.
michael@0 530 *
michael@0 531 * If the measurement is not found, an Error is thrown.
michael@0 532 */
michael@0 533 getMeasurement: function (name, version) {
michael@0 534 if (!Number.isInteger(version)) {
michael@0 535 throw new Error("getMeasurement expects an integer version. Got: " + version);
michael@0 536 }
michael@0 537
michael@0 538 let m = this.measurements.get([name, version].join(":"));
michael@0 539
michael@0 540 if (!m) {
michael@0 541 throw new Error("Unknown measurement: " + name + " v" + version);
michael@0 542 }
michael@0 543
michael@0 544 return m;
michael@0 545 },
michael@0 546
michael@0 547 init: function (storage) {
michael@0 548 if (this.storage !== null) {
michael@0 549 throw new Error("Provider() not called. Did the sub-type forget to call it?");
michael@0 550 }
michael@0 551
michael@0 552 if (this.storage) {
michael@0 553 throw new Error("Provider has already been initialized.");
michael@0 554 }
michael@0 555
michael@0 556 this.measurements = new Map();
michael@0 557 this.storage = storage;
michael@0 558
michael@0 559 let self = this;
michael@0 560 return Task.spawn(function init() {
michael@0 561 let pre = self.preInit();
michael@0 562 if (!pre || typeof(pre.then) != "function") {
michael@0 563 throw new Error("preInit() does not return a promise.");
michael@0 564 }
michael@0 565 yield pre;
michael@0 566
michael@0 567 for (let measurementType of self.measurementTypes) {
michael@0 568 let measurement = new measurementType();
michael@0 569
michael@0 570 measurement.provider = self;
michael@0 571 measurement.storage = self.storage;
michael@0 572
michael@0 573 let id = yield storage.registerMeasurement(self.name, measurement.name,
michael@0 574 measurement.version);
michael@0 575
michael@0 576 measurement.id = id;
michael@0 577
michael@0 578 yield measurement._configureStorage();
michael@0 579
michael@0 580 self.measurements.set([measurement.name, measurement.version].join(":"),
michael@0 581 measurement);
michael@0 582 }
michael@0 583
michael@0 584 let post = self.postInit();
michael@0 585 if (!post || typeof(post.then) != "function") {
michael@0 586 throw new Error("postInit() does not return a promise.");
michael@0 587 }
michael@0 588 yield post;
michael@0 589 });
michael@0 590 },
michael@0 591
michael@0 592 shutdown: function () {
michael@0 593 let promise = this.onShutdown();
michael@0 594
michael@0 595 if (!promise || typeof(promise.then) != "function") {
michael@0 596 throw new Error("onShutdown implementation does not return a promise.");
michael@0 597 }
michael@0 598
michael@0 599 return promise;
michael@0 600 },
michael@0 601
michael@0 602 /**
michael@0 603 * Hook point for implementations to perform pre-initialization activity.
michael@0 604 *
michael@0 605 * This method will be called before measurement registration.
michael@0 606 *
michael@0 607 * Implementations should return a promise which is resolved when
michael@0 608 * initialization activities have completed.
michael@0 609 */
michael@0 610 preInit: function () {
michael@0 611 return CommonUtils.laterTickResolvingPromise();
michael@0 612 },
michael@0 613
michael@0 614 /**
michael@0 615 * Hook point for implementations to perform post-initialization activity.
michael@0 616 *
michael@0 617 * This method will be called after `preInit` and measurement registration,
michael@0 618 * but before initialization is finished.
michael@0 619 *
michael@0 620 * If a `Provider` instance needs to register observers, etc, it should
michael@0 621 * implement this function.
michael@0 622 *
michael@0 623 * Implementations should return a promise which is resolved when
michael@0 624 * initialization activities have completed.
michael@0 625 */
michael@0 626 postInit: function () {
michael@0 627 return CommonUtils.laterTickResolvingPromise();
michael@0 628 },
michael@0 629
michael@0 630 /**
michael@0 631 * Hook point for shutdown of instances.
michael@0 632 *
michael@0 633 * This is the opposite of `onInit`. If a `Provider` needs to unregister
michael@0 634 * observers, etc, this is where it should do it.
michael@0 635 *
michael@0 636 * Implementations should return a promise which is resolved when
michael@0 637 * shutdown activities have completed.
michael@0 638 */
michael@0 639 onShutdown: function () {
michael@0 640 return CommonUtils.laterTickResolvingPromise();
michael@0 641 },
michael@0 642
michael@0 643 /**
michael@0 644 * Collects data that doesn't change during the application's lifetime.
michael@0 645 *
michael@0 646 * Implementations should return a promise that resolves when all data has
michael@0 647 * been collected and storage operations have been finished.
michael@0 648 *
michael@0 649 * @return Promise<>
michael@0 650 */
michael@0 651 collectConstantData: function () {
michael@0 652 return CommonUtils.laterTickResolvingPromise();
michael@0 653 },
michael@0 654
michael@0 655 /**
michael@0 656 * Collects data approximately every day.
michael@0 657 *
michael@0 658 * For long-running applications, this is called approximately every day.
michael@0 659 * It may or may not be called every time the application is run. It also may
michael@0 660 * be called more than once per day.
michael@0 661 *
michael@0 662 * Implementations should return a promise that resolves when all data has
michael@0 663 * been collected and storage operations have completed.
michael@0 664 *
michael@0 665 * @return Promise<>
michael@0 666 */
michael@0 667 collectDailyData: function () {
michael@0 668 return CommonUtils.laterTickResolvingPromise();
michael@0 669 },
michael@0 670
michael@0 671 /**
michael@0 672 * Queue a deferred storage operation.
michael@0 673 *
michael@0 674 * Deferred storage operations are the preferred method for providers to
michael@0 675 * interact with storage. When collected data is to be added to storage,
michael@0 676 * the provider creates a function that performs the necessary storage
michael@0 677 * interactions and then passes that function to this function. Pending
michael@0 678 * storage operations will be executed sequentially by a coordinator.
michael@0 679 *
michael@0 680 * The passed function should return a promise which will be resolved upon
michael@0 681 * completion of storage interaction.
michael@0 682 */
michael@0 683 enqueueStorageOperation: function (func) {
michael@0 684 return this.storage.enqueueOperation(func);
michael@0 685 },
michael@0 686
michael@0 687 /**
michael@0 688 * Obtain persisted provider state.
michael@0 689 *
michael@0 690 * Provider state consists of key-value pairs of string names and values.
michael@0 691 * Providers can stuff whatever they want into state. They are encouraged to
michael@0 692 * store as little as possible for performance reasons.
michael@0 693 *
michael@0 694 * State is backed by storage and is robust.
michael@0 695 *
michael@0 696 * These functions do not enqueue on storage automatically, so they should
michael@0 697 * be guarded by `enqueueStorageOperation` or some other mutex.
michael@0 698 *
michael@0 699 * @param key
michael@0 700 * (string) The property to retrieve.
michael@0 701 *
michael@0 702 * @return Promise<string|null> String value on success. null if no state
michael@0 703 * is available under this key.
michael@0 704 */
michael@0 705 getState: function (key) {
michael@0 706 return this.storage.getProviderState(this.name, key);
michael@0 707 },
michael@0 708
michael@0 709 /**
michael@0 710 * Set state for this provider.
michael@0 711 *
michael@0 712 * This is the complementary API for `getState` and obeys the same
michael@0 713 * storage restrictions.
michael@0 714 */
michael@0 715 setState: function (key, value) {
michael@0 716 return this.storage.setProviderState(this.name, key, value);
michael@0 717 },
michael@0 718
michael@0 719 _dateToDays: function (date) {
michael@0 720 return Math.floor(date.getTime() / MILLISECONDS_PER_DAY);
michael@0 721 },
michael@0 722
michael@0 723 _daysToDate: function (days) {
michael@0 724 return new Date(days * MILLISECONDS_PER_DAY);
michael@0 725 },
michael@0 726 });
michael@0 727

mercurial