gecko/services/metrics/collector.jsm

250 lines
7.0 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
#ifndef MERGED_COMPARTMENT
this.EXPORTED_SYMBOLS = ["Collector"];
const {utils: Cu} = Components;
Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
#endif
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://services-common/log4moz.js");
Cu.import("resource://services-common/utils.js");
/**
* Handles and coordinates the collection of metrics data from providers.
*
* This provides an interface for managing `Metrics.Provider` instances. It
* provides APIs for bulk collection of data.
*/
this.Collector = function (storage) {
this._log = Log4Moz.repository.getLogger("Services.Metrics.Collector");
this._providers = new Map();
this._storage = storage;
this._providerInitQueue = [];
this._providerInitializing = false;
this.providerErrors = new Map();
}
Collector.prototype = Object.freeze({
get providers() {
let providers = [];
for (let [name, entry] of this._providers) {
providers.push(entry.provider);
}
return providers;
},
/**
* Obtain a provider from its name.
*/
getProvider: function (name) {
let provider = this._providers.get(name);
if (!provider) {
return null;
}
return provider.provider;
},
/**
* Registers a `MetricsProvider` with this collector.
*
* Once a `MetricsProvider` is registered, data will be collected from it
* whenever we collect data.
*
* The returned value is a promise that will be resolved once registration
* is complete.
*
* Providers are initialized as part of registration by calling
* provider.init().
*
* @param provider
* (Metrics.Provider) The provider instance to register.
*
* @return Promise<null>
*/
registerProvider: function (provider) {
if (!(provider instanceof Provider)) {
throw new Error("Argument must be a Provider instance.");
}
if (this._providers.has(provider.name)) {
return Promise.resolve();
}
let deferred = Promise.defer();
this._providerInitQueue.push([provider, deferred]);
if (this._providerInitQueue.length == 1) {
this._popAndInitProvider();
}
return deferred.promise;
},
/**
* Remove a named provider from the collector.
*
* It is the caller's responsibility to shut down the provider
* instance.
*/
unregisterProvider: function (name) {
this._providers.delete(name);
},
_popAndInitProvider: function () {
if (!this._providerInitQueue.length || this._providerInitializing) {
return;
}
let [provider, deferred] = this._providerInitQueue.shift();
this._providerInitializing = true;
this._log.info("Initializing provider with storage: " + provider.name);
Task.spawn(function initProvider() {
try {
let result = yield provider.init(this._storage);
this._log.info("Provider successfully initialized: " + provider.name);
this._providers.set(provider.name, {
provider: provider,
constantsCollected: false,
});
this.providerErrors.set(provider.name, []);
deferred.resolve(result);
} catch (ex) {
this._log.warn("Provider failed to initialize: " + provider.name +
": " + CommonUtils.exceptionStr(ex));
deferred.reject(ex);
} finally {
this._providerInitializing = false;
this._popAndInitProvider();
}
}.bind(this));
},
/**
* Collects all constant measurements from all providers.
*
* Returns a Promise that will be fulfilled once all data providers have
* provided their constant data. A side-effect of this promise fulfillment
* is that the collector is populated with the obtained collection results.
* The resolved value to the promise is this `Collector` instance.
*/
collectConstantData: function () {
let entries = [];
for (let [name, entry] of this._providers) {
if (entry.constantsCollected) {
this._log.trace("Provider has already provided constant data: " +
name);
continue;
}
entries.push(entry);
}
let onCollect = function (entry, result) {
entry.constantsCollected = true;
};
return this._callCollectOnProviders(entries, "collectConstantData",
onCollect);
},
/**
* Calls collectDailyData on all providers.
*/
collectDailyData: function () {
return this._callCollectOnProviders(this._providers.values(),
"collectDailyData");
},
_callCollectOnProviders: function (entries, fnProperty, onCollect=null) {
let promises = [];
for (let entry of entries) {
let provider = entry.provider;
let collectPromise;
try {
collectPromise = provider[fnProperty].call(provider);
} catch (ex) {
this._log.warn("Exception when calling " + provider.name + "." +
fnProperty + ": " + CommonUtils.exceptionStr(ex));
this.providerErrors.get(provider.name).push(ex);
continue;
}
if (!collectPromise) {
this._log.warn("Provider does not return a promise from " +
fnProperty + "(): " + provider.name);
continue;
}
let promise = collectPromise.then(function onCollected(result) {
if (onCollect) {
try {
onCollect(entry, result);
} catch (ex) {
this._log.warn("onCollect callback threw: " +
CommonUtils.exceptionStr(ex));
}
}
return Promise.resolve(result);
});
promises.push([provider.name, promise]);
}
return this._handleCollectionPromises(promises);
},
/**
* Handles promises returned by the collect* functions.
*
* This consumes the data resolved by the promises and returns a new promise
* that will be resolved once all promises have been resolved.
*
* The promise is resolved even if one of the underlying collection
* promises is rejected.
*/
_handleCollectionPromises: function (promises) {
return Task.spawn(function waitForPromises() {
for (let [name, promise] of promises) {
try {
yield promise;
this._log.debug("Provider collected successfully: " + name);
} catch (ex) {
this._log.warn("Provider failed to collect: " + name + ": " +
CommonUtils.exceptionStr(ex));
try {
this.providerErrors.get(name).push(ex);
} catch (ex2) {
this._log.error("Error updating provider errors. This should " +
"never happen: " + CommonUtils.exceptionStr(ex2));
}
}
}
throw new Task.Result(this);
}.bind(this));
},
});