author | Gregory Szorc <gps@mozilla.com> |
Sun, 06 Jan 2013 12:13:27 -0800 | |
changeset 117843 | ee9453c65e339986b43ecdb7ffb72c3eff5fafe6 |
parent 117842 | f3f08413e8635fcb94f9365cb947a63ab92af796 |
child 117844 | d4ce01fb62551508ec19742b8143db19de5d6d90 |
push id | 24116 |
push user | gszorc@mozilla.com |
push date | Mon, 07 Jan 2013 08:22:48 +0000 |
treeherder | mozilla-central@66d595814554 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | rnewman |
bugs | 812608 |
milestone | 20.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/services/healthreport/HealthReportService.js +++ b/services/healthreport/HealthReportService.js @@ -5,127 +5,146 @@ "use strict"; const {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://services-common/preferences.js"); -const INITIAL_STARTUP_DELAY_MSEC = 10 * 1000; const BRANCH = "healthreport."; -const JS_PROVIDERS_CATEGORY = "healthreport-js-provider"; - +const DEFAULT_LOAD_DELAY_MSEC = 10 * 1000; /** * The Firefox Health Report XPCOM service. * - * This instantiates an instance of HealthReporter (assuming it is enabled) - * and starts it upon application startup. + * External consumers will be interested in the "reporter" property of this + * service. This property is a `HealthReporter` instance that powers the + * service. The property may be null if the Health Report service is not + * enabled. + * + * EXAMPLE USAGE + * ============= + * + * let reporter = Cc["@mozilla.org/healthreport/service;1"] + * .getService(Ci.nsISupports) + * .wrappedJSObject + * .reporter; * - * One can obtain a reference to the underlying HealthReporter instance by - * accessing .reporter. If this property is null, the reporter isn't running - * yet or has been disabled. + * if (reporter.haveRemoteData) { + * // ... + * } + * + * IMPLEMENTATION NOTES + * ==================== + * + * In order to not adversely impact application start time, the `HealthReporter` + * instance is not initialized until a few seconds after "final-ui-startup." + * The exact delay is configurable via preferences so it can be adjusted with + * a hotfix extension if the default value is ever problematic. + * + * Shutdown of the `HealthReporter` instance is handled completely within the + * instance (it registers observers on initialization). See the notes on that + * type for more. */ this.HealthReportService = function HealthReportService() { this.wrappedJSObject = this; - this.prefs = new Preferences(BRANCH); + this._prefs = new Preferences(BRANCH); + this._reporter = null; } HealthReportService.prototype = { classID: Components.ID("{e354c59b-b252-4040-b6dd-b71864e3e35c}"), QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), observe: function observe(subject, topic, data) { // If the background service is disabled, don't do anything. - if (!this.prefs.get("serviceEnabled", true)) { + if (!this._prefs.get("service.enabled", true)) { return; } let os = Cc["@mozilla.org/observer-service;1"] .getService(Ci.nsIObserverService); switch (topic) { case "app-startup": os.addObserver(this, "final-ui-startup", true); break; case "final-ui-startup": os.removeObserver(this, "final-ui-startup"); - os.addObserver(this, "quit-application", true); + + let delayInterval = this._prefs.get("service.loadDelayMsec") || + DEFAULT_LOAD_DELAY_MSEC; // Delay service loading a little more so things have an opportunity // to cool down first. this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this.timer.initWithCallback({ notify: function notify() { // Side effect: instantiates the reporter instance if not already // accessed. let reporter = this.reporter; delete this.timer; }.bind(this), - }, INITIAL_STARTUP_DELAY_MSEC, this.timer.TYPE_ONE_SHOT); - - break; + }, delayInterval, this.timer.TYPE_ONE_SHOT); - case "quit-application-granted": - if (this.reporter) { - this.reporter.stop(); - } - - os.removeObserver(this, "quit-application"); break; } }, /** * The HealthReporter instance associated with this service. + * + * If the service is disabled, this will return null. + * + * The obtained instance may not be fully initialized. */ get reporter() { - if (!this.prefs.get("serviceEnabled", true)) { + if (!this._prefs.get("service.enabled", true)) { return null; } if (this._reporter) { return this._reporter; } + let ns = {}; // Lazy import so application startup isn't adversely affected. - let ns = {}; + Cu.import("resource://gre/modules/Task.jsm", ns); + Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm", ns); Cu.import("resource://services-common/log4moz.js", ns); - Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm", ns); // How many times will we rewrite this code before rolling it up into a // generic module? See also bug 451283. const LOGGERS = [ - "Metrics", "Services.HealthReport", "Services.Metrics", "Services.BagheeraClient", + "Sqlite.Connection.healthreport", ]; let prefs = new Preferences(BRANCH + "logging."); if (prefs.get("consoleEnabled", true)) { let level = prefs.get("consoleLevel", "Warn"); let appender = new ns.Log4Moz.ConsoleAppender(); appender.level = ns.Log4Moz.Level[level] || ns.Log4Moz.Level.Warn; for (let name of LOGGERS) { let logger = ns.Log4Moz.repository.getLogger(name); logger.addAppender(appender); } } + // The reporter initializes in the background. this._reporter = new ns.HealthReporter(BRANCH); - this._reporter.registerProvidersFromCategoryManager(JS_PROVIDERS_CATEGORY); - this._reporter.start(); return this._reporter; }, }; Object.freeze(HealthReportService.prototype); this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HealthReportService]);
--- a/services/healthreport/healthreport-prefs.js +++ b/services/healthreport/healthreport-prefs.js @@ -1,22 +1,24 @@ /* 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/. */ pref("healthreport.documentServerURI", "https://data.mozilla.com/"); pref("healthreport.documentServerNamespace", "metrics"); -pref("healthreport.serviceEnabled", true); pref("healthreport.logging.consoleEnabled", true); pref("healthreport.logging.consoleLevel", "Warn"); pref("healthreport.policy.currentDaySubmissionFailureCount", 0); pref("healthreport.policy.dataSubmissionEnabled", true); pref("healthreport.policy.dataSubmissionPolicyAccepted", false); pref("healthreport.policy.dataSubmissionPolicyBypassAcceptance", false); pref("healthreport.policy.dataSubmissionPolicyNotifiedTime", "0"); pref("healthreport.policy.dataSubmissionPolicyResponseType", ""); pref("healthreport.policy.dataSubmissionPolicyResponseTime", "0"); pref("healthreport.policy.firstRunTime", "0"); pref("healthreport.policy.lastDataSubmissionFailureTime", "0"); pref("healthreport.policy.lastDataSubmissionRequestedTime", "0"); pref("healthreport.policy.lastDataSubmissionSuccessfulTime", "0"); pref("healthreport.policy.nextDataSubmissionTime", "0"); +pref("healthreport.service.enabled", true); +pref("healthreport.service.loadDelayMsec", 10000); +pref("healthreport.service.providerCategories", "healthreport-js-provider");
--- a/services/healthreport/healthreporter.jsm +++ b/services/healthreport/healthreporter.jsm @@ -3,74 +3,142 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; this.EXPORTED_SYMBOLS = ["HealthReporter"]; const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +Cu.import("resource://services-common/async.js"); Cu.import("resource://services-common/bagheeraclient.js"); Cu.import("resource://services-common/log4moz.js"); Cu.import("resource://services-common/observers.js"); Cu.import("resource://services-common/preferences.js"); Cu.import("resource://services-common/utils.js"); Cu.import("resource://gre/modules/commonjs/promise/core.js"); +Cu.import("resource://gre/modules/Metrics.jsm"); Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/services/healthreport/policy.jsm"); -Cu.import("resource://gre/modules/services/metrics/collector.jsm"); // Oldest year to allow in date preferences. This module was implemented in // 2012 and no dates older than that should be encountered. const OLDEST_ALLOWED_YEAR = 2012; +const DAYS_IN_PAYLOAD = 180; +const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; + +const DEFAULT_DATABASE_NAME = "healthreport.sqlite"; + /** - * Coordinates collection and submission of metrics. + * Coordinates collection and submission of health report metrics. * * This is the main type for Firefox Health Report. It glues all the * lower-level components (such as collection and submission) together. * * An instance of this type is created as an XPCOM service. See * HealthReportService.js and HealthReportComponents.manifest. * * It is theoretically possible to have multiple instances of this running * in the application. For example, this type may one day handle submission * of telemetry data as well. However, there is some moderate coupling between * this type and *the* Firefox Health Report (e.g. the policy). This could * be abstracted if needed. * + * IMPLEMENTATION NOTES + * ==================== + * + * Initialization and shutdown are somewhat complicated and worth explaining + * in extra detail. + * + * The complexity is driven by the requirements of SQLite connection management. + * Once you have a SQLite connection, it isn't enough to just let the + * application shut down. If there is an open connection or if there are + * outstanding SQL statements come XPCOM shutdown time, Storage will assert. + * On debug builds you will crash. On release builds you will get a shutdown + * hang. This must be avoided! + * + * During initialization, the second we create a SQLite connection (via + * Metrics.Storage) we register observers for application shutdown. The + * "quit-application" notification initiates our shutdown procedure. The + * subsequent "profile-do-change" notification ensures it has completed. + * + * The handler for "profile-do-change" may result in event loop spinning. This + * is because of race conditions between our shutdown code and application + * shutdown. + * + * All of our shutdown routines are async. There is the potential that these + * async functions will not complete before XPCOM shutdown. If they don't + * finish in time, we could get assertions in Storage. Our solution is to + * initiate storage early in the shutdown cycle ("quit-application"). + * Hopefully all the async operations have completed by the time we reach + * "profile-do-change." If so, great. If not, we spin the event loop until + * they have completed, avoiding potential race conditions. + * * @param branch * (string) The preferences branch to use for state storage. The value * must end with a period (.). */ -this.HealthReporter = function HealthReporter(branch) { +function HealthReporter(branch) { if (!branch.endsWith(".")) { - throw new Error("Branch argument must end with a period (.): " + branch); + throw new Error("Branch must end with a period (.): " + branch); } this._log = Log4Moz.repository.getLogger("Services.HealthReport.HealthReporter"); + this._log.info("Initializing health reporter instance against " + branch); this._prefs = new Preferences(branch); - let policyBranch = new Preferences(branch + "policy."); - this._policy = new HealthReportPolicy(policyBranch, this); - this._collector = new MetricsCollector(); - if (!this.serverURI) { throw new Error("No server URI defined. Did you forget to define the pref?"); } if (!this.serverNamespace) { throw new Error("No server namespace defined. Did you forget a pref?"); } + + this._dbName = this._prefs.get("dbName") || DEFAULT_DATABASE_NAME; + + let policyBranch = new Preferences(branch + "policy."); + this._policy = new HealthReportPolicy(policyBranch, this); + + this._storage = null; + this._storageInProgress = false; + this._collector = null; + this._initialized = false; + this._initializeHadError = false; + this._initializedDeferred = Promise.defer(); + this._shutdownRequested = false; + this._shutdownInitiated = false; + this._shutdownComplete = false; + this._shutdownCompleteCallback = null; + + this._ensureDirectoryExists(this._stateDir) + .then(this._onStateDirCreated.bind(this), + this._onInitError.bind(this)); + } -HealthReporter.prototype = { +HealthReporter.prototype = Object.freeze({ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + /** + * Whether the service is fully initialized and running. + * + * If this is false, it is not safe to call most functions. + */ + get initialized() { + return this._initialized; + }, + /** * When we last successfully submitted data to the server. * * This is sent as part of the upload. This is redundant with similar data * in the policy because we like the modules to be loosely coupled and the * similar data in the policy is only used for forensic purposes. */ get lastPingDate() { @@ -141,147 +209,347 @@ HealthReporter.prototype = { this._prefs.set("lastSubmitID", value || ""); }, /** * Whether remote data is currently stored. * * @return bool */ - haveRemoteData: function haveRemoteData() { + haveRemoteData: function () { return !!this.lastSubmitID; }, + //---------------------------------------------------- + // SERVICE CONTROL FUNCTIONS + // + // You shouldn't need to call any of these externally. + //---------------------------------------------------- + + _onInitError: function (error) { + this._log.error("Error during initialization: " + + CommonUtils.exceptionStr(error)); + this._initializeHadError = true; + this._initiateShutdown(); + this._initializedDeferred.reject(error); + + // FUTURE consider poisoning prototype's functions so calls fail with a + // useful error message. + }, + + _onStateDirCreated: function () { + // As soon as we have could storage, we need to register cleanup or + // else bad things happen on shutdown. + Services.obs.addObserver(this, "quit-application", false); + Services.obs.addObserver(this, "profile-before-change", false); + + this._storageInProgress = true; + Metrics.Storage(this._dbName).then(this._onStorageCreated.bind(this), + this._onInitError.bind(this)); + }, + + // Called when storage has been opened. + _onStorageCreated: function (storage) { + this._log.info("Storage initialized."); + this._storage = storage; + this._storageInProgress = false; + + if (this._shutdownRequested) { + this._initiateShutdown(); + return; + } + + Task.spawn(this._initializeCollector.bind(this)) + .then(this._onCollectorInitialized.bind(this), + this._onInitError.bind(this)); + }, + + _initializeCollector: function () { + if (this._collector) { + throw new Error("Collector has already been initialized."); + } + + this._log.info("Initializing collector."); + this._collector = new Metrics.Collector(this._storage); + + let catString = this._prefs.get("service.providerCategories") || ""; + if (catString.length) { + for (let category of catString.split(",")) { + yield this.registerProvidersFromCategoryManager(category); + } + } + }, + + _onCollectorInitialized: function () { + if (this._shutdownRequested) { + this._initiateShutdown(); + return; + } + + this._policy.startPolling(); + this._log.info("HealthReporter started."); + this._initialized = true; + Services.obs.addObserver(this, "idle-daily", false); + this._initializedDeferred.resolve(this); + }, + + // nsIObserver to handle shutdown. + observe: function (subject, topic, data) { + switch (topic) { + case "quit-application": + Services.obs.removeObserver(this, "quit-application"); + this._initiateShutdown(); + break; + + case "profile-before-change": + Services.obs.removeObserver(this, "profile-before-change"); + this._waitForShutdown(); + break; + + case "idle-daily": + this._performDailyMaintenance(); + break; + } + }, + + _initiateShutdown: function () { + // Ensure we only begin the main shutdown sequence once. + if (this._shutdownInitiated) { + this._log.warn("Shutdown has already been initiated. No-op."); + return; + } + + this._log.info("Request to shut down."); + + this._initialized = false; + this._shutdownRequested = true; + + // Safe to call multiple times. + this._policy.stopPolling(); + + // If storage is in the process of initializing, we need to wait for it + // to finish before continuing. The initialization process will call us + // again once storage has initialized. + if (this._storageInProgress) { + this._log.warn("Storage is in progress of initializing. Waiting to finish."); + return; + } + + // Everything from here must only be performed once or else race conditions + // could occur. + this._shutdownInitiated = true; + + Services.obs.removeObserver(this, "idle-daily"); + + // If we have collectors, we need to shut down providers. + if (this._collector) { + let onShutdown = this._onCollectorShutdown.bind(this); + Task.spawn(this._shutdownCollector.bind(this)) + .then(onShutdown, onShutdown); + return; + } + + this._onCollectorShutdown(); + }, + + _shutdownCollector: function () { + for (let provider of this._collector.providers) { + try { + yield provider.shutdown(); + } catch (ex) { + this._log.warn("Error when shutting down provider: " + + CommonUtils.exceptionStr(ex)); + } + } + }, + + _onCollectorShutdown: function () { + this._collector = null; + + if (this._storage) { + let onClose = this._onStorageClose.bind(this); + this._storage.close().then(onClose, onClose); + return; + } + + this._onStorageClose(); + }, + + _onStorageClose: function (error) { + if (error) { + this._log.warn("Error when closing storage: " + + CommonUtils.exceptionStr(error)); + } + + this._storage = null; + this._shutdownComplete = true; + + if (this._shutdownCompleteCallback) { + this._shutdownCompleteCallback(); + } + }, + + _waitForShutdown: function () { + if (this._shutdownComplete) { + return; + } + + this._shutdownCompleteCallback = Async.makeSpinningCallback(); + this._shutdownCompleteCallback.wait(); + this._shutdownCompleteCallback = null; + }, + /** - * Perform post-construction initialization and start background activity. - * - * If this isn't called, no data upload will occur. + * Convenience method to shut down the instance. * - * This returns a promise that will be fulfilled when all initialization - * activity is completed. It is not safe for this instance to perform - * additional actions until this promise has been resolved. + * This should *not* be called outside of tests. */ - start: function start() { - let onExists = function onExists() { - this._policy.startPolling(); - this._log.info("HealthReporter started."); - - return Promise.resolve(); - }.bind(this); - - return this._ensureDirectoryExists(this._stateDir) - .then(onExists); + _shutdown: function () { + this._initiateShutdown(); + this._waitForShutdown(); }, /** - * Stop background functionality. + * Return a promise that is resolved once the service has been initialized. */ - stop: function stop() { - this._policy.stopPolling(); + onInit: function () { + if (this._initializeHadError) { + throw new Error("Service failed to initialize."); + } + + if (this._initialized) { + return Promise.resolve(this); + } + + return this._initializedDeferred.promise; }, + _performDailyMaintenance: function () { + this._log.info("Request to perform daily maintenance."); + + if (!this._initialized) { + return; + } + + let now = new Date(); + let cutoff = new Date(now.getTime() - MILLISECONDS_PER_DAY * (DAYS_IN_PAYLOAD - 1)); + + // The operation is enqueued and put in a transaction by the storage module. + this._storage.pruneDataBefore(cutoff); + }, + + //-------------------- + // Provider Management + //-------------------- + /** - * Register a `MetricsProvider` with this instance. + * Register a `Metrics.Provider` with this instance. * * This needs to be called or no data will be collected. See also * registerProvidersFromCategoryManager`. * * @param provider - * (MetricsProvider) The provider to register for collection. + * (Metrics.Provider) The provider to register for collection. */ - registerProvider: function registerProvider(provider) { + registerProvider: function (provider) { return this._collector.registerProvider(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 `MetricsProvider` instance. + * 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 FooProvider resource://gre/modules/foo.jsm * * Then to load them: * - * let reporter = new HealthReporter("healthreport."); + * let reporter = getHealthReporter("healthreport."); * reporter.registerProvidersFromCategoryManager("healthreport-js-provider"); * * @param category * (string) Name of category to query and load from. */ - registerProvidersFromCategoryManager: - function registerProvidersFromCategoryManager(category) { - + 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 provider = new ns[entry](); - this.registerProvider(provider); + promises.push(this.registerProvider(provider)); } catch (ex) { this._log.warn("Error registering provider from category manager: " + entry + "; " + CommonUtils.exceptionStr(ex)); continue; } } + + return Task.spawn(function wait() { + for (let promise of promises) { + yield promise; + } + }); }, /** * Collect all measurements for all registered providers. */ - collectMeasurements: function collectMeasurements() { - return this._collector.collectConstantMeasurements(); + collectMeasurements: function () { + return this._collector.collectConstantData(); }, /** * Record the user's rejection of the data submission policy. * * This should be what everything uses to disable data submission. * * @param reason * (string) Why data submission is being disabled. */ - recordPolicyRejection: function recordPolicyRejection(reason) { + recordPolicyRejection: function (reason) { this._policy.recordUserRejection(reason); }, /** * Record the user's acceptance of the data submission policy. * * This should be what everything uses to enable data submission. * * @param reason * (string) Why data submission is being enabled. */ - recordPolicyAcceptance: function recordPolicyAcceptance(reason) { + recordPolicyAcceptance: function (reason) { this._policy.recordUserAcceptance(reason); }, /** * Whether the data submission policy has been accepted. * * If this is true, health data will be submitted unless one of the kill * switches is active. @@ -301,44 +569,128 @@ HealthReporter.prototype = { /** * Request that server data be deleted. * * If deletion is scheduled to occur immediately, a promise will be returned * that will be fulfilled when the deletion attempt finishes. Otherwise, * callers should poll haveRemoteData() to determine when remote data is * deleted. */ - requestDeleteRemoteData: function requestDeleteRemoteData(reason) { + requestDeleteRemoteData: function (reason) { if (!this.lastSubmitID) { return; } return this._policy.deleteRemoteData(reason); }, - getJSONPayload: function getJSONPayload() { + getJSONPayload: function () { + return Task.spawn(this._getJSONPayload.bind(this, this._now())); + }, + + _getJSONPayload: function (now) { + let pingDateString = this._formatDate(now); + this._log.info("Producing JSON payload for " + pingDateString); + let o = { version: 1, - thisPingDate: this._formatDate(this._now()), - providers: {}, + thisPingDate: pingDateString, + data: {last: {}, days: {}}, }; + let outputDataDays = o.data.days; + + // We need to be careful that data in errors does not leak potentially + // private information. + // FUTURE ask Privacy if we can put exception stacks in here. + let errors = []; + let lastPingDate = this.lastPingDate; if (lastPingDate.getTime() > 0) { o.lastPingDate = this._formatDate(lastPingDate); } - for (let [name, provider] of this._collector.collectionResults) { - o.providers[name] = provider; + for (let provider of this._collector.providers) { + let providerName = provider.name; + + let providerEntry = { + measurements: {}, + }; + + for (let [measurementKey, measurement] of provider.measurements) { + let name = providerName + "." + measurement.name + "." + measurement.version; + + let serializer; + try { + serializer = measurement.serializer(measurement.SERIALIZE_JSON); + } catch (ex) { + this._log.warn("Error obtaining serializer for measurement: " + name + + ": " + CommonUtils.exceptionStr(ex)); + errors.push("Could not obtain serializer: " + name); + continue; + } + + let data; + try { + data = yield this._storage.getMeasurementValues(measurement.id); + } catch (ex) { + this._log.warn("Error obtaining data for measurement: " + + name + ": " + CommonUtils.exceptionStr(ex)); + errors.push("Could not obtain data: " + name); + continue; + } + + if (data.singular.size) { + try { + o.data.last[name] = serializer.singular(data.singular); + } catch (ex) { + this._log.warn("Error serializing data: " + CommonUtils.exceptionStr(ex)); + errors.push("Error serializing singular: " + name); + continue; + } + } + + let dataDays = data.days; + for (let i = 0; i < DAYS_IN_PAYLOAD; i++) { + let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY); + if (!dataDays.hasDay(date)) { + continue; + } + let dateFormatted = this._formatDate(date); + + try { + let serialized = serializer.daily(dataDays.getDay(date)); + if (!serialized) { + continue; + } + + if (!(dateFormatted in outputDataDays)) { + outputDataDays[dateFormatted] = {}; + } + + outputDataDays[dateFormatted][name] = serialized; + } catch (ex) { + this._log.warn("Error populating data for day: " + + CommonUtils.exceptionStr(ex)); + errors.push("Could not serialize day: " + name + + " ( " + dateFormatted + ")"); + continue; + } + } + } } - return JSON.stringify(o); + if (errors.length) { + o.errors = errors; + } + + throw new Task.Result(JSON.stringify(o)); }, - _onBagheeraResult: function _onBagheeraResult(request, isDelete, result) { + _onBagheeraResult: function (request, isDelete, result) { this._log.debug("Received Bagheera result."); let promise = Promise.resolve(null); if (!result.transportSuccess) { request.onSubmissionFailureSoft("Network transport error."); return promise; } @@ -357,46 +709,44 @@ HealthReporter.prototype = { this.lastPingDate = now; } request.onSubmissionSuccess(now); return promise; }, - _onSubmitDataRequestFailure: function _onSubmitDataRequestFailure(error) { + _onSubmitDataRequestFailure: function (error) { this._log.error("Error processing request to submit data: " + CommonUtils.exceptionStr(error)); }, - _formatDate: function _formatDate(date) { + _formatDate: function (date) { // Why, oh, why doesn't JS have a strftime() equivalent? return date.toISOString().substr(0, 10); }, - _uploadData: function _uploadData(request) { + _uploadData: function (request) { let id = CommonUtils.generateUUID(); this._log.info("Uploading data to server: " + this.serverURI + " " + this.serverNamespace + ":" + id); let client = new BagheeraClient(this.serverURI); - let payload = this.getJSONPayload(); - - return this._saveLastPayload(payload) - .then(client.uploadJSON.bind(client, - this.serverNamespace, - id, - payload, - this.lastSubmitID)) - .then(this._onBagheeraResult.bind(this, request, false)); + return Task.spawn(function doUpload() { + let payload = yield this.getJSONPayload(); + yield this._saveLastPayload(payload); + let result = yield client.uploadJSON(this.serverNamespace, id, payload, + this.lastSubmitID); + yield this._onBagheeraResult(request, false, result); + }.bind(this)); }, - _deleteRemoteData: function _deleteRemoteData(request) { + _deleteRemoteData: function (request) { if (!this.lastSubmitID) { this._log.info("Received request to delete remote data but no data stored."); request.onNoDataAvailable(); return; } this._log.warn("Deleting remote data."); let client = new BagheeraClient(this.serverURI); @@ -414,17 +764,17 @@ HealthReporter.prototype = { if (!profD || !profD.length) { throw new Error("Could not obtain profile directory. OS.File not " + "initialized properly?"); } return OS.Path.join(profD, "healthreport"); }, - _ensureDirectoryExists: function _ensureDirectoryExists(path) { + _ensureDirectoryExists: function (path) { let deferred = Promise.defer(); OS.File.makeDir(path).then( function onResult() { deferred.resolve(true); }, function onError(error) { if (error.becauseExists) { @@ -438,17 +788,17 @@ HealthReporter.prototype = { return deferred.promise; }, get _lastPayloadPath() { return OS.Path.join(this._stateDir, "lastpayload.json"); }, - _saveLastPayload: function _saveLastPayload(payload) { + _saveLastPayload: function (payload) { let path = this._lastPayloadPath; let pathTmp = path + ".tmp"; let encoder = new TextEncoder(); let buffer = encoder.encode(payload); return OS.File.writeAtomic(path, buffer, {tmpPath: pathTmp}); }, @@ -457,17 +807,17 @@ HealthReporter.prototype = { * Obtain the last uploaded payload. * * The promise is resolved to a JSON-decoded object on success. The promise * is rejected if the last uploaded payload could not be found or there was * an error reading or parsing it. * * @return Promise<object> */ - getLastPayload: function getLoadPayload() { + getLastPayload: function () { let path = this._lastPayloadPath; return OS.File.read(path).then( function onData(buffer) { let decoder = new TextDecoder(); let json = JSON.parse(decoder.decode(buffer)); return Promise.resolve(json); @@ -481,31 +831,29 @@ HealthReporter.prototype = { _now: function _now() { return new Date(); }, //----------------------------- // HealthReportPolicy listeners //----------------------------- - onRequestDataUpload: function onRequestDataSubmission(request) { + onRequestDataUpload: function (request) { this.collectMeasurements() .then(this._uploadData.bind(this, request), this._onSubmitDataRequestFailure.bind(this)); }, - onNotifyDataPolicy: function onNotifyDataPolicy(request) { + onNotifyDataPolicy: function (request) { // This isn't very loosely coupled. We may want to have this call // registered listeners instead. Observers.notify("healthreport:notify-data-policy:request", request); }, - onRequestRemoteDelete: function onRequestRemoteDelete(request) { + onRequestRemoteDelete: function (request) { this._deleteRemoteData(request); }, //------------------------------------ // End of HealthReportPolicy listeners //------------------------------------ -}; +}); -Object.freeze(HealthReporter.prototype); -
--- a/services/healthreport/profile.jsm +++ b/services/healthreport/profile.jsm @@ -6,34 +6,36 @@ this.EXPORTED_SYMBOLS = [ "ProfileCreationTimeAccessor", "ProfileMetadataProvider", ]; const {utils: Cu, classes: Cc, interfaces: Ci} = Components; -const DEFAULT_PROFILE_MEASUREMENT_NAME = "org.mozilla.profile"; +const DEFAULT_PROFILE_MEASUREMENT_NAME = "age"; const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"}; Cu.import("resource://gre/modules/commonjs/promise/core.js"); +Cu.import("resource://gre/modules/Metrics.jsm"); Cu.import("resource://gre/modules/osfile.jsm") -Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://services-common/log4moz.js"); Cu.import("resource://services-common/utils.js"); // Profile creation time access. // This is separate from the provider to simplify testing and enable extraction // to a shared location in the future. -function ProfileCreationTimeAccessor(profile) { +function ProfileCreationTimeAccessor(profile, log) { this.profilePath = profile || OS.Constants.Path.profileDir; if (!this.profilePath) { throw new Error("No profile directory."); } + this._log = log || {"debug": function (s) { dump(s + "\n"); }}; } ProfileCreationTimeAccessor.prototype = { /** * There are three ways we can get our creation time: * * 1. From our own saved value (to avoid redundant work). * 2. From the on-disk JSON file. * 3. By calculating it from the filesystem. @@ -110,41 +112,57 @@ ProfileCreationTimeAccessor.prototype = .then(onOldest.bind(this)); }, /** * Traverse the contents of the profile directory, finding the oldest file * and returning its creation timestamp. */ getOldestProfileTimestamp: function () { + let self = this; let oldest = Date.now() + 1000; let iterator = new OS.File.DirectoryIterator(this.profilePath); -dump("Iterating over profile " + this.profilePath); + self._log.debug("Iterating over profile " + this.profilePath); if (!iterator) { throw new Error("Unable to fetch oldest profile entry: no profile iterator."); } function onEntry(entry) { - if ("winLastWriteDate" in entry) { - // Under Windows, additional information allow us to sort files immediately - // without having to perform additional I/O. - let timestamp = entry.winCreationDate.getTime(); - if (timestamp < oldest) { - oldest = timestamp; + function onStatSuccess(info) { + // OS.File doesn't seem to be behaving. See Bug 827148. + // Let's do the best we can. This whole function is defensive. + let date; + if ("winBirthDate" in info) { + date = info.winBirthDate; + } else if ("macBirthDate" in info) { + date = info.macBirthDate; } - return; - } - // Under other OSes, we need to call OS.File.stat. - function onStatSuccess(info) { - let date = info.creationDate; - let timestamp = date.getTime(); - dump("CREATION DATE: " + entry.path + " = " + date); - if (timestamp < oldest) { - oldest = timestamp; + if (!date || !date.getTime()) { + // Hack: as of this writing, OS.File will only return file + // creation times of any kind of Mac and Windows, where birthTime + // is defined. That means we're unable to function on Linux. + // Use ctime, fall back to mtime. + // Oh, and info.macBirthDate doesn't work. + self._log.debug("No birth date: using ctime/mtime."); + try { + date = info.creationDate || + info.lastModificationDate || + info.unixLastStatusChangeDate; + } catch (ex) { + self._log.debug("Exception fetching creation date: " + ex); + } + } + + if (date) { + let timestamp = date.getTime(); + self._log.debug("Using date: " + entry.path + " = " + date); + if (timestamp < oldest) { + oldest = timestamp; + } } } return OS.File.stat(entry.path) .then(onStatSuccess); } let promise = iterator.forEach(onEntry); @@ -160,69 +178,66 @@ dump("Iterating over profile " + this.pr return promise.then(onSuccess, onFailure); }, } /** * Measurements pertaining to the user's profile. */ -function ProfileMetadataMeasurement(name=DEFAULT_PROFILE_MEASUREMENT_NAME) { - MetricsMeasurement.call(this, name, 1); +function ProfileMetadataMeasurement() { + Metrics.Measurement.call(this); } ProfileMetadataMeasurement.prototype = { - __proto__: MetricsMeasurement.prototype, + __proto__: Metrics.Measurement.prototype, - fields: { + name: DEFAULT_PROFILE_MEASUREMENT_NAME, + version: 1, + + configureStorage: function () { // Profile creation date. Number of days since Unix epoch. - "profileCreation": REQUIRED_UINT32_TYPE, + return this.registerStorageField("profileCreation", this.storage.FIELD_LAST_NUMERIC); }, }; /** * Turn a millisecond timestamp into a day timestamp. * * @param msec a number of milliseconds since epoch. * @return the number of whole days denoted by the input. */ function truncate(msec) { return Math.floor(msec / MILLISECONDS_PER_DAY); } /** - * A MetricsProvider for profile metadata, such as profile creation time. + * A Metrics.Provider for profile metadata, such as profile creation time. */ -function ProfileMetadataProvider(name="ProfileMetadataProvider") { - MetricsProvider.call(this, name); +function ProfileMetadataProvider() { + Metrics.Provider.call(this); } ProfileMetadataProvider.prototype = { - __proto__: MetricsProvider.prototype, + __proto__: Metrics.Provider.prototype, + + name: "org.mozilla.profile", + + measurementTypes: [ProfileMetadataMeasurement], getProfileCreationDays: function () { - let accessor = new ProfileCreationTimeAccessor(); + let accessor = new ProfileCreationTimeAccessor(null, this._log); return accessor.created .then(truncate); }, - collectConstantMeasurements: function () { - let result = this.createResult(); - result.expectMeasurement("org.mozilla.profile"); - result.populate = this._populateConstants.bind(this); - return result; - }, + collectConstantData: function () { + let m = this.getMeasurement(DEFAULT_PROFILE_MEASUREMENT_NAME, 1); - _populateConstants: function (result) { - let name = DEFAULT_PROFILE_MEASUREMENT_NAME; - result.addMeasurement(new ProfileMetadataMeasurement(name)); - function onSuccess(days) { - result.setValue(name, "profileCreation", days); - result.finish(); - } - function onFailure(ex) { - result.addError(ex); - result.finish(); - } - return this.getProfileCreationDays() - .then(onSuccess, onFailure); + return Task.spawn(function collectConstant() { + let createdDays = yield this.getProfileCreationDays(); + + yield this.enqueueStorageOperation(function storeDays() { + return m.setLastNumeric("profileCreation", createdDays); + }); + }.bind(this)); }, };
--- a/services/healthreport/providers.jsm +++ b/services/healthreport/providers.jsm @@ -16,97 +16,107 @@ this.EXPORTED_SYMBOLS = [ "AppInfoProvider", "SysInfoProvider", ]; const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +Cu.import("resource://gre/modules/Metrics.jsm"); Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm"); Cu.import("resource://services-common/preferences.js"); Cu.import("resource://services-common/utils.js"); -const REQUIRED_STRING_TYPE = {type: "TYPE_STRING"}; -const OPTIONAL_STRING_TYPE = {type: "TYPE_STRING", optional: true}; -const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"}; - XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel", "resource://gre/modules/UpdateChannel.jsm"); /** * Represents basic application state. * * This is roughly a union of nsIXULAppInfo, nsIXULRuntime, with a few extra * pieces thrown in. */ function AppInfoMeasurement() { - MetricsMeasurement.call(this, "appinfo", 1); + Metrics.Measurement.call(this); } -AppInfoMeasurement.prototype = { - __proto__: MetricsMeasurement.prototype, +AppInfoMeasurement.prototype = Object.freeze({ + __proto__: Metrics.Measurement.prototype, + + name: "appinfo", + version: 1, - fields: { - vendor: REQUIRED_STRING_TYPE, - name: REQUIRED_STRING_TYPE, - id: REQUIRED_STRING_TYPE, - version: REQUIRED_STRING_TYPE, - appBuildID: REQUIRED_STRING_TYPE, - platformVersion: REQUIRED_STRING_TYPE, - platformBuildID: REQUIRED_STRING_TYPE, - os: REQUIRED_STRING_TYPE, - xpcomabi: REQUIRED_STRING_TYPE, - updateChannel: REQUIRED_STRING_TYPE, - distributionID: REQUIRED_STRING_TYPE, - distributionVersion: REQUIRED_STRING_TYPE, - hotfixVersion: REQUIRED_STRING_TYPE, - locale: REQUIRED_STRING_TYPE, + LAST_TEXT_FIELDS: [ + "vendor", + "name", + "id", + "version", + "appBuildID", + "platformVersion", + "platformBuildID", + "os", + "xpcomabi", + "updateChannel", + "distributionID", + "distributionVersion", + "hotfixVersion", + "locale", + ], + + configureStorage: function () { + let self = this; + return Task.spawn(function configureStorage() { + for (let field of self.LAST_TEXT_FIELDS) { + yield self.registerStorageField(field, self.storage.FIELD_LAST_TEXT); + } + }); }, -}; - -Object.freeze(AppInfoMeasurement.prototype); +}); this.AppInfoProvider = function AppInfoProvider() { - MetricsProvider.call(this, "app-info"); + Metrics.Provider.call(this); this._prefs = new Preferences({defaultBranch: null}); } -AppInfoProvider.prototype = { - __proto__: MetricsProvider.prototype, +AppInfoProvider.prototype = Object.freeze({ + __proto__: Metrics.Provider.prototype, + + name: "org.mozilla.appInfo", + + measurementTypes: [AppInfoMeasurement], appInfoFields: { // From nsIXULAppInfo. vendor: "vendor", name: "name", id: "ID", version: "version", appBuildID: "appBuildID", platformVersion: "platformVersion", platformBuildID: "platformBuildID", // From nsIXULRuntime. os: "OS", xpcomabi: "XPCOMABI", }, - collectConstantMeasurements: function collectConstantMeasurements() { - let result = this.createResult(); - result.expectMeasurement("appinfo"); - - result.populate = this._populateConstants.bind(this); - return result; + collectConstantData: function () { + return this.enqueueStorageOperation(function collect() { + return Task.spawn(this._populateConstants.bind(this)); + }.bind(this)); }, - _populateConstants: function _populateConstants(result) { - result.addMeasurement(new AppInfoMeasurement()); + _populateConstants: function () { + let m = this.getMeasurement(AppInfoMeasurement.prototype.name, + AppInfoMeasurement.prototype.version); let ai; try { ai = Services.appinfo; } catch (ex) { this._log.warn("Could not obtain Services.appinfo: " + CommonUtils.exceptionStr(ex)); throw ex; @@ -114,141 +124,132 @@ AppInfoProvider.prototype = { if (!ai) { this._log.warn("Services.appinfo is unavailable."); throw ex; } for (let [k, v] in Iterator(this.appInfoFields)) { try { - result.setValue("appinfo", k, ai[v]); + yield m.setLastText(k, ai[v]); } catch (ex) { this._log.warn("Error obtaining Services.appinfo." + v); - result.addError(ex); } } try { - result.setValue("appinfo", "updateChannel", UpdateChannel.get()); + yield m.setLastText("updateChannel", UpdateChannel.get()); } catch (ex) { this._log.warn("Could not obtain update channel: " + CommonUtils.exceptionStr(ex)); - result.addError(ex); } - result.setValue("appinfo", "distributionID", this._prefs.get("distribution.id", "")); - result.setValue("appinfo", "distributionVersion", this._prefs.get("distribution.version", "")); - result.setValue("appinfo", "hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", "")); + yield m.setLastText("distributionID", this._prefs.get("distribution.id", "")); + yield m.setLastText("distributionVersion", this._prefs.get("distribution.version", "")); + yield m.setLastText("hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", "")); try { let locale = Cc["@mozilla.org/chrome/chrome-registry;1"] .getService(Ci.nsIXULChromeRegistry) .getSelectedLocale("global"); - result.setValue("appinfo", "locale", locale); + yield m.setLastText("locale", locale); } catch (ex) { this._log.warn("Could not obtain application locale: " + CommonUtils.exceptionStr(ex)); - result.addError(ex); } - - result.finish(); }, -}; - -Object.freeze(AppInfoProvider.prototype); +}); function SysInfoMeasurement() { - MetricsMeasurement.call(this, "sysinfo", 1); + Metrics.Measurement.call(this); } -SysInfoMeasurement.prototype = { - __proto__: MetricsMeasurement.prototype, +SysInfoMeasurement.prototype = Object.freeze({ + __proto__: Metrics.Measurement.prototype, + + name: "sysinfo", + version: 1, - fields: { - cpuCount: REQUIRED_UINT32_TYPE, - memoryMB: REQUIRED_UINT32_TYPE, - manufacturer: OPTIONAL_STRING_TYPE, - device: OPTIONAL_STRING_TYPE, - hardware: OPTIONAL_STRING_TYPE, - name: OPTIONAL_STRING_TYPE, - version: OPTIONAL_STRING_TYPE, - architecture: OPTIONAL_STRING_TYPE, + configureStorage: function () { + return Task.spawn(function configureStorage() { + yield this.registerStorageField("cpuCount", this.storage.FIELD_LAST_NUMERIC); + yield this.registerStorageField("memoryMB", this.storage.FIELD_LAST_NUMERIC); + yield this.registerStorageField("manufacturer", this.storage.FIELD_LAST_TEXT); + yield this.registerStorageField("device", this.storage.FIELD_LAST_TEXT); + yield this.registerStorageField("hardware", this.storage.FIELD_LAST_TEXT); + yield this.registerStorageField("name", this.storage.FIELD_LAST_TEXT); + yield this.registerStorageField("version", this.storage.FIELD_LAST_TEXT); + yield this.registerStorageField("architecture", this.storage.FIELD_LAST_TEXT); + }.bind(this)); }, -}, - -Object.freeze(SysInfoMeasurement.prototype); +}); this.SysInfoProvider = function SysInfoProvider() { - MetricsProvider.call(this, "sys-info"); + Metrics.Provider.call(this); }; -SysInfoProvider.prototype = { - __proto__: MetricsProvider.prototype, +SysInfoProvider.prototype = Object.freeze({ + __proto__: Metrics.Provider.prototype, + + name: "org.mozilla.sysinfo", + + measurementTypes: [SysInfoMeasurement], sysInfoFields: { cpucount: "cpuCount", memsize: "memoryMB", manufacturer: "manufacturer", device: "device", hardware: "hardware", name: "name", version: "version", arch: "architecture", }, - INT_FIELDS: new Set("cpucount", "memsize"), - - collectConstantMeasurements: function collectConstantMeasurements() { - let result = this.createResult(); - result.expectMeasurement("sysinfo"); - - result.populate = this._populateConstants.bind(this); - - return result; + collectConstantData: function () { + return this.enqueueStorageOperation(function collection() { + return Task.spawn(this._populateConstants.bind(this)); + }.bind(this)); }, - _populateConstants: function _populateConstants(result) { - result.addMeasurement(new SysInfoMeasurement()); + _populateConstants: function () { + let m = this.getMeasurement(SysInfoMeasurement.prototype.name, + SysInfoMeasurement.prototype.version); let si = Cc["@mozilla.org/system-info;1"] .getService(Ci.nsIPropertyBag2); for (let [k, v] in Iterator(this.sysInfoFields)) { try { if (!si.hasKey(k)) { this._log.debug("Property not available: " + k); continue; } let value = si.getProperty(k); + let method = "setLastText"; - if (this.INT_FIELDS.has(k)) { + if (["cpucount", "memsize"].indexOf(k) != -1) { let converted = parseInt(value, 10); if (Number.isNaN(converted)) { - result.addError(new Error("Value is not an integer: " + k + "=" + - value)); continue; } value = converted; + method = "setLastNumeric"; } // Round memory to mebibytes. if (k == "memsize") { value = Math.round(value / 1048576); } - result.setValue("sysinfo", v, value); + yield m[method](v, value); } catch (ex) { this._log.warn("Error obtaining system info field: " + k + " " + CommonUtils.exceptionStr(ex)); - result.addError(ex); } } + }, +}); - result.finish(); - }, -}; - -Object.freeze(SysInfoProvider.prototype); -
--- a/services/healthreport/tests/xpcshell/test_healthreporter.js +++ b/services/healthreport/tests/xpcshell/test_healthreporter.js @@ -5,23 +5,26 @@ const {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://services-common/observers.js"); Cu.import("resource://services-common/preferences.js"); Cu.import("resource://gre/modules/commonjs/promise/core.js"); Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm"); Cu.import("resource://gre/modules/services/healthreport/policy.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://testing-common/services-common/bagheeraserver.js"); Cu.import("resource://testing-common/services/metrics/mocks.jsm"); const SERVER_HOSTNAME = "localhost"; const SERVER_PORT = 8080; const SERVER_URI = "http://" + SERVER_HOSTNAME + ":" + SERVER_PORT; +const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; function defineNow(policy, now) { print("Adjusting fake system clock to " + now); Object.defineProperty(policy, "now", { value: function customNow() { return now; }, @@ -29,257 +32,293 @@ function defineNow(policy, now) { }); } function getReporter(name, uri=SERVER_URI) { let branch = "healthreport.testing. " + name + "."; let prefs = new Preferences(branch); prefs.set("documentServerURI", uri); + prefs.set("dbName", name); - return new HealthReporter(branch); + let reporter = new HealthReporter(branch); + return reporter.onInit(); } function getReporterAndServer(name, namespace="test") { - let reporter = getReporter(name, SERVER_URI); - reporter.serverNamespace = namespace; + return Task.spawn(function get() { + let reporter = yield getReporter(name, SERVER_URI); + reporter.serverNamespace = namespace; + + let server = new BagheeraServer(SERVER_URI); + server.createNamespace(namespace); + + server.start(SERVER_PORT); - let server = new BagheeraServer(SERVER_URI); - server.createNamespace(namespace); + throw new Task.Result([reporter, server]); + }); +} - server.start(SERVER_PORT); +function shutdownServer(server) { + let deferred = Promise.defer(); + server.stop(deferred.resolve.bind(deferred)); - return [reporter, server]; + return deferred.promise; } function run_test() { run_next_test(); } -add_test(function test_constructor() { - let reporter = getReporter("constructor"); +add_task(function test_constructor() { + let reporter = yield getReporter("constructor"); do_check_eq(reporter.lastPingDate.getTime(), 0); do_check_null(reporter.lastSubmitID); reporter.lastSubmitID = "foo"; do_check_eq(reporter.lastSubmitID, "foo"); reporter.lastSubmitID = null; do_check_null(reporter.lastSubmitID); let failed = false; try { new HealthReporter("foo.bar"); } catch (ex) { failed = true; - do_check_true(ex.message.startsWith("Branch argument must end")); + do_check_true(ex.message.startsWith("Branch must end")); } finally { do_check_true(failed); failed = false; } - run_next_test(); + reporter._shutdown(); }); -add_test(function test_register_providers_from_category_manager() { +add_task(function test_register_providers_from_category_manager() { const category = "healthreporter-js-modules"; let cm = Cc["@mozilla.org/categorymanager;1"] .getService(Ci.nsICategoryManager); cm.addCategoryEntry(category, "DummyProvider", "resource://testing-common/services/metrics/mocks.jsm", false, true); - let reporter = getReporter("category_manager"); - do_check_eq(reporter._collector._providers.length, 0); - reporter.registerProvidersFromCategoryManager(category); - do_check_eq(reporter._collector._providers.length, 1); + let reporter = yield getReporter("category_manager"); + do_check_eq(reporter._collector._providers.size, 0); + yield reporter.registerProvidersFromCategoryManager(category); + do_check_eq(reporter._collector._providers.size, 1); - run_next_test(); + reporter._shutdown(); }); -add_test(function test_start() { - let reporter = getReporter("start"); - reporter.start().then(function onStarted() { - reporter.stop(); - run_next_test(); - }); -}); - -add_test(function test_json_payload_simple() { - let reporter = getReporter("json_payload_simple"); +add_task(function test_json_payload_simple() { + let reporter = yield getReporter("json_payload_simple"); let now = new Date(); - let payload = reporter.getJSONPayload(); + let payload = yield reporter.getJSONPayload(); let original = JSON.parse(payload); do_check_eq(original.version, 1); do_check_eq(original.thisPingDate, reporter._formatDate(now)); - do_check_eq(Object.keys(original.providers).length, 0); + do_check_eq(Object.keys(original.data.last).length, 0); + do_check_eq(Object.keys(original.data.days).length, 0); reporter.lastPingDate = new Date(now.getTime() - 24 * 60 * 60 * 1000 - 10); - original = JSON.parse(reporter.getJSONPayload()); + original = JSON.parse(yield reporter.getJSONPayload()); do_check_eq(original.lastPingDate, reporter._formatDate(reporter.lastPingDate)); // This could fail if we cross UTC day boundaries at the exact instance the // test is executed. Let's tempt fate. do_check_eq(original.thisPingDate, reporter._formatDate(now)); - run_next_test(); + reporter._shutdown(); +}); + +add_task(function test_json_payload_dummy_provider() { + let reporter = yield getReporter("json_payload_dummy_provider"); + + yield reporter.registerProvider(new DummyProvider()); + yield reporter.collectMeasurements(); + let payload = yield reporter.getJSONPayload(); + print(payload); + let o = JSON.parse(payload); + + do_check_eq(Object.keys(o.data.last).length, 1); + do_check_true("DummyProvider.DummyMeasurement.1" in o.data.last); + + reporter._shutdown(); }); -add_test(function test_json_payload_dummy_provider() { - let reporter = getReporter("json_payload_dummy_provider"); - - reporter.registerProvider(new DummyProvider()); - reporter.collectMeasurements().then(function onResult() { - let o = JSON.parse(reporter.getJSONPayload()); +add_task(function test_json_payload_multiple_days() { + let reporter = yield getReporter("json_payload_multiple_days"); + let provider = new DummyProvider(); + yield reporter.registerProvider(provider); - do_check_eq(Object.keys(o.providers).length, 1); - do_check_true("DummyProvider" in o.providers); - do_check_true("measurements" in o.providers.DummyProvider); - do_check_true("DummyMeasurement" in o.providers.DummyProvider.measurements); + let now = new Date(); + let m = provider.getMeasurement("DummyMeasurement", 1); + for (let i = 0; i < 200; i++) { + let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY); + yield m.incrementDailyCounter("daily-counter", date); + yield m.addDailyDiscreteNumeric("daily-discrete-numeric", i, date); + yield m.addDailyDiscreteNumeric("daily-discrete-numeric", i + 100, date); + yield m.addDailyDiscreteText("daily-discrete-text", "" + i, date); + yield m.addDailyDiscreteText("daily-discrete-text", "" + (i + 50), date); + yield m.setDailyLastNumeric("daily-last-numeric", date.getTime(), date); + } - run_next_test(); - }); + let payload = yield reporter.getJSONPayload(); + print(payload); + let o = JSON.parse(payload); + + do_check_eq(Object.keys(o.data.days).length, 180); + let today = reporter._formatDate(now); + do_check_true(today in o.data.days); + + reporter._shutdown(); }); -add_test(function test_notify_policy_observers() { - let reporter = getReporter("notify_policy_observers"); - - Observers.add("healthreport:notify-data-policy:request", - function onObserver(subject, data) { - Observers.remove("healthreport:notify-data-policy:request", onObserver); +add_task(function test_idle_daily() { + let reporter = yield getReporter("idle_daily"); + let provider = new DummyProvider(); + yield reporter.registerProvider(provider); - do_check_true("foo" in subject); + let now = new Date(); + let m = provider.getMeasurement("DummyMeasurement", 1); + for (let i = 0; i < 200; i++) { + let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY); + yield m.incrementDailyCounter("daily-counter", date); + } - run_next_test(); - }); + let values = yield m.getValues(); + do_check_eq(values.days.size, 200); + + Services.obs.notifyObservers(null, "idle-daily", null); - reporter.onNotifyDataPolicy({foo: "bar"}); + let values = yield m.getValues(); + do_check_eq(values.days.size, 180); + + reporter._shutdown(); }); -add_test(function test_data_submission_transport_failure() { - let reporter = getReporter("data_submission_transport_failure"); +add_task(function test_data_submission_transport_failure() { + let reporter = yield getReporter("data_submission_transport_failure"); reporter.serverURI = "http://localhost:8080/"; reporter.serverNamespace = "test00"; let deferred = Promise.defer(); - deferred.promise.then(function onResult(request) { - do_check_eq(request.state, request.SUBMISSION_FAILURE_SOFT); - - run_next_test(); - }); - let request = new DataSubmissionRequest(deferred, new Date(Date.now + 30000)); reporter.onRequestDataUpload(request); + + yield deferred.promise; + do_check_eq(request.state, request.SUBMISSION_FAILURE_SOFT); + + reporter._shutdown(); }); -add_test(function test_data_submission_success() { - let [reporter, server] = getReporterAndServer("data_submission_success"); +add_task(function test_data_submission_success() { + let [reporter, server] = yield getReporterAndServer("data_submission_success"); do_check_eq(reporter.lastPingDate.getTime(), 0); do_check_false(reporter.haveRemoteData()); let deferred = Promise.defer(); - deferred.promise.then(function onResult(request) { - do_check_eq(request.state, request.SUBMISSION_SUCCESS); - do_check_neq(reporter.lastPingDate.getTime(), 0); - do_check_true(reporter.haveRemoteData()); - - server.stop(run_next_test); - }); let request = new DataSubmissionRequest(deferred, new Date()); reporter.onRequestDataUpload(request); + yield deferred.promise; + do_check_eq(request.state, request.SUBMISSION_SUCCESS); + do_check_true(reporter.lastPingDate.getTime() > 0); + do_check_true(reporter.haveRemoteData()); + + reporter._shutdown(); + yield shutdownServer(server); }); -add_test(function test_recurring_daily_pings() { - let [reporter, server] = getReporterAndServer("recurring_daily_pings"); +add_task(function test_recurring_daily_pings() { + let [reporter, server] = yield getReporterAndServer("recurring_daily_pings"); reporter.registerProvider(new DummyProvider()); let policy = reporter._policy; defineNow(policy, policy._futureDate(-24 * 60 * 68 * 1000)); policy.recordUserAcceptance(); defineNow(policy, policy.nextDataSubmissionDate); let promise = policy.checkStateAndTrigger(); do_check_neq(promise, null); + yield promise; - promise.then(function onUploadComplete() { - let lastID = reporter.lastSubmitID; - - do_check_neq(lastID, null); - do_check_true(server.hasDocument(reporter.serverNamespace, lastID)); + let lastID = reporter.lastSubmitID; + do_check_neq(lastID, null); + do_check_true(server.hasDocument(reporter.serverNamespace, lastID)); - // Skip forward to next scheduled submission time. - defineNow(policy, policy.nextDataSubmissionDate); - let promise = policy.checkStateAndTrigger(); - do_check_neq(promise, null); - promise.then(function onSecondUploadCOmplete() { - do_check_neq(reporter.lastSubmitID, lastID); - do_check_true(server.hasDocument(reporter.serverNamespace, reporter.lastSubmitID)); - do_check_false(server.hasDocument(reporter.serverNamespace, lastID)); + // Skip forward to next scheduled submission time. + defineNow(policy, policy.nextDataSubmissionDate); + promise = policy.checkStateAndTrigger(); + do_check_neq(promise, null); + yield promise; + do_check_neq(reporter.lastSubmitID, lastID); + do_check_true(server.hasDocument(reporter.serverNamespace, reporter.lastSubmitID)); + do_check_false(server.hasDocument(reporter.serverNamespace, lastID)); - server.stop(run_next_test); - }); - }); + reporter._shutdown(); + yield shutdownServer(server); }); -add_test(function test_request_remote_data_deletion() { - let [reporter, server] = getReporterAndServer("request_remote_data_deletion"); +add_task(function test_request_remote_data_deletion() { + let [reporter, server] = yield getReporterAndServer("request_remote_data_deletion"); let policy = reporter._policy; defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000)); policy.recordUserAcceptance(); defineNow(policy, policy.nextDataSubmissionDate); - policy.checkStateAndTrigger().then(function onUploadComplete() { - let id = reporter.lastSubmitID; - do_check_neq(id, null); - do_check_true(server.hasDocument(reporter.serverNamespace, id)); + yield policy.checkStateAndTrigger(); + let id = reporter.lastSubmitID; + do_check_neq(id, null); + do_check_true(server.hasDocument(reporter.serverNamespace, id)); - defineNow(policy, policy._futureDate(10 * 1000)); + defineNow(policy, policy._futureDate(10 * 1000)); - let promise = reporter.requestDeleteRemoteData(); - do_check_neq(promise, null); - promise.then(function onDeleteComplete() { - do_check_null(reporter.lastSubmitID); - do_check_false(reporter.haveRemoteData()); - do_check_false(server.hasDocument(reporter.serverNamespace, id)); + let promise = reporter.requestDeleteRemoteData(); + do_check_neq(promise, null); + yield promise; + do_check_null(reporter.lastSubmitID); + do_check_false(reporter.haveRemoteData()); + do_check_false(server.hasDocument(reporter.serverNamespace, id)); - server.stop(run_next_test); - }); - }); + reporter._shutdown(); + yield shutdownServer(server); }); -add_test(function test_policy_accept_reject() { - let [reporter, server] = getReporterAndServer("policy_accept_reject"); +add_task(function test_policy_accept_reject() { + let [reporter, server] = yield getReporterAndServer("policy_accept_reject"); do_check_false(reporter.dataSubmissionPolicyAccepted); do_check_false(reporter.willUploadData); reporter.recordPolicyAcceptance(); do_check_true(reporter.dataSubmissionPolicyAccepted); do_check_true(reporter.willUploadData); reporter.recordPolicyRejection(); do_check_false(reporter.dataSubmissionPolicyAccepted); do_check_false(reporter.willUploadData); - server.stop(run_next_test); + reporter._shutdown(); + yield shutdownServer(server); }); -add_test(function test_upload_save_payload() { - let [reporter, server] = getReporterAndServer("upload_save_payload"); +add_task(function test_upload_save_payload() { + let [reporter, server] = yield getReporterAndServer("upload_save_payload"); let deferred = Promise.defer(); let request = new DataSubmissionRequest(deferred, new Date(), false); - reporter._uploadData(request).then(function onUpload() { - reporter.getLastPayload().then(function onJSON(json) { - do_check_true("thisPingDate" in json); - server.stop(run_next_test); - }); - }); + yield reporter._uploadData(request); + let json = yield reporter.getLastPayload(); + do_check_true("thisPingDate" in json); + + reporter._shutdown(); + yield shutdownServer(server); });
--- a/services/healthreport/tests/xpcshell/test_profile.js +++ b/services/healthreport/tests/xpcshell/test_profile.js @@ -8,46 +8,39 @@ const {utils: Cu} = Components; const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; // Create profile directory before use. // It can be no older than a day ago…. let profile_creation_lower = Date.now() - MILLISECONDS_PER_DAY; do_get_profile(); Cu.import("resource://gre/modules/commonjs/promise/core.js"); -Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm"); +Cu.import("resource://gre/modules/Metrics.jsm"); Cu.import("resource://gre/modules/services/healthreport/profile.jsm"); Cu.import("resource://gre/modules/Task.jsm"); + function MockProfileMetadataProvider(name="MockProfileMetadataProvider") { - ProfileMetadataProvider.call(this, name); + this.name = name; + ProfileMetadataProvider.call(this); } MockProfileMetadataProvider.prototype = { __proto__: ProfileMetadataProvider.prototype, getProfileCreationDays: function getProfileCreationDays() { return Promise.resolve(1234); }, }; function run_test() { run_next_test(); } /** - * Treat the provided function as a generator of promises, - * suitable for use with Task.spawn. Success runs next test; - * failure throws. - */ -function testTask(promiseFunction) { - Task.spawn(promiseFunction).then(run_next_test, do_throw); -} - -/** * Ensure that OS.File works in our environment. * This test can go once there are xpcshell tests for OS.File. */ add_test(function use_os_file() { Cu.import("resource://gre/modules/osfile.jsm") // Ensure that we get constants, too. do_check_neq(OS.Constants.Path.profileDir, null); @@ -79,77 +72,62 @@ add_test(function test_time_accessor_no_ .then(function onSuccess(json) { do_throw("File existed!"); }, function onFailure() { run_next_test(); }); }); -add_test(function test_time_accessor_named_file() { +add_task(function test_time_accessor_named_file() { let acc = getAccessor(); - testTask(function () { - // There should be no file yet. - yield acc.writeTimes({created: 12345}, "test.json"); - yield acc.readTimes("test.json") - .then(function onSuccess(json) { - print("Read: " + JSON.stringify(json)); - do_check_eq(12345, json.created); - run_next_test(); - }); - }); + // There should be no file yet. + yield acc.writeTimes({created: 12345}, "test.json"); + let json = yield acc.readTimes("test.json") + print("Read: " + JSON.stringify(json)); + do_check_eq(12345, json.created); }); -add_test(function test_time_accessor_creates_file() { +add_task(function test_time_accessor_creates_file() { let lower = profile_creation_lower; // Ensure that provided contents are merged, and existing // files can be overwritten. These two things occur if we // read and then decide that we have to write. let acc = getAccessor(); let existing = {abc: "123", easy: "abc"}; let expected; - testTask(function () { - yield acc.computeAndPersistTimes(existing, "test2.json") - .then(function onSuccess(created) { - let upper = Date.now() + 1000; - print(lower + " < " + created + " <= " + upper); - do_check_true(lower < created); - do_check_true(upper >= created); - expected = created; - }); - yield acc.readTimes("test2.json") - .then(function onSuccess(json) { - print("Read: " + JSON.stringify(json)); - do_check_eq("123", json.abc); - do_check_eq("abc", json.easy); - do_check_eq(expected, json.created); - }); - }); + let created = yield acc.computeAndPersistTimes(existing, "test2.json") + let upper = Date.now() + 1000; + print(lower + " < " + created + " <= " + upper); + do_check_true(lower < created); + do_check_true(upper >= created); + expected = created; + + let json = yield acc.readTimes("test2.json") + print("Read: " + JSON.stringify(json)); + do_check_eq("123", json.abc); + do_check_eq("abc", json.easy); + do_check_eq(expected, json.created); }); -add_test(function test_time_accessor_all() { +add_task(function test_time_accessor_all() { let lower = profile_creation_lower; let acc = getAccessor(); let expected; - testTask(function () { - yield acc.created - .then(function onSuccess(created) { - let upper = Date.now() + 1000; - do_check_true(lower < created); - do_check_true(upper >= created); - expected = created; - }); - yield acc.created - .then(function onSuccess(again) { - do_check_eq(expected, again); - }); - }); + let created = yield acc.created + let upper = Date.now() + 1000; + do_check_true(lower < created); + do_check_true(upper >= created); + expected = created; + + let again = yield acc.created + do_check_eq(expected, again); }); add_test(function test_constructor() { let provider = new ProfileMetadataProvider("named"); run_next_test(); }); add_test(function test_profile_files() { @@ -166,48 +144,53 @@ add_test(function test_profile_files() { do_throw("Directory iteration failed: " + ex); } provider.getProfileCreationDays().then(onSuccess, onFailure); }); // A generic test helper. We use this with both real // and mock providers in these tests. -function test_collect_constant(provider, valueTest) { - let result = provider.collectConstantMeasurements(); - do_check_true(result instanceof MetricsCollectionResult); +function test_collect_constant(provider) { + return Task.spawn(function () { + yield provider.collectConstantData(); - result.onFinished(function onFinished() { - do_check_eq(result.expectedMeasurements.size, 1); - do_check_true(result.expectedMeasurements.has("org.mozilla.profile")); - let m = result.measurements.get("org.mozilla.profile"); - do_check_true(!!m); - valueTest(m.getValue("profileCreation")); + let m = provider.getMeasurement("age", 1); + do_check_neq(m, null); + let values = yield m.getValues(); + do_check_eq(values.singular.size, 1); + do_check_true(values.singular.has("profileCreation")); - run_next_test(); + throw new Task.Result(values.singular.get("profileCreation")[1]); }); - - result.populate(result); } -add_test(function test_collect_constant_mock() { +add_task(function test_collect_constant_mock() { + let storage = yield Metrics.Storage("collect_constant_mock"); let provider = new MockProfileMetadataProvider(); - function valueTest(v) { - do_check_eq(v, 1234); - } - test_collect_constant(provider, valueTest); + yield provider.init(storage); + + let v = yield test_collect_constant(provider); + do_check_eq(v, 1234); + + yield storage.close(); }); -add_test(function test_collect_constant_real() { +add_task(function test_collect_constant_real() { let provider = new ProfileMetadataProvider(); - function valueTest(v) { - let ms = v * MILLISECONDS_PER_DAY; - let lower = profile_creation_lower; - let upper = Date.now() + 1000; - print("Day: " + v); - print("msec: " + ms); - print("Lower: " + lower); - print("Upper: " + upper); - do_check_true(lower <= ms); - do_check_true(upper >= ms); - } - test_collect_constant(provider, valueTest); + let storage = yield Metrics.Storage("collect_constant_real"); + yield provider.init(storage); + + let v = yield test_collect_constant(provider); + + let ms = v * MILLISECONDS_PER_DAY; + let lower = profile_creation_lower; + let upper = Date.now() + 1000; + print("Day: " + v); + print("msec: " + ms); + print("Lower: " + lower); + print("Upper: " + upper); + do_check_true(lower <= ms); + do_check_true(upper >= ms); + + yield storage.close(); }); +
--- a/services/healthreport/tests/xpcshell/test_provider_appinfo.js +++ b/services/healthreport/tests/xpcshell/test_provider_appinfo.js @@ -1,50 +1,46 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const {interfaces: Ci, results: Cr, utils: Cu} = Components; +Cu.import("resource://gre/modules/Metrics.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/services/healthreport/providers.jsm"); -Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm"); function run_test() { run_next_test(); } add_test(function test_constructor() { let provider = new AppInfoProvider(); run_next_test(); }); -add_test(function test_collect_smoketest() { +add_task(function test_collect_smoketest() { + let storage = yield Metrics.Storage("collect_smoketest"); let provider = new AppInfoProvider(); - - let result = provider.collectConstantMeasurements(); - do_check_true(result instanceof MetricsCollectionResult); + yield provider.init(storage); - result.onFinished(function onFinished() { - do_check_eq(result.expectedMeasurements.size, 1); - do_check_true(result.expectedMeasurements.has("appinfo")); - do_check_eq(result.measurements.size, 1); - do_check_true(result.measurements.has("appinfo")); - do_check_eq(result.errors.length, 0); + yield provider.collectConstantData(); + + let m = provider.getMeasurement("appinfo", 1); + let data = yield storage.getMeasurementValues(m.id); + let serializer = m.serializer(m.SERIALIZE_JSON); + let d = serializer.singular(data.singular); - let ai = result.measurements.get("appinfo"); - do_check_eq(ai.getValue("vendor"), "Mozilla"); - do_check_eq(ai.getValue("name"), "xpcshell"); - do_check_eq(ai.getValue("id"), "xpcshell@tests.mozilla.org"); - do_check_eq(ai.getValue("version"), "1"); - do_check_eq(ai.getValue("appBuildID"), "20121107"); - do_check_eq(ai.getValue("platformVersion"), "p-ver"); - do_check_eq(ai.getValue("platformBuildID"), "20121106"); - do_check_eq(ai.getValue("os"), "XPCShell"); - do_check_eq(ai.getValue("xpcomabi"), "noarch-spidermonkey"); + do_check_eq(d.vendor, "Mozilla"); + do_check_eq(d.name, "xpcshell"); + do_check_eq(d.id, "xpcshell@tests.mozilla.org"); + do_check_eq(d.version, "1"); + do_check_eq(d.appBuildID, "20121107"); + do_check_eq(d.platformVersion, "p-ver"); + do_check_eq(d.platformBuildID, "20121106"); + do_check_eq(d.os, "XPCShell"); + do_check_eq(d.xpcomabi, "noarch-spidermonkey"); - run_next_test(); - }); + yield storage.close(); +}); - result.populate(result); -});
--- a/services/healthreport/tests/xpcshell/test_provider_sysinfo.js +++ b/services/healthreport/tests/xpcshell/test_provider_sysinfo.js @@ -1,45 +1,40 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const {interfaces: Ci, results: Cr, utils: Cu} = Components; +Cu.import("resource://gre/modules/Metrics.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/services/healthreport/providers.jsm"); -Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm"); function run_test() { run_next_test(); } add_test(function test_constructor() { let provider = new SysInfoProvider(); run_next_test(); }); -add_test(function test_collect_smoketest() { +add_task(function test_collect_smoketest() { + let storage = yield Metrics.Storage("collect_smoketest"); let provider = new SysInfoProvider(); + yield provider.init(storage); - let result = provider.collectConstantMeasurements(); - do_check_true(result instanceof MetricsCollectionResult); + yield provider.collectConstantData(); - result.onFinished(function onFinished() { - do_check_eq(result.expectedMeasurements.size, 1); - do_check_true(result.expectedMeasurements.has("sysinfo")); - do_check_eq(result.measurements.size, 1); - do_check_true(result.measurements.has("sysinfo")); - do_check_eq(result.errors.length, 0); + let m = provider.getMeasurement("sysinfo", 1); + let data = yield storage.getMeasurementValues(m.id); + let serializer = m.serializer(m.SERIALIZE_JSON); + let d = serializer.singular(data.singular); - let si = result.measurements.get("sysinfo"); - do_check_true(si.getValue("cpuCount") > 0); - do_check_neq(si.getValue("name"), null); + do_check_true(d.cpuCount > 0); + do_check_neq(d.name, null); - run_next_test(); - }); - - result.populate(result); + yield storage.close(); });