services/metrics/providermanager.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial