|
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 this.EXPORTED_SYMBOLS = ["ProviderManager"]; |
|
9 |
|
10 const {classes: Cc, interfaces: Ci, utils: Cu} = Components; |
|
11 |
|
12 Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm"); |
|
13 #endif |
|
14 |
|
15 Cu.import("resource://gre/modules/Promise.jsm"); |
|
16 Cu.import("resource://gre/modules/Task.jsm"); |
|
17 Cu.import("resource://gre/modules/Log.jsm"); |
|
18 Cu.import("resource://services-common/utils.js"); |
|
19 |
|
20 |
|
21 /** |
|
22 * Handles and coordinates the collection of metrics data from providers. |
|
23 * |
|
24 * This provides an interface for managing `Metrics.Provider` instances. It |
|
25 * provides APIs for bulk collection of data. |
|
26 */ |
|
27 this.ProviderManager = function (storage) { |
|
28 this._log = Log.repository.getLogger("Services.Metrics.ProviderManager"); |
|
29 |
|
30 this._providers = new Map(); |
|
31 this._storage = storage; |
|
32 |
|
33 this._providerInitQueue = []; |
|
34 this._providerInitializing = false; |
|
35 |
|
36 this._pullOnlyProviders = {}; |
|
37 this._pullOnlyProvidersRegisterCount = 0; |
|
38 this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED; |
|
39 this._pullOnlyProvidersCurrentPromise = null; |
|
40 |
|
41 // Callback to allow customization of providers after they are constructed |
|
42 // but before they call out into their initialization code. |
|
43 this.onProviderInit = null; |
|
44 } |
|
45 |
|
46 this.ProviderManager.prototype = Object.freeze({ |
|
47 PULL_ONLY_NOT_REGISTERED: "none", |
|
48 PULL_ONLY_REGISTERING: "registering", |
|
49 PULL_ONLY_UNREGISTERING: "unregistering", |
|
50 PULL_ONLY_REGISTERED: "registered", |
|
51 |
|
52 get providers() { |
|
53 let providers = []; |
|
54 for (let [name, entry] of this._providers) { |
|
55 providers.push(entry.provider); |
|
56 } |
|
57 |
|
58 return providers; |
|
59 }, |
|
60 |
|
61 /** |
|
62 * Obtain a provider from its name. |
|
63 */ |
|
64 getProvider: function (name) { |
|
65 let provider = this._providers.get(name); |
|
66 |
|
67 if (!provider) { |
|
68 return null; |
|
69 } |
|
70 |
|
71 return provider.provider; |
|
72 }, |
|
73 |
|
74 /** |
|
75 * Registers providers from a category manager category. |
|
76 * |
|
77 * This examines the specified category entries and registers found |
|
78 * providers. |
|
79 * |
|
80 * Category entries are essentially JS modules and the name of the symbol |
|
81 * within that module that is a `Metrics.Provider` instance. |
|
82 * |
|
83 * The category entry name is the name of the JS type for the provider. The |
|
84 * value is the resource:// URI to import which makes this type available. |
|
85 * |
|
86 * Example entry: |
|
87 * |
|
88 * FooProvider resource://gre/modules/foo.jsm |
|
89 * |
|
90 * One can register entries in the application's .manifest file. e.g. |
|
91 * |
|
92 * category healthreport-js-provider-default FooProvider resource://gre/modules/foo.jsm |
|
93 * category healthreport-js-provider-nightly EyeballProvider resource://gre/modules/eyeball.jsm |
|
94 * |
|
95 * Then to load them: |
|
96 * |
|
97 * let reporter = getHealthReporter("healthreport."); |
|
98 * reporter.registerProvidersFromCategoryManager("healthreport-js-provider-default"); |
|
99 * |
|
100 * If the category has no defined members, this call has no effect, and no error is raised. |
|
101 * |
|
102 * @param category |
|
103 * (string) Name of category from which to query and load. |
|
104 * @return a newly spawned Task. |
|
105 */ |
|
106 registerProvidersFromCategoryManager: function (category) { |
|
107 this._log.info("Registering providers from category: " + category); |
|
108 let cm = Cc["@mozilla.org/categorymanager;1"] |
|
109 .getService(Ci.nsICategoryManager); |
|
110 |
|
111 let promises = []; |
|
112 let enumerator = cm.enumerateCategory(category); |
|
113 while (enumerator.hasMoreElements()) { |
|
114 let entry = enumerator.getNext() |
|
115 .QueryInterface(Ci.nsISupportsCString) |
|
116 .toString(); |
|
117 |
|
118 let uri = cm.getCategoryEntry(category, entry); |
|
119 this._log.info("Attempting to load provider from category manager: " + |
|
120 entry + " from " + uri); |
|
121 |
|
122 try { |
|
123 let ns = {}; |
|
124 Cu.import(uri, ns); |
|
125 |
|
126 let promise = this.registerProviderFromType(ns[entry]); |
|
127 if (promise) { |
|
128 promises.push(promise); |
|
129 } |
|
130 } catch (ex) { |
|
131 this._recordProviderError(entry, |
|
132 "Error registering provider from category manager", |
|
133 ex); |
|
134 continue; |
|
135 } |
|
136 } |
|
137 |
|
138 return Task.spawn(function wait() { |
|
139 for (let promise of promises) { |
|
140 yield promise; |
|
141 } |
|
142 }); |
|
143 }, |
|
144 |
|
145 /** |
|
146 * Registers a `MetricsProvider` with this manager. |
|
147 * |
|
148 * Once a `MetricsProvider` is registered, data will be collected from it |
|
149 * whenever we collect data. |
|
150 * |
|
151 * The returned value is a promise that will be resolved once registration |
|
152 * is complete. |
|
153 * |
|
154 * Providers are initialized as part of registration by calling |
|
155 * provider.init(). |
|
156 * |
|
157 * @param provider |
|
158 * (Metrics.Provider) The provider instance to register. |
|
159 * |
|
160 * @return Promise<null> |
|
161 */ |
|
162 registerProvider: function (provider) { |
|
163 // We should perform an instanceof check here. However, due to merged |
|
164 // compartments, the Provider type may belong to one of two JSMs |
|
165 // isinstance gets confused depending on which module Provider comes |
|
166 // from. Some code references Provider from dataprovider.jsm; others from |
|
167 // Metrics.jsm. |
|
168 if (!provider.name) { |
|
169 throw new Error("Provider is not valid: does not have a name."); |
|
170 } |
|
171 if (this._providers.has(provider.name)) { |
|
172 return CommonUtils.laterTickResolvingPromise(); |
|
173 } |
|
174 |
|
175 let deferred = Promise.defer(); |
|
176 this._providerInitQueue.push([provider, deferred]); |
|
177 |
|
178 if (this._providerInitQueue.length == 1) { |
|
179 this._popAndInitProvider(); |
|
180 } |
|
181 |
|
182 return deferred.promise; |
|
183 }, |
|
184 |
|
185 /** |
|
186 * Registers a provider from its constructor function. |
|
187 * |
|
188 * If the provider is pull-only, it will be stashed away and |
|
189 * initialized later. Null will be returned. |
|
190 * |
|
191 * If it is not pull-only, it will be initialized immediately and a |
|
192 * promise will be returned. The promise will be resolved when the |
|
193 * provider has finished initializing. |
|
194 */ |
|
195 registerProviderFromType: function (type) { |
|
196 let proto = type.prototype; |
|
197 if (proto.pullOnly) { |
|
198 this._log.info("Provider is pull-only. Deferring initialization: " + |
|
199 proto.name); |
|
200 this._pullOnlyProviders[proto.name] = type; |
|
201 |
|
202 return null; |
|
203 } |
|
204 |
|
205 let provider = this._initProviderFromType(type); |
|
206 return this.registerProvider(provider); |
|
207 }, |
|
208 |
|
209 /** |
|
210 * Initializes a provider from its type. |
|
211 * |
|
212 * This is how a constructor function should be turned into a provider |
|
213 * instance. |
|
214 * |
|
215 * A side-effect is the provider is registered with the manager. |
|
216 */ |
|
217 _initProviderFromType: function (type) { |
|
218 let provider = new type(); |
|
219 if (this.onProviderInit) { |
|
220 this.onProviderInit(provider); |
|
221 } |
|
222 |
|
223 return provider; |
|
224 }, |
|
225 |
|
226 /** |
|
227 * Remove a named provider from the manager. |
|
228 * |
|
229 * It is the caller's responsibility to shut down the provider |
|
230 * instance. |
|
231 */ |
|
232 unregisterProvider: function (name) { |
|
233 this._providers.delete(name); |
|
234 }, |
|
235 |
|
236 /** |
|
237 * Ensure that pull-only providers are registered. |
|
238 */ |
|
239 ensurePullOnlyProvidersRegistered: function () { |
|
240 let state = this._pullOnlyProvidersState; |
|
241 |
|
242 this._pullOnlyProvidersRegisterCount++; |
|
243 |
|
244 if (state == this.PULL_ONLY_REGISTERED) { |
|
245 this._log.debug("Requested pull-only provider registration and " + |
|
246 "providers are already registered."); |
|
247 return CommonUtils.laterTickResolvingPromise(); |
|
248 } |
|
249 |
|
250 // If we're in the process of registering, chain off that request. |
|
251 if (state == this.PULL_ONLY_REGISTERING) { |
|
252 this._log.debug("Requested pull-only provider registration and " + |
|
253 "registration is already in progress."); |
|
254 return this._pullOnlyProvidersCurrentPromise; |
|
255 } |
|
256 |
|
257 this._log.debug("Pull-only provider registration requested."); |
|
258 |
|
259 // A side-effect of setting this is that an active unregistration will |
|
260 // effectively short circuit and finish as soon as the in-flight |
|
261 // unregistration (if any) finishes. |
|
262 this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERING; |
|
263 |
|
264 let inFlightPromise = this._pullOnlyProvidersCurrentPromise; |
|
265 |
|
266 this._pullOnlyProvidersCurrentPromise = |
|
267 Task.spawn(function registerPullProviders() { |
|
268 |
|
269 if (inFlightPromise) { |
|
270 this._log.debug("Waiting for in-flight pull-only provider activity " + |
|
271 "to finish before registering."); |
|
272 try { |
|
273 yield inFlightPromise; |
|
274 } catch (ex) { |
|
275 this._log.warn("Error when waiting for existing pull-only promise: " + |
|
276 CommonUtils.exceptionStr(ex)); |
|
277 } |
|
278 } |
|
279 |
|
280 for each (let providerType in this._pullOnlyProviders) { |
|
281 // Short-circuit if we're no longer registering. |
|
282 if (this._pullOnlyProvidersState != this.PULL_ONLY_REGISTERING) { |
|
283 this._log.debug("Aborting pull-only provider registration."); |
|
284 break; |
|
285 } |
|
286 |
|
287 try { |
|
288 let provider = this._initProviderFromType(providerType); |
|
289 |
|
290 // This is a no-op if the provider is already registered. So, the |
|
291 // only overhead is constructing an instance. This should be cheap |
|
292 // and isn't worth optimizing. |
|
293 yield this.registerProvider(provider); |
|
294 } catch (ex) { |
|
295 this._recordProviderError(providerType.prototype.name, |
|
296 "Error registering pull-only provider", |
|
297 ex); |
|
298 } |
|
299 } |
|
300 |
|
301 // It's possible we changed state while registering. Only mark as |
|
302 // registered if we didn't change state. |
|
303 if (this._pullOnlyProvidersState == this.PULL_ONLY_REGISTERING) { |
|
304 this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERED; |
|
305 this._pullOnlyProvidersCurrentPromise = null; |
|
306 } |
|
307 }.bind(this)); |
|
308 return this._pullOnlyProvidersCurrentPromise; |
|
309 }, |
|
310 |
|
311 ensurePullOnlyProvidersUnregistered: function () { |
|
312 let state = this._pullOnlyProvidersState; |
|
313 |
|
314 // If we're not registered, this is a no-op. |
|
315 if (state == this.PULL_ONLY_NOT_REGISTERED) { |
|
316 this._log.debug("Requested pull-only provider unregistration but none " + |
|
317 "are registered."); |
|
318 return CommonUtils.laterTickResolvingPromise(); |
|
319 } |
|
320 |
|
321 // If we're currently unregistering, recycle the promise from last time. |
|
322 if (state == this.PULL_ONLY_UNREGISTERING) { |
|
323 this._log.debug("Requested pull-only provider unregistration and " + |
|
324 "unregistration is in progress."); |
|
325 this._pullOnlyProvidersRegisterCount = |
|
326 Math.max(0, this._pullOnlyProvidersRegisterCount - 1); |
|
327 |
|
328 return this._pullOnlyProvidersCurrentPromise; |
|
329 } |
|
330 |
|
331 // We ignore this request while multiple entities have requested |
|
332 // registration because we don't want a request from an "inner," |
|
333 // short-lived request to overwrite the desire of the "parent," |
|
334 // longer-lived request. |
|
335 if (this._pullOnlyProvidersRegisterCount > 1) { |
|
336 this._log.debug("Requested pull-only provider unregistration while " + |
|
337 "other callers still want them registered. Ignoring."); |
|
338 this._pullOnlyProvidersRegisterCount--; |
|
339 return CommonUtils.laterTickResolvingPromise(); |
|
340 } |
|
341 |
|
342 // We are either fully registered or registering with a single consumer. |
|
343 // In both cases we are authoritative and can commence unregistration. |
|
344 |
|
345 this._log.debug("Pull-only providers being unregistered."); |
|
346 this._pullOnlyProvidersRegisterCount = |
|
347 Math.max(0, this._pullOnlyProvidersRegisterCount - 1); |
|
348 this._pullOnlyProvidersState = this.PULL_ONLY_UNREGISTERING; |
|
349 let inFlightPromise = this._pullOnlyProvidersCurrentPromise; |
|
350 |
|
351 this._pullOnlyProvidersCurrentPromise = |
|
352 Task.spawn(function unregisterPullProviders() { |
|
353 |
|
354 if (inFlightPromise) { |
|
355 this._log.debug("Waiting for in-flight pull-only provider activity " + |
|
356 "to complete before unregistering."); |
|
357 try { |
|
358 yield inFlightPromise; |
|
359 } catch (ex) { |
|
360 this._log.warn("Error when waiting for existing pull-only promise: " + |
|
361 CommonUtils.exceptionStr(ex)); |
|
362 } |
|
363 } |
|
364 |
|
365 for (let provider of this.providers) { |
|
366 if (this._pullOnlyProvidersState != this.PULL_ONLY_UNREGISTERING) { |
|
367 return; |
|
368 } |
|
369 |
|
370 if (!provider.pullOnly) { |
|
371 continue; |
|
372 } |
|
373 |
|
374 this._log.info("Shutting down pull-only provider: " + |
|
375 provider.name); |
|
376 |
|
377 try { |
|
378 yield provider.shutdown(); |
|
379 } catch (ex) { |
|
380 this._recordProviderError(provider.name, |
|
381 "Error when shutting down provider", |
|
382 ex); |
|
383 } finally { |
|
384 this.unregisterProvider(provider.name); |
|
385 } |
|
386 } |
|
387 |
|
388 if (this._pullOnlyProvidersState == this.PULL_ONLY_UNREGISTERING) { |
|
389 this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED; |
|
390 this._pullOnlyProvidersCurrentPromise = null; |
|
391 } |
|
392 }.bind(this)); |
|
393 return this._pullOnlyProvidersCurrentPromise; |
|
394 }, |
|
395 |
|
396 _popAndInitProvider: function () { |
|
397 if (!this._providerInitQueue.length || this._providerInitializing) { |
|
398 return; |
|
399 } |
|
400 |
|
401 let [provider, deferred] = this._providerInitQueue.shift(); |
|
402 this._providerInitializing = true; |
|
403 |
|
404 this._log.info("Initializing provider with storage: " + provider.name); |
|
405 |
|
406 Task.spawn(function initProvider() { |
|
407 try { |
|
408 let result = yield provider.init(this._storage); |
|
409 this._log.info("Provider successfully initialized: " + provider.name); |
|
410 |
|
411 this._providers.set(provider.name, { |
|
412 provider: provider, |
|
413 constantsCollected: false, |
|
414 }); |
|
415 |
|
416 deferred.resolve(result); |
|
417 } catch (ex) { |
|
418 this._recordProviderError(provider.name, "Failed to initialize", ex); |
|
419 deferred.reject(ex); |
|
420 } finally { |
|
421 this._providerInitializing = false; |
|
422 this._popAndInitProvider(); |
|
423 } |
|
424 }.bind(this)); |
|
425 }, |
|
426 |
|
427 /** |
|
428 * Collects all constant measurements from all providers. |
|
429 * |
|
430 * Returns a Promise that will be fulfilled once all data providers have |
|
431 * provided their constant data. A side-effect of this promise fulfillment |
|
432 * is that the manager is populated with the obtained collection results. |
|
433 * The resolved value to the promise is this `ProviderManager` instance. |
|
434 */ |
|
435 collectConstantData: function () { |
|
436 let entries = []; |
|
437 |
|
438 for (let [name, entry] of this._providers) { |
|
439 if (entry.constantsCollected) { |
|
440 this._log.trace("Provider has already provided constant data: " + |
|
441 name); |
|
442 continue; |
|
443 } |
|
444 |
|
445 entries.push(entry); |
|
446 } |
|
447 |
|
448 let onCollect = function (entry, result) { |
|
449 entry.constantsCollected = true; |
|
450 }; |
|
451 |
|
452 return this._callCollectOnProviders(entries, "collectConstantData", |
|
453 onCollect); |
|
454 }, |
|
455 |
|
456 /** |
|
457 * Calls collectDailyData on all providers. |
|
458 */ |
|
459 collectDailyData: function () { |
|
460 return this._callCollectOnProviders(this._providers.values(), |
|
461 "collectDailyData"); |
|
462 }, |
|
463 |
|
464 _callCollectOnProviders: function (entries, fnProperty, onCollect=null) { |
|
465 let promises = []; |
|
466 |
|
467 for (let entry of entries) { |
|
468 let provider = entry.provider; |
|
469 let collectPromise; |
|
470 try { |
|
471 collectPromise = provider[fnProperty].call(provider); |
|
472 } catch (ex) { |
|
473 this._recordProviderError(provider.name, "Exception when calling " + |
|
474 "collect function: " + fnProperty, ex); |
|
475 continue; |
|
476 } |
|
477 |
|
478 if (!collectPromise) { |
|
479 this._recordProviderError(provider.name, "Does not return a promise " + |
|
480 "from " + fnProperty + "()"); |
|
481 continue; |
|
482 } |
|
483 |
|
484 let promise = collectPromise.then(function onCollected(result) { |
|
485 if (onCollect) { |
|
486 try { |
|
487 onCollect(entry, result); |
|
488 } catch (ex) { |
|
489 this._log.warn("onCollect callback threw: " + |
|
490 CommonUtils.exceptionStr(ex)); |
|
491 } |
|
492 } |
|
493 |
|
494 return CommonUtils.laterTickResolvingPromise(result); |
|
495 }); |
|
496 |
|
497 promises.push([provider.name, promise]); |
|
498 } |
|
499 |
|
500 return this._handleCollectionPromises(promises); |
|
501 }, |
|
502 |
|
503 /** |
|
504 * Handles promises returned by the collect* functions. |
|
505 * |
|
506 * This consumes the data resolved by the promises and returns a new promise |
|
507 * that will be resolved once all promises have been resolved. |
|
508 * |
|
509 * The promise is resolved even if one of the underlying collection |
|
510 * promises is rejected. |
|
511 */ |
|
512 _handleCollectionPromises: function (promises) { |
|
513 return Task.spawn(function waitForPromises() { |
|
514 for (let [name, promise] of promises) { |
|
515 try { |
|
516 yield promise; |
|
517 this._log.debug("Provider collected successfully: " + name); |
|
518 } catch (ex) { |
|
519 this._recordProviderError(name, "Failed to collect", ex); |
|
520 } |
|
521 } |
|
522 |
|
523 throw new Task.Result(this); |
|
524 }.bind(this)); |
|
525 }, |
|
526 |
|
527 /** |
|
528 * Record an error that occurred operating on a provider. |
|
529 */ |
|
530 _recordProviderError: function (name, msg, ex) { |
|
531 let msg = "Provider error: " + name + ": " + msg; |
|
532 if (ex) { |
|
533 msg += ": " + CommonUtils.exceptionStr(ex); |
|
534 } |
|
535 this._log.warn(msg); |
|
536 |
|
537 if (this.onProviderError) { |
|
538 try { |
|
539 this.onProviderError(msg); |
|
540 } catch (callError) { |
|
541 this._log.warn("Exception when calling onProviderError callback: " + |
|
542 CommonUtils.exceptionStr(callError)); |
|
543 } |
|
544 } |
|
545 }, |
|
546 }); |
|
547 |