1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/metrics/providermanager.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,547 @@ 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 +this.EXPORTED_SYMBOLS = ["ProviderManager"]; 1.12 + 1.13 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; 1.14 + 1.15 +Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm"); 1.16 +#endif 1.17 + 1.18 +Cu.import("resource://gre/modules/Promise.jsm"); 1.19 +Cu.import("resource://gre/modules/Task.jsm"); 1.20 +Cu.import("resource://gre/modules/Log.jsm"); 1.21 +Cu.import("resource://services-common/utils.js"); 1.22 + 1.23 + 1.24 +/** 1.25 + * Handles and coordinates the collection of metrics data from providers. 1.26 + * 1.27 + * This provides an interface for managing `Metrics.Provider` instances. It 1.28 + * provides APIs for bulk collection of data. 1.29 + */ 1.30 +this.ProviderManager = function (storage) { 1.31 + this._log = Log.repository.getLogger("Services.Metrics.ProviderManager"); 1.32 + 1.33 + this._providers = new Map(); 1.34 + this._storage = storage; 1.35 + 1.36 + this._providerInitQueue = []; 1.37 + this._providerInitializing = false; 1.38 + 1.39 + this._pullOnlyProviders = {}; 1.40 + this._pullOnlyProvidersRegisterCount = 0; 1.41 + this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED; 1.42 + this._pullOnlyProvidersCurrentPromise = null; 1.43 + 1.44 + // Callback to allow customization of providers after they are constructed 1.45 + // but before they call out into their initialization code. 1.46 + this.onProviderInit = null; 1.47 +} 1.48 + 1.49 +this.ProviderManager.prototype = Object.freeze({ 1.50 + PULL_ONLY_NOT_REGISTERED: "none", 1.51 + PULL_ONLY_REGISTERING: "registering", 1.52 + PULL_ONLY_UNREGISTERING: "unregistering", 1.53 + PULL_ONLY_REGISTERED: "registered", 1.54 + 1.55 + get providers() { 1.56 + let providers = []; 1.57 + for (let [name, entry] of this._providers) { 1.58 + providers.push(entry.provider); 1.59 + } 1.60 + 1.61 + return providers; 1.62 + }, 1.63 + 1.64 + /** 1.65 + * Obtain a provider from its name. 1.66 + */ 1.67 + getProvider: function (name) { 1.68 + let provider = this._providers.get(name); 1.69 + 1.70 + if (!provider) { 1.71 + return null; 1.72 + } 1.73 + 1.74 + return provider.provider; 1.75 + }, 1.76 + 1.77 + /** 1.78 + * Registers providers from a category manager category. 1.79 + * 1.80 + * This examines the specified category entries and registers found 1.81 + * providers. 1.82 + * 1.83 + * Category entries are essentially JS modules and the name of the symbol 1.84 + * within that module that is a `Metrics.Provider` instance. 1.85 + * 1.86 + * The category entry name is the name of the JS type for the provider. The 1.87 + * value is the resource:// URI to import which makes this type available. 1.88 + * 1.89 + * Example entry: 1.90 + * 1.91 + * FooProvider resource://gre/modules/foo.jsm 1.92 + * 1.93 + * One can register entries in the application's .manifest file. e.g. 1.94 + * 1.95 + * category healthreport-js-provider-default FooProvider resource://gre/modules/foo.jsm 1.96 + * category healthreport-js-provider-nightly EyeballProvider resource://gre/modules/eyeball.jsm 1.97 + * 1.98 + * Then to load them: 1.99 + * 1.100 + * let reporter = getHealthReporter("healthreport."); 1.101 + * reporter.registerProvidersFromCategoryManager("healthreport-js-provider-default"); 1.102 + * 1.103 + * If the category has no defined members, this call has no effect, and no error is raised. 1.104 + * 1.105 + * @param category 1.106 + * (string) Name of category from which to query and load. 1.107 + * @return a newly spawned Task. 1.108 + */ 1.109 + registerProvidersFromCategoryManager: function (category) { 1.110 + this._log.info("Registering providers from category: " + category); 1.111 + let cm = Cc["@mozilla.org/categorymanager;1"] 1.112 + .getService(Ci.nsICategoryManager); 1.113 + 1.114 + let promises = []; 1.115 + let enumerator = cm.enumerateCategory(category); 1.116 + while (enumerator.hasMoreElements()) { 1.117 + let entry = enumerator.getNext() 1.118 + .QueryInterface(Ci.nsISupportsCString) 1.119 + .toString(); 1.120 + 1.121 + let uri = cm.getCategoryEntry(category, entry); 1.122 + this._log.info("Attempting to load provider from category manager: " + 1.123 + entry + " from " + uri); 1.124 + 1.125 + try { 1.126 + let ns = {}; 1.127 + Cu.import(uri, ns); 1.128 + 1.129 + let promise = this.registerProviderFromType(ns[entry]); 1.130 + if (promise) { 1.131 + promises.push(promise); 1.132 + } 1.133 + } catch (ex) { 1.134 + this._recordProviderError(entry, 1.135 + "Error registering provider from category manager", 1.136 + ex); 1.137 + continue; 1.138 + } 1.139 + } 1.140 + 1.141 + return Task.spawn(function wait() { 1.142 + for (let promise of promises) { 1.143 + yield promise; 1.144 + } 1.145 + }); 1.146 + }, 1.147 + 1.148 + /** 1.149 + * Registers a `MetricsProvider` with this manager. 1.150 + * 1.151 + * Once a `MetricsProvider` is registered, data will be collected from it 1.152 + * whenever we collect data. 1.153 + * 1.154 + * The returned value is a promise that will be resolved once registration 1.155 + * is complete. 1.156 + * 1.157 + * Providers are initialized as part of registration by calling 1.158 + * provider.init(). 1.159 + * 1.160 + * @param provider 1.161 + * (Metrics.Provider) The provider instance to register. 1.162 + * 1.163 + * @return Promise<null> 1.164 + */ 1.165 + registerProvider: function (provider) { 1.166 + // We should perform an instanceof check here. However, due to merged 1.167 + // compartments, the Provider type may belong to one of two JSMs 1.168 + // isinstance gets confused depending on which module Provider comes 1.169 + // from. Some code references Provider from dataprovider.jsm; others from 1.170 + // Metrics.jsm. 1.171 + if (!provider.name) { 1.172 + throw new Error("Provider is not valid: does not have a name."); 1.173 + } 1.174 + if (this._providers.has(provider.name)) { 1.175 + return CommonUtils.laterTickResolvingPromise(); 1.176 + } 1.177 + 1.178 + let deferred = Promise.defer(); 1.179 + this._providerInitQueue.push([provider, deferred]); 1.180 + 1.181 + if (this._providerInitQueue.length == 1) { 1.182 + this._popAndInitProvider(); 1.183 + } 1.184 + 1.185 + return deferred.promise; 1.186 + }, 1.187 + 1.188 + /** 1.189 + * Registers a provider from its constructor function. 1.190 + * 1.191 + * If the provider is pull-only, it will be stashed away and 1.192 + * initialized later. Null will be returned. 1.193 + * 1.194 + * If it is not pull-only, it will be initialized immediately and a 1.195 + * promise will be returned. The promise will be resolved when the 1.196 + * provider has finished initializing. 1.197 + */ 1.198 + registerProviderFromType: function (type) { 1.199 + let proto = type.prototype; 1.200 + if (proto.pullOnly) { 1.201 + this._log.info("Provider is pull-only. Deferring initialization: " + 1.202 + proto.name); 1.203 + this._pullOnlyProviders[proto.name] = type; 1.204 + 1.205 + return null; 1.206 + } 1.207 + 1.208 + let provider = this._initProviderFromType(type); 1.209 + return this.registerProvider(provider); 1.210 + }, 1.211 + 1.212 + /** 1.213 + * Initializes a provider from its type. 1.214 + * 1.215 + * This is how a constructor function should be turned into a provider 1.216 + * instance. 1.217 + * 1.218 + * A side-effect is the provider is registered with the manager. 1.219 + */ 1.220 + _initProviderFromType: function (type) { 1.221 + let provider = new type(); 1.222 + if (this.onProviderInit) { 1.223 + this.onProviderInit(provider); 1.224 + } 1.225 + 1.226 + return provider; 1.227 + }, 1.228 + 1.229 + /** 1.230 + * Remove a named provider from the manager. 1.231 + * 1.232 + * It is the caller's responsibility to shut down the provider 1.233 + * instance. 1.234 + */ 1.235 + unregisterProvider: function (name) { 1.236 + this._providers.delete(name); 1.237 + }, 1.238 + 1.239 + /** 1.240 + * Ensure that pull-only providers are registered. 1.241 + */ 1.242 + ensurePullOnlyProvidersRegistered: function () { 1.243 + let state = this._pullOnlyProvidersState; 1.244 + 1.245 + this._pullOnlyProvidersRegisterCount++; 1.246 + 1.247 + if (state == this.PULL_ONLY_REGISTERED) { 1.248 + this._log.debug("Requested pull-only provider registration and " + 1.249 + "providers are already registered."); 1.250 + return CommonUtils.laterTickResolvingPromise(); 1.251 + } 1.252 + 1.253 + // If we're in the process of registering, chain off that request. 1.254 + if (state == this.PULL_ONLY_REGISTERING) { 1.255 + this._log.debug("Requested pull-only provider registration and " + 1.256 + "registration is already in progress."); 1.257 + return this._pullOnlyProvidersCurrentPromise; 1.258 + } 1.259 + 1.260 + this._log.debug("Pull-only provider registration requested."); 1.261 + 1.262 + // A side-effect of setting this is that an active unregistration will 1.263 + // effectively short circuit and finish as soon as the in-flight 1.264 + // unregistration (if any) finishes. 1.265 + this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERING; 1.266 + 1.267 + let inFlightPromise = this._pullOnlyProvidersCurrentPromise; 1.268 + 1.269 + this._pullOnlyProvidersCurrentPromise = 1.270 + Task.spawn(function registerPullProviders() { 1.271 + 1.272 + if (inFlightPromise) { 1.273 + this._log.debug("Waiting for in-flight pull-only provider activity " + 1.274 + "to finish before registering."); 1.275 + try { 1.276 + yield inFlightPromise; 1.277 + } catch (ex) { 1.278 + this._log.warn("Error when waiting for existing pull-only promise: " + 1.279 + CommonUtils.exceptionStr(ex)); 1.280 + } 1.281 + } 1.282 + 1.283 + for each (let providerType in this._pullOnlyProviders) { 1.284 + // Short-circuit if we're no longer registering. 1.285 + if (this._pullOnlyProvidersState != this.PULL_ONLY_REGISTERING) { 1.286 + this._log.debug("Aborting pull-only provider registration."); 1.287 + break; 1.288 + } 1.289 + 1.290 + try { 1.291 + let provider = this._initProviderFromType(providerType); 1.292 + 1.293 + // This is a no-op if the provider is already registered. So, the 1.294 + // only overhead is constructing an instance. This should be cheap 1.295 + // and isn't worth optimizing. 1.296 + yield this.registerProvider(provider); 1.297 + } catch (ex) { 1.298 + this._recordProviderError(providerType.prototype.name, 1.299 + "Error registering pull-only provider", 1.300 + ex); 1.301 + } 1.302 + } 1.303 + 1.304 + // It's possible we changed state while registering. Only mark as 1.305 + // registered if we didn't change state. 1.306 + if (this._pullOnlyProvidersState == this.PULL_ONLY_REGISTERING) { 1.307 + this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERED; 1.308 + this._pullOnlyProvidersCurrentPromise = null; 1.309 + } 1.310 + }.bind(this)); 1.311 + return this._pullOnlyProvidersCurrentPromise; 1.312 + }, 1.313 + 1.314 + ensurePullOnlyProvidersUnregistered: function () { 1.315 + let state = this._pullOnlyProvidersState; 1.316 + 1.317 + // If we're not registered, this is a no-op. 1.318 + if (state == this.PULL_ONLY_NOT_REGISTERED) { 1.319 + this._log.debug("Requested pull-only provider unregistration but none " + 1.320 + "are registered."); 1.321 + return CommonUtils.laterTickResolvingPromise(); 1.322 + } 1.323 + 1.324 + // If we're currently unregistering, recycle the promise from last time. 1.325 + if (state == this.PULL_ONLY_UNREGISTERING) { 1.326 + this._log.debug("Requested pull-only provider unregistration and " + 1.327 + "unregistration is in progress."); 1.328 + this._pullOnlyProvidersRegisterCount = 1.329 + Math.max(0, this._pullOnlyProvidersRegisterCount - 1); 1.330 + 1.331 + return this._pullOnlyProvidersCurrentPromise; 1.332 + } 1.333 + 1.334 + // We ignore this request while multiple entities have requested 1.335 + // registration because we don't want a request from an "inner," 1.336 + // short-lived request to overwrite the desire of the "parent," 1.337 + // longer-lived request. 1.338 + if (this._pullOnlyProvidersRegisterCount > 1) { 1.339 + this._log.debug("Requested pull-only provider unregistration while " + 1.340 + "other callers still want them registered. Ignoring."); 1.341 + this._pullOnlyProvidersRegisterCount--; 1.342 + return CommonUtils.laterTickResolvingPromise(); 1.343 + } 1.344 + 1.345 + // We are either fully registered or registering with a single consumer. 1.346 + // In both cases we are authoritative and can commence unregistration. 1.347 + 1.348 + this._log.debug("Pull-only providers being unregistered."); 1.349 + this._pullOnlyProvidersRegisterCount = 1.350 + Math.max(0, this._pullOnlyProvidersRegisterCount - 1); 1.351 + this._pullOnlyProvidersState = this.PULL_ONLY_UNREGISTERING; 1.352 + let inFlightPromise = this._pullOnlyProvidersCurrentPromise; 1.353 + 1.354 + this._pullOnlyProvidersCurrentPromise = 1.355 + Task.spawn(function unregisterPullProviders() { 1.356 + 1.357 + if (inFlightPromise) { 1.358 + this._log.debug("Waiting for in-flight pull-only provider activity " + 1.359 + "to complete before unregistering."); 1.360 + try { 1.361 + yield inFlightPromise; 1.362 + } catch (ex) { 1.363 + this._log.warn("Error when waiting for existing pull-only promise: " + 1.364 + CommonUtils.exceptionStr(ex)); 1.365 + } 1.366 + } 1.367 + 1.368 + for (let provider of this.providers) { 1.369 + if (this._pullOnlyProvidersState != this.PULL_ONLY_UNREGISTERING) { 1.370 + return; 1.371 + } 1.372 + 1.373 + if (!provider.pullOnly) { 1.374 + continue; 1.375 + } 1.376 + 1.377 + this._log.info("Shutting down pull-only provider: " + 1.378 + provider.name); 1.379 + 1.380 + try { 1.381 + yield provider.shutdown(); 1.382 + } catch (ex) { 1.383 + this._recordProviderError(provider.name, 1.384 + "Error when shutting down provider", 1.385 + ex); 1.386 + } finally { 1.387 + this.unregisterProvider(provider.name); 1.388 + } 1.389 + } 1.390 + 1.391 + if (this._pullOnlyProvidersState == this.PULL_ONLY_UNREGISTERING) { 1.392 + this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED; 1.393 + this._pullOnlyProvidersCurrentPromise = null; 1.394 + } 1.395 + }.bind(this)); 1.396 + return this._pullOnlyProvidersCurrentPromise; 1.397 + }, 1.398 + 1.399 + _popAndInitProvider: function () { 1.400 + if (!this._providerInitQueue.length || this._providerInitializing) { 1.401 + return; 1.402 + } 1.403 + 1.404 + let [provider, deferred] = this._providerInitQueue.shift(); 1.405 + this._providerInitializing = true; 1.406 + 1.407 + this._log.info("Initializing provider with storage: " + provider.name); 1.408 + 1.409 + Task.spawn(function initProvider() { 1.410 + try { 1.411 + let result = yield provider.init(this._storage); 1.412 + this._log.info("Provider successfully initialized: " + provider.name); 1.413 + 1.414 + this._providers.set(provider.name, { 1.415 + provider: provider, 1.416 + constantsCollected: false, 1.417 + }); 1.418 + 1.419 + deferred.resolve(result); 1.420 + } catch (ex) { 1.421 + this._recordProviderError(provider.name, "Failed to initialize", ex); 1.422 + deferred.reject(ex); 1.423 + } finally { 1.424 + this._providerInitializing = false; 1.425 + this._popAndInitProvider(); 1.426 + } 1.427 + }.bind(this)); 1.428 + }, 1.429 + 1.430 + /** 1.431 + * Collects all constant measurements from all providers. 1.432 + * 1.433 + * Returns a Promise that will be fulfilled once all data providers have 1.434 + * provided their constant data. A side-effect of this promise fulfillment 1.435 + * is that the manager is populated with the obtained collection results. 1.436 + * The resolved value to the promise is this `ProviderManager` instance. 1.437 + */ 1.438 + collectConstantData: function () { 1.439 + let entries = []; 1.440 + 1.441 + for (let [name, entry] of this._providers) { 1.442 + if (entry.constantsCollected) { 1.443 + this._log.trace("Provider has already provided constant data: " + 1.444 + name); 1.445 + continue; 1.446 + } 1.447 + 1.448 + entries.push(entry); 1.449 + } 1.450 + 1.451 + let onCollect = function (entry, result) { 1.452 + entry.constantsCollected = true; 1.453 + }; 1.454 + 1.455 + return this._callCollectOnProviders(entries, "collectConstantData", 1.456 + onCollect); 1.457 + }, 1.458 + 1.459 + /** 1.460 + * Calls collectDailyData on all providers. 1.461 + */ 1.462 + collectDailyData: function () { 1.463 + return this._callCollectOnProviders(this._providers.values(), 1.464 + "collectDailyData"); 1.465 + }, 1.466 + 1.467 + _callCollectOnProviders: function (entries, fnProperty, onCollect=null) { 1.468 + let promises = []; 1.469 + 1.470 + for (let entry of entries) { 1.471 + let provider = entry.provider; 1.472 + let collectPromise; 1.473 + try { 1.474 + collectPromise = provider[fnProperty].call(provider); 1.475 + } catch (ex) { 1.476 + this._recordProviderError(provider.name, "Exception when calling " + 1.477 + "collect function: " + fnProperty, ex); 1.478 + continue; 1.479 + } 1.480 + 1.481 + if (!collectPromise) { 1.482 + this._recordProviderError(provider.name, "Does not return a promise " + 1.483 + "from " + fnProperty + "()"); 1.484 + continue; 1.485 + } 1.486 + 1.487 + let promise = collectPromise.then(function onCollected(result) { 1.488 + if (onCollect) { 1.489 + try { 1.490 + onCollect(entry, result); 1.491 + } catch (ex) { 1.492 + this._log.warn("onCollect callback threw: " + 1.493 + CommonUtils.exceptionStr(ex)); 1.494 + } 1.495 + } 1.496 + 1.497 + return CommonUtils.laterTickResolvingPromise(result); 1.498 + }); 1.499 + 1.500 + promises.push([provider.name, promise]); 1.501 + } 1.502 + 1.503 + return this._handleCollectionPromises(promises); 1.504 + }, 1.505 + 1.506 + /** 1.507 + * Handles promises returned by the collect* functions. 1.508 + * 1.509 + * This consumes the data resolved by the promises and returns a new promise 1.510 + * that will be resolved once all promises have been resolved. 1.511 + * 1.512 + * The promise is resolved even if one of the underlying collection 1.513 + * promises is rejected. 1.514 + */ 1.515 + _handleCollectionPromises: function (promises) { 1.516 + return Task.spawn(function waitForPromises() { 1.517 + for (let [name, promise] of promises) { 1.518 + try { 1.519 + yield promise; 1.520 + this._log.debug("Provider collected successfully: " + name); 1.521 + } catch (ex) { 1.522 + this._recordProviderError(name, "Failed to collect", ex); 1.523 + } 1.524 + } 1.525 + 1.526 + throw new Task.Result(this); 1.527 + }.bind(this)); 1.528 + }, 1.529 + 1.530 + /** 1.531 + * Record an error that occurred operating on a provider. 1.532 + */ 1.533 + _recordProviderError: function (name, msg, ex) { 1.534 + let msg = "Provider error: " + name + ": " + msg; 1.535 + if (ex) { 1.536 + msg += ": " + CommonUtils.exceptionStr(ex); 1.537 + } 1.538 + this._log.warn(msg); 1.539 + 1.540 + if (this.onProviderError) { 1.541 + try { 1.542 + this.onProviderError(msg); 1.543 + } catch (callError) { 1.544 + this._log.warn("Exception when calling onProviderError callback: " + 1.545 + CommonUtils.exceptionStr(callError)); 1.546 + } 1.547 + } 1.548 + }, 1.549 +}); 1.550 +