[2d75b9]: services / metrics / providermanager.jsm  Maximize  Restore  History

Download this file

434 lines (366 with data), 12.8 kB

/* 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 = ["ProviderManager"];

const {classes: Cc, interfaces: Ci, 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.ProviderManager = function (storage) {
  this._log = Log4Moz.repository.getLogger("Services.Metrics.ProviderManager");

  this._providers = new Map();
  this._storage = storage;

  this._providerInitQueue = [];
  this._providerInitializing = false;

  this._pullOnlyProviders = {};
  this._pullOnlyProvidersRegistered = false;

  // Callback to allow customization of providers after they are constructed
  // but before they call out into their initialization code.
  this.onProviderInit = null;
}

this.ProviderManager.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 providers from a category manager category.
   *
   * This examines the specified category entries and registers found
   * providers.
   *
   * Category entries are essentially JS modules and the name of the symbol
   * within that module that is a `Metrics.Provider` instance.
   *
   * The category entry name is the name of the JS type for the provider. The
   * value is the resource:// URI to import which makes this type available.
   *
   * Example entry:
   *
   *   FooProvider resource://gre/modules/foo.jsm
   *
   * One can register entries in the application's .manifest file. e.g.
   *
   *   category healthreport-js-provider-default FooProvider resource://gre/modules/foo.jsm
   *   category healthreport-js-provider-nightly EyeballProvider resource://gre/modules/eyeball.jsm
   *
   * Then to load them:
   *
   *   let reporter = getHealthReporter("healthreport.");
   *   reporter.registerProvidersFromCategoryManager("healthreport-js-provider-default");
   *
   * If the category has no defined members, this call has no effect, and no error is raised.
   *
   * @param category
   *        (string) Name of category from which to query and load.
   * @return a newly spawned Task.
   */
  registerProvidersFromCategoryManager: function (category) {
    this._log.info("Registering providers from category: " + category);
    let cm = Cc["@mozilla.org/categorymanager;1"]
               .getService(Ci.nsICategoryManager);

    let promises = [];
    let enumerator = cm.enumerateCategory(category);
    while (enumerator.hasMoreElements()) {
      let entry = enumerator.getNext()
                            .QueryInterface(Ci.nsISupportsCString)
                            .toString();

      let uri = cm.getCategoryEntry(category, entry);
      this._log.info("Attempting to load provider from category manager: " +
                     entry + " from " + uri);

      try {
        let ns = {};
        Cu.import(uri, ns);

        let promise = this.registerProviderFromType(ns[entry]);
        if (promise) {
          promises.push(promise);
        }
      } catch (ex) {
        this._recordError("Error registering provider from category manager : " +
                          entry + ": ", ex);
        continue;
      }
    }

    return Task.spawn(function wait() {
      for (let promise of promises) {
        yield promise;
      }
    });
  },

  /**
   * Registers a `MetricsProvider` with this manager.
   *
   * 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 CommonUtils.laterTickResolvingPromise();
    }

    let deferred = Promise.defer();
    this._providerInitQueue.push([provider, deferred]);

    if (this._providerInitQueue.length == 1) {
      this._popAndInitProvider();
    }

    return deferred.promise;
  },

  /**
   * Registers a provider from its constructor function.
   *
   * If the provider is pull-only, it will be stashed away and
   * initialized later. Null will be returned.
   *
   * If it is not pull-only, it will be initialized immediately and a
   * promise will be returned. The promise will be resolved when the
   * provider has finished initializing.
   */
  registerProviderFromType: function (type) {
    let proto = type.prototype;
    if (proto.pullOnly) {
      this._log.info("Provider is pull-only. Deferring initialization: " +
                     proto.name);
      this._pullOnlyProviders[proto.name] = type;

      return null;
    }

    let provider = this._initProviderFromType(type);
    return this.registerProvider(provider);
  },

  /**
   * Initializes a provider from its type.
   *
   * This is how a constructor function should be turned into a provider
   * instance.
   *
   * A side-effect is the provider is registered with the manager.
   */
  _initProviderFromType: function (type) {
    let provider = new type();
    if (this.onProviderInit) {
      this.onProviderInit(provider);
    }

    return provider;
  },

  /**
   * Remove a named provider from the manager.
   *
   * It is the caller's responsibility to shut down the provider
   * instance.
   */
  unregisterProvider: function (name) {
    this._providers.delete(name);
  },

  /**
   * Ensure that pull-only providers are registered.
   */
  ensurePullOnlyProvidersRegistered: function () {
    if (this._pullOnlyProvidersRegistered) {
      return CommonUtils.laterTickResolvingPromise();
    }

    let onFinished = function () {
      this._pullOnlyProvidersRegistered = true;

      return CommonUtils.laterTickResolvingPromise();
    }.bind(this);

    return Task.spawn(function registerPullProviders() {
      for each (let providerType in this._pullOnlyProviders) {
        try {
          let provider = this._initProviderFromType(providerType);
          yield this.registerProvider(provider);
        } catch (ex) {
          this._recordError("Error registering pull-only provider", ex);
        }
      }
    }.bind(this)).then(onFinished, onFinished);
  },

  ensurePullOnlyProvidersUnregistered: function () {
    if (!this._pullOnlyProvidersRegistered) {
      return CommonUtils.laterTickResolvingPromise();
    }

    let onFinished = function () {
      this._pullOnlyProvidersRegistered = false;

      return CommonUtils.laterTickResolvingPromise();
    }.bind(this);

    return Task.spawn(function unregisterPullProviders() {
      for (let provider of this.providers) {
        if (!provider.pullOnly) {
          continue;
        }

        this._log.info("Shutting down pull-only provider: " +
                       provider.name);

        try {
          yield provider.shutdown();
        } catch (ex) {
          this._recordError("Error when shutting down provider: " +
                            provider.name, ex);
        } finally {
          this.unregisterProvider(provider.name);
        }
      }
    }.bind(this)).then(onFinished, onFinished);
  },

  _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,
        });

        deferred.resolve(result);
      } catch (ex) {
        this._recordProviderError(provider.name, "Failed to initialize", 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 manager is populated with the obtained collection results.
   * The resolved value to the promise is this `ProviderManager` 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._recordProviderError(provider.name, "Exception when calling " +
                                  "collect function: " + fnProperty, ex);
        continue;
      }

      if (!collectPromise) {
        this._recordProviderError(provider.name, "Does not return a promise " +
                                  "from " + fnProperty + "()");
        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 CommonUtils.laterTickResolvingPromise(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._recordProviderError(name, "Failed to collect", ex);
        }
      }

      throw new Task.Result(this);
    }.bind(this));
  },

  /**
   * Record an error that occurred operating on a provider.
   */
  _recordProviderError: function (name, msg, ex) {
    let msg = "Provider error: " + name + ": " + msg;
    if (ex) {
      msg += ": " + CommonUtils.exceptionStr(ex);
    }
    this._log.warn(msg);

    if (this.onProviderError) {
      try {
        this.onProviderError(msg);
      } catch (callError) {
        this._log.warn("Exception when calling onProviderError callback: " +
                       CommonUtils.exceptionStr(callError));
      }
    }
  },
});

Get latest updates about Open Source Projects, Conferences and News.

Sign up for the SourceForge newsletter:





No, thanks