services/metrics/dataprovider.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/services/metrics/dataprovider.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,727 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +#ifndef MERGED_COMPARTMENT
    1.11 +
    1.12 +this.EXPORTED_SYMBOLS = [
    1.13 +  "Measurement",
    1.14 +  "Provider",
    1.15 +];
    1.16 +
    1.17 +const {utils: Cu} = Components;
    1.18 +
    1.19 +const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
    1.20 +
    1.21 +#endif
    1.22 +
    1.23 +Cu.import("resource://gre/modules/Promise.jsm");
    1.24 +Cu.import("resource://gre/modules/Preferences.jsm");
    1.25 +Cu.import("resource://gre/modules/Task.jsm");
    1.26 +Cu.import("resource://gre/modules/Log.jsm");
    1.27 +Cu.import("resource://services-common/utils.js");
    1.28 +
    1.29 +
    1.30 +
    1.31 +/**
    1.32 + * Represents a collection of related pieces/fields of data.
    1.33 + *
    1.34 + * This is an abstract base type.
    1.35 + *
    1.36 + * This type provides the primary interface for storing, retrieving, and
    1.37 + * serializing data.
    1.38 + *
    1.39 + * Each measurement consists of a set of named fields. Each field is primarily
    1.40 + * identified by a string name, which must be unique within the measurement.
    1.41 + *
    1.42 + * Each derived type must define the following properties:
    1.43 + *
    1.44 + *   name -- String name of this measurement. This is the primary way
    1.45 + *     measurements are distinguished within a provider.
    1.46 + *
    1.47 + *   version -- Integer version of this measurement. This is a secondary
    1.48 + *     identifier for a measurement within a provider. The version denotes
    1.49 + *     the behavior of this measurement and the composition of its fields over
    1.50 + *     time. When a new field is added or the behavior of an existing field
    1.51 + *     changes, the version should be incremented. The initial version of a
    1.52 + *     measurement is typically 1.
    1.53 + *
    1.54 + *   fields -- Object defining the fields this measurement holds. Keys in the
    1.55 + *     object are string field names. Values are objects describing how the
    1.56 + *     field works. The following properties are recognized:
    1.57 + *
    1.58 + *       type -- The string type of this field. This is typically one of the
    1.59 + *         FIELD_* constants from the Metrics.Storage type.
    1.60 + *
    1.61 + *
    1.62 + * FUTURE: provide hook points for measurements to supplement with custom
    1.63 + * storage needs.
    1.64 + */
    1.65 +this.Measurement = function () {
    1.66 +  if (!this.name) {
    1.67 +    throw new Error("Measurement must have a name.");
    1.68 +  }
    1.69 +
    1.70 +  if (!this.version) {
    1.71 +    throw new Error("Measurement must have a version.");
    1.72 +  }
    1.73 +
    1.74 +  if (!Number.isInteger(this.version)) {
    1.75 +    throw new Error("Measurement's version must be an integer: " + this.version);
    1.76 +  }
    1.77 +
    1.78 +  if (!this.fields) {
    1.79 +    throw new Error("Measurement must define fields.");
    1.80 +  }
    1.81 +
    1.82 +  for (let [name, info] in Iterator(this.fields)) {
    1.83 +    if (!info) {
    1.84 +      throw new Error("Field does not contain metadata: " + name);
    1.85 +    }
    1.86 +
    1.87 +    if (!info.type) {
    1.88 +      throw new Error("Field is missing required type property: " + name);
    1.89 +    }
    1.90 +  }
    1.91 +
    1.92 +  this._log = Log.repository.getLogger("Services.Metrics.Measurement." + this.name);
    1.93 +
    1.94 +  this.id = null;
    1.95 +  this.storage = null;
    1.96 +  this._fields = {};
    1.97 +
    1.98 +  this._serializers = {};
    1.99 +  this._serializers[this.SERIALIZE_JSON] = {
   1.100 +    singular: this._serializeJSONSingular.bind(this),
   1.101 +    daily: this._serializeJSONDay.bind(this),
   1.102 +  };
   1.103 +}
   1.104 +
   1.105 +Measurement.prototype = Object.freeze({
   1.106 +  SERIALIZE_JSON: "json",
   1.107 +
   1.108 +  /**
   1.109 +   * Obtain a serializer for this measurement.
   1.110 +   *
   1.111 +   * Implementations should return an object with the following keys:
   1.112 +   *
   1.113 +   *   singular -- Serializer for singular data.
   1.114 +   *   daily -- Serializer for daily data.
   1.115 +   *
   1.116 +   * Each item is a function that takes a single argument: the data to
   1.117 +   * serialize. The passed data is a subset of that returned from
   1.118 +   * this.getValues(). For "singular," data.singular is passed. For "daily",
   1.119 +   * data.days.get(<day>) is passed.
   1.120 +   *
   1.121 +   * This function receives a single argument: the serialization format we
   1.122 +   * are requesting. This is one of the SERIALIZE_* constants on this base type.
   1.123 +   *
   1.124 +   * For SERIALIZE_JSON, the function should return an object that
   1.125 +   * JSON.stringify() knows how to handle. This could be an anonymous object or
   1.126 +   * array or any object with a property named `toJSON` whose value is a
   1.127 +   * function. The returned object will be added to a larger document
   1.128 +   * containing the results of all `serialize` calls.
   1.129 +   *
   1.130 +   * The default implementation knows how to serialize built-in types using
   1.131 +   * very simple logic. If small encoding size is a goal, the default
   1.132 +   * implementation may not be suitable. If an unknown field type is
   1.133 +   * encountered, the default implementation will error.
   1.134 +   *
   1.135 +   * @param format
   1.136 +   *        (string) A SERIALIZE_* constant defining what serialization format
   1.137 +   *        to use.
   1.138 +   */
   1.139 +  serializer: function (format) {
   1.140 +    if (!(format in this._serializers)) {
   1.141 +      throw new Error("Don't know how to serialize format: " + format);
   1.142 +    }
   1.143 +
   1.144 +    return this._serializers[format];
   1.145 +  },
   1.146 +
   1.147 +  /**
   1.148 +   * Whether this measurement contains the named field.
   1.149 +   *
   1.150 +   * @param name
   1.151 +   *        (string) Name of field.
   1.152 +   *
   1.153 +   * @return bool
   1.154 +   */
   1.155 +  hasField: function (name) {
   1.156 +    return name in this.fields;
   1.157 +  },
   1.158 +
   1.159 +  /**
   1.160 +   * The unique identifier for a named field.
   1.161 +   *
   1.162 +   * This will throw if the field is not known.
   1.163 +   *
   1.164 +   * @param name
   1.165 +   *        (string) Name of field.
   1.166 +   */
   1.167 +  fieldID: function (name) {
   1.168 +    let entry = this._fields[name];
   1.169 +
   1.170 +    if (!entry) {
   1.171 +      throw new Error("Unknown field: " + name);
   1.172 +    }
   1.173 +
   1.174 +    return entry[0];
   1.175 +  },
   1.176 +
   1.177 +  fieldType: function (name) {
   1.178 +    let entry = this._fields[name];
   1.179 +
   1.180 +    if (!entry) {
   1.181 +      throw new Error("Unknown field: " + name);
   1.182 +    }
   1.183 +
   1.184 +    return entry[1];
   1.185 +  },
   1.186 +
   1.187 +  _configureStorage: function () {
   1.188 +    let missing = [];
   1.189 +    for (let [name, info] in Iterator(this.fields)) {
   1.190 +      if (this.storage.hasFieldFromMeasurement(this.id, name)) {
   1.191 +        this._fields[name] =
   1.192 +          [this.storage.fieldIDFromMeasurement(this.id, name), info.type];
   1.193 +        continue;
   1.194 +      }
   1.195 +
   1.196 +      missing.push([name, info.type]);
   1.197 +    }
   1.198 +
   1.199 +    if (!missing.length) {
   1.200 +      return CommonUtils.laterTickResolvingPromise();
   1.201 +    }
   1.202 +
   1.203 +    // We only perform a transaction if we have work to do (to avoid
   1.204 +    // extra SQLite overhead).
   1.205 +    return this.storage.enqueueTransaction(function registerFields() {
   1.206 +      for (let [name, type] of missing) {
   1.207 +        this._log.debug("Registering field: " + name + " " + type);
   1.208 +        let id = yield this.storage.registerField(this.id, name, type);
   1.209 +        this._fields[name] = [id, type];
   1.210 +      }
   1.211 +    }.bind(this));
   1.212 +  },
   1.213 +
   1.214 +  //---------------------------------------------------------------------------
   1.215 +  // Data Recording Functions
   1.216 +  //
   1.217 +  // Functions in this section are used to record new values against this
   1.218 +  // measurement instance.
   1.219 +  //
   1.220 +  // Generally speaking, these functions will throw if the specified field does
   1.221 +  // not exist or if the storage function requested is not appropriate for the
   1.222 +  // type of that field. These functions will also return a promise that will
   1.223 +  // be resolved when the underlying storage operation has completed.
   1.224 +  //---------------------------------------------------------------------------
   1.225 +
   1.226 +  /**
   1.227 +   * Increment a daily counter field in this measurement by 1.
   1.228 +   *
   1.229 +   * By default, the counter for the current day will be incremented.
   1.230 +   *
   1.231 +   * If the field is not known or is not a daily counter, this will throw.
   1.232 +   *
   1.233 +   *
   1.234 +   *
   1.235 +   * @param field
   1.236 +   *        (string) The name of the field whose value to increment.
   1.237 +   * @param date
   1.238 +   *        (Date) Day on which to increment the counter.
   1.239 +   * @param by
   1.240 +   *        (integer) How much to increment by.
   1.241 +   * @return Promise<>
   1.242 +   */
   1.243 +  incrementDailyCounter: function (field, date=new Date(), by=1) {
   1.244 +    return this.storage.incrementDailyCounterFromFieldID(this.fieldID(field),
   1.245 +                                                         date, by);
   1.246 +  },
   1.247 +
   1.248 +  /**
   1.249 +   * Record a new numeric value for a daily discrete numeric field.
   1.250 +   *
   1.251 +   * @param field
   1.252 +   *        (string) The name of the field to append a value to.
   1.253 +   * @param value
   1.254 +   *        (Number) Number to append.
   1.255 +   * @param date
   1.256 +   *        (Date) Day on which to append the value.
   1.257 +   *
   1.258 +   * @return Promise<>
   1.259 +   */
   1.260 +  addDailyDiscreteNumeric: function (field, value, date=new Date()) {
   1.261 +    return this.storage.addDailyDiscreteNumericFromFieldID(
   1.262 +                          this.fieldID(field), value, date);
   1.263 +  },
   1.264 +
   1.265 +  /**
   1.266 +   * Record a new text value for a daily discrete text field.
   1.267 +   *
   1.268 +   * This is like `addDailyDiscreteNumeric` but for daily discrete text fields.
   1.269 +   */
   1.270 +  addDailyDiscreteText: function (field, value, date=new Date()) {
   1.271 +    return this.storage.addDailyDiscreteTextFromFieldID(
   1.272 +                          this.fieldID(field), value, date);
   1.273 +  },
   1.274 +
   1.275 +  /**
   1.276 +   * Record the last seen value for a last numeric field.
   1.277 +   *
   1.278 +   * @param field
   1.279 +   *        (string) The name of the field to set the value of.
   1.280 +   * @param value
   1.281 +   *        (Number) The value to set.
   1.282 +   * @param date
   1.283 +   *        (Date) When this value was recorded.
   1.284 +   *
   1.285 +   * @return Promise<>
   1.286 +   */
   1.287 +  setLastNumeric: function (field, value, date=new Date()) {
   1.288 +    return this.storage.setLastNumericFromFieldID(this.fieldID(field), value,
   1.289 +                                                  date);
   1.290 +  },
   1.291 +
   1.292 +  /**
   1.293 +   * Record the last seen value for a last text field.
   1.294 +   *
   1.295 +   * This is like `setLastNumeric` except for last text fields.
   1.296 +   */
   1.297 +  setLastText: function (field, value, date=new Date()) {
   1.298 +    return this.storage.setLastTextFromFieldID(this.fieldID(field), value,
   1.299 +                                               date);
   1.300 +  },
   1.301 +
   1.302 +  /**
   1.303 +   * Record the most recent value for a daily last numeric field.
   1.304 +   *
   1.305 +   * @param field
   1.306 +   *        (string) The name of a daily last numeric field.
   1.307 +   * @param value
   1.308 +   *        (Number) The value to set.
   1.309 +   * @param date
   1.310 +   *        (Date) Day on which to record the last value.
   1.311 +   *
   1.312 +   * @return Promise<>
   1.313 +   */
   1.314 +  setDailyLastNumeric: function (field, value, date=new Date()) {
   1.315 +    return this.storage.setDailyLastNumericFromFieldID(this.fieldID(field),
   1.316 +                                                       value, date);
   1.317 +  },
   1.318 +
   1.319 +  /**
   1.320 +   * Record the most recent value for a daily last text field.
   1.321 +   *
   1.322 +   * This is like `setDailyLastNumeric` except for a daily last text field.
   1.323 +   */
   1.324 +  setDailyLastText: function (field, value, date=new Date()) {
   1.325 +    return this.storage.setDailyLastTextFromFieldID(this.fieldID(field),
   1.326 +                                                    value, date);
   1.327 +  },
   1.328 +
   1.329 +  //---------------------------------------------------------------------------
   1.330 +  // End of data recording APIs.
   1.331 +  //---------------------------------------------------------------------------
   1.332 +
   1.333 +  /**
   1.334 +   * Obtain all values stored for this measurement.
   1.335 +   *
   1.336 +   * The default implementation obtains all known types from storage. If the
   1.337 +   * measurement provides custom types or stores values somewhere other than
   1.338 +   * storage, it should define its own implementation.
   1.339 +   *
   1.340 +   * This returns a promise that resolves to a data structure which is
   1.341 +   * understood by the measurement's serialize() function.
   1.342 +   */
   1.343 +  getValues: function () {
   1.344 +    return this.storage.getMeasurementValues(this.id);
   1.345 +  },
   1.346 +
   1.347 +  deleteLastNumeric: function (field) {
   1.348 +    return this.storage.deleteLastNumericFromFieldID(this.fieldID(field));
   1.349 +  },
   1.350 +
   1.351 +  deleteLastText: function (field) {
   1.352 +    return this.storage.deleteLastTextFromFieldID(this.fieldID(field));
   1.353 +  },
   1.354 +
   1.355 +  /**
   1.356 +   * This method is used by the default serializers to control whether a field
   1.357 +   * is included in the output.
   1.358 +   *
   1.359 +   * There could be legacy fields in storage we no longer care about.
   1.360 +   *
   1.361 +   * This method is a hook to allow measurements to change this behavior, e.g.,
   1.362 +   * to implement a dynamic fieldset.
   1.363 +   *
   1.364 +   * You will also need to override `fieldType`.
   1.365 +   *
   1.366 +   * @return (boolean) true if the specified field should be included in
   1.367 +   *                   payload output.
   1.368 +   */
   1.369 +  shouldIncludeField: function (field) {
   1.370 +    return field in this._fields;
   1.371 +  },
   1.372 +
   1.373 +  _serializeJSONSingular: function (data) {
   1.374 +    let result = {"_v": this.version};
   1.375 +
   1.376 +    for (let [field, data] of data) {
   1.377 +      // There could be legacy fields in storage we no longer care about.
   1.378 +      if (!this.shouldIncludeField(field)) {
   1.379 +        continue;
   1.380 +      }
   1.381 +
   1.382 +      let type = this.fieldType(field);
   1.383 +
   1.384 +      switch (type) {
   1.385 +        case this.storage.FIELD_LAST_NUMERIC:
   1.386 +        case this.storage.FIELD_LAST_TEXT:
   1.387 +          result[field] = data[1];
   1.388 +          break;
   1.389 +
   1.390 +        case this.storage.FIELD_DAILY_COUNTER:
   1.391 +        case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
   1.392 +        case this.storage.FIELD_DAILY_DISCRETE_TEXT:
   1.393 +        case this.storage.FIELD_DAILY_LAST_NUMERIC:
   1.394 +        case this.storage.FIELD_DAILY_LAST_TEXT:
   1.395 +          continue;
   1.396 +
   1.397 +        default:
   1.398 +          throw new Error("Unknown field type: " + type);
   1.399 +      }
   1.400 +    }
   1.401 +
   1.402 +    return result;
   1.403 +  },
   1.404 +
   1.405 +  _serializeJSONDay: function (data) {
   1.406 +    let result = {"_v": this.version};
   1.407 +
   1.408 +    for (let [field, data] of data) {
   1.409 +      if (!this.shouldIncludeField(field)) {
   1.410 +        continue;
   1.411 +      }
   1.412 +
   1.413 +      let type = this.fieldType(field);
   1.414 +
   1.415 +      switch (type) {
   1.416 +        case this.storage.FIELD_DAILY_COUNTER:
   1.417 +        case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
   1.418 +        case this.storage.FIELD_DAILY_DISCRETE_TEXT:
   1.419 +        case this.storage.FIELD_DAILY_LAST_NUMERIC:
   1.420 +        case this.storage.FIELD_DAILY_LAST_TEXT:
   1.421 +          result[field] = data;
   1.422 +          break;
   1.423 +
   1.424 +        case this.storage.FIELD_LAST_NUMERIC:
   1.425 +        case this.storage.FIELD_LAST_TEXT:
   1.426 +          continue;
   1.427 +
   1.428 +        default:
   1.429 +          throw new Error("Unknown field type: " + type);
   1.430 +      }
   1.431 +    }
   1.432 +
   1.433 +    return result;
   1.434 +  },
   1.435 +});
   1.436 +
   1.437 +
   1.438 +/**
   1.439 + * An entity that emits data.
   1.440 + *
   1.441 + * A `Provider` consists of a string name (must be globally unique among all
   1.442 + * known providers) and a set of `Measurement` instances.
   1.443 + *
   1.444 + * The main role of a `Provider` is to produce metrics data and to store said
   1.445 + * data in the storage backend.
   1.446 + *
   1.447 + * Metrics data collection is initiated either by a manager calling a
   1.448 + * `collect*` function on `Provider` instances or by the `Provider` registering
   1.449 + * to some external event and then reacting whenever they occur.
   1.450 + *
   1.451 + * `Provider` implementations interface directly with a storage backend. For
   1.452 + * common stored values (daily counters, daily discrete values, etc),
   1.453 + * implementations should interface with storage via the various helper
   1.454 + * functions on the `Measurement` instances. For custom stored value types,
   1.455 + * implementations will interact directly with the low-level storage APIs.
   1.456 + *
   1.457 + * Because multiple providers exist and could be responding to separate
   1.458 + * external events simultaneously and because not all operations performed by
   1.459 + * storage can safely be performed in parallel, writing directly to storage at
   1.460 + * event time is dangerous. Therefore, interactions with storage must be
   1.461 + * deferred until it is safe to perform them.
   1.462 + *
   1.463 + * This typically looks something like:
   1.464 + *
   1.465 + *   // This gets called when an external event worthy of recording metrics
   1.466 + *   // occurs. The function receives a numeric value associated with the event.
   1.467 + *   function onExternalEvent (value) {
   1.468 + *     let now = new Date();
   1.469 + *     let m = this.getMeasurement("foo", 1);
   1.470 + *
   1.471 + *     this.enqueueStorageOperation(function storeExternalEvent() {
   1.472 + *
   1.473 + *       // We interface with storage via the `Measurement` helper functions.
   1.474 + *       // These each return a promise that will be resolved when the
   1.475 + *       // operation finishes. We rely on behavior of storage where operations
   1.476 + *       // are executed single threaded and sequentially. Therefore, we only
   1.477 + *       // need to return the final promise.
   1.478 + *       m.incrementDailyCounter("foo", now);
   1.479 + *       return m.addDailyDiscreteNumericValue("my_value", value, now);
   1.480 + *     }.bind(this));
   1.481 + *
   1.482 + *   }
   1.483 + *
   1.484 + *
   1.485 + * `Provider` is an abstract base class. Implementations must define a few
   1.486 + * properties:
   1.487 + *
   1.488 + *   name
   1.489 + *     The `name` property should be a string defining the provider's name. The
   1.490 + *     name must be globally unique for the application. The name is used as an
   1.491 + *     identifier to distinguish providers from each other.
   1.492 + *
   1.493 + *   measurementTypes
   1.494 + *     This must be an array of `Measurement`-derived types. Note that elements
   1.495 + *     in the array are the type functions, not instances. Instances of the
   1.496 + *     `Measurement` are created at run-time by the `Provider` and are bound
   1.497 + *     to the provider and to a specific storage backend.
   1.498 + */
   1.499 +this.Provider = function () {
   1.500 +  if (!this.name) {
   1.501 +    throw new Error("Provider must define a name.");
   1.502 +  }
   1.503 +
   1.504 +  if (!Array.isArray(this.measurementTypes)) {
   1.505 +    throw new Error("Provider must define measurement types.");
   1.506 +  }
   1.507 +
   1.508 +  this._log = Log.repository.getLogger("Services.Metrics.Provider." + this.name);
   1.509 +
   1.510 +  this.measurements = null;
   1.511 +  this.storage = null;
   1.512 +}
   1.513 +
   1.514 +Provider.prototype = Object.freeze({
   1.515 +  /**
   1.516 +   * Whether the provider only pulls data from other sources.
   1.517 +   *
   1.518 +   * If this is true, the provider pulls data from other sources. By contrast,
   1.519 +   * "push-based" providers subscribe to foreign sources and record/react to
   1.520 +   * external events as they happen.
   1.521 +   *
   1.522 +   * Pull-only providers likely aren't instantiated until a data collection
   1.523 +   * is performed. Thus, implementations cannot rely on a provider instance
   1.524 +   * always being alive. This is an optimization so provider instances aren't
   1.525 +   * dead weight while the application is running.
   1.526 +   *
   1.527 +   * This must be set on the prototype to have an effect.
   1.528 +   */
   1.529 +  pullOnly: false,
   1.530 +
   1.531 +  /**
   1.532 +   * Obtain a `Measurement` from its name and version.
   1.533 +   *
   1.534 +   * If the measurement is not found, an Error is thrown.
   1.535 +   */
   1.536 +  getMeasurement: function (name, version) {
   1.537 +    if (!Number.isInteger(version)) {
   1.538 +      throw new Error("getMeasurement expects an integer version. Got: " + version);
   1.539 +    }
   1.540 +
   1.541 +    let m = this.measurements.get([name, version].join(":"));
   1.542 +
   1.543 +    if (!m) {
   1.544 +      throw new Error("Unknown measurement: " + name + " v" + version);
   1.545 +    }
   1.546 +
   1.547 +    return m;
   1.548 +  },
   1.549 +
   1.550 +  init: function (storage) {
   1.551 +    if (this.storage !== null) {
   1.552 +      throw new Error("Provider() not called. Did the sub-type forget to call it?");
   1.553 +    }
   1.554 +
   1.555 +    if (this.storage) {
   1.556 +      throw new Error("Provider has already been initialized.");
   1.557 +    }
   1.558 +
   1.559 +    this.measurements = new Map();
   1.560 +    this.storage = storage;
   1.561 +
   1.562 +    let self = this;
   1.563 +    return Task.spawn(function init() {
   1.564 +      let pre = self.preInit();
   1.565 +      if (!pre || typeof(pre.then) != "function") {
   1.566 +        throw new Error("preInit() does not return a promise.");
   1.567 +      }
   1.568 +      yield pre;
   1.569 +
   1.570 +      for (let measurementType of self.measurementTypes) {
   1.571 +        let measurement = new measurementType();
   1.572 +
   1.573 +        measurement.provider = self;
   1.574 +        measurement.storage = self.storage;
   1.575 +
   1.576 +        let id = yield storage.registerMeasurement(self.name, measurement.name,
   1.577 +                                                   measurement.version);
   1.578 +
   1.579 +        measurement.id = id;
   1.580 +
   1.581 +        yield measurement._configureStorage();
   1.582 +
   1.583 +        self.measurements.set([measurement.name, measurement.version].join(":"),
   1.584 +                              measurement);
   1.585 +      }
   1.586 +
   1.587 +      let post = self.postInit();
   1.588 +      if (!post || typeof(post.then) != "function") {
   1.589 +        throw new Error("postInit() does not return a promise.");
   1.590 +      }
   1.591 +      yield post;
   1.592 +    });
   1.593 +  },
   1.594 +
   1.595 +  shutdown: function () {
   1.596 +    let promise = this.onShutdown();
   1.597 +
   1.598 +    if (!promise || typeof(promise.then) != "function") {
   1.599 +      throw new Error("onShutdown implementation does not return a promise.");
   1.600 +    }
   1.601 +
   1.602 +    return promise;
   1.603 +  },
   1.604 +
   1.605 +  /**
   1.606 +   * Hook point for implementations to perform pre-initialization activity.
   1.607 +   *
   1.608 +   * This method will be called before measurement registration.
   1.609 +   *
   1.610 +   * Implementations should return a promise which is resolved when
   1.611 +   * initialization activities have completed.
   1.612 +   */
   1.613 +  preInit: function () {
   1.614 +    return CommonUtils.laterTickResolvingPromise();
   1.615 +  },
   1.616 +
   1.617 +  /**
   1.618 +   * Hook point for implementations to perform post-initialization activity.
   1.619 +   *
   1.620 +   * This method will be called after `preInit` and measurement registration,
   1.621 +   * but before initialization is finished.
   1.622 +   *
   1.623 +   * If a `Provider` instance needs to register observers, etc, it should
   1.624 +   * implement this function.
   1.625 +   *
   1.626 +   * Implementations should return a promise which is resolved when
   1.627 +   * initialization activities have completed.
   1.628 +   */
   1.629 +  postInit: function () {
   1.630 +    return CommonUtils.laterTickResolvingPromise();
   1.631 +  },
   1.632 +
   1.633 +  /**
   1.634 +   * Hook point for shutdown of instances.
   1.635 +   *
   1.636 +   * This is the opposite of `onInit`. If a `Provider` needs to unregister
   1.637 +   * observers, etc, this is where it should do it.
   1.638 +   *
   1.639 +   * Implementations should return a promise which is resolved when
   1.640 +   * shutdown activities have completed.
   1.641 +   */
   1.642 +  onShutdown: function () {
   1.643 +    return CommonUtils.laterTickResolvingPromise();
   1.644 +  },
   1.645 +
   1.646 +  /**
   1.647 +   * Collects data that doesn't change during the application's lifetime.
   1.648 +   *
   1.649 +   * Implementations should return a promise that resolves when all data has
   1.650 +   * been collected and storage operations have been finished.
   1.651 +   *
   1.652 +   * @return Promise<>
   1.653 +   */
   1.654 +  collectConstantData: function () {
   1.655 +    return CommonUtils.laterTickResolvingPromise();
   1.656 +  },
   1.657 +
   1.658 +  /**
   1.659 +   * Collects data approximately every day.
   1.660 +   *
   1.661 +   * For long-running applications, this is called approximately every day.
   1.662 +   * It may or may not be called every time the application is run. It also may
   1.663 +   * be called more than once per day.
   1.664 +   *
   1.665 +   * Implementations should return a promise that resolves when all data has
   1.666 +   * been collected and storage operations have completed.
   1.667 +   *
   1.668 +   * @return Promise<>
   1.669 +   */
   1.670 +  collectDailyData: function () {
   1.671 +    return CommonUtils.laterTickResolvingPromise();
   1.672 +  },
   1.673 +
   1.674 +  /**
   1.675 +   * Queue a deferred storage operation.
   1.676 +   *
   1.677 +   * Deferred storage operations are the preferred method for providers to
   1.678 +   * interact with storage. When collected data is to be added to storage,
   1.679 +   * the provider creates a function that performs the necessary storage
   1.680 +   * interactions and then passes that function to this function. Pending
   1.681 +   * storage operations will be executed sequentially by a coordinator.
   1.682 +   *
   1.683 +   * The passed function should return a promise which will be resolved upon
   1.684 +   * completion of storage interaction.
   1.685 +   */
   1.686 +  enqueueStorageOperation: function (func) {
   1.687 +    return this.storage.enqueueOperation(func);
   1.688 +  },
   1.689 +
   1.690 +  /**
   1.691 +   * Obtain persisted provider state.
   1.692 +   *
   1.693 +   * Provider state consists of key-value pairs of string names and values.
   1.694 +   * Providers can stuff whatever they want into state. They are encouraged to
   1.695 +   * store as little as possible for performance reasons.
   1.696 +   *
   1.697 +   * State is backed by storage and is robust.
   1.698 +   *
   1.699 +   * These functions do not enqueue on storage automatically, so they should
   1.700 +   * be guarded by `enqueueStorageOperation` or some other mutex.
   1.701 +   *
   1.702 +   * @param key
   1.703 +   *        (string) The property to retrieve.
   1.704 +   *
   1.705 +   * @return Promise<string|null> String value on success. null if no state
   1.706 +   *         is available under this key.
   1.707 +   */
   1.708 +  getState: function (key) {
   1.709 +    return this.storage.getProviderState(this.name, key);
   1.710 +  },
   1.711 +
   1.712 +  /**
   1.713 +   * Set state for this provider.
   1.714 +   *
   1.715 +   * This is the complementary API for `getState` and obeys the same
   1.716 +   * storage restrictions.
   1.717 +   */
   1.718 +  setState: function (key, value) {
   1.719 +    return this.storage.setProviderState(this.name, key, value);
   1.720 +  },
   1.721 +
   1.722 +  _dateToDays: function (date) {
   1.723 +    return Math.floor(date.getTime() / MILLISECONDS_PER_DAY);
   1.724 +  },
   1.725 +
   1.726 +  _daysToDate: function (days) {
   1.727 +    return new Date(days * MILLISECONDS_PER_DAY);
   1.728 +  },
   1.729 +});
   1.730 +

mercurial