michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: #ifndef MERGED_COMPARTMENT michael@0: this.EXPORTED_SYMBOLS = ["ProviderManager"]; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm"); michael@0: #endif michael@0: michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: Cu.import("resource://services-common/utils.js"); michael@0: michael@0: michael@0: /** michael@0: * Handles and coordinates the collection of metrics data from providers. michael@0: * michael@0: * This provides an interface for managing `Metrics.Provider` instances. It michael@0: * provides APIs for bulk collection of data. michael@0: */ michael@0: this.ProviderManager = function (storage) { michael@0: this._log = Log.repository.getLogger("Services.Metrics.ProviderManager"); michael@0: michael@0: this._providers = new Map(); michael@0: this._storage = storage; michael@0: michael@0: this._providerInitQueue = []; michael@0: this._providerInitializing = false; michael@0: michael@0: this._pullOnlyProviders = {}; michael@0: this._pullOnlyProvidersRegisterCount = 0; michael@0: this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED; michael@0: this._pullOnlyProvidersCurrentPromise = null; michael@0: michael@0: // Callback to allow customization of providers after they are constructed michael@0: // but before they call out into their initialization code. michael@0: this.onProviderInit = null; michael@0: } michael@0: michael@0: this.ProviderManager.prototype = Object.freeze({ michael@0: PULL_ONLY_NOT_REGISTERED: "none", michael@0: PULL_ONLY_REGISTERING: "registering", michael@0: PULL_ONLY_UNREGISTERING: "unregistering", michael@0: PULL_ONLY_REGISTERED: "registered", michael@0: michael@0: get providers() { michael@0: let providers = []; michael@0: for (let [name, entry] of this._providers) { michael@0: providers.push(entry.provider); michael@0: } michael@0: michael@0: return providers; michael@0: }, michael@0: michael@0: /** michael@0: * Obtain a provider from its name. michael@0: */ michael@0: getProvider: function (name) { michael@0: let provider = this._providers.get(name); michael@0: michael@0: if (!provider) { michael@0: return null; michael@0: } michael@0: michael@0: return provider.provider; michael@0: }, michael@0: michael@0: /** michael@0: * Registers providers from a category manager category. michael@0: * michael@0: * This examines the specified category entries and registers found michael@0: * providers. michael@0: * michael@0: * Category entries are essentially JS modules and the name of the symbol michael@0: * within that module that is a `Metrics.Provider` instance. michael@0: * michael@0: * The category entry name is the name of the JS type for the provider. The michael@0: * value is the resource:// URI to import which makes this type available. michael@0: * michael@0: * Example entry: michael@0: * michael@0: * FooProvider resource://gre/modules/foo.jsm michael@0: * michael@0: * One can register entries in the application's .manifest file. e.g. michael@0: * michael@0: * category healthreport-js-provider-default FooProvider resource://gre/modules/foo.jsm michael@0: * category healthreport-js-provider-nightly EyeballProvider resource://gre/modules/eyeball.jsm michael@0: * michael@0: * Then to load them: michael@0: * michael@0: * let reporter = getHealthReporter("healthreport."); michael@0: * reporter.registerProvidersFromCategoryManager("healthreport-js-provider-default"); michael@0: * michael@0: * If the category has no defined members, this call has no effect, and no error is raised. michael@0: * michael@0: * @param category michael@0: * (string) Name of category from which to query and load. michael@0: * @return a newly spawned Task. michael@0: */ michael@0: registerProvidersFromCategoryManager: function (category) { michael@0: this._log.info("Registering providers from category: " + category); michael@0: let cm = Cc["@mozilla.org/categorymanager;1"] michael@0: .getService(Ci.nsICategoryManager); michael@0: michael@0: let promises = []; michael@0: let enumerator = cm.enumerateCategory(category); michael@0: while (enumerator.hasMoreElements()) { michael@0: let entry = enumerator.getNext() michael@0: .QueryInterface(Ci.nsISupportsCString) michael@0: .toString(); michael@0: michael@0: let uri = cm.getCategoryEntry(category, entry); michael@0: this._log.info("Attempting to load provider from category manager: " + michael@0: entry + " from " + uri); michael@0: michael@0: try { michael@0: let ns = {}; michael@0: Cu.import(uri, ns); michael@0: michael@0: let promise = this.registerProviderFromType(ns[entry]); michael@0: if (promise) { michael@0: promises.push(promise); michael@0: } michael@0: } catch (ex) { michael@0: this._recordProviderError(entry, michael@0: "Error registering provider from category manager", michael@0: ex); michael@0: continue; michael@0: } michael@0: } michael@0: michael@0: return Task.spawn(function wait() { michael@0: for (let promise of promises) { michael@0: yield promise; michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Registers a `MetricsProvider` with this manager. michael@0: * michael@0: * Once a `MetricsProvider` is registered, data will be collected from it michael@0: * whenever we collect data. michael@0: * michael@0: * The returned value is a promise that will be resolved once registration michael@0: * is complete. michael@0: * michael@0: * Providers are initialized as part of registration by calling michael@0: * provider.init(). michael@0: * michael@0: * @param provider michael@0: * (Metrics.Provider) The provider instance to register. michael@0: * michael@0: * @return Promise michael@0: */ michael@0: registerProvider: function (provider) { michael@0: // We should perform an instanceof check here. However, due to merged michael@0: // compartments, the Provider type may belong to one of two JSMs michael@0: // isinstance gets confused depending on which module Provider comes michael@0: // from. Some code references Provider from dataprovider.jsm; others from michael@0: // Metrics.jsm. michael@0: if (!provider.name) { michael@0: throw new Error("Provider is not valid: does not have a name."); michael@0: } michael@0: if (this._providers.has(provider.name)) { michael@0: return CommonUtils.laterTickResolvingPromise(); michael@0: } michael@0: michael@0: let deferred = Promise.defer(); michael@0: this._providerInitQueue.push([provider, deferred]); michael@0: michael@0: if (this._providerInitQueue.length == 1) { michael@0: this._popAndInitProvider(); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Registers a provider from its constructor function. michael@0: * michael@0: * If the provider is pull-only, it will be stashed away and michael@0: * initialized later. Null will be returned. michael@0: * michael@0: * If it is not pull-only, it will be initialized immediately and a michael@0: * promise will be returned. The promise will be resolved when the michael@0: * provider has finished initializing. michael@0: */ michael@0: registerProviderFromType: function (type) { michael@0: let proto = type.prototype; michael@0: if (proto.pullOnly) { michael@0: this._log.info("Provider is pull-only. Deferring initialization: " + michael@0: proto.name); michael@0: this._pullOnlyProviders[proto.name] = type; michael@0: michael@0: return null; michael@0: } michael@0: michael@0: let provider = this._initProviderFromType(type); michael@0: return this.registerProvider(provider); michael@0: }, michael@0: michael@0: /** michael@0: * Initializes a provider from its type. michael@0: * michael@0: * This is how a constructor function should be turned into a provider michael@0: * instance. michael@0: * michael@0: * A side-effect is the provider is registered with the manager. michael@0: */ michael@0: _initProviderFromType: function (type) { michael@0: let provider = new type(); michael@0: if (this.onProviderInit) { michael@0: this.onProviderInit(provider); michael@0: } michael@0: michael@0: return provider; michael@0: }, michael@0: michael@0: /** michael@0: * Remove a named provider from the manager. michael@0: * michael@0: * It is the caller's responsibility to shut down the provider michael@0: * instance. michael@0: */ michael@0: unregisterProvider: function (name) { michael@0: this._providers.delete(name); michael@0: }, michael@0: michael@0: /** michael@0: * Ensure that pull-only providers are registered. michael@0: */ michael@0: ensurePullOnlyProvidersRegistered: function () { michael@0: let state = this._pullOnlyProvidersState; michael@0: michael@0: this._pullOnlyProvidersRegisterCount++; michael@0: michael@0: if (state == this.PULL_ONLY_REGISTERED) { michael@0: this._log.debug("Requested pull-only provider registration and " + michael@0: "providers are already registered."); michael@0: return CommonUtils.laterTickResolvingPromise(); michael@0: } michael@0: michael@0: // If we're in the process of registering, chain off that request. michael@0: if (state == this.PULL_ONLY_REGISTERING) { michael@0: this._log.debug("Requested pull-only provider registration and " + michael@0: "registration is already in progress."); michael@0: return this._pullOnlyProvidersCurrentPromise; michael@0: } michael@0: michael@0: this._log.debug("Pull-only provider registration requested."); michael@0: michael@0: // A side-effect of setting this is that an active unregistration will michael@0: // effectively short circuit and finish as soon as the in-flight michael@0: // unregistration (if any) finishes. michael@0: this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERING; michael@0: michael@0: let inFlightPromise = this._pullOnlyProvidersCurrentPromise; michael@0: michael@0: this._pullOnlyProvidersCurrentPromise = michael@0: Task.spawn(function registerPullProviders() { michael@0: michael@0: if (inFlightPromise) { michael@0: this._log.debug("Waiting for in-flight pull-only provider activity " + michael@0: "to finish before registering."); michael@0: try { michael@0: yield inFlightPromise; michael@0: } catch (ex) { michael@0: this._log.warn("Error when waiting for existing pull-only promise: " + michael@0: CommonUtils.exceptionStr(ex)); michael@0: } michael@0: } michael@0: michael@0: for each (let providerType in this._pullOnlyProviders) { michael@0: // Short-circuit if we're no longer registering. michael@0: if (this._pullOnlyProvidersState != this.PULL_ONLY_REGISTERING) { michael@0: this._log.debug("Aborting pull-only provider registration."); michael@0: break; michael@0: } michael@0: michael@0: try { michael@0: let provider = this._initProviderFromType(providerType); michael@0: michael@0: // This is a no-op if the provider is already registered. So, the michael@0: // only overhead is constructing an instance. This should be cheap michael@0: // and isn't worth optimizing. michael@0: yield this.registerProvider(provider); michael@0: } catch (ex) { michael@0: this._recordProviderError(providerType.prototype.name, michael@0: "Error registering pull-only provider", michael@0: ex); michael@0: } michael@0: } michael@0: michael@0: // It's possible we changed state while registering. Only mark as michael@0: // registered if we didn't change state. michael@0: if (this._pullOnlyProvidersState == this.PULL_ONLY_REGISTERING) { michael@0: this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERED; michael@0: this._pullOnlyProvidersCurrentPromise = null; michael@0: } michael@0: }.bind(this)); michael@0: return this._pullOnlyProvidersCurrentPromise; michael@0: }, michael@0: michael@0: ensurePullOnlyProvidersUnregistered: function () { michael@0: let state = this._pullOnlyProvidersState; michael@0: michael@0: // If we're not registered, this is a no-op. michael@0: if (state == this.PULL_ONLY_NOT_REGISTERED) { michael@0: this._log.debug("Requested pull-only provider unregistration but none " + michael@0: "are registered."); michael@0: return CommonUtils.laterTickResolvingPromise(); michael@0: } michael@0: michael@0: // If we're currently unregistering, recycle the promise from last time. michael@0: if (state == this.PULL_ONLY_UNREGISTERING) { michael@0: this._log.debug("Requested pull-only provider unregistration and " + michael@0: "unregistration is in progress."); michael@0: this._pullOnlyProvidersRegisterCount = michael@0: Math.max(0, this._pullOnlyProvidersRegisterCount - 1); michael@0: michael@0: return this._pullOnlyProvidersCurrentPromise; michael@0: } michael@0: michael@0: // We ignore this request while multiple entities have requested michael@0: // registration because we don't want a request from an "inner," michael@0: // short-lived request to overwrite the desire of the "parent," michael@0: // longer-lived request. michael@0: if (this._pullOnlyProvidersRegisterCount > 1) { michael@0: this._log.debug("Requested pull-only provider unregistration while " + michael@0: "other callers still want them registered. Ignoring."); michael@0: this._pullOnlyProvidersRegisterCount--; michael@0: return CommonUtils.laterTickResolvingPromise(); michael@0: } michael@0: michael@0: // We are either fully registered or registering with a single consumer. michael@0: // In both cases we are authoritative and can commence unregistration. michael@0: michael@0: this._log.debug("Pull-only providers being unregistered."); michael@0: this._pullOnlyProvidersRegisterCount = michael@0: Math.max(0, this._pullOnlyProvidersRegisterCount - 1); michael@0: this._pullOnlyProvidersState = this.PULL_ONLY_UNREGISTERING; michael@0: let inFlightPromise = this._pullOnlyProvidersCurrentPromise; michael@0: michael@0: this._pullOnlyProvidersCurrentPromise = michael@0: Task.spawn(function unregisterPullProviders() { michael@0: michael@0: if (inFlightPromise) { michael@0: this._log.debug("Waiting for in-flight pull-only provider activity " + michael@0: "to complete before unregistering."); michael@0: try { michael@0: yield inFlightPromise; michael@0: } catch (ex) { michael@0: this._log.warn("Error when waiting for existing pull-only promise: " + michael@0: CommonUtils.exceptionStr(ex)); michael@0: } michael@0: } michael@0: michael@0: for (let provider of this.providers) { michael@0: if (this._pullOnlyProvidersState != this.PULL_ONLY_UNREGISTERING) { michael@0: return; michael@0: } michael@0: michael@0: if (!provider.pullOnly) { michael@0: continue; michael@0: } michael@0: michael@0: this._log.info("Shutting down pull-only provider: " + michael@0: provider.name); michael@0: michael@0: try { michael@0: yield provider.shutdown(); michael@0: } catch (ex) { michael@0: this._recordProviderError(provider.name, michael@0: "Error when shutting down provider", michael@0: ex); michael@0: } finally { michael@0: this.unregisterProvider(provider.name); michael@0: } michael@0: } michael@0: michael@0: if (this._pullOnlyProvidersState == this.PULL_ONLY_UNREGISTERING) { michael@0: this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED; michael@0: this._pullOnlyProvidersCurrentPromise = null; michael@0: } michael@0: }.bind(this)); michael@0: return this._pullOnlyProvidersCurrentPromise; michael@0: }, michael@0: michael@0: _popAndInitProvider: function () { michael@0: if (!this._providerInitQueue.length || this._providerInitializing) { michael@0: return; michael@0: } michael@0: michael@0: let [provider, deferred] = this._providerInitQueue.shift(); michael@0: this._providerInitializing = true; michael@0: michael@0: this._log.info("Initializing provider with storage: " + provider.name); michael@0: michael@0: Task.spawn(function initProvider() { michael@0: try { michael@0: let result = yield provider.init(this._storage); michael@0: this._log.info("Provider successfully initialized: " + provider.name); michael@0: michael@0: this._providers.set(provider.name, { michael@0: provider: provider, michael@0: constantsCollected: false, michael@0: }); michael@0: michael@0: deferred.resolve(result); michael@0: } catch (ex) { michael@0: this._recordProviderError(provider.name, "Failed to initialize", ex); michael@0: deferred.reject(ex); michael@0: } finally { michael@0: this._providerInitializing = false; michael@0: this._popAndInitProvider(); michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Collects all constant measurements from all providers. michael@0: * michael@0: * Returns a Promise that will be fulfilled once all data providers have michael@0: * provided their constant data. A side-effect of this promise fulfillment michael@0: * is that the manager is populated with the obtained collection results. michael@0: * The resolved value to the promise is this `ProviderManager` instance. michael@0: */ michael@0: collectConstantData: function () { michael@0: let entries = []; michael@0: michael@0: for (let [name, entry] of this._providers) { michael@0: if (entry.constantsCollected) { michael@0: this._log.trace("Provider has already provided constant data: " + michael@0: name); michael@0: continue; michael@0: } michael@0: michael@0: entries.push(entry); michael@0: } michael@0: michael@0: let onCollect = function (entry, result) { michael@0: entry.constantsCollected = true; michael@0: }; michael@0: michael@0: return this._callCollectOnProviders(entries, "collectConstantData", michael@0: onCollect); michael@0: }, michael@0: michael@0: /** michael@0: * Calls collectDailyData on all providers. michael@0: */ michael@0: collectDailyData: function () { michael@0: return this._callCollectOnProviders(this._providers.values(), michael@0: "collectDailyData"); michael@0: }, michael@0: michael@0: _callCollectOnProviders: function (entries, fnProperty, onCollect=null) { michael@0: let promises = []; michael@0: michael@0: for (let entry of entries) { michael@0: let provider = entry.provider; michael@0: let collectPromise; michael@0: try { michael@0: collectPromise = provider[fnProperty].call(provider); michael@0: } catch (ex) { michael@0: this._recordProviderError(provider.name, "Exception when calling " + michael@0: "collect function: " + fnProperty, ex); michael@0: continue; michael@0: } michael@0: michael@0: if (!collectPromise) { michael@0: this._recordProviderError(provider.name, "Does not return a promise " + michael@0: "from " + fnProperty + "()"); michael@0: continue; michael@0: } michael@0: michael@0: let promise = collectPromise.then(function onCollected(result) { michael@0: if (onCollect) { michael@0: try { michael@0: onCollect(entry, result); michael@0: } catch (ex) { michael@0: this._log.warn("onCollect callback threw: " + michael@0: CommonUtils.exceptionStr(ex)); michael@0: } michael@0: } michael@0: michael@0: return CommonUtils.laterTickResolvingPromise(result); michael@0: }); michael@0: michael@0: promises.push([provider.name, promise]); michael@0: } michael@0: michael@0: return this._handleCollectionPromises(promises); michael@0: }, michael@0: michael@0: /** michael@0: * Handles promises returned by the collect* functions. michael@0: * michael@0: * This consumes the data resolved by the promises and returns a new promise michael@0: * that will be resolved once all promises have been resolved. michael@0: * michael@0: * The promise is resolved even if one of the underlying collection michael@0: * promises is rejected. michael@0: */ michael@0: _handleCollectionPromises: function (promises) { michael@0: return Task.spawn(function waitForPromises() { michael@0: for (let [name, promise] of promises) { michael@0: try { michael@0: yield promise; michael@0: this._log.debug("Provider collected successfully: " + name); michael@0: } catch (ex) { michael@0: this._recordProviderError(name, "Failed to collect", ex); michael@0: } michael@0: } michael@0: michael@0: throw new Task.Result(this); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Record an error that occurred operating on a provider. michael@0: */ michael@0: _recordProviderError: function (name, msg, ex) { michael@0: let msg = "Provider error: " + name + ": " + msg; michael@0: if (ex) { michael@0: msg += ": " + CommonUtils.exceptionStr(ex); michael@0: } michael@0: this._log.warn(msg); michael@0: michael@0: if (this.onProviderError) { michael@0: try { michael@0: this.onProviderError(msg); michael@0: } catch (callError) { michael@0: this._log.warn("Exception when calling onProviderError callback: " + michael@0: CommonUtils.exceptionStr(callError)); michael@0: } michael@0: } michael@0: }, michael@0: }); michael@0: