|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 #ifndef MERGED_COMPARTMENT |
|
8 |
|
9 this.EXPORTED_SYMBOLS = [ |
|
10 "Measurement", |
|
11 "Provider", |
|
12 ]; |
|
13 |
|
14 const {utils: Cu} = Components; |
|
15 |
|
16 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; |
|
17 |
|
18 #endif |
|
19 |
|
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"); |
|
25 |
|
26 |
|
27 |
|
28 /** |
|
29 * Represents a collection of related pieces/fields of data. |
|
30 * |
|
31 * This is an abstract base type. |
|
32 * |
|
33 * This type provides the primary interface for storing, retrieving, and |
|
34 * serializing data. |
|
35 * |
|
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. |
|
38 * |
|
39 * Each derived type must define the following properties: |
|
40 * |
|
41 * name -- String name of this measurement. This is the primary way |
|
42 * measurements are distinguished within a provider. |
|
43 * |
|
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. |
|
50 * |
|
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: |
|
54 * |
|
55 * type -- The string type of this field. This is typically one of the |
|
56 * FIELD_* constants from the Metrics.Storage type. |
|
57 * |
|
58 * |
|
59 * FUTURE: provide hook points for measurements to supplement with custom |
|
60 * storage needs. |
|
61 */ |
|
62 this.Measurement = function () { |
|
63 if (!this.name) { |
|
64 throw new Error("Measurement must have a name."); |
|
65 } |
|
66 |
|
67 if (!this.version) { |
|
68 throw new Error("Measurement must have a version."); |
|
69 } |
|
70 |
|
71 if (!Number.isInteger(this.version)) { |
|
72 throw new Error("Measurement's version must be an integer: " + this.version); |
|
73 } |
|
74 |
|
75 if (!this.fields) { |
|
76 throw new Error("Measurement must define fields."); |
|
77 } |
|
78 |
|
79 for (let [name, info] in Iterator(this.fields)) { |
|
80 if (!info) { |
|
81 throw new Error("Field does not contain metadata: " + name); |
|
82 } |
|
83 |
|
84 if (!info.type) { |
|
85 throw new Error("Field is missing required type property: " + name); |
|
86 } |
|
87 } |
|
88 |
|
89 this._log = Log.repository.getLogger("Services.Metrics.Measurement." + this.name); |
|
90 |
|
91 this.id = null; |
|
92 this.storage = null; |
|
93 this._fields = {}; |
|
94 |
|
95 this._serializers = {}; |
|
96 this._serializers[this.SERIALIZE_JSON] = { |
|
97 singular: this._serializeJSONSingular.bind(this), |
|
98 daily: this._serializeJSONDay.bind(this), |
|
99 }; |
|
100 } |
|
101 |
|
102 Measurement.prototype = Object.freeze({ |
|
103 SERIALIZE_JSON: "json", |
|
104 |
|
105 /** |
|
106 * Obtain a serializer for this measurement. |
|
107 * |
|
108 * Implementations should return an object with the following keys: |
|
109 * |
|
110 * singular -- Serializer for singular data. |
|
111 * daily -- Serializer for daily data. |
|
112 * |
|
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. |
|
117 * |
|
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. |
|
120 * |
|
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. |
|
126 * |
|
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. |
|
131 * |
|
132 * @param format |
|
133 * (string) A SERIALIZE_* constant defining what serialization format |
|
134 * to use. |
|
135 */ |
|
136 serializer: function (format) { |
|
137 if (!(format in this._serializers)) { |
|
138 throw new Error("Don't know how to serialize format: " + format); |
|
139 } |
|
140 |
|
141 return this._serializers[format]; |
|
142 }, |
|
143 |
|
144 /** |
|
145 * Whether this measurement contains the named field. |
|
146 * |
|
147 * @param name |
|
148 * (string) Name of field. |
|
149 * |
|
150 * @return bool |
|
151 */ |
|
152 hasField: function (name) { |
|
153 return name in this.fields; |
|
154 }, |
|
155 |
|
156 /** |
|
157 * The unique identifier for a named field. |
|
158 * |
|
159 * This will throw if the field is not known. |
|
160 * |
|
161 * @param name |
|
162 * (string) Name of field. |
|
163 */ |
|
164 fieldID: function (name) { |
|
165 let entry = this._fields[name]; |
|
166 |
|
167 if (!entry) { |
|
168 throw new Error("Unknown field: " + name); |
|
169 } |
|
170 |
|
171 return entry[0]; |
|
172 }, |
|
173 |
|
174 fieldType: function (name) { |
|
175 let entry = this._fields[name]; |
|
176 |
|
177 if (!entry) { |
|
178 throw new Error("Unknown field: " + name); |
|
179 } |
|
180 |
|
181 return entry[1]; |
|
182 }, |
|
183 |
|
184 _configureStorage: function () { |
|
185 let missing = []; |
|
186 for (let [name, info] in Iterator(this.fields)) { |
|
187 if (this.storage.hasFieldFromMeasurement(this.id, name)) { |
|
188 this._fields[name] = |
|
189 [this.storage.fieldIDFromMeasurement(this.id, name), info.type]; |
|
190 continue; |
|
191 } |
|
192 |
|
193 missing.push([name, info.type]); |
|
194 } |
|
195 |
|
196 if (!missing.length) { |
|
197 return CommonUtils.laterTickResolvingPromise(); |
|
198 } |
|
199 |
|
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]; |
|
207 } |
|
208 }.bind(this)); |
|
209 }, |
|
210 |
|
211 //--------------------------------------------------------------------------- |
|
212 // Data Recording Functions |
|
213 // |
|
214 // Functions in this section are used to record new values against this |
|
215 // measurement instance. |
|
216 // |
|
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 //--------------------------------------------------------------------------- |
|
222 |
|
223 /** |
|
224 * Increment a daily counter field in this measurement by 1. |
|
225 * |
|
226 * By default, the counter for the current day will be incremented. |
|
227 * |
|
228 * If the field is not known or is not a daily counter, this will throw. |
|
229 * |
|
230 * |
|
231 * |
|
232 * @param field |
|
233 * (string) The name of the field whose value to increment. |
|
234 * @param date |
|
235 * (Date) Day on which to increment the counter. |
|
236 * @param by |
|
237 * (integer) How much to increment by. |
|
238 * @return Promise<> |
|
239 */ |
|
240 incrementDailyCounter: function (field, date=new Date(), by=1) { |
|
241 return this.storage.incrementDailyCounterFromFieldID(this.fieldID(field), |
|
242 date, by); |
|
243 }, |
|
244 |
|
245 /** |
|
246 * Record a new numeric value for a daily discrete numeric field. |
|
247 * |
|
248 * @param field |
|
249 * (string) The name of the field to append a value to. |
|
250 * @param value |
|
251 * (Number) Number to append. |
|
252 * @param date |
|
253 * (Date) Day on which to append the value. |
|
254 * |
|
255 * @return Promise<> |
|
256 */ |
|
257 addDailyDiscreteNumeric: function (field, value, date=new Date()) { |
|
258 return this.storage.addDailyDiscreteNumericFromFieldID( |
|
259 this.fieldID(field), value, date); |
|
260 }, |
|
261 |
|
262 /** |
|
263 * Record a new text value for a daily discrete text field. |
|
264 * |
|
265 * This is like `addDailyDiscreteNumeric` but for daily discrete text fields. |
|
266 */ |
|
267 addDailyDiscreteText: function (field, value, date=new Date()) { |
|
268 return this.storage.addDailyDiscreteTextFromFieldID( |
|
269 this.fieldID(field), value, date); |
|
270 }, |
|
271 |
|
272 /** |
|
273 * Record the last seen value for a last numeric field. |
|
274 * |
|
275 * @param field |
|
276 * (string) The name of the field to set the value of. |
|
277 * @param value |
|
278 * (Number) The value to set. |
|
279 * @param date |
|
280 * (Date) When this value was recorded. |
|
281 * |
|
282 * @return Promise<> |
|
283 */ |
|
284 setLastNumeric: function (field, value, date=new Date()) { |
|
285 return this.storage.setLastNumericFromFieldID(this.fieldID(field), value, |
|
286 date); |
|
287 }, |
|
288 |
|
289 /** |
|
290 * Record the last seen value for a last text field. |
|
291 * |
|
292 * This is like `setLastNumeric` except for last text fields. |
|
293 */ |
|
294 setLastText: function (field, value, date=new Date()) { |
|
295 return this.storage.setLastTextFromFieldID(this.fieldID(field), value, |
|
296 date); |
|
297 }, |
|
298 |
|
299 /** |
|
300 * Record the most recent value for a daily last numeric field. |
|
301 * |
|
302 * @param field |
|
303 * (string) The name of a daily last numeric field. |
|
304 * @param value |
|
305 * (Number) The value to set. |
|
306 * @param date |
|
307 * (Date) Day on which to record the last value. |
|
308 * |
|
309 * @return Promise<> |
|
310 */ |
|
311 setDailyLastNumeric: function (field, value, date=new Date()) { |
|
312 return this.storage.setDailyLastNumericFromFieldID(this.fieldID(field), |
|
313 value, date); |
|
314 }, |
|
315 |
|
316 /** |
|
317 * Record the most recent value for a daily last text field. |
|
318 * |
|
319 * This is like `setDailyLastNumeric` except for a daily last text field. |
|
320 */ |
|
321 setDailyLastText: function (field, value, date=new Date()) { |
|
322 return this.storage.setDailyLastTextFromFieldID(this.fieldID(field), |
|
323 value, date); |
|
324 }, |
|
325 |
|
326 //--------------------------------------------------------------------------- |
|
327 // End of data recording APIs. |
|
328 //--------------------------------------------------------------------------- |
|
329 |
|
330 /** |
|
331 * Obtain all values stored for this measurement. |
|
332 * |
|
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. |
|
336 * |
|
337 * This returns a promise that resolves to a data structure which is |
|
338 * understood by the measurement's serialize() function. |
|
339 */ |
|
340 getValues: function () { |
|
341 return this.storage.getMeasurementValues(this.id); |
|
342 }, |
|
343 |
|
344 deleteLastNumeric: function (field) { |
|
345 return this.storage.deleteLastNumericFromFieldID(this.fieldID(field)); |
|
346 }, |
|
347 |
|
348 deleteLastText: function (field) { |
|
349 return this.storage.deleteLastTextFromFieldID(this.fieldID(field)); |
|
350 }, |
|
351 |
|
352 /** |
|
353 * This method is used by the default serializers to control whether a field |
|
354 * is included in the output. |
|
355 * |
|
356 * There could be legacy fields in storage we no longer care about. |
|
357 * |
|
358 * This method is a hook to allow measurements to change this behavior, e.g., |
|
359 * to implement a dynamic fieldset. |
|
360 * |
|
361 * You will also need to override `fieldType`. |
|
362 * |
|
363 * @return (boolean) true if the specified field should be included in |
|
364 * payload output. |
|
365 */ |
|
366 shouldIncludeField: function (field) { |
|
367 return field in this._fields; |
|
368 }, |
|
369 |
|
370 _serializeJSONSingular: function (data) { |
|
371 let result = {"_v": this.version}; |
|
372 |
|
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)) { |
|
376 continue; |
|
377 } |
|
378 |
|
379 let type = this.fieldType(field); |
|
380 |
|
381 switch (type) { |
|
382 case this.storage.FIELD_LAST_NUMERIC: |
|
383 case this.storage.FIELD_LAST_TEXT: |
|
384 result[field] = data[1]; |
|
385 break; |
|
386 |
|
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: |
|
392 continue; |
|
393 |
|
394 default: |
|
395 throw new Error("Unknown field type: " + type); |
|
396 } |
|
397 } |
|
398 |
|
399 return result; |
|
400 }, |
|
401 |
|
402 _serializeJSONDay: function (data) { |
|
403 let result = {"_v": this.version}; |
|
404 |
|
405 for (let [field, data] of data) { |
|
406 if (!this.shouldIncludeField(field)) { |
|
407 continue; |
|
408 } |
|
409 |
|
410 let type = this.fieldType(field); |
|
411 |
|
412 switch (type) { |
|
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; |
|
419 break; |
|
420 |
|
421 case this.storage.FIELD_LAST_NUMERIC: |
|
422 case this.storage.FIELD_LAST_TEXT: |
|
423 continue; |
|
424 |
|
425 default: |
|
426 throw new Error("Unknown field type: " + type); |
|
427 } |
|
428 } |
|
429 |
|
430 return result; |
|
431 }, |
|
432 }); |
|
433 |
|
434 |
|
435 /** |
|
436 * An entity that emits data. |
|
437 * |
|
438 * A `Provider` consists of a string name (must be globally unique among all |
|
439 * known providers) and a set of `Measurement` instances. |
|
440 * |
|
441 * The main role of a `Provider` is to produce metrics data and to store said |
|
442 * data in the storage backend. |
|
443 * |
|
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. |
|
447 * |
|
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. |
|
453 * |
|
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. |
|
459 * |
|
460 * This typically looks something like: |
|
461 * |
|
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); |
|
467 * |
|
468 * this.enqueueStorageOperation(function storeExternalEvent() { |
|
469 * |
|
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); |
|
477 * }.bind(this)); |
|
478 * |
|
479 * } |
|
480 * |
|
481 * |
|
482 * `Provider` is an abstract base class. Implementations must define a few |
|
483 * properties: |
|
484 * |
|
485 * name |
|
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. |
|
489 * |
|
490 * measurementTypes |
|
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. |
|
495 */ |
|
496 this.Provider = function () { |
|
497 if (!this.name) { |
|
498 throw new Error("Provider must define a name."); |
|
499 } |
|
500 |
|
501 if (!Array.isArray(this.measurementTypes)) { |
|
502 throw new Error("Provider must define measurement types."); |
|
503 } |
|
504 |
|
505 this._log = Log.repository.getLogger("Services.Metrics.Provider." + this.name); |
|
506 |
|
507 this.measurements = null; |
|
508 this.storage = null; |
|
509 } |
|
510 |
|
511 Provider.prototype = Object.freeze({ |
|
512 /** |
|
513 * Whether the provider only pulls data from other sources. |
|
514 * |
|
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. |
|
518 * |
|
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. |
|
523 * |
|
524 * This must be set on the prototype to have an effect. |
|
525 */ |
|
526 pullOnly: false, |
|
527 |
|
528 /** |
|
529 * Obtain a `Measurement` from its name and version. |
|
530 * |
|
531 * If the measurement is not found, an Error is thrown. |
|
532 */ |
|
533 getMeasurement: function (name, version) { |
|
534 if (!Number.isInteger(version)) { |
|
535 throw new Error("getMeasurement expects an integer version. Got: " + version); |
|
536 } |
|
537 |
|
538 let m = this.measurements.get([name, version].join(":")); |
|
539 |
|
540 if (!m) { |
|
541 throw new Error("Unknown measurement: " + name + " v" + version); |
|
542 } |
|
543 |
|
544 return m; |
|
545 }, |
|
546 |
|
547 init: function (storage) { |
|
548 if (this.storage !== null) { |
|
549 throw new Error("Provider() not called. Did the sub-type forget to call it?"); |
|
550 } |
|
551 |
|
552 if (this.storage) { |
|
553 throw new Error("Provider has already been initialized."); |
|
554 } |
|
555 |
|
556 this.measurements = new Map(); |
|
557 this.storage = storage; |
|
558 |
|
559 let self = this; |
|
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."); |
|
564 } |
|
565 yield pre; |
|
566 |
|
567 for (let measurementType of self.measurementTypes) { |
|
568 let measurement = new measurementType(); |
|
569 |
|
570 measurement.provider = self; |
|
571 measurement.storage = self.storage; |
|
572 |
|
573 let id = yield storage.registerMeasurement(self.name, measurement.name, |
|
574 measurement.version); |
|
575 |
|
576 measurement.id = id; |
|
577 |
|
578 yield measurement._configureStorage(); |
|
579 |
|
580 self.measurements.set([measurement.name, measurement.version].join(":"), |
|
581 measurement); |
|
582 } |
|
583 |
|
584 let post = self.postInit(); |
|
585 if (!post || typeof(post.then) != "function") { |
|
586 throw new Error("postInit() does not return a promise."); |
|
587 } |
|
588 yield post; |
|
589 }); |
|
590 }, |
|
591 |
|
592 shutdown: function () { |
|
593 let promise = this.onShutdown(); |
|
594 |
|
595 if (!promise || typeof(promise.then) != "function") { |
|
596 throw new Error("onShutdown implementation does not return a promise."); |
|
597 } |
|
598 |
|
599 return promise; |
|
600 }, |
|
601 |
|
602 /** |
|
603 * Hook point for implementations to perform pre-initialization activity. |
|
604 * |
|
605 * This method will be called before measurement registration. |
|
606 * |
|
607 * Implementations should return a promise which is resolved when |
|
608 * initialization activities have completed. |
|
609 */ |
|
610 preInit: function () { |
|
611 return CommonUtils.laterTickResolvingPromise(); |
|
612 }, |
|
613 |
|
614 /** |
|
615 * Hook point for implementations to perform post-initialization activity. |
|
616 * |
|
617 * This method will be called after `preInit` and measurement registration, |
|
618 * but before initialization is finished. |
|
619 * |
|
620 * If a `Provider` instance needs to register observers, etc, it should |
|
621 * implement this function. |
|
622 * |
|
623 * Implementations should return a promise which is resolved when |
|
624 * initialization activities have completed. |
|
625 */ |
|
626 postInit: function () { |
|
627 return CommonUtils.laterTickResolvingPromise(); |
|
628 }, |
|
629 |
|
630 /** |
|
631 * Hook point for shutdown of instances. |
|
632 * |
|
633 * This is the opposite of `onInit`. If a `Provider` needs to unregister |
|
634 * observers, etc, this is where it should do it. |
|
635 * |
|
636 * Implementations should return a promise which is resolved when |
|
637 * shutdown activities have completed. |
|
638 */ |
|
639 onShutdown: function () { |
|
640 return CommonUtils.laterTickResolvingPromise(); |
|
641 }, |
|
642 |
|
643 /** |
|
644 * Collects data that doesn't change during the application's lifetime. |
|
645 * |
|
646 * Implementations should return a promise that resolves when all data has |
|
647 * been collected and storage operations have been finished. |
|
648 * |
|
649 * @return Promise<> |
|
650 */ |
|
651 collectConstantData: function () { |
|
652 return CommonUtils.laterTickResolvingPromise(); |
|
653 }, |
|
654 |
|
655 /** |
|
656 * Collects data approximately every day. |
|
657 * |
|
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. |
|
661 * |
|
662 * Implementations should return a promise that resolves when all data has |
|
663 * been collected and storage operations have completed. |
|
664 * |
|
665 * @return Promise<> |
|
666 */ |
|
667 collectDailyData: function () { |
|
668 return CommonUtils.laterTickResolvingPromise(); |
|
669 }, |
|
670 |
|
671 /** |
|
672 * Queue a deferred storage operation. |
|
673 * |
|
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. |
|
679 * |
|
680 * The passed function should return a promise which will be resolved upon |
|
681 * completion of storage interaction. |
|
682 */ |
|
683 enqueueStorageOperation: function (func) { |
|
684 return this.storage.enqueueOperation(func); |
|
685 }, |
|
686 |
|
687 /** |
|
688 * Obtain persisted provider state. |
|
689 * |
|
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. |
|
693 * |
|
694 * State is backed by storage and is robust. |
|
695 * |
|
696 * These functions do not enqueue on storage automatically, so they should |
|
697 * be guarded by `enqueueStorageOperation` or some other mutex. |
|
698 * |
|
699 * @param key |
|
700 * (string) The property to retrieve. |
|
701 * |
|
702 * @return Promise<string|null> String value on success. null if no state |
|
703 * is available under this key. |
|
704 */ |
|
705 getState: function (key) { |
|
706 return this.storage.getProviderState(this.name, key); |
|
707 }, |
|
708 |
|
709 /** |
|
710 * Set state for this provider. |
|
711 * |
|
712 * This is the complementary API for `getState` and obeys the same |
|
713 * storage restrictions. |
|
714 */ |
|
715 setState: function (key, value) { |
|
716 return this.storage.setProviderState(this.name, key, value); |
|
717 }, |
|
718 |
|
719 _dateToDays: function (date) { |
|
720 return Math.floor(date.getTime() / MILLISECONDS_PER_DAY); |
|
721 }, |
|
722 |
|
723 _daysToDate: function (days) { |
|
724 return new Date(days * MILLISECONDS_PER_DAY); |
|
725 }, |
|
726 }); |
|
727 |