summaryrefslogtreecommitdiff
path: root/dom/resourcestats
diff options
context:
space:
mode:
authorPale Moon <git-repo@palemoon.org>2016-09-01 13:39:08 +0200
committerPale Moon <git-repo@palemoon.org>2016-09-01 13:39:08 +0200
commit3d8ce1a11a7347cc94a937719c4bc8df46fb8d14 (patch)
tree8c26ca375a6312751c00a27e1653fb6f189f0463 /dom/resourcestats
parente449bdb1ec3a82f204bffdd9c3c54069d086eee3 (diff)
downloadpalemoon-gre-3d8ce1a11a7347cc94a937719c4bc8df46fb8d14.tar.gz
Base import of Tycho code (warning: huge commit)
Diffstat (limited to 'dom/resourcestats')
-rw-r--r--dom/resourcestats/ResourceStatsDB.jsm538
-rw-r--r--dom/resourcestats/ResourceStatsManager.js480
-rw-r--r--dom/resourcestats/ResourceStatsManager.manifest14
-rw-r--r--dom/resourcestats/ResourceStatsService.jsm334
-rw-r--r--dom/resourcestats/moz.build22
-rw-r--r--dom/resourcestats/tests/mochitest/mochitest.ini16
-rw-r--r--dom/resourcestats/tests/mochitest/test_basic.html41
-rw-r--r--dom/resourcestats/tests/mochitest/test_disabled_pref.html41
-rw-r--r--dom/resourcestats/tests/mochitest/test_network_alarm.html356
-rw-r--r--dom/resourcestats/tests/mochitest/test_network_stats.html345
-rw-r--r--dom/resourcestats/tests/mochitest/test_no_perm.html38
-rw-r--r--dom/resourcestats/tests/mochitest/test_not_supported_type.html41
-rw-r--r--dom/resourcestats/tests/mochitest/test_power_alarm.html355
-rw-r--r--dom/resourcestats/tests/mochitest/test_power_stats.html345
-rw-r--r--dom/resourcestats/tests/xpcshell/test_resourcestats_db.js985
-rw-r--r--dom/resourcestats/tests/xpcshell/xpcshell.ini5
16 files changed, 3956 insertions, 0 deletions
diff --git a/dom/resourcestats/ResourceStatsDB.jsm b/dom/resourcestats/ResourceStatsDB.jsm
new file mode 100644
index 000000000..a4ee943ca
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsDB.jsm
@@ -0,0 +1,538 @@
+/* This Source Code Form is subject to the terms of the Mozilla public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ['ResourceStatsDB'];
+
+const DEBUG = false;
+function debug(s) { dump("-*- ResourceStatsDB: " + s + "\n"); }
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.importGlobalProperties(["indexedDB"]);
+
+XPCOMUtils.defineLazyServiceGetter(this, "appsService",
+ "@mozilla.org/AppsService;1",
+ "nsIAppsService");
+
+const DB_NAME = "resource_stats";
+const DB_VERSION = 1;
+const POWER_STATS_STORE = "power_stats_store";
+const NETWORK_STATS_STORE = "network_stats_store";
+const ALARM_STORE = "alarm_store";
+
+const statsStoreNames = {
+ power: POWER_STATS_STORE,
+ network: NETWORK_STATS_STORE
+};
+
+// Constant defining the sampling rate.
+const SAMPLE_RATE = 24 * 60 * 60 * 1000; // 1 day.
+
+// Constant defining the MAX age of stored stats.
+const MAX_STORAGE_AGE = 180 * SAMPLE_RATE; // 180 days.
+
+this.ResourceStatsDB = function ResourceStatsDB() {
+ if (DEBUG) {
+ debug("Constructor()");
+ }
+
+ this.initDBHelper(DB_NAME, DB_VERSION,
+ [POWER_STATS_STORE, NETWORK_STATS_STORE, ALARM_STORE]);
+};
+
+ResourceStatsDB.prototype = {
+ __proto__: IndexedDBHelper.prototype,
+
+ _dbNewTxn: function(aStoreName, aTxnType, aCallback, aTxnCb) {
+ function successCb(aResult) {
+ aTxnCb(null, aResult);
+ }
+ function errorCb(aError) {
+ aTxnCb(aError, null);
+ }
+ return this.newTxn(aTxnType, aStoreName, aCallback, successCb, errorCb);
+ },
+
+ upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
+ if (DEBUG) {
+ debug("Upgrade DB from ver." + aOldVersion + " to ver." + aNewVersion);
+ }
+
+ let objectStore;
+
+ // Create PowerStatsStore.
+ objectStore = aDb.createObjectStore(POWER_STATS_STORE, {
+ keyPath: ["appId", "serviceType", "component", "timestamp"]
+ });
+ objectStore.createIndex("component", "component", { unique: false });
+
+ // Create NetworkStatsStore.
+ objectStore = aDb.createObjectStore(NETWORK_STATS_STORE, {
+ keyPath: ["appId", "serviceType", "component", "timestamp"]
+ });
+ objectStore.createIndex("component", "component", { unique: false });
+
+ // Create AlarmStore.
+ objectStore = aDb.createObjectStore(ALARM_STORE, {
+ keyPath: "alarmId",
+ autoIncrement: true
+ });
+ objectStore.createIndex("type", "type", { unique: false });
+ // Index for resource control target.
+ objectStore.createIndex("controlTarget",
+ ["type", "manifestURL", "serviceType", "component"],
+ { unique: false });
+ },
+
+ // Convert to UTC according to the current timezone and the filter timestamp
+ // to get SAMPLE_RATE precission.
+ _normalizeTime: function(aTime, aOffset) {
+ let time = Math.floor((aTime - aOffset) / SAMPLE_RATE) * SAMPLE_RATE;
+
+ return time;
+ },
+
+ /**
+ * aRecordArray contains an array of json objects storing network stats.
+ * The structure of the json object =
+ * {
+ * appId: XX,
+ * serviceType: "XX",
+ * componentStats: {
+ * "component_1": { receivedBytes: XX, sentBytes: XX },
+ * "component_2": { receivedBytes: XX, sentBytes: XX },
+ * ...
+ * },
+ * }
+ */
+ saveNetworkStats: function(aRecordArray, aTimestamp, aResultCb) {
+ if (DEBUG) {
+ debug("saveNetworkStats()");
+ }
+
+ let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+ let timestamp = this._normalizeTime(aTimestamp, offset);
+
+ this._dbNewTxn(NETWORK_STATS_STORE, "readwrite", function(aTxn, aStore) {
+ aRecordArray.forEach(function(aRecord) {
+ let stats = {
+ appId: aRecord.appId,
+ serviceType: aRecord.serviceType,
+ component: "",
+ timestamp: timestamp,
+ receivedBytes: 0,
+ sentBytes: 0
+ };
+
+ let totalReceivedBytes = 0;
+ let totalSentBytes = 0;
+
+ // Save stats of each component.
+ let data = aRecord.componentStats;
+ for (let component in data) {
+ // Save stats to database.
+ stats.component = component;
+ stats.receivedBytes = data[component].receivedBytes;
+ stats.sentBytes = data[component].sentBytes;
+ aStore.put(stats);
+ if (DEBUG) {
+ debug("Save network stats: " + JSON.stringify(stats));
+ }
+
+ // Accumulated to tatal stats.
+ totalReceivedBytes += stats.receivedBytes;
+ totalSentBytes += stats.sentBytes;
+ }
+
+ // Save total stats.
+ stats.component = "";
+ stats.receivedBytes = totalReceivedBytes;
+ stats.sentBytes = totalSentBytes;
+ aStore.put(stats);
+ if (DEBUG) {
+ debug("Save network stats: " + JSON.stringify(stats));
+ }
+ });
+ }, aResultCb);
+ },
+
+ /**
+ * aRecordArray contains an array of json objects storing power stats.
+ * The structure of the json object =
+ * {
+ * appId: XX,
+ * serviceType: "XX",
+ * componentStats: {
+ * "component_1": XX, // consumedPower
+ * "component_2": XX,
+ * ...
+ * },
+ * }
+ */
+ savePowerStats: function(aRecordArray, aTimestamp, aResultCb) {
+ if (DEBUG) {
+ debug("savePowerStats()");
+ }
+ let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+ let timestamp = this._normalizeTime(aTimestamp, offset);
+
+ this._dbNewTxn(POWER_STATS_STORE, "readwrite", function(aTxn, aStore) {
+ aRecordArray.forEach(function(aRecord) {
+ let stats = {
+ appId: aRecord.appId,
+ serviceType: aRecord.serviceType,
+ component: "",
+ timestamp: timestamp,
+ consumedPower: aRecord.totalStats
+ };
+
+ let totalConsumedPower = 0;
+
+ // Save stats of each component to database.
+ let data = aRecord.componentStats;
+ for (let component in data) {
+ // Save stats to database.
+ stats.component = component;
+ stats.consumedPower = data[component];
+ aStore.put(stats);
+ if (DEBUG) {
+ debug("Save power stats: " + JSON.stringify(stats));
+ }
+ // Accumulated to total stats.
+ totalConsumedPower += stats.consumedPower;
+ }
+
+ // Save total stats.
+ stats.component = "";
+ stats.consumedPower = totalConsumedPower;
+ aStore.put(stats);
+ if (DEBUG) {
+ debug("Save power stats: " + JSON.stringify(stats));
+ }
+ });
+ }, aResultCb);
+ },
+
+ // Get stats from a store.
+ getStats: function(aType, aManifestURL, aServiceType, aComponent,
+ aStart, aEnd, aResultCb) {
+ if (DEBUG) {
+ debug(aType + "Mgr.getStats()");
+ }
+
+ let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+
+ // Get appId and check whether manifestURL is a valid app.
+ let appId = 0;
+ if (aManifestURL) {
+ appId = appsService.getAppLocalIdByManifestURL(aManifestURL);
+
+ if (!appId) {
+ aResultCb("Invalid manifestURL", null);
+ return;
+ }
+ }
+
+ // Get store name.
+ let storeName = statsStoreNames[aType];
+
+ // Normalize start time and end time to SAMPLE_RATE precission.
+ let start = this._normalizeTime(aStart, offset);
+ let end = this._normalizeTime(aEnd, offset);
+ if (DEBUG) {
+ debug("Query time range: " + start + " to " + end);
+ debug("[appId, serviceType, component] = [" + appId + ", " + aServiceType
+ + ", " + aComponent + "]");
+ }
+
+ // Create filters.
+ let lowerFilter = [appId, aServiceType, aComponent, start];
+ let upperFilter = [appId, aServiceType, aComponent, end];
+
+ // Execute DB query.
+ this._dbNewTxn(storeName, "readonly", function(aTxn, aStore) {
+ let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
+
+ let statsData = [];
+
+ if (!aTxn.result) {
+ aTxn.result = Object.create(null);
+ }
+ aTxn.result.type = aType;
+ aTxn.result.component = aComponent;
+ aTxn.result.serviceType = aServiceType;
+ aTxn.result.manifestURL = aManifestURL;
+ aTxn.result.start = start + offset;
+ aTxn.result.end = end + offset;
+ // Since ResourceStats() would require SAMPLE_RATE when filling the empty
+ // entries of statsData array, we append SAMPLE_RATE to the result field
+ // to save an IPC call.
+ aTxn.result.sampleRate = SAMPLE_RATE;
+
+ let request = aStore.openCursor(range, "prev");
+ if (aType == "power") {
+ request.onsuccess = function(aEvent) {
+ var cursor = aEvent.target.result;
+ if (cursor) {
+ if (DEBUG) {
+ debug("Get " + JSON.stringify(cursor.value));
+ }
+
+ // Covert timestamp to current timezone.
+ statsData.push({
+ timestamp: cursor.value.timestamp + offset,
+ consumedPower: cursor.value.consumedPower
+ });
+ cursor.continue();
+ return;
+ }
+ aTxn.result.statsData = statsData;
+ };
+ } else if (aType == "network") {
+ request.onsuccess = function(aEvent) {
+ var cursor = aEvent.target.result;
+ if (cursor) {
+ if (DEBUG) {
+ debug("Get " + JSON.stringify(cursor.value));
+ }
+
+ // Covert timestamp to current timezone.
+ statsData.push({
+ timestamp: cursor.value.timestamp + offset,
+ receivedBytes: cursor.value.receivedBytes,
+ sentBytes: cursor.value.sentBytes
+ });
+ cursor.continue();
+ return;
+ }
+ aTxn.result.statsData = statsData;
+ };
+ }
+ }, aResultCb);
+ },
+
+ // Delete the stats of a specific app/service (within a specified time range).
+ clearStats: function(aType, aAppId, aServiceType, aComponent,
+ aStart, aEnd, aResultCb) {
+ if (DEBUG) {
+ debug(aType + "Mgr.clearStats()");
+ }
+
+ let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+
+ // Get store name.
+ let storeName = statsStoreNames[aType];
+
+ // Normalize start and end time to SAMPLE_RATE precission.
+ let start = this._normalizeTime(aStart, offset);
+ let end = this._normalizeTime(aEnd, offset);
+ if (DEBUG) {
+ debug("Query time range: " + start + " to " + end);
+ debug("[appId, serviceType, component] = [" + aAppId + ", " + aServiceType
+ + ", " + aComponent + "]");
+ }
+
+ // Create filters.
+ let lowerFilter = [aAppId, aServiceType, aComponent, start];
+ let upperFilter = [aAppId, aServiceType, aComponent, end];
+
+ // Execute clear operation.
+ this._dbNewTxn(storeName, "readwrite", function(aTxn, aStore) {
+ let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
+ let request = aStore.openCursor(range).onsuccess = function(aEvent) {
+ let cursor = aEvent.target.result;
+ if (cursor) {
+ if (DEBUG) {
+ debug("Delete " + JSON.stringify(cursor.value) + " from database");
+ }
+ cursor.delete();
+ cursor.continue();
+ return;
+ }
+ };
+ }, aResultCb);
+ },
+
+ // Delete all stats saved in a store.
+ clearAllStats: function(aType, aResultCb) {
+ if (DEBUG) {
+ debug(aType + "Mgr.clearAllStats()");
+ }
+
+ let storeName = statsStoreNames[aType];
+
+ // Execute clear operation.
+ this._dbNewTxn(storeName, "readwrite", function(aTxn, aStore) {
+ if (DEBUG) {
+ debug("Clear " + aType + " stats from datastore");
+ }
+ aStore.clear();
+ }, aResultCb);
+ },
+
+ addAlarm: function(aAlarm, aResultCb) {
+ if (DEBUG) {
+ debug(aAlarm.type + "Mgr.addAlarm()");
+ debug("alarm = " + JSON.stringify(aAlarm));
+ }
+
+ this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
+ aStore.put(aAlarm).onsuccess = function setResult(aEvent) {
+ // Get alarmId.
+ aTxn.result = aEvent.target.result;
+ if (DEBUG) {
+ debug("New alarm ID: " + aTxn.result);
+ }
+ };
+ }, aResultCb);
+ },
+
+ // Convert DB record to alarm object.
+ _recordToAlarm: function(aRecord) {
+ let alarm = {
+ alarmId: aRecord.alarmId,
+ type: aRecord.type,
+ component: aRecord.component,
+ serviceType: aRecord.serviceType,
+ manifestURL: aRecord.manifestURL,
+ threshold: aRecord.threshold,
+ data: aRecord.data
+ };
+
+ return alarm;
+ },
+
+ getAlarms: function(aType, aOptions, aResultCb) {
+ if (DEBUG) {
+ debug(aType + "Mgr.getAlarms()");
+ debug("[appId, serviceType, component] = [" + aOptions.appId + ", "
+ + aOptions.serviceType + ", " + aOptions.component + "]");
+ }
+
+ // Execute clear operation.
+ this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
+ if (!aTxn.result) {
+ aTxn.result = [];
+ }
+
+ let indexName = null;
+ let range = null;
+
+ if (aOptions) { // Get alarms associated to specified statsOptions.
+ indexName = "controlTarget";
+ range = IDBKeyRange.only([aType, aOptions.manifestURL,
+ aOptions.serviceType, aOptions.component]);
+ } else { // Get all alarms of the specified type.
+ indexName = "type";
+ range = IDBKeyRange.only(aType);
+ }
+
+ let request = aStore.index(indexName).openCursor(range);
+ request.onsuccess = function onsuccess(aEvent) {
+ let cursor = aEvent.target.result;
+ if (cursor) {
+ aTxn.result.push(this._recordToAlarm(cursor.value));
+ cursor.continue();
+ return;
+ }
+ }.bind(this);
+ }.bind(this), aResultCb);
+ },
+
+ removeAlarm: function(aType, aAlarmId, aResultCb) {
+ if (DEBUG) {
+ debug("removeAlarms(" + aAlarmId + ")");
+ }
+
+ // Execute clear operation.
+ this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
+ aStore.get(aAlarmId).onsuccess = function onsuccess(aEvent) {
+ let alarm = aEvent.target.result;
+ aTxn.result = false;
+ if (!alarm || aType !== alarm.type) {
+ return;
+ }
+
+ if (DEBUG) {
+ debug("Remove alarm " + JSON.stringify(alarm) + " from datastore");
+ }
+ aStore.delete(aAlarmId);
+ aTxn.result = true;
+ };
+ }, aResultCb);
+ },
+
+ removeAllAlarms: function(aType, aResultCb) {
+ if (DEBUG) {
+ debug(aType + "Mgr.removeAllAlarms()");
+ }
+
+ // Execute clear operation.
+ this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
+ if (DEBUG) {
+ debug("Remove all " + aType + " alarms from datastore.");
+ }
+
+ let range = IDBKeyRange.only(aType);
+ let request = aStore.index("type").openCursor(range);
+ request.onsuccess = function onsuccess(aEvent) {
+ let cursor = aEvent.target.result;
+ if (cursor) {
+ if (DEBUG) {
+ debug("Remove " + JSON.stringify(cursor.value) + " from database.");
+ }
+ cursor.delete();
+ cursor.continue();
+ return;
+ }
+ };
+ }, aResultCb);
+ },
+
+ // Get all index keys of the component.
+ getComponents: function(aType, aResultCb) {
+ if (DEBUG) {
+ debug(aType + "Mgr.getComponents()");
+ }
+
+ let storeName = statsStoreNames[aType];
+
+ this._dbNewTxn(storeName, "readonly", function(aTxn, aStore) {
+ if (!aTxn.result) {
+ aTxn.result = [];
+ }
+
+ let request = aStore.index("component").openKeyCursor(null, "nextunique");
+ request.onsuccess = function onsuccess(aEvent) {
+ let cursor = aEvent.target.result;
+ if (cursor) {
+ aTxn.result.push(cursor.key);
+ cursor.continue();
+ return;
+ }
+
+ // Remove "" from the result, which indicates sum of all
+ // components' stats.
+ let index = aTxn.result.indexOf("");
+ if (index > -1) {
+ aTxn.result.splice(index, 1);
+ }
+ };
+ }, aResultCb);
+ },
+
+ get sampleRate () {
+ return SAMPLE_RATE;
+ },
+
+ get maxStorageAge() {
+ return MAX_STORAGE_AGE;
+ },
+};
+
diff --git a/dom/resourcestats/ResourceStatsManager.js b/dom/resourcestats/ResourceStatsManager.js
new file mode 100644
index 000000000..649f1873a
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsManager.js
@@ -0,0 +1,480 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const DEBUG = false;
+function debug(s) { dump("-*- ResourceStatsManager: " + s + "\n"); }
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
+
+// Constant defines supported statistics.
+const resourceTypeList = ["network", "power"];
+
+function NetworkStatsData(aStatsData) {
+ if (DEBUG) {
+ debug("NetworkStatsData(): " + JSON.stringify(aStatsData));
+ }
+
+ this.receivedBytes = aStatsData.receivedBytes || 0;
+ this.sentBytes = aStatsData.sentBytes || 0;
+ this.timestamp = aStatsData.timestamp;
+}
+
+NetworkStatsData.prototype = {
+ classID: Components.ID("{dce5729a-ba92-4185-8854-e29e71b9e8a2}"),
+ contractID: "@mozilla.org/networkStatsData;1",
+ QueryInterface: XPCOMUtils.generateQI([])
+};
+
+function PowerStatsData(aStatsData) {
+ if (DEBUG) {
+ debug("PowerStatsData(): " + JSON.stringify(aStatsData));
+ }
+
+ this.consumedPower = aStatsData.consumedPower || 0;
+ this.timestamp = aStatsData.timestamp;
+}
+
+PowerStatsData.prototype = {
+ classID: Components.ID("{acb9af6c-8143-4e59-bc18-4bb1736a4004}"),
+ contractID: "@mozilla.org/powerStatsData;1",
+ QueryInterface: XPCOMUtils.generateQI([])
+};
+
+function ResourceStats(aWindow, aStats) {
+ if (DEBUG) {
+ debug("ResourceStats(): " + JSON.stringify(aStats));
+ }
+
+ this._window = aWindow;
+ this.type = aStats.type;
+ this.component = aStats.component || null;
+ this.serviceType = aStats.serviceType || null;
+ this.manifestURL = aStats.manifestURL || null;
+ this.start = aStats.start;
+ this.end = aStats.end;
+ this.statsData = new aWindow.Array();
+
+ // A function creates a StatsData object according to type.
+ let createStatsDataObject = null;
+ let self = this;
+ switch (this.type) {
+ case "power":
+ createStatsDataObject = function(aStats) {
+ let chromeObj = new PowerStatsData(aStats);
+ return self._window.PowerStatsData._create(self._window, chromeObj);
+ };
+ break;
+ case "network":
+ createStatsDataObject = function(aStats) {
+ let chromeObj = new NetworkStatsData(aStats);
+ return self._window.NetworkStatsData._create(self._window, chromeObj);
+ };
+ break;
+ }
+
+ let sampleRate = aStats.sampleRate;
+ let queryResult = aStats.statsData;
+ let stats = queryResult.pop(); // Pop out the last element.
+ let timestamp = this.start;
+
+ // Push query result to statsData, and fill empty elements so that:
+ // 1. the timestamp of the first element of statsData is equal to start;
+ // 2. the timestamp of the last element of statsData is equal to end;
+ // 3. the timestamp difference of every neighboring elements is SAMPLE_RATE.
+ for (; timestamp <= this.end; timestamp += sampleRate) {
+ if (!stats || stats.timestamp != timestamp) {
+ // If dataArray is empty or timestamp are not equal, push a dummy object
+ // (which stats are set to 0) to statsData.
+ this.statsData.push(createStatsDataObject({ timestamp: timestamp }));
+ } else {
+ // Push stats to statsData and pop a new element form queryResult.
+ this.statsData.push(createStatsDataObject(stats));
+ stats = queryResult.pop();
+ }
+ }
+}
+
+ResourceStats.prototype = {
+ getData: function() {
+ return this.statsData;
+ },
+
+ classID: Components.ID("{b7c970f2-3d58-4966-9633-2024feb5132b}"),
+ contractID: "@mozilla.org/resourceStats;1",
+ QueryInterface: XPCOMUtils.generateQI([])
+};
+
+function ResourceStatsAlarm(aWindow, aAlarm) {
+ if (DEBUG) {
+ debug("ResourceStatsAlarm(): " + JSON.stringify(aAlarm));
+ }
+
+ this.alarmId = aAlarm.alarmId;
+ this.type = aAlarm.type;
+ this.component = aAlarm.component || null;
+ this.serviceType = aAlarm.serviceType || null;
+ this.manifestURL = aAlarm.manifestURL || null;
+ this.threshold = aAlarm.threshold;
+
+ // Clone data object using structured clone algorithm.
+ this.data = null;
+ if (aAlarm.data) {
+ this.data = Cu.cloneInto(aAlarm.data, aWindow);
+ }
+}
+
+ResourceStatsAlarm.prototype = {
+ classID: Components.ID("{e2b66e7a-0ff1-4015-8690-a2a3f6a5b63a}"),
+ contractID: "@mozilla.org/resourceStatsAlarm;1",
+ QueryInterface: XPCOMUtils.generateQI([]),
+};
+
+function ResourceStatsManager() {
+ if (DEBUG) {
+ debug("constructor()");
+ }
+}
+
+ResourceStatsManager.prototype = {
+ __proto__: DOMRequestIpcHelper.prototype,
+
+ _getPromise: function(aCallback) {
+ let self = this;
+ return this.createPromise(function(aResolve, aReject) {
+ let resolverId = self.getPromiseResolverId({
+ resolve: aResolve,
+ reject: aReject
+ });
+
+ aCallback(resolverId);
+ });
+ },
+
+ // Check time range.
+ _checkTimeRange: function(aStart, aEnd) {
+ if (DEBUG) {
+ debug("_checkTimeRange(): " + aStart + " to " + aEnd);
+ }
+
+ let start = aStart ? aStart : 0;
+ let end = aEnd ? aEnd : Date.now();
+
+ if (start > end) {
+ throw Cr.NS_ERROR_INVALID_ARG;
+ }
+
+ return { start: start, end: end };
+ },
+
+ getStats: function(aStatsOptions, aStart, aEnd) {
+ // Check time range.
+ let { start: start, end: end } = this._checkTimeRange(aStart, aEnd);
+
+ // Create Promise.
+ let self = this;
+ return this._getPromise(function(aResolverId) {
+ self.cpmm.sendAsyncMessage("ResourceStats:GetStats", {
+ resolverId: aResolverId,
+ type: self.type,
+ statsOptions: aStatsOptions,
+ manifestURL: self._manifestURL,
+ start: start,
+ end: end
+ });
+ });
+ },
+
+ clearStats: function(aStatsOptions, aStart, aEnd) {
+ // Check time range.
+ let {start: start, end: end} = this._checkTimeRange(aStart, aEnd);
+
+ // Create Promise.
+ let self = this;
+ return this._getPromise(function(aResolverId) {
+ self.cpmm.sendAsyncMessage("ResourceStats:ClearStats", {
+ resolverId: aResolverId,
+ type: self.type,
+ statsOptions: aStatsOptions,
+ manifestURL: self._manifestURL,
+ start: start,
+ end: end
+ });
+ });
+ },
+
+ clearAllStats: function() {
+ // Create Promise.
+ let self = this;
+ return this._getPromise(function(aResolverId) {
+ self.cpmm.sendAsyncMessage("ResourceStats:ClearAllStats", {
+ resolverId: aResolverId,
+ type: self.type,
+ manifestURL: self._manifestURL
+ });
+ });
+ },
+
+ addAlarm: function(aThreshold, aStatsOptions, aAlarmOptions) {
+ if (DEBUG) {
+ debug("aStatsOptions: " + JSON.stringify(aAlarmOptions));
+ debug("aAlarmOptions: " + JSON.stringify(aAlarmOptions));
+ }
+
+ // Parse alarm options.
+ let startTime = aAlarmOptions.startTime || 0;
+
+ // Clone data object using structured clone algorithm.
+ let data = null;
+ if (aAlarmOptions.data) {
+ data = Cu.cloneInto(aAlarmOptions.data, this._window);
+ }
+
+ // Create Promise.
+ let self = this;
+ return this._getPromise(function(aResolverId) {
+ self.cpmm.sendAsyncMessage("ResourceStats:AddAlarm", {
+ resolverId: aResolverId,
+ type: self.type,
+ threshold: aThreshold,
+ statsOptions: aStatsOptions,
+ manifestURL: self._manifestURL,
+ startTime: startTime,
+ data: data
+ });
+ });
+ },
+
+ getAlarms: function(aStatsOptions) {
+ // Create Promise.
+ let self = this;
+ return this._getPromise(function(aResolverId) {
+ self.cpmm.sendAsyncMessage("ResourceStats:GetAlarms", {
+ resolverId: aResolverId,
+ type: self.type,
+ statsOptions: aStatsOptions,
+ manifestURL: self._manifestURL
+ });
+ });
+ },
+
+ removeAlarm: function(aAlarmId) {
+ // Create Promise.
+ let self = this;
+ return this._getPromise(function(aResolverId) {
+ self.cpmm.sendAsyncMessage("ResourceStats:RemoveAlarm", {
+ resolverId: aResolverId,
+ type: self.type,
+ manifestURL: self._manifestURL,
+ alarmId: aAlarmId
+ });
+ });
+ },
+
+ removeAllAlarms: function() {
+ // Create Promise.
+ let self = this;
+ return this._getPromise(function(aResolverId) {
+ self.cpmm.sendAsyncMessage("ResourceStats:RemoveAllAlarms", {
+ resolverId: aResolverId,
+ type: self.type,
+ manifestURL: self._manifestURL
+ });
+ });
+ },
+
+ getAvailableComponents: function() {
+ // Create Promise.
+ let self = this;
+ return this._getPromise(function(aResolverId) {
+ self.cpmm.sendAsyncMessage("ResourceStats:GetComponents", {
+ resolverId: aResolverId,
+ type: self.type,
+ manifestURL: self._manifestURL
+ });
+ });
+ },
+
+ get resourceTypes() {
+ let types = new this._window.Array();
+ resourceTypeList.forEach(function(aType) {
+ types.push(aType);
+ });
+
+ return types;
+ },
+
+ get sampleRate() {
+ let msg = { manifestURL: this._manifestURL };
+
+ return this.cpmm.sendSyncMessage("ResourceStats:SampleRate", msg)[0];
+ },
+
+ get maxStorageAge() {
+ let msg = { manifestURL: this._manifestURL };
+
+ return this.cpmm.sendSyncMessage("ResourceStats:MaxStorageAge", msg)[0];
+ },
+
+ receiveMessage: function(aMessage) {
+ let data = aMessage.data;
+ let chromeObj = null;
+ let webidlObj = null;
+ let self = this;
+
+ if (DEBUG) {
+ debug("receiveMessage(): " + aMessage.name + " " + data.resolverId);
+ }
+
+ let resolver = this.takePromiseResolver(data.resolverId);
+ if (!resolver) {
+ return;
+ }
+
+ switch (aMessage.name) {
+ case "ResourceStats:GetStats:Resolved":
+ if (DEBUG) {
+ debug("data.value = " + JSON.stringify(data.value));
+ }
+
+ chromeObj = new ResourceStats(this._window, data.value);
+ webidlObj = this._window.ResourceStats._create(this._window, chromeObj);
+ resolver.resolve(webidlObj);
+ break;
+
+ case "ResourceStats:AddAlarm:Resolved":
+ if (DEBUG) {
+ debug("data.value = " + JSON.stringify(data.value));
+ }
+
+ resolver.resolve(data.value); // data.value is alarmId.
+ break;
+
+ case "ResourceStats:GetAlarms:Resolved":
+ if (DEBUG) {
+ debug("data.value = " + JSON.stringify(data.value));
+ }
+
+ let alarmArray = this._window.Array();
+ data.value.forEach(function(aAlarm) {
+ chromeObj = new ResourceStatsAlarm(self._window, aAlarm);
+ webidlObj = self._window.ResourceStatsAlarm._create(self._window,
+ chromeObj);
+ alarmArray.push(webidlObj);
+ });
+ resolver.resolve(alarmArray);
+ break;
+
+ case "ResourceStats:GetComponents:Resolved":
+ if (DEBUG) {
+ debug("data.value = " + JSON.stringify(data.value));
+ }
+
+ let components = this._window.Array();
+ data.value.forEach(function(aComponent) {
+ components.push(aComponent);
+ });
+ resolver.resolve(components);
+ break;
+
+ case "ResourceStats:ClearStats:Resolved":
+ case "ResourceStats:ClearAllStats:Resolved":
+ case "ResourceStats:RemoveAlarm:Resolved":
+ case "ResourceStats:RemoveAllAlarms:Resolved":
+ if (DEBUG) {
+ debug("data.value = " + JSON.stringify(data.value));
+ }
+
+ resolver.resolve(data.value);
+ break;
+
+ case "ResourceStats:GetStats:Rejected":
+ case "ResourceStats:ClearStats:Rejected":
+ case "ResourceStats:ClearAllStats:Rejected":
+ case "ResourceStats:AddAlarm:Rejected":
+ case "ResourceStats:GetAlarms:Rejected":
+ case "ResourceStats:RemoveAlarm:Rejected":
+ case "ResourceStats:RemoveAllAlarms:Rejected":
+ case "ResourceStats:GetComponents:Rejected":
+ if (DEBUG) {
+ debug("data.reason = " + JSON.stringify(data.reason));
+ }
+
+ resolver.reject(data.reason);
+ break;
+
+ default:
+ if (DEBUG) {
+ debug("Could not find a handler for " + aMessage.name);
+ }
+ resolver.reject();
+ }
+ },
+
+ __init: function(aType) {
+ if (resourceTypeList.indexOf(aType) < 0) {
+ if (DEBUG) {
+ debug("Do not support resource statistics for " + aType);
+ }
+ throw Cr.NS_ERROR_INVALID_ARG;
+ } else {
+ if (DEBUG) {
+ debug("Create " + aType + "Mgr");
+ }
+ this.type = aType;
+ }
+ },
+
+ init: function(aWindow) {
+ if (DEBUG) {
+ debug("init()");
+ }
+
+ // Get the manifestURL if this is an installed app
+ let principal = aWindow.document.nodePrincipal;
+ let appsService = Cc["@mozilla.org/AppsService;1"]
+ .getService(Ci.nsIAppsService);
+ this._manifestURL = appsService.getManifestURLByLocalId(principal.appId);
+
+ const messages = ["ResourceStats:GetStats:Resolved",
+ "ResourceStats:ClearStats:Resolved",
+ "ResourceStats:ClearAllStats:Resolved",
+ "ResourceStats:AddAlarm:Resolved",
+ "ResourceStats:GetAlarms:Resolved",
+ "ResourceStats:RemoveAlarm:Resolved",
+ "ResourceStats:RemoveAllAlarms:Resolved",
+ "ResourceStats:GetComponents:Resolved",
+ "ResourceStats:GetStats:Rejected",
+ "ResourceStats:ClearStats:Rejected",
+ "ResourceStats:ClearAllStats:Rejected",
+ "ResourceStats:AddAlarm:Rejected",
+ "ResourceStats:GetAlarms:Rejected",
+ "ResourceStats:RemoveAlarm:Rejected",
+ "ResourceStats:RemoveAllAlarms:Rejected",
+ "ResourceStats:GetComponents:Rejected"];
+ this.initDOMRequestHelper(aWindow, messages);
+
+ this.cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+ },
+
+ classID: Components.ID("{101ed1f8-31b3-491c-95ea-04091e6e8027}"),
+ contractID: "@mozilla.org/resourceStatsManager;1",
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer,
+ Ci.nsISupportsWeakReference,
+ Ci.nsIObserver])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NetworkStatsData,
+ PowerStatsData,
+ ResourceStats,
+ ResourceStatsAlarm,
+ ResourceStatsManager]);
+
diff --git a/dom/resourcestats/ResourceStatsManager.manifest b/dom/resourcestats/ResourceStatsManager.manifest
new file mode 100644
index 000000000..985126ff2
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsManager.manifest
@@ -0,0 +1,14 @@
+component {dce5729a-ba92-4185-8854-e29e71b9e8a2} ResourceStatsManager.js
+contract @mozilla.org/networkStatsData;1 {dce5729a-ba92-4185-8854-e29e71b9e8a2}
+
+component {acb9af6c-8143-4e59-bc18-4bb1736a4004} ResourceStatsManager.js
+contract @mozilla.org/powerStatsData;1 {acb9af6c-8143-4e59-bc18-4bb1736a4004}
+
+component {b7c970f2-3d58-4966-9633-2024feb5132b} ResourceStatsManager.js
+contract @mozilla.org/resourceStats;1 {b7c970f2-3d58-4966-9633-2024feb5132b}
+
+component {e2b66e7a-0ff1-4015-8690-a2a3f6a5b63a} ResourceStatsManager.js
+contract @mozilla.org/resourceStatsAlarm;1 {e2b66e7a-0ff1-4015-8690-a2a3f6a5b63a}
+
+component {101ed1f8-31b3-491c-95ea-04091e6e8027} ResourceStatsManager.js
+contract @mozilla.org/resourceStatsManager;1 {101ed1f8-31b3-491c-95ea-04091e6e8027}
diff --git a/dom/resourcestats/ResourceStatsService.jsm b/dom/resourcestats/ResourceStatsService.jsm
new file mode 100644
index 000000000..df4481eab
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsService.jsm
@@ -0,0 +1,334 @@
+/* This Source Code Form is subject to the terms of the Mozilla public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ResourceStatsService"];
+
+const DEBUG = false;
+function debug(s) { dump("-*- ResourceStatsService: " + s + "\n"); }
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+// Load ResourceStatsDB.
+Cu.import("resource://gre/modules/ResourceStatsDB.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gIDBManager",
+ "@mozilla.org/dom/indexeddb/manager;1",
+ "nsIIndexedDatabaseManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+ "@mozilla.org/parentprocessmessagemanager;1",
+ "nsIMessageListenerManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "appsService",
+ "@mozilla.org/AppsService;1",
+ "nsIAppsService");
+
+this.ResourceStatsService = {
+
+ init: function() {
+ if (DEBUG) {
+ debug("Service started");
+ }
+
+ // Set notification to observe.
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+
+ // Add message listener.
+ this.messages = ["ResourceStats:GetStats",
+ "ResourceStats:ClearStats",
+ "ResourceStats:ClearAllStats",
+ "ResourceStats:AddAlarm",
+ "ResourceStats:GetAlarms",
+ "ResourceStats:RemoveAlarm",
+ "ResourceStats:RemoveAllAlarms",
+ "ResourceStats:GetComponents",
+ "ResourceStats:SampleRate",
+ "ResourceStats:MaxStorageAge"];
+
+ this.messages.forEach(function(aMessageName){
+ ppmm.addMessageListener(aMessageName, this);
+ }, this);
+
+ // Create indexedDB.
+ this._db = new ResourceStatsDB();
+ },
+
+ receiveMessage: function(aMessage) {
+ if (DEBUG) {
+ debug("receiveMessage(): " + aMessage.name);
+ }
+
+ let mm = aMessage.target;
+ let data = aMessage.data;
+
+ if (DEBUG) {
+ debug("received aMessage.data = " + JSON.stringify(data));
+ }
+
+ // To prevent the hacked child process from sending commands to parent,
+ // we need to check its permission and manifest URL.
+ if (!aMessage.target.assertPermission("resourcestats-manage")) {
+ return;
+ }
+ if (!aMessage.target.assertContainApp(data.manifestURL)) {
+ if (DEBUG) {
+ debug("Got msg from a child process containing illegal manifestURL.");
+ }
+ return;
+ }
+
+ switch (aMessage.name) {
+ case "ResourceStats:GetStats":
+ this.getStats(mm, data);
+ break;
+ case "ResourceStats:ClearStats":
+ this.clearStats(mm, data);
+ break;
+ case "ResourceStats:ClearAllStats":
+ this.clearAllStats(mm, data);
+ break;
+ case "ResourceStats:AddAlarm":
+ this.addAlarm(mm, data);
+ break;
+ case "ResourceStats:GetAlarms":
+ this.getAlarms(mm, data);
+ break;
+ case "ResourceStats:RemoveAlarm":
+ this.removeAlarm(mm, data);
+ break;
+ case "ResourceStats:RemoveAllAlarms":
+ this.removeAllAlarms(mm, data);
+ break;
+ case "ResourceStats:GetComponents":
+ this.getComponents(mm, data);
+ break;
+ case "ResourceStats:SampleRate":
+ // This message is sync.
+ return this._db.sampleRate;
+ case "ResourceStats:MaxStorageAge":
+ // This message is sync.
+ return this._db.maxStorageAge;
+ }
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "xpcom-shutdown":
+ if (DEBUG) {
+ debug("Service shutdown " + aData);
+ }
+
+ this.messages.forEach(function(aMessageName) {
+ ppmm.removeMessageListener(aMessageName, this);
+ }, this);
+
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ break;
+ default:
+ return;
+ }
+ },
+
+ // Closure generates callback function for DB request.
+ _createDbCallback: function(aMm, aId, aMessage) {
+ let resolveMsg = aMessage + ":Resolved";
+ let rejectMsg = aMessage + ":Rejected";
+
+ return function(aError, aResult) {
+ if (aError) {
+ aMm.sendAsyncMessage(rejectMsg, { resolverId: aId, reason: aError });
+ return;
+ }
+ aMm.sendAsyncMessage(resolveMsg, { resolverId: aId, value: aResult });
+ };
+ },
+
+ getStats: function(aMm, aData) {
+ if (DEBUG) {
+ debug("getStats(): " + JSON.stringify(aData));
+ }
+
+ // Note: we validate the manifestURL in _db.getStats().
+ let manifestURL = aData.statsOptions.manifestURL || "";
+ let serviceType = aData.statsOptions.serviceType || "";
+ let component = aData.statsOptions.component || "";
+
+ // Execute DB operation.
+ let onStatsGot = this._createDbCallback(aMm, aData.resolverId,
+ "ResourceStats:GetStats");
+ this._db.getStats(aData.type, manifestURL, serviceType, component,
+ aData.start, aData.end, onStatsGot);
+ },
+
+ clearStats: function(aMm, aData) {
+ if (DEBUG) {
+ debug("clearStats(): " + JSON.stringify(aData));
+ }
+
+ // Get appId and check whether manifestURL is a valid app.
+ let appId = 0;
+ let manifestURL = aData.statsOptions.manifestURL || "";
+ if (manifestURL) {
+ appId = appsService.getAppLocalIdByManifestURL(manifestURL);
+
+ if (!appId) {
+ aMm.sendAsyncMessage("ResourceStats:GetStats:Rejected", {
+ resolverId: aData.resolverId,
+ reason: "Invalid manifestURL"
+ });
+ return;
+ }
+ }
+
+ let serviceType = aData.statsOptions.serviceType || "";
+ let component = aData.statsOptions.component || "";
+
+ // Execute DB operation.
+ let onStatsCleared = this._createDbCallback(aMm, aData.resolverId,
+ "ResourceStats:ClearStats");
+ this._db.clearStats(aData.type, appId, serviceType, component,
+ aData.start, aData.end, onStatsCleared);
+ },
+
+ clearAllStats: function(aMm, aData) {
+ if (DEBUG) {
+ debug("clearAllStats(): " + JSON.stringify(aData));
+ }
+
+ // Execute DB operation.
+ let onAllStatsCleared = this._createDbCallback(aMm, aData.resolverId,
+ "ResourceStats:ClearAllStats");
+ this._db.clearAllStats(aData.type, onAllStatsCleared);
+ },
+
+ addAlarm: function(aMm, aData) {
+ if (DEBUG) {
+ debug("addAlarm(): " + JSON.stringify(aData));
+ }
+
+ // Get appId and check whether manifestURL is a valid app.
+ let manifestURL = aData.statsOptions.manifestURL;
+ if (manifestURL) {
+ let appId = appsService.getAppLocalIdByManifestURL(manifestURL);
+
+ if (!appId) {
+ aMm.sendAsyncMessage("ResourceStats:GetStats:Rejected", {
+ resolverId: aData.resolverId,
+ reason: "Invalid manifestURL"
+ });
+ return;
+ }
+ }
+
+ // Create an alarm object.
+ let newAlarm = {
+ type: aData.type,
+ component: aData.statsOptions.component || "",
+ serviceType: aData.statsOptions.serviceType || "",
+ manifestURL: manifestURL || "",
+ threshold: aData.threshold,
+ startTime: aData.startTime,
+ data: aData.data
+ };
+
+ // Execute DB operation.
+ let onAlarmAdded = this._createDbCallback(aMm, aData.resolverId,
+ "ResourceStats:AddAlarm");
+ this._db.addAlarm(newAlarm, onAlarmAdded);
+ },
+
+ getAlarms: function(aMm, aData) {
+ if (DEBUG) {
+ debug("getAlarms(): " + JSON.stringify(aData));
+ }
+
+ let options = null;
+ let statsOptions = aData.statsOptions;
+ // If all keys in statsOptions are set to null, treat this call as quering
+ // all alarms; otherwise, resolve the statsOptions and perform DB query
+ // according to the resolved result.
+ if (statsOptions.manifestURL || statsOptions.serviceType ||
+ statsOptions.component) {
+ // Validate manifestURL.
+ let manifestURL = statsOptions.manifestURL || "";
+ if (manifestURL) {
+ let appId = appsService.getAppLocalIdByManifestURL(manifestURL);
+
+ if (!appId) {
+ aMm.sendAsyncMessage("ResourceStats:GetStats:Rejected", {
+ resolverId: aData.resolverId,
+ reason: "Invalid manifestURL"
+ });
+ return;
+ }
+ }
+
+ options = {
+ manifestURL: manifestURL,
+ serviceType: statsOptions.serviceType || "",
+ component: statsOptions.component || ""
+ };
+ }
+
+ // Execute DB operation.
+ let onAlarmsGot = this._createDbCallback(aMm, aData.resolverId,
+ "ResourceStats:GetAlarms");
+ this._db.getAlarms(aData.type, options, onAlarmsGot);
+ },
+
+ removeAlarm: function(aMm, aData) {
+ if (DEBUG) {
+ debug("removeAlarm(): " + JSON.stringify(aData));
+ }
+
+ // Execute DB operation.
+ let onAlarmRemoved = function(aError, aResult) {
+ if (aError) {
+ aMm.sendAsyncMessage("ResourceStats:RemoveAlarm:Rejected",
+ { resolverId: aData.resolverId, reason: aError });
+ }
+
+ if (!aResult) {
+ aMm.sendAsyncMessage("ResourceStats:RemoveAlarm:Rejected",
+ { resolverId: aData.resolverId,
+ reason: "alarm not existed" });
+ }
+
+ aMm.sendAsyncMessage("ResourceStats:RemoveAlarm:Resolved",
+ { resolverId: aData.resolverId, value: aResult });
+ };
+
+ this._db.removeAlarm(aData.type, aData.alarmId, onAlarmRemoved);
+ },
+
+ removeAllAlarms: function(aMm, aData) {
+ if (DEBUG) {
+ debug("removeAllAlarms(): " + JSON.stringify(aData));
+ }
+
+ // Execute DB operation.
+ let onAllAlarmsRemoved = this._createDbCallback(aMm, aData.resolverId,
+ "ResourceStats:RemoveAllAlarms");
+ this._db.removeAllAlarms(aData.type, onAllAlarmsRemoved);
+ },
+
+ getComponents: function(aMm, aData) {
+ if (DEBUG) {
+ debug("getComponents(): " + JSON.stringify(aData));
+ }
+
+ // Execute DB operation.
+ let onComponentsGot = this._createDbCallback(aMm, aData.resolverId,
+ "ResourceStats:GetComponents");
+ this._db.getComponents(aData.type, onComponentsGot);
+ },
+};
+
+this.ResourceStatsService.init();
+
diff --git a/dom/resourcestats/moz.build b/dom/resourcestats/moz.build
new file mode 100644
index 000000000..437457a63
--- /dev/null
+++ b/dom/resourcestats/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_COMPONENTS += [
+ 'ResourceStatsManager.js',
+ 'ResourceStatsManager.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'ResourceStatsDB.jsm',
+ 'ResourceStatsService.jsm',
+]
+
+FINAL_LIBRARY = 'xul'
+
+if CONFIG['MOZ_B2G_RIL']:
+ XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
+
+MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini']
diff --git a/dom/resourcestats/tests/mochitest/mochitest.ini b/dom/resourcestats/tests/mochitest/mochitest.ini
new file mode 100644
index 000000000..dbcb05a5f
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/mochitest.ini
@@ -0,0 +1,16 @@
+[test_basic.html]
+skip-if = toolkit != "gonk"
+[test_network_stats.html]
+skip-if = toolkit != "gonk"
+[test_power_stats.html]
+skip-if = toolkit != "gonk"
+[test_network_alarm.html]
+skip-if = toolkit != "gonk"
+[test_power_alarm.html]
+skip-if = toolkit != "gonk"
+[test_disabled_pref.html]
+skip-if = toolkit != "gonk"
+[test_no_perm.html]
+skip-if = toolkit != "gonk"
+[test_not_supported_type.html]
+skip-if = toolkit != "gonk"
diff --git a/dom/resourcestats/tests/mochitest/test_basic.html b/dom/resourcestats/tests/mochitest/test_basic.html
new file mode 100644
index 000000000..d01762ddf
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_basic.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test accessibility of interfaces</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// Test accessibility of interfaces.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+ ["dom.resource_stats.enabled", true],
+ ["dom.ignore_webidl_scope_checks", true]
+ ]}, function() {
+ ok(SpecialPowers.hasPermission("resourcestats-manage", document),
+ "Has permission 'resourcestats-manage'.");
+ ok(SpecialPowers.getBoolPref("dom.resource_stats.enabled"),
+ "Preference 'dom.resource_stats.enabled' is true.");
+
+ // Check all interfaces are accessible.
+ ok('ResourceStatsManager' in window, "ResourceStatsManager exist.");
+ ok('ResourceStatsAlarm' in window, "ResourceStatsAlarm exist.");
+ ok('ResourceStats' in window, "ResourceStats exist.");
+ ok('NetworkStatsData' in window, "NetworkStatsData exist.");
+ ok('PowerStatsData' in window, "PowerStatsData exist.");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/resourcestats/tests/mochitest/test_disabled_pref.html b/dom/resourcestats/tests/mochitest/test_disabled_pref.html
new file mode 100644
index 000000000..1708ac0ae
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_disabled_pref.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test to ensure interface is not accessible when preference is disabled</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// Test to ensure interface is not accessible when preference is disabled.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+ ["dom.resource_stats.enabled", false],
+ ["dom.ignore_webidl_scope_checks", true]
+ ]}, function() {
+ ok(SpecialPowers.hasPermission("resourcestats-manage", document),
+ "Has permission 'resourcestats-manage'.");
+ ok(!(SpecialPowers.getBoolPref("dom.resource_stats.enabled")),
+ "Preference 'dom.resource_stats.enabled' is false.");
+
+ // Check accessibility.
+ is('ResourceStatsManager' in window, false, "ResourceStatsManager should not exist.");
+ is('ResourceStatsAlarm' in window, false, "ResourceStatsAlarm should not exist.");
+ is('ResourceStats' in window, false, "ResourceStats should not exist.");
+ is('NetworkStatsData' in window, false, "NetworkStatsData should not exist.");
+ is('PowerStatsData' in window, false, "PowerStatsData should not exist.");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/resourcestats/tests/mochitest/test_network_alarm.html b/dom/resourcestats/tests/mochitest/test_network_alarm.html
new file mode 100644
index 000000000..2b4afd677
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_network_alarm.html
@@ -0,0 +1,356 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for ResourceStats methods realted to network resource control</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const invalidManifestURL = "app://invalid.gaiamobile.org/manifest.webapp";
+const wifiComponent = "wifi:0";
+const mobileComponent = "mobile:1";
+var networkStatsMgr = null; // ResourceStatsManager for network statistics.
+
+function errorCb(reason) {
+ ok(false, reason);
+}
+
+// Check the content returned by getAlarms.
+function checkAlarmsArray(alarms) {
+ // Check if data is an array.
+ if (!Array.isArray(alarms)) {
+ throw "getAlarms does not return an array.";
+ } else {
+ ok(true, "getAlarms returns an array.")
+ }
+
+ // Iterate the array and check the type of each element.
+ var obj = null;
+ var message = null; // Message for exception
+
+ for (var i = 0; i < alarms.length; i++) {
+ obj = alarms[i];
+
+ // Check if obj is an instance os ResourceStatsAlarm.
+ if (!(obj instanceof ResourceStatsAlarm)) {
+ message = "The array contains a non-ResourceStatsAlarm object.";
+ break;
+ }
+
+ // Check if obj.type is network.
+ if (obj.type != "network") {
+ message = "The type of a ResourceStatsAlarm object is not network.";
+ break;
+ }
+ }
+
+ if (message) {
+ throw message;
+ }
+
+ ok(true, "The return is an array of ResourceStatsAlarm objects.");
+}
+
+// Test Cases for testing WebIDL methods related to resource control.
+var testCases = [
+ function() {
+ // Test removeAllAlarms.
+ var promise = networkStatsMgr.removeAllAlarms();
+ promise.then(function() {
+ ok(true, "removeAllAlarms deleted all network alarms.");
+ testMethods();
+ }, function() {
+ ok(false, "removeAllAlarms failed to delete network alarms.");
+ });
+ },
+
+ function() {
+ // Test addAlarm.
+ var threshold = Math.floor(Math.random() * 1000);
+ var promise = networkStatsMgr.addAlarm(threshold,
+ { 'component': wifiComponent },
+ { 'startTime': Date.now() });
+ promise.then(function(value) {
+ // Check the value (alarmId).
+ if (value < 0) {
+ ok(false, "addAlarm failed to create an alarm.");
+ } else {
+ ok(true, "addAlarm created an alarm.");
+ testMethods();
+ }
+ }, function() {
+ ok(false, "addAlarm failed to create an alarm.");
+ });
+ },
+
+ function() {
+ // Test addAlarm with negative threshold.
+ var threshold = -1;
+ var promise = networkStatsMgr.addAlarm(threshold,
+ { 'component': wifiComponent },
+ { 'startTime': Date.now() });
+ promise.then(function() {
+ // Check the value.
+ ok(false,
+ "addAlarm did not throw an exception when negative threshold is set.");
+ }, function() {
+ ok(true, "addAlarm threw an exception when negative threshold is set.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test addAlarm with no threshold.
+ var promise = networkStatsMgr.addAlarm();
+ promise.then(function() {
+ // Check the value.
+ ok(false, "addAlarm did not throw an exception when no threshold.");
+ }, function() {
+ ok(true, "addAlarm threw an exception when no threshold.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test addAlarm with negative startTime.
+ var threshold = Math.floor(Math.random() * 1000);
+ var promise = networkStatsMgr.addAlarm(threshold,
+ { 'component': wifiComponent },
+ { 'startTime': -1 });
+ promise.then(function() {
+ // Check the value.
+ ok(false,
+ "addAlarm did not throw an exception when negative startTime is set.");
+ }, function() {
+ ok(true, "addAlarm threw an exception when negative startTime is set.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test addAlarm when manifestURL is invalid.
+ var threshold = Math.floor(Math.random() * 1000);
+ var promise = networkStatsMgr.addAlarm(threshold,
+ { 'component': wifiComponent,
+ 'manifestURL': invalidManifestURL },
+ { 'startTime': Date.now() });
+ promise.then(function() {
+ // Check the value.
+ ok(false, "addAlarm did not throw an exception when manifestURL is invalid.");
+ }, function() {
+ ok(true, "addAlarm threw an exception when manifestURL is invalid.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test getAlarms.
+ var alarmId;
+ var alarms;
+ var threshold = Math.floor(Math.random() * 1000);
+
+ // Execution steps:
+ // 1. Add a new alarm.
+ var promise = networkStatsMgr.addAlarm(threshold,
+ { 'component': wifiComponent },
+ { 'startTime': Date.now() });
+
+ // 2. Test getAlarms if new alarm is added.
+ var runGetAlarms = function(value) {
+ alarmId = value;
+ return networkStatsMgr.getAlarms({ 'component': wifiComponent });
+ };
+
+ // 3. Check the content returned by getAlarms.
+ var checkGetAlarmsReturn = function(value) {
+ alarms = value;
+ checkAlarmsArray(value);
+ };
+
+ // 4. Check the alarm added in step 1 is inside the return of getAlarms.
+ var checkAlarm = function (value) {
+ // Find the alarm.
+ var index = alarms.map(function(e) { return e.alarmId; })
+ .indexOf(alarmId);
+ if (index < 0) {
+ throw "getAlarms does not get the alarm added in previous step.";
+ }
+ var alarm = alarms[index];
+
+ // Evaluate the alarm.
+ ok(alarm.threshold == threshold, "threshold is equal.");
+ ok(alarm.component == wifiComponent, "component is equal.");
+ ok(alarm.serviceType == null, "serviceType should be null.");
+ ok(alarm.manifestURL == null, "manifestURL should be null.");
+ };
+
+ // Create promise chaining.
+ promise.then(runGetAlarms)
+ .then(checkGetAlarmsReturn)
+ .then(checkAlarm)
+ .then(testMethods, errorCb); // Execute next test case.
+ },
+
+ function() {
+ // Test getAlarms with invalid manifestURL.
+ var threshold = Math.floor(Math.random() * 1000);
+ var promise = networkStatsMgr.addAlarm(threshold,
+ { 'component': wifiComponent,
+ 'manifestURL': invalidManifestURL },
+ { 'startTime': Date.now() });
+
+ promise.then(function() {
+ // Check the value.
+ ok(false, "getAlarms did not throw an exception when manifestURL is invalid.");
+ }, function() {
+ ok(true, "getAlarms threw an exception when manifestURL is invalid.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test getAlarms with incorrect parameter.
+ var threshold = Math.floor(Math.random() * 1000);
+
+ // Execution steps:
+ // 1. Add a new alarm.
+ var promise = networkStatsMgr.addAlarm(threshold,
+ { 'component': wifiComponent },
+ { 'startTime': Date.now() });
+
+ // 2. Call getAlarms with incorrect parameter.
+ var runGetAlarms = function() {
+ return networkStatsMgr.getAlarms({ 'component': mobileComponent });
+ };
+
+ // 3. check the content returned by getAlarms.
+ var checkGetAlarmsReturn = function(value) {
+ // Check array length
+ if (value.length) {
+ throw "getAlarms gets an alarm when using incorrect parameter.";
+ } else {
+ ok(true,
+ "getAlarms returns an empty array when using incorrect parameter.");
+ }
+ };
+
+ // Create pomise chaining.
+ promise.then(runGetAlarms)
+ .then(checkGetAlarmsReturn)
+ .then(testMethods, errorCb); // Execute next test case.
+ },
+
+ function() {
+ // Test removeAlarm
+ var alarmId;
+ var threshold = Math.floor(Math.random() * 1000);
+
+ // Execution steps:
+ // 1. Add a new alarm.
+ var promise = networkStatsMgr.addAlarm(threshold,
+ { 'component': wifiComponent },
+ { 'startTime': Date.now() });
+
+ // 2. Try to remove the new alarm.
+ var runRemoveAlarm = function(value) {
+ alarmId = value;
+ return networkStatsMgr.removeAlarm(alarmId);
+ }
+
+ // Create promise chaining.
+ promise.then(runRemoveAlarm)
+ .then(function() {
+ ok(true, "removeAlarm deleted the alarm.");
+ testMethods();
+ }, errorCb);
+ },
+
+ function() {
+ // Test removeAlarm with negative alarmId
+ var alarmId = -1;
+ var promise = networkStatsMgr.removeAlarm(alarmId);
+ promise.then(function() {
+ ok(false,
+ "removeAlarm did not throw an exception when negative alarmId is set.");
+ }, function() {
+ ok(true, "removeAlarm threw an exception when negative alarmId is set.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test removeAlarm with invalid alarmId
+ var alarmId;
+ var threshold = Math.floor(Math.random() * 1000);
+
+ // Execution steps:
+ // 1. Add a new alarm.
+ var promise = networkStatsMgr.addAlarm(threshold,
+ { 'component': wifiComponent },
+ { 'startTime': Date.now() });
+
+ // 2. Try to remove an invalid alarm.
+ var runRemoveAlarm = function(value) {
+ alarmId = value;
+ // Because alarmId is auto-increment, any alarmId larger than the
+ // latest alarmId should be invalid.
+ return networkStatsMgr.removeAlarm(alarmId + 10);
+ }
+
+ // Create promise chaining.
+ promise.then(runRemoveAlarm)
+ .then(function() {
+ // Input with incorrect alarmId should not be resolved.
+ throw "removeAlarm should fail with invalid alarmId.";
+ }, function(reason) {
+ if (reason == "alarm not existed") {
+ ok(true, "removeAlarm with invalid alarmId should fail.")
+ } else {
+ throw reason;
+ }
+ })
+ .then(testMethods, errorCb);
+ }
+];
+
+// Test WebIDL methods related stats operation.
+function testMethods() {
+ if (!testCases.length) {
+ ok(true, "Done.");
+ SpecialPowers.removePermission("resourcestats-manage", document);
+ SimpleTest.finish();
+ return;
+ }
+
+ var testCase = testCases.shift();
+ testCase();
+}
+
+function startTest() {
+ // Create an instance of ResourceStatsManager for network.
+ networkStatsMgr = new ResourceStatsManager("network");
+ ok(networkStatsMgr, "Create networkStatsMgr.");
+
+ // Test WebIDL methods related to resource control.
+ testMethods();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// Enable permission and preference.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+ ["dom.resource_stats.enabled", true],
+ ["dom.ignore_webidl_scope_checks", true]
+ ]}, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/resourcestats/tests/mochitest/test_network_stats.html b/dom/resourcestats/tests/mochitest/test_network_stats.html
new file mode 100644
index 000000000..bee1914b3
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_network_stats.html
@@ -0,0 +1,345 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for ResourceStats methods realted to network statistics</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const invalidManifestURL = "app://invalid.gaiamobile.org/manifest.webapp";
+var networkStatsMgr = null; // ResourceStatsManager for network statistics.
+var sampleRate = 0;
+
+// Test WebIDL attributes.
+function testAttributes() {
+ // Test sampleRate.
+ ok('sampleRate' in networkStatsMgr,
+ "sampleRate should be a ResourceStatsManager attribute.");
+ sampleRate = networkStatsMgr.sampleRate;
+ ok(sampleRate > 0, "sampleRate is greater than 0.");
+
+ // Test maxStorageAge.
+ ok('maxStorageAge' in networkStatsMgr,
+ "maxStorageAge should be a ResourceStatsManager attribute.");
+ ok(networkStatsMgr.maxStorageAge > 0,
+ "maxStorageAge is greater than 0.");
+
+ // Test whether "network" in resourceTypes array.
+ ok('resourceTypes' in networkStatsMgr,
+ "resourceTypes should be a ResourceStatsManager attribute.");
+ ok(Array.isArray(networkStatsMgr.resourceTypes),
+ "networkStatsMgr.resourceTypes is an array.");
+ ok(networkStatsMgr.resourceTypes.indexOf("network") > -1,
+ "'network' is an element of networkStatsMgr.resourceTypes.");
+}
+
+// Check the content returned by ResourceStats.getData().
+function checkData(data, start, end) {
+ // Check if data is an array.
+ if (!Array.isArray(data)) {
+ ok(false, "getData does not return an array.")
+ return;
+ } else {
+ ok(true, "getData returns an array.")
+ }
+
+ // Iterate the array and check the timestamp and type of each element.
+ var success = true;
+ var obj = null;
+ var timestamp = start;
+ var i = 0;
+ var length = data.length;
+
+ do {
+ obj = data[i++];
+
+ // Check object type.
+ if (!(obj instanceof NetworkStatsData)) {
+ success = false;
+ ok(false, "The array contains a non-NetworkStatsData object.");
+ break;
+ }
+
+ // Check if the timestamp is continuous.
+ if (obj.timestamp !== timestamp) {
+ success = false;
+ ok(false, "The timestamp of NetworkStatsData object is correct.");
+ break;
+ }
+
+ timestamp += sampleRate;
+ } while (i < length);
+
+ if (!success) {
+ return;
+ }
+
+ // Check the timestamp of the last element is equal to end.
+ if (obj.timestamp != end) {
+ ok(false,
+ "The timestamp of the last element of the array is equal to end.");
+ return;
+ }
+
+ // Execute next test case.
+ ok(true, "The return of getData is an array of NetworkStatsData objects.");
+ testMethods();
+}
+
+// Test Cases for testing WebIDL methods.
+var testCases = [
+ function() {
+ // Test clearAllStats.
+ var promise = networkStatsMgr.clearAllStats();
+ promise.then(function() {
+ ok(true, "clearAllStats clears the network store.");
+ testMethods();
+ }, function() {
+ ok(false, "clearAllStats fails to clear the network store.");
+ });
+ },
+
+ function() {
+ // Test clearStats.
+ var promise = networkStatsMgr.clearStats();
+ promise.then(function() {
+ ok(true, "clearStats clears the network store.");
+ testMethods();
+ }, function() {
+ ok(false, "clearStats fails to clear the network store.");
+ });
+ },
+
+ function() {
+ // Check if clearStats throw exception when start is great than end.
+ var end = Date.now();
+ var start = end + 1000;
+ var promise = networkStatsMgr.clearStats(null, start, end);
+ promise.then(function() {
+ ok(false,
+ "clearStats does not throw exception when start is great than end.");
+ }, function() {
+ ok(true, "clearStats throw exception when start is great than end.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Check if clearStats throw exception when start is less than 0.
+ var end = Date.now();
+ var start = -1;
+ var promise = networkStatsMgr.clearStats(null, start, end);
+ promise.then(function() {
+ ok(false,
+ "clearStats dose not throw exception when start is less than 0.");
+ }, function() {
+ ok(true, "clearStats throw exception when start is less than 0.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Check if clearStats throw exception when manifestURL is invalid.
+ var options = {manifestURL: invalidManifestURL};
+ var promise = networkStatsMgr.clearStats(options);
+ promise.then(function() {
+ ok(false,
+ "clearStats does not throw exception when manifestURL is invalid.");
+ }, function() {
+ ok(true, "clearStats throw exception when manifestURL is invalid.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test getAvailableComponents.
+ var promise = networkStatsMgr.getAvailableComponents();
+ promise.then(function(value) {
+ if (Array.isArray(value)) {
+ ok(true, "getAvailableComponents returns an array.");
+ testMethods();
+ } else {
+ ok(false, "getAvailableComponents does not return an array.");
+ }
+ }, function() {
+ ok(false, "Fail to execute getAvailableComponents.");
+ });
+ },
+
+ function() {
+ // Test getStats.
+ ok(true, "Get system stats when start and end are adapted to sampleRate.");
+
+ // Prepare start and end.
+ var offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+ var end = Math.floor((Date.now() - offset) / sampleRate) * sampleRate + offset;
+ var start = end - sampleRate * 10;
+
+ // Launch request.
+ var promise = networkStatsMgr.getStats(null, start, end);
+ promise.then(function(value) {
+ // Check the object type.
+ if (value instanceof ResourceStats) {
+ ok(true, "Get a ResourceStats object.");
+ } else {
+ ok(false, "Fail to get a ResourceStats object.");
+ return;
+ }
+
+ // Check attributes of ResourceStats.
+ ok(value.type == "network", "type should be network.");
+ ok(value.component == null, "component should be null.");
+ ok(value.serviceType == null, "serviceType should be null.");
+ ok(value.manifestURL == null, "manifestURL should be null.");
+
+ // Check if the time range of ResourceStats is equal to the request.
+ ok(value.start == start, "start timestamp should be equal.");
+ ok(value.end == end, "end timestamp should be equal.");
+
+ // Check stats stored inside ResourceStats.
+ if ('getData' in value) {
+ checkData(value.getData(), start, end);
+ } else {
+ ok(false, "getData is not a method of ResourceStats.");
+ return;
+ }
+ }, function() {
+ ok(false, "Get network stats failed.");
+ });
+ },
+
+ function() {
+ // Test getStats when start and end are not adapted to sampleRate.
+ ok(true,
+ "Get system stats when start and end are not adapted to sampleRate.");
+
+ // Prepare start and end.
+ var end = Date.now();
+ var start = end - sampleRate * 10;
+
+ // Normalize start and end.
+ var offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+ var normEnd = Math.floor((end - offset) / sampleRate)
+ * sampleRate + offset;
+ var normStart = Math.floor((start - offset) / sampleRate)
+ * sampleRate + offset;
+
+ // Launch request.
+ var promise = networkStatsMgr.getStats(null, start, end);
+ promise.then(function(value) {
+ // Check the object type.
+ if (value instanceof ResourceStats) {
+ ok(true, "Get a ResourceStats object.");
+ } else {
+ ok(false, "Fail to get a ResourceStats object.");
+ return;
+ }
+
+ // Check attributes of ResourceStats.
+ ok(value.type == "network", "type should be network.");
+ ok(value.component == null, "component should be null.");
+ ok(value.serviceType == null, "serviceType should be null.");
+ ok(value.manifestURL == null, "manifestURL should be null.");
+
+ // Check if time range of ResourceStats are normalized.
+ ok(value.start == normStart, "start timestamp should be normalized.");
+ ok(value.end == normEnd, "end timestamp should be normalized.");
+
+ // Check stats stored inside ResourceStats.
+ if ('getData' in value) {
+ checkData(value.getData(), normStart, normEnd);
+ } else {
+ ok(false, "getData is not a method of ResourceStats.");
+ return;
+ }
+ }, function() {
+ ok(false, "Get network stats failed.");
+ });
+ },
+
+ function () {
+ // Check if getStats throw exception when start is greater than end.
+ var end = Date.now();
+ var start = end + 1000;
+ var promise = networkStatsMgr.getStats(null, start, end);
+ promise.then(function() {
+ ok(false,
+ "getStats dose not throw exception when start is great than end.");
+ }, function() {
+ ok(true, "getStats throw exception when start is great than end.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Check if getStats throw exception when start is less than 0.
+ var end = Date.now();
+ var start = -1;
+ var promise = networkStatsMgr.getStats(null, start, end);
+ promise.then(function() {
+ ok(false,
+ "getStats dose not throw exception when start is less than 0.");
+ }, function() {
+ ok(true, "getStats throw exception when start is less than 0.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Check if getStats throw exception when manifestURL is invalid.
+ var options = {manifestURL: invalidManifestURL};
+ var promise = networkStatsMgr.getStats(options);
+ promise.then(function(value) {
+ ok(false,
+ "getStats does not throw exception when manifestURL is invalid.");
+ }, function() {
+ ok(true, "getStats throw exception when manifestURL is invalid.");
+ testMethods();
+ });
+ }
+];
+
+// Test WebIDL methods related stats operation.
+function testMethods() {
+ if (!testCases.length) {
+ ok(true, "Done.");
+ SpecialPowers.removePermission("resourcestats-manage", document);
+ SimpleTest.finish();
+ return;
+ }
+
+ var testCase = testCases.shift();
+ testCase();
+}
+
+function startTest() {
+ // Create an instance of ResourceStatsManager for network stats.
+ networkStatsMgr = new window.ResourceStatsManager("network");
+ ok(networkStatsMgr, "Create networkStatsMgr.");
+
+ // Test WebIDL attributes.
+ testAttributes();
+
+ // Test WebIDL methods related to stats operation.
+ testMethods();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// Enable permission and preference.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+ ["dom.resource_stats.enabled", true],
+ ["dom.ignore_webidl_scope_checks", true]
+ ]}, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/resourcestats/tests/mochitest/test_no_perm.html b/dom/resourcestats/tests/mochitest/test_no_perm.html
new file mode 100644
index 000000000..d32c5ee88
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_no_perm.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test to ensure ResourceStatsManager is not accessible without permission</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// Test to ensure ResourceStatsManager is not accessible without permission.
+SpecialPowers.removePermission("resourcestats-manage", document);
+SpecialPowers.pushPrefEnv({ 'set': [
+ ["dom.resource_stats.enabled", true],
+ ["dom.ignore_webidl_scope_checks", true]
+ ]}, function() {
+ ok(!(SpecialPowers.hasPermission("resourcestats-manage", document)),
+ "Do not have permission 'resourcestats-manage'.");
+ ok(SpecialPowers.getBoolPref("dom.resource_stats.enabled"),
+ "Preference 'dom.resource_stats.enabled' is true.");
+
+ // Check ResourceStatsManager exist.
+ ok(!('ResourceStatsManager' in window),
+ "Cannot access window.ResourceStatsManager when have no permission");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/resourcestats/tests/mochitest/test_not_supported_type.html b/dom/resourcestats/tests/mochitest/test_not_supported_type.html
new file mode 100644
index 000000000..50ffb3b59
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_not_supported_type.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test to ensure ResourceStatsManager does not create an instance for non-supported type</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const type = "non-supported";
+
+SimpleTest.waitForExplicitFinish();
+
+// Test to ensure ResourceStatsManager does not create an instance for
+// non-supported type.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+ ["dom.resource_stats.enabled", true],
+ ["dom.ignore_webidl_scope_checks", true]
+ ]}, function() {
+ try {
+ var mgr = ResourceStatsManager(type);
+ ok(false,
+ "Creating an instance for non-supported type should throw an exeception.");
+ } catch (ex) {
+ ok(ex,
+ "Got an exception when creating an instance for non-supported type.");
+ }
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/resourcestats/tests/mochitest/test_power_alarm.html b/dom/resourcestats/tests/mochitest/test_power_alarm.html
new file mode 100644
index 000000000..abce5cfa4
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_power_alarm.html
@@ -0,0 +1,355 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for ResourceStats methods realted to power resource control</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const invalidManifestURL = "app://invalid.gaiamobile.org/manifest.webapp";
+const cpuComponent = "cpu:0";
+const gpsComponent = "gps:0";
+var powerStatsMgr = null; // ResourceStatsManager for power statistics.
+
+function errorCb(reason) {
+ ok(false, reason);
+}
+
+// Check the content returned by getAlarms.
+function checkAlarmsArray(alarms) {
+ // Check if data is an array.
+ if (!Array.isArray(alarms)) {
+ throw "getAlarms does not return an array.";
+ } else {
+ ok(true, "getAlarms returns an array.")
+ }
+
+ // Iterate the array and check the type of each element.
+ var obj = null;
+ var message = null; // Message for exception
+
+ for (var i = 0; i < alarms.length; i++) {
+ obj = alarms[i];
+
+ // Check if obj is an instance os ResourceStatsAlarm.
+ if (!(obj instanceof ResourceStatsAlarm)) {
+ message = "The array contains a non-ResourceStatsAlarm object.";
+ break;
+ }
+
+ // Check if obj.type is power.
+ if (obj.type != "power") {
+ message = "The type of a ResourceStatsAlarm object is not power.";
+ break;
+ }
+ }
+
+ if (message) {
+ throw message;
+ }
+
+ ok(true, "The return is an array of ResourceStatsAlarm objects.");
+}
+
+// Test Cases for testing WebIDL methods related to resource control.
+var testCases = [
+ function() {
+ // Test removeAllAlarms.
+ var promise = powerStatsMgr.removeAllAlarms();
+ promise.then(function() {
+ ok(true, "removeAllAlarms deleted all power alarms.");
+ testMethods();
+ }, function() {
+ ok(false, "removeAllAlarms failed to delete power alarms.");
+ });
+ },
+
+ function() {
+ // Test addAlarm.
+ var threshold = Math.floor(Math.random() * 1000);
+ var promise = powerStatsMgr.addAlarm(threshold,
+ { 'component': cpuComponent },
+ { 'startTime': Date.now() });
+ promise.then(function(value) {
+ // Check the value (alarmId).
+ if (value < 0) {
+ ok(false, "addAlarm failed to create an alarm.");
+ } else {
+ ok(true, "addAlarm created an alarm.");
+ testMethods();
+ }
+ }, function() {
+ ok(false, "addAlarm failed to create an alarm.");
+ });
+ },
+
+ function() {
+ // Test addAlarm with negative threshold.
+ var threshold = -1;
+ var promise = powerStatsMgr.addAlarm(threshold,
+ { 'component': cpuComponent },
+ { 'startTime': Date.now() });
+ promise.then(function() {
+ // Check the value.
+ ok(false,
+ "addAlarm did not throw an exception when negative threshold is set.");
+ }, function() {
+ ok(true, "addAlarm threw an exception when negative threshold is set.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test addAlarm with no threshold.
+ var promise = powerStatsMgr.addAlarm();
+ promise.then(function() {
+ // Check the value.
+ ok(false, "addAlarm did not throw an exception when no threshold.");
+ }, function() {
+ ok(true, "addAlarm threw an exception when no threshold.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test addAlarm with negative startTime.
+ var threshold = Math.floor(Math.random() * 1000);
+ var promise = powerStatsMgr.addAlarm(threshold,
+ { 'component': cpuComponent },
+ { 'startTime': -1 });
+ promise.then(function() {
+ // Check the value.
+ ok(false,
+ "addAlarm did not throw an exception when negative startTime is set.");
+ }, function() {
+ ok(true, "addAlarm threw an exception when negative startTime is set.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test addAlarm when manifestURL is invalid.
+ var threshold = Math.floor(Math.random() * 1000);
+ var promise = powerStatsMgr.addAlarm(threshold,
+ { 'component': cpuComponent,
+ 'manifestURL': invalidManifestURL },
+ { 'startTime': Date.now() });
+ promise.then(function() {
+ // Check the value.
+ ok(false, "addAlarm did not throw an exception when manifestURL is invalid.");
+ }, function() {
+ ok(true, "addAlarm threw an exception when manifestURL is invalid.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test getAlarms.
+ var alarmId;
+ var alarms;
+ var threshold = Math.floor(Math.random() * 1000);
+
+ // Execution steps:
+ // 1. Add a new alarm.
+ var promise = powerStatsMgr.addAlarm(threshold,
+ { 'component': cpuComponent },
+ { 'startTime': Date.now() });
+
+ // 2. Test getAlarms if new alarm is added.
+ var runGetAlarms = function(value) {
+ alarmId = value;
+ return powerStatsMgr.getAlarms({ 'component': cpuComponent });
+ };
+
+ // 3. Check the content returned by getAlarms.
+ var checkGetAlarmsReturn = function(value) {
+ alarms = value;
+ checkAlarmsArray(value);
+ };
+
+ // 4. Check the alarm added in step 1 is inside the return of getAlarms.
+ var checkAlarm = function (value) {
+ // Find the alarm.
+ var index = alarms.map(function(e) { return e.alarmId; })
+ .indexOf(alarmId);
+ if (index < 0) {
+ throw "getAlarms does not get the alarm added in previous step.";
+ }
+ var alarm = alarms[index];
+
+ // Evaluate the alarm.
+ ok(alarm.threshold == threshold, "threshold is equal.");
+ ok(alarm.component == cpuComponent, "component is equal.");
+ ok(alarm.serviceType == null, "serviceType should be null.");
+ ok(alarm.manifestURL == null, "manifestURL should be null.");
+ };
+
+ // Create promise chaining.
+ promise.then(runGetAlarms)
+ .then(checkGetAlarmsReturn)
+ .then(checkAlarm)
+ .then(testMethods, errorCb); // Execute next test case.
+ },
+
+ function() {
+ // Test getAlarms with invalid manifestURL.
+ var threshold = Math.floor(Math.random() * 1000);
+ var promise = powerStatsMgr.addAlarm(threshold,
+ { 'component': cpuComponent,
+ 'manifestURL': invalidManifestURL },
+ { 'startTime': Date.now() });
+
+ promise.then(function() {
+ // Check the value.
+ ok(false, "getAlarms did not throw an exception when manifestURL is invalid.");
+ }, function() {
+ ok(true, "getAlarms threw an exception when manifestURL is invalid.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test getAlarms with incorrect parameter.
+ var threshold = Math.floor(Math.random() * 1000);
+
+ // Execution steps:
+ // 1. Add a new alarm.
+ var promise = powerStatsMgr.addAlarm(threshold,
+ { 'component': cpuComponent },
+ { 'startTime': Date.now() });
+
+ // 2. Call getAlarms with incorrect parameter.
+ var runGetAlarms = function() {
+ return powerStatsMgr.getAlarms({ 'component': gpsComponent });
+ };
+
+ // 3. check the content returned by getAlarms.
+ var checkGetAlarmsReturn = function(value) {
+ // Check array length
+ if (value.length) {
+ throw "getAlarms gets an alarm when using incorrect parameter.";
+ } else {
+ ok(true,
+ "getAlarms returns an empty array when using incorrect parameter.");
+ }
+ };
+
+ // Create pomise chaining.
+ promise.then(runGetAlarms)
+ .then(checkGetAlarmsReturn)
+ .then(testMethods, errorCb); // Execute next test case.
+ },
+
+ function() {
+ // Test removeAlarm
+ var alarmId;
+ var threshold = Math.floor(Math.random() * 1000);
+
+ // Execution steps:
+ // 1. Add a new alarm.
+ var promise = powerStatsMgr.addAlarm(threshold,
+ { 'component': cpuComponent },
+ { 'startTime': Date.now() });
+
+ // 2. Try to remove the new alarm.
+ var runRemoveAlarm = function(value) {
+ alarmId = value;
+ return powerStatsMgr.removeAlarm(alarmId);
+ }
+
+ // Create promise chaining.
+ promise.then(runRemoveAlarm)
+ .then(function() {
+ ok(true, "removeAlarm deleted the alarm.");
+ testMethods();
+ }, errorCb);
+ },
+
+ function() {
+ // Test removeAlarm with negative alarmId
+ var alarmId = -1;
+ var promise = powerStatsMgr.removeAlarm(alarmId);
+ promise.then(function() {
+ ok(false,
+ "removeAlarm did not throw an exception when negative alarmId is set.");
+ }, function() {
+ ok(true, "removeAlarm threw an exception when negative alarmId is set.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test removeAlarm with invalid alarmId
+ var alarmId;
+ var threshold = Math.floor(Math.random() * 1000);
+
+ // Execution steps:
+ // 1. Add a new alarm.
+ var promise = powerStatsMgr.addAlarm(threshold,
+ { 'component': cpuComponent },
+ { 'startTime': Date.now() });
+
+ // 2. Try to remove an invalid alarm.
+ var runRemoveAlarm = function(value) {
+ alarmId = value;
+ // Because alarmId is auto-increment, any alarmId larger than the
+ // latest alarmId should be invalid.
+ return powerStatsMgr.removeAlarm(alarmId + 10);
+ }
+
+ // Create promise chaining.
+ promise.then(runRemoveAlarm)
+ .then(function() {
+ // Input with incorrect alarmId should not be resolved.
+ throw "removeAlarm should fail with invalid alarmId.";
+ }, function(reason) {
+ if (reason == "alarm not existed") {
+ ok(true, "removeAlarm with invalid alarmId should fail.")
+ } else {
+ throw reason;
+ }
+ })
+ .then(testMethods, errorCb);
+ }
+];
+
+// Test WebIDL methods related stats operation.
+function testMethods() {
+ if (!testCases.length) {
+ ok(true, "Done.");
+ SpecialPowers.removePermission("resourcestats-manage", document);
+ SimpleTest.finish();
+ return;
+ }
+
+ var testCase = testCases.shift();
+ testCase();
+}
+
+function startTest() {
+ // Create an instance of ResourceStatsManager for power.
+ powerStatsMgr = new ResourceStatsManager("power");
+ ok(powerStatsMgr, "Create powerStatsMgr.");
+
+ // Test WebIDL methods related to resource control.
+ testMethods();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// Enable permission and preference.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+ ["dom.resource_stats.enabled", true],
+ ["dom.ignore_webidl_scope_checks", true]
+ ]}, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/resourcestats/tests/mochitest/test_power_stats.html b/dom/resourcestats/tests/mochitest/test_power_stats.html
new file mode 100644
index 000000000..a1327df59
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_power_stats.html
@@ -0,0 +1,345 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for ResourceStats methods realted to power statistics</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const invalidManifestURL = "app://invalid.gaiamobile.org/manifest.webapp";
+var powerStatsMgr = null; // ResourceStatsManager for power statistics.
+var sampleRate = 0;
+
+// Test WebIDL attributes.
+function testAttributes() {
+ // Test sampleRate.
+ ok('sampleRate' in powerStatsMgr,
+ "sampleRate should be a ResourceStatsManager attribute.");
+ sampleRate = powerStatsMgr.sampleRate;
+ ok(sampleRate > 0, "sampleRate is greater than 0.");
+
+ // Test maxStorageAge.
+ ok('maxStorageAge' in powerStatsMgr,
+ "maxStorageAge should be a ResourceStatsManager attribute.");
+ ok(powerStatsMgr.maxStorageAge > 0,
+ "maxStorageAge is greater than 0.");
+
+ // Test whether "power" in resourceTypes array.
+ ok('resourceTypes' in powerStatsMgr,
+ "resourceTypes should be a ResourceStatsManager attribute.");
+ ok(Array.isArray(powerStatsMgr.resourceTypes),
+ "powerStatsMgr.resourceTypes is an array.");
+ ok(powerStatsMgr.resourceTypes.indexOf("power") > -1,
+ "'power' is an element of powerStatsMgr.resourceTypes.");
+}
+
+// Check the content returned by ResourceStats.getData().
+function checkData(data, start, end) {
+ // Check if data is an array.
+ if (!Array.isArray(data)) {
+ ok(false, "getData does not return an array.")
+ return;
+ } else {
+ ok(true, "getData returns an array.")
+ }
+
+ // Iterate the array and check the timestamp and type of each element.
+ var success = true;
+ var obj = null;
+ var timestamp = start;
+ var i = 0;
+ var length = data.length;
+
+ do {
+ obj = data[i++];
+
+ // Check object type.
+ if (!(obj instanceof PowerStatsData)) {
+ success = false;
+ ok(false, "The array contains a non-PowerStatsData object.");
+ break;
+ }
+
+ // Check if the timestamp is continuous.
+ if (obj.timestamp != timestamp) {
+ success = false;
+ ok(false, "The timestamp of PowerStatsData object is correct.");
+ break;
+ }
+
+ timestamp += sampleRate;
+ } while (i < length);
+
+ if (!success) {
+ return;
+ }
+
+ // Check the timestamp of the last element is equal to end.
+ if (obj.timestamp != end) {
+ ok(false,
+ "The timestamp of the last element of the array is equal to end.");
+ return;
+ }
+
+ // Execute next test case.
+ ok(true, "The return of getData is an array of PowerStatsData objects.");
+ testMethods();
+}
+
+// Test Cases for testing WebIDL methods.
+var testCases = [
+ function() {
+ // Test clearAllStats.
+ var promise = powerStatsMgr.clearAllStats();
+ promise.then(function() {
+ ok(true, "clearAllStats clears the power store.");
+ testMethods();
+ }, function() {
+ ok(false, "clearAllStats fails to clear the power store.");
+ });
+ },
+
+ function() {
+ // Test clearStats.
+ var promise = powerStatsMgr.clearStats();
+ promise.then(function() {
+ ok(true, "clearStats clears the power store.");
+ testMethods();
+ }, function() {
+ ok(false, "clearStats fails to clear the power store.");
+ });
+ },
+
+ function() {
+ // Check if clearStats throw exception when start is great than end.
+ var end = Date.now();
+ var start = end + 1000;
+ var promise = powerStatsMgr.clearStats(null, start, end);
+ promise.then(function() {
+ ok(false,
+ "clearStats does not throw exception when start is great than end.");
+ }, function() {
+ ok(true, "clearStats throw exception when start is great than end.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Check if clearStats throw exception when start is less than 0.
+ var end = Date.now();
+ var start = -1;
+ var promise = powerStatsMgr.clearStats(null, start, end);
+ promise.then(function() {
+ ok(false,
+ "clearStats dose not throw exception when start is less than 0.");
+ }, function() {
+ ok(true, "clearStats throw exception when start is less than 0.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Check if clearStats throw exception when manifestURL is invalid.
+ var options = {manifestURL: invalidManifestURL};
+ var promise = powerStatsMgr.clearStats(options);
+ promise.then(function() {
+ ok(false,
+ "clearStats does not throw exception when manifestURL is invalid.");
+ }, function() {
+ ok(true, "clearStats throw exception when manifestURL is invalid.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Test getAvailableComponents.
+ var promise = powerStatsMgr.getAvailableComponents();
+ promise.then(function(value) {
+ if (Array.isArray(value)) {
+ ok(true, "getAvailableComponents returns an array.");
+ testMethods();
+ } else {
+ ok(false, "getAvailableComponents does not return an array.");
+ }
+ }, function() {
+ ok(false, "Fail to execute getAvailableComponents.");
+ });
+ },
+
+ function() {
+ // Test getStats.
+ ok(true, "Get system stats when start and end are adapted to sampleRate.");
+
+ // Prepare start and end.
+ var offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+ var end = Math.floor((Date.now() - offset) / sampleRate) * sampleRate + offset;
+ var start = end - sampleRate * 10;
+
+ // Launch request.
+ var promise = powerStatsMgr.getStats(null, start, end);
+ promise.then(function(value) {
+ // Check the object type.
+ if (value instanceof ResourceStats) {
+ ok(true, "Get a ResourceStats object.");
+ } else {
+ ok(false, "Fail to get a ResourceStats object.");
+ return;
+ }
+
+ // Check attributes of ResourceStats.
+ ok(value.type == "power", "type should be power.");
+ ok(value.component == null, "component should be null.");
+ ok(value.serviceType == null, "serviceType should be null.");
+ ok(value.manifestURL == null, "manifestURL should be null.");
+
+ // Check if the time range of ResourceStats is equal to the request.
+ ok(value.start == start, "start timestamp should be equal.");
+ ok(value.end == end, "end timestamp should be equal.");
+
+ // Check stats stored inside ResourceStats.
+ if ('getData' in value) {
+ checkData(value.getData(), start, end);
+ } else {
+ ok(false, "getData is not a method of ResourceStats.");
+ return;
+ }
+ }, function() {
+ ok(false, "Get power stats failed.");
+ });
+ },
+
+ function() {
+ // Test getStats when start and end are not adapted to sampleRate.
+ ok(true,
+ "Get system stats when start and end are not adapted to sampleRate.");
+
+ // Prepare start and end.
+ var end = Date.now();
+ var start = end - sampleRate * 10;
+
+ // Normalize start and end.
+ var offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+ var normEnd = Math.floor((end - offset) / sampleRate)
+ * sampleRate + offset;
+ var normStart = Math.floor((start - offset) / sampleRate)
+ * sampleRate + offset;
+
+ // Launch request.
+ var promise = powerStatsMgr.getStats(null, start, end);
+ promise.then(function(value) {
+ // Check the object type.
+ if (value instanceof ResourceStats) {
+ ok(true, "Get a ResourceStats object.");
+ } else {
+ ok(false, "Fail to get a ResourceStats object.");
+ return;
+ }
+
+ // Check attributes of ResourceStats.
+ ok(value.type == "power", "type should be power.");
+ ok(value.component == null, "component should be null.");
+ ok(value.serviceType == null, "serviceType should be null.");
+ ok(value.manifestURL == null, "manifestURL should be null.");
+
+ // Check if time range of ResourceStats are normalized.
+ ok(value.start == normStart, "start timestamp should be normalized.");
+ ok(value.end == normEnd, "end timestamp should be normalized.");
+
+ // Check stats stored inside ResourceStats.
+ if ('getData' in value) {
+ checkData(value.getData(), normStart, normEnd);
+ } else {
+ ok(false, "getData is not a method of ResourceStats.");
+ return;
+ }
+ }, function() {
+ ok(false, "Get power stats failed.");
+ });
+ },
+
+ function() {
+ // Check if getStats throw exception when start is greater than end.
+ var end = Date.now();
+ var start = end + 1000;
+ var promise = powerStatsMgr.getStats(null, start, end);
+ promise.then(function() {
+ ok(false,
+ "getStats dose not throw exception when start is great than end.");
+ }, function() {
+ ok(true, "getStats throw exception when start is great than end.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Check if getStats throw exception when start is less than 0.
+ var end = Date.now();
+ var start = -1;
+ var promise = powerStatsMgr.getStats(null, start, end);
+ promise.then(function() {
+ ok(false,
+ "getStats dose not throw exception when start is less than 0.");
+ }, function() {
+ ok(true, "getStats throw exception when start is less than 0.");
+ testMethods();
+ });
+ },
+
+ function() {
+ // Check if getStats throw exception when manifestURL is invalid.
+ var options = {manifestURL: invalidManifestURL};
+ var promise = powerStatsMgr.getStats(options);
+ promise.then(function(value) {
+ ok(false,
+ "getStats does not throw exception when manifestURL is invalid.");
+ }, function() {
+ ok(true, "getStats throw exception when manifestURL is invalid.");
+ testMethods();
+ });
+ }
+];
+
+// Test WebIDL methods related stats operation.
+function testMethods() {
+ if (!testCases.length) {
+ ok(true, "Done.");
+ SpecialPowers.removePermission("resourcestats-manage", document);
+ SimpleTest.finish();
+ return;
+ }
+
+ var testCase = testCases.shift();
+ testCase();
+}
+
+function startTest() {
+ // Create an instance of ResourceStatsManager for power stats.
+ powerStatsMgr = new ResourceStatsManager("power");
+ ok(powerStatsMgr, "Create powerStatsMgr.");
+
+ // Test WebIDL attributes.
+ testAttributes();
+
+ // Test WebIDL methods related to stats operation.
+ testMethods();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// Enable permission and preference.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+ ["dom.resource_stats.enabled", true],
+ ["dom.ignore_webidl_scope_checks", true]
+ ]}, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/resourcestats/tests/xpcshell/test_resourcestats_db.js b/dom/resourcestats/tests/xpcshell/test_resourcestats_db.js
new file mode 100644
index 000000000..c38fbf75f
--- /dev/null
+++ b/dom/resourcestats/tests/xpcshell/test_resourcestats_db.js
@@ -0,0 +1,985 @@
+/* Any: copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/ResourceStatsDB.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const db = new ResourceStatsDB();
+
+// Components.
+const wifiComponent = "wifi:0";
+const mobileComponent = "mobile:1";
+const cpuComponent = "cpu:0";
+const gpsComponent = "gps:0";
+
+// List of available components.
+const networkComponents = [wifiComponent, mobileComponent];
+const powerComponents = [cpuComponent, gpsComponent];
+const offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+
+// Clear store.
+function clearStore(store, callback) {
+ db._dbNewTxn(store, "readwrite", function(aTxn, aStore) {
+ aStore.openCursor().onsuccess = function (event) {
+ let cursor = event.target.result;
+ if (cursor){
+ cursor.delete();
+ cursor.continue();
+ }
+ };
+ }, callback);
+}
+
+// Clear all stores to avoid starting tests with unknown state.
+add_test(function prepareDatabase() {
+ clearStore('power_stats_store', function() {
+ clearStore('network_stats_store', function() {
+ clearStore('alarm_store', function() {
+ run_next_test();
+ });
+ });
+ });
+});
+
+// Dump data saved in a store.
+function dumpStore(store, callback) {
+ db._dbNewTxn(store, "readonly", function(aTxn, aStore) {
+ aStore.mozGetAll().onsuccess = function onsuccess(event) {
+ aTxn.result = event.target.result;
+ };
+ }, callback);
+}
+
+// Check sampleRate is unchangeable.
+add_test(function test_sampleRate() {
+ var sampleRate = db.sampleRate;
+ do_check_true(sampleRate > 0);
+
+ db.sampleRate = 0;
+ sampleRate = db.sampleRate;
+ do_check_true(sampleRate > 0);
+
+ run_next_test();
+});
+
+// Test maxStorageAge is unchangeable.
+add_test(function test_maxStorageAge() {
+ var maxStorageAge = db.maxStorageAge;
+ do_check_true(maxStorageAge > 0);
+
+ db.maxStorageAge = 0;
+ maxStorageAge = db.maxStorageAge;
+ do_check_true(maxStorageAge > 0);
+
+ run_next_test();
+});
+
+// Normalize timestamp to sampleRate precision.
+function normalizeTime(aTimeStamp) {
+ var sampleRate = db.sampleRate;
+
+ return Math.floor((aTimeStamp - offset) / sampleRate) * sampleRate;
+}
+
+// Generte record as input for saveNetworkStats() as well as the expected
+// result when calling getStats().
+function generateNetworkRecord(aAppId, aServiceType, aComponents) {
+ var result = [];
+ var componentStats = {};
+ var receivedBytes;
+ var sentBytes;
+ var totalReceivedBytes = 0;
+ var totalSentBytes = 0;
+
+ aComponents.forEach(function(comp) {
+ // Step 1: generate random value for receivedBytes and sentBytes.
+ receivedBytes = Math.floor(Math.random() * 1000);
+ sentBytes = Math.floor(Math.random() * 1000);
+ totalReceivedBytes += receivedBytes;
+ totalSentBytes += sentBytes;
+
+ // Step 2: add stats to record.componentStats.
+ componentStats[comp] = {
+ receivedBytes: receivedBytes,
+ sentBytes: sentBytes
+ };
+
+ // Step 3: generate expected results.
+ result.push({
+ appId: aAppId,
+ serviceType: aServiceType,
+ component: comp,
+ receivedBytes: receivedBytes,
+ sentBytes: sentBytes
+ });
+ });
+
+ // Step 4: generate expected total stats.
+ result.push({
+ appId: aAppId,
+ serviceType: aServiceType,
+ component: "",
+ receivedBytes: totalReceivedBytes,
+ sentBytes: totalSentBytes
+ });
+
+ // Step 5: get record.
+ var record = { appId: aAppId,
+ serviceType: aServiceType,
+ componentStats: componentStats };
+
+ return { record: record, result: result };
+}
+
+// Generte record as input for savePowerStats() as well as the expected
+// result when calling getStats().
+function generatePowerRecord(aAppId, aServiceType, aComponents) {
+ var result = [];
+ var componentStats = {};
+ var consumedPower;
+ var totalConsumedPower = 0;
+
+ aComponents.forEach(function(comp) {
+ // Step 1: generate random value for consumedPower.
+ consumedPower = Math.floor(Math.random() * 1000);
+ totalConsumedPower += consumedPower;
+
+ // Step 2: add stats to record.componentStats.
+ componentStats[comp] = consumedPower;
+
+ // Step 3: generate expected results.
+ result.push({
+ appId: aAppId,
+ serviceType: aServiceType,
+ component: comp,
+ consumedPower: consumedPower
+ });
+ });
+
+ // Step 4: generate expected total stats.
+ result.push({
+ appId: aAppId,
+ serviceType: aServiceType,
+ component: "",
+ consumedPower: totalConsumedPower
+ });
+
+ // Step 5: get record.
+ var record = { appId: aAppId,
+ serviceType: aServiceType,
+ componentStats: componentStats };
+
+ return { record: record, result: result };
+}
+
+// Compare stats saved in network store with expected results.
+function checkNetworkStatsStore(aExpectedResult, aDumpResult, aTimestamp) {
+ // Step 1: a quick check for the length of arrays first.
+ do_check_eq(aExpectedResult.length, aDumpResult.length);
+
+ // Step 2: create a map array for search by receivedBytes.
+ var mapArray = aExpectedResult.map(function(e) {return e.receivedBytes;});
+
+ // Step 3: compare each element to make sure both array are equal.
+ var index;
+ var target;
+
+ aDumpResult.forEach(function(stats) {
+ index = 0;
+ // Find the first equal receivedBytes since index.
+ while ((index = mapArray.indexOf(stats.receivedBytes, index)) > -1) {
+ // Compare all attributes.
+ target = aExpectedResult[index];
+ if (target.appId != stats.appId ||
+ target.serviceType != stats.serviceType ||
+ target.component != stats.component ||
+ target.sentBytes != stats.sentBytes ||
+ aTimestamp != stats.timestamp) {
+ index += 1;
+ continue;
+ } else {
+ // If found, remove that element from aExpectedResult and mapArray.
+ aExpectedResult.splice(index, 1);
+ mapArray.splice(index, 1);
+ break;
+ }
+ }
+ do_check_neq(index, -1);
+ });
+ run_next_test();
+}
+
+// Compare stats saved in power store with expected results.
+function checkPowerStatsStore(aExpectedResult, aDumpResult, aTimestamp) {
+ // Step 1: a quick check for the length of arrays first.
+ do_check_eq(aExpectedResult.length, aDumpResult.length);
+
+ // Step 2: create a map array for search by consumedPower.
+ var mapArray = aExpectedResult.map(function(e) {return e.consumedPower;});
+
+ // Step 3: compare each element to make sure both array are equal.
+ var index;
+ var target;
+
+ aDumpResult.forEach(function(stats) {
+ index = 0;
+ // Find the first equal consumedPower since index.
+ while ((index = mapArray.indexOf(stats.consumedPower, index)) > -1) {
+ // Compare all attributes.
+ target = aExpectedResult[index];
+ if (target.appId != stats.appId ||
+ target.serviceType != stats.serviceType ||
+ target.component != stats.component ||
+ aTimestamp != stats.timestamp) {
+ index += 1;
+ continue;
+ } else {
+ // If found, remove that element from aExpectedResult and mapArray.
+ aExpectedResult.splice(index, 1);
+ mapArray.splice(index, 1);
+ break;
+ }
+ }
+ do_check_neq(index, -1);
+ });
+ run_next_test();
+}
+
+// Prepare network store for testing.
+function prepareNetworkStatsStore(recordArray, timestamp, callback) {
+ // Step 1: clear store.
+ clearStore("network_stats_store", function() {
+ // Step 2: save record to store.
+ db.saveNetworkStats(recordArray, timestamp, callback);
+ });
+}
+
+// Prepare power store for testing.
+function preparePowerStatsStore(recordArray, timestamp, callback) {
+ // Step 1: clear store.
+ clearStore("power_stats_store", function() {
+ // Step 2: save record to store.
+ db.savePowerStats(recordArray, timestamp, callback);
+ });
+}
+
+// Test saveNetworkStats.
+add_test(function test_saveNetworkStats() {
+ var appId = 1;
+ var serviceType = "";
+
+ // Step 1: generate data saved to store.
+ var { record: record, result: expectedResult } =
+ generateNetworkRecord(appId, serviceType, networkComponents);
+ var recordArray = [record];
+ var timestamp = Date.now();
+
+ // Step 2: save recordArray to network store.
+ prepareNetworkStatsStore(recordArray, timestamp, function(error, callback) {
+ // Step 3: check if the function call succeed.
+ do_check_eq(error, null);
+ // Step 4: dump store for comparison.
+ dumpStore("network_stats_store", function(error, result) {
+ do_check_eq(error, null);
+ checkNetworkStatsStore(expectedResult, result, normalizeTime(timestamp));
+ });
+ });
+});
+
+// Test savePowerStats.
+add_test(function test_savePowerStats() {
+ var appId = 1;
+ var serviceType = "";
+
+ // Step 1: generate data saved to store.
+ var { record: record, result: expectedResult } =
+ generatePowerRecord(appId, serviceType, powerComponents);
+ var recordArray = [record];
+ var timestamp = Date.now();
+
+ // Step 2: save recordArray to power store.
+ preparePowerStatsStore(recordArray, timestamp, function(error, callback) {
+ // Step 3: check if the function call succeed.
+ do_check_eq(error, null);
+ // Step 4: dump store for comparison.
+ dumpStore("power_stats_store", function(error, result) {
+ do_check_eq(error, null);
+ checkPowerStatsStore(expectedResult, result, normalizeTime(timestamp));
+ });
+ });
+});
+
+// Test getting network stats via getStats.
+add_test(function test_getNetworkStats() {
+ var appId = 0;
+ var manifestURL = "";
+ var serviceType = "";
+ var component = "";
+
+ // Step 1: generate data saved to store.
+ var { record: record, result: result } =
+ generateNetworkRecord(appId, serviceType, networkComponents);
+ var recordArray = [record];
+ var expectedStats = result[result.length - 1]; // Check total stats only.
+ var timestamp = Date.now();
+ var end = normalizeTime(timestamp) + offset;
+ var start = end;
+
+ // Step 2: save record and prepare network store.
+ prepareNetworkStatsStore(recordArray, timestamp, function(error, callback) {
+ // Step 3: get network stats.
+ db.getStats("network", manifestURL, serviceType, component, start, end,
+ function(error, result) {
+ do_check_eq(error, null);
+
+ // Step 4: verify result.
+ do_check_eq(result.type, "network");
+ do_check_eq(result.manifestURL, manifestURL);
+ do_check_eq(result.serviceType, serviceType);
+ do_check_eq(result.component, component);
+ do_check_eq(result.start, start);
+ do_check_eq(result.end, end);
+ do_check_eq(result.sampleRate, db.sampleRate);
+ do_check_true(Array.isArray(result.statsData));
+ do_check_eq(result.statsData.length, 1);
+ var stats = result.statsData[0];
+ do_check_eq(stats.receivedBytes, expectedStats.receivedBytes);
+ do_check_eq(stats.sentBytes, expectedStats.sentBytes);
+
+ run_next_test(); // If success, run next test.
+ });
+ });
+});
+
+// Test getting power stats via getStats.
+add_test(function test_getPowerStats() {
+ var appId = 0;
+ var manifestURL = "";
+ var serviceType = "";
+ var component = "";
+
+ // Step 1: generate data saved to store.
+ var { record: record, result: result } =
+ generatePowerRecord(appId, serviceType, powerComponents);
+ var recordArray = [record];
+ var expectedStats = result[result.length - 1]; // Check total stats only.
+ var timestamp = Date.now();
+ var end = normalizeTime(timestamp) + offset;
+ var start = end;
+
+ // Step 2: save record and prepare power store.
+ preparePowerStatsStore(recordArray, timestamp, function(error, callback) {
+ // Step 3: get power stats.
+ db.getStats("power", manifestURL, serviceType, component, start, end,
+ function(error, result) {
+ do_check_eq(error, null);
+
+ // Step 4: verify result
+ do_check_eq(result.type, "power");
+ do_check_eq(result.manifestURL, manifestURL);
+ do_check_eq(result.serviceType, serviceType);
+ do_check_eq(result.component, component);
+ do_check_eq(result.start, start);
+ do_check_eq(result.end, end);
+ do_check_eq(result.sampleRate, db.sampleRate);
+ do_check_true(Array.isArray(result.statsData));
+ do_check_eq(result.statsData.length, 1);
+ var stats = result.statsData[0];
+ do_check_eq(stats.consumedPower, expectedStats.consumedPower);
+
+ run_next_test(); // If success, run next test.
+ });
+ });
+});
+
+// Test deleting network stats via clearStats.
+add_test(function test_clearNetworkStats() {
+ var appId = 0;
+ var manifestURL = "";
+ var serviceType = "";
+ var component = "";
+
+ // Step 1: genrate data saved to network store.
+ var { record: record, result: result } =
+ generateNetworkRecord(appId, serviceType, networkComponents);
+ var recordArray = [record];
+ var timestamp = Date.now();
+ var end = normalizeTime(timestamp) + offset;
+ var start = end;
+
+ // Step 2: save record and prepare network store.
+ prepareNetworkStatsStore(recordArray, timestamp, function(error, callback) {
+ // Step 3: clear network stats.
+ db.clearStats("network", appId, serviceType, component, start, end,
+ function(error, result) {
+ do_check_eq(error, null);
+
+ // Step 4: check if the stats is deleted.
+ db.getStats("network", manifestURL, serviceType, component, start, end,
+ function(error, result) {
+ do_check_eq(result.statsData.length, 0);
+ run_next_test();
+ });
+ });
+ });
+});
+
+// Test deleting power stats via clearStats.
+add_test(function test_clearPowerStats() {
+ var appId = 0;
+ var manifestURL = "";
+ var serviceType = "";
+ var component = "";
+
+ // Step 1: genrate data saved to power store.
+ var { record: record, result: result } =
+ generatePowerRecord(appId, serviceType, powerComponents);
+ var recordArray = [record];
+ var timestamp = Date.now();
+ var end = normalizeTime(timestamp) + offset;
+ var start = end;
+
+ // Step 2: save record and prepare power store.
+ preparePowerStatsStore(recordArray, timestamp, function(error, callback) {
+ // Step 3: clear power stats.
+ db.clearStats("power", appId, serviceType, component, start, end,
+ function(error, result) {
+ do_check_eq(error, null);
+
+ // Step 4: check if the stats is deleted.
+ db.getStats("power", manifestURL, serviceType, component, start, end,
+ function(error, result) {
+ do_check_eq(result.statsData.length, 0);
+ run_next_test();
+ });
+ });
+ });
+});
+
+// Test clearing all network stats.
+add_test(function test_clearAllNetworkStats() {
+ db.clearAllStats("network", function(error, result) {
+ do_check_eq(error, null);
+ run_next_test();
+ });
+});
+
+// Test clearing all power stats.
+add_test(function test_clearAllPowerStats() {
+ db.clearAllStats("power", function(error, result) {
+ do_check_eq(error, null);
+ run_next_test();
+ });
+});
+
+// Test getting network components.
+add_test(function test_getNetworkComponents() {
+ var appId = 0;
+ var serviceType = "";
+
+ // Step 1: generate data saved to store.
+ var { record: record, result: expectedResult } =
+ generateNetworkRecord(appId, serviceType, networkComponents);
+ var recordArray = [record];
+ var timestamp = Date.now();
+
+ // Step 2: save recordArray to network store.
+ prepareNetworkStatsStore(recordArray, timestamp, function(error, callback) {
+ // Step 3: call getComponents.
+ db.getComponents("network", function(error, result) {
+ do_check_eq(error, null);
+ do_check_true(Array.isArray(result));
+ do_check_eq(result.length, networkComponents.length);
+
+ // Check each element in result array is an element of networkComponents.
+ result.forEach(function(component) {
+ do_check_true(networkComponents.indexOf(component) > -1);
+ });
+
+ run_next_test(); // If success, run next test.
+ });
+ });
+});
+
+// Test getting power components.
+add_test(function test_getPowerComponents() {
+ var appId = 0;
+ var serviceType = "";
+
+ // Step 1: generate data saved to store.
+ var { record: record, result: expectedResult } =
+ generatePowerRecord(appId, serviceType, powerComponents);
+ var recordArray = [record];
+ var timestamp = Date.now();
+
+ // Step 2: save recordArray to power store.
+ preparePowerStatsStore(recordArray, timestamp, function(error, callback) {
+ // Step 3: call getComponents.
+ db.getComponents("power", function(error, result) {
+ do_check_eq(error, null);
+ do_check_true(Array.isArray(result));
+ do_check_eq(result.length, powerComponents.length);
+
+ // Check each element in result array is an element of powerComponents.
+ result.forEach(function(component) {
+ do_check_true(powerComponents.indexOf(component) > -1);
+ });
+
+ run_next_test(); // If success, run next test.
+ });
+ });
+});
+
+// Generate alarm object for addAlarm().
+function generateAlarmObject(aType, aManifestURL, aServiceType, aComponent) {
+ let alarm = {
+ type: aType,
+ component: aComponent,
+ serviceType: aServiceType,
+ manifestURL: aManifestURL,
+ threshold: Math.floor(Math.random() * 1000),
+ startTime: Math.floor(Math.random() * 1000),
+ data: null
+ };
+
+ return alarm;
+}
+
+// Test adding a network alarm.
+add_test(function test_addNetowrkAlarm() {
+ var manifestURL = "";
+ var serviceType = "";
+
+ // Step 1: generate a network alarm.
+ var alarm =
+ generateAlarmObject("network", manifestURL, serviceType, wifiComponent);
+
+ // Step 2: clear store.
+ clearStore("alarm_store", function() {
+ // Step 3: save the alarm to store.
+ db.addAlarm(alarm, function(error, result) {
+ // Step 4: check if the function call succeed.
+ do_check_eq(error, null);
+ do_check_true(result > -1);
+ let alarmId = result;
+
+ // Step 5: dump store for comparison.
+ dumpStore("alarm_store", function(error, result) {
+ do_check_eq(error, null);
+ do_check_true(Array.isArray(result));
+ do_check_true(result.length == 1);
+ do_check_eq(result[0].type, alarm.type);
+ do_check_eq(result[0].manifestURL, alarm.manifestURL);
+ do_check_eq(result[0].serviceType, alarm.serviceType);
+ do_check_eq(result[0].component, alarm.component);
+ do_check_eq(result[0].threshold, alarm.threshold);
+ do_check_eq(result[0].startTime, alarm.startTime);
+ do_check_eq(result[0].data, alarm.data);
+ do_check_eq(result[0].alarmId, alarmId);
+
+ run_next_test(); // If success, run next test.
+ });
+ });
+ });
+});
+
+// Test adding a power alarm.
+add_test(function test_addPowerAlarm() {
+ var manifestURL = "";
+ var serviceType = "";
+
+ // Step 1: generate a power alarm.
+ var alarm =
+ generateAlarmObject("power", manifestURL, serviceType, cpuComponent);
+
+ // Step 2: clear store.
+ clearStore("alarm_store", function() {
+ // Step 3: save the alarm to store.
+ db.addAlarm(alarm, function(error, result) {
+ // Step 4: check if the function call succeed.
+ do_check_eq(error, null);
+ do_check_true(result > -1);
+ let alarmId = result;
+
+ // Step 5: dump store for comparison.
+ dumpStore("alarm_store", function(error, result) {
+ do_check_eq(error, null);
+ do_check_true(Array.isArray(result));
+ do_check_true(result.length == 1);
+ do_check_eq(result[0].type, alarm.type);
+ do_check_eq(result[0].manifestURL, alarm.manifestURL);
+ do_check_eq(result[0].serviceType, alarm.serviceType);
+ do_check_eq(result[0].component, alarm.component);
+ do_check_eq(result[0].threshold, alarm.threshold);
+ do_check_eq(result[0].startTime, alarm.startTime);
+ do_check_eq(result[0].data, alarm.data);
+ do_check_eq(result[0].alarmId, alarmId);
+
+ run_next_test(); // If success, run next test.
+ });
+ });
+ });
+});
+
+// Add multiple alarms to store and record the obtained alarmId in each
+// alarm object.
+function addAlarmsToStore(alarms, index, callback) {
+ var alarm = alarms[index++];
+ if (index < alarms.length) {
+ db.addAlarm(alarm, function(error, result) {
+ alarm.alarmId = result;
+ addAlarmsToStore(alarms, index, callback);
+ });
+ } else {
+ db.addAlarm(alarm, function(error, result) {
+ alarm.alarmId = result;
+ callback(error, result);
+ });
+ }
+}
+
+// Prepare alarm store for testging.
+function prepareAlarmStore(alarms, callback) {
+ // Step 1: clear srore.
+ clearStore("alarm_store", function() {
+ // Step 2: save alarms to store one by one.
+ addAlarmsToStore(alarms, 0, callback);
+ });
+}
+
+// Compare alrams returned by getAlarms().
+function compareAlarms(aExpectedResult, aResult) {
+ // Step 1: a quick check for the length of arrays first.
+ do_check_eq(aExpectedResult.length, aResult.length);
+
+ // Step 2: create a map array for search by threshold.
+ var mapArray = aExpectedResult.map(function(e) {return e.threshold;});
+
+ // Step 3: compare each element to make sure both array are equal.
+ var index;
+ var target;
+
+ aResult.forEach(function(alarm) {
+ index = 0;
+ // Find the first equal receivedBytes since index.
+ while ((index = mapArray.indexOf(alarm.threshold, index)) > -1) {
+ // Compare all attributes.
+ target = aExpectedResult[index];
+ if (target.alarmId != alarm.alarmId ||
+ target.type != alarm.type ||
+ target.manifestURL != alarm.manifestURL ||
+ target.serviceType != alarm.serviceType ||
+ target.component != alarm.component ||
+ target.data != alarm.data) {
+ index += 1;
+ continue;
+ } else {
+ // If found, remove that element from aExpectedResult and mapArray.
+ aExpectedResult.splice(index, 1);
+ mapArray.splice(index, 1);
+ break;
+ }
+ }
+ do_check_neq(index, -1);
+ });
+ run_next_test();
+}
+
+// Test getting designate network alarms from store.
+add_test(function test_getNetworkAlarms() {
+ var manifestURL = "";
+ var serviceType = "";
+ var alarms = [];
+
+ // Step 1: generate two network alarms using same parameters.
+ alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+ wifiComponent));
+ alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+ wifiComponent));
+
+ // Step 2: generate another network alarm using different parameters.
+ alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+ mobileComponent));
+
+ // Step 3: clear alarm store and save new alarms to store.
+ prepareAlarmStore(alarms, function(error, result) {
+ // Step 4: call getAlarms.
+ let options = {
+ manifestURL: manifestURL,
+ serviceType: serviceType,
+ component: wifiComponent
+ };
+ db.getAlarms("network", options, function(error, result) {
+ // Step 5: check if the function call succeed.
+ do_check_eq(error, null);
+
+ // Step 6: check results.
+ // The last element in alarms array is not our expected result,
+ // so pop that out first.
+ alarms.pop();
+ compareAlarms(alarms, result);
+ });
+ });
+});
+
+// Test getting designate power alarms from store.
+add_test(function test_getPowerAlarms() {
+ var manifestURL = "";
+ var serviceType = "";
+ var alarms = [];
+
+ // Step 1: generate two power alarms using same parameters.
+ alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+ cpuComponent));
+ alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+ cpuComponent));
+
+ // Step 2: generate another power alarm using different parameters.
+ alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+ gpsComponent));
+
+ // Step 3: clear alarm store and save new alarms to store.
+ prepareAlarmStore(alarms, function(error, result) {
+ // Step 4: call getAlarms.
+ let options = {
+ manifestURL: manifestURL,
+ serviceType: serviceType,
+ component: cpuComponent
+ };
+ db.getAlarms("power", options, function(error, result) {
+ // Step 5: check if the function call succeed.
+ do_check_eq(error, null);
+
+ // Step 6: check results.
+ // The last element in alarms array is not our expected result,
+ // so pop that out first.
+ alarms.pop();
+ compareAlarms(alarms, result);
+ });
+ });
+});
+
+// Test getting all network alarms from store.
+add_test(function test_getAllNetworkAlarms() {
+ var manifestURL = "";
+ var serviceType = "";
+ var alarms = [];
+
+ // Step 1: generate two network alarms.
+ alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+ wifiComponent));
+ alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+ mobileComponent));
+
+ // Step 2: generate another power alarm.
+ alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+ cpuComponent));
+
+ // Step 3: clear alarm store and save new alarms to store.
+ prepareAlarmStore(alarms, function(error, result) {
+ // Step 4: call getAlarms.
+ let options = null;
+ db.getAlarms("network", options, function(error, result) {
+ // Step 5: check if the function call succeed.
+ do_check_eq(error, null);
+
+ // Step 6: check results.
+ // The last element in alarms array is not our expected result,
+ // so pop that out first.
+ alarms.pop();
+ compareAlarms(alarms, result);
+ });
+ });
+});
+
+// Test getting all power alarms from store.
+add_test(function test_getAllPowerAlarms() {
+ var manifestURL = "";
+ var serviceType = "";
+ var alarms = [];
+
+ // Step 1: generate two power alarms.
+ alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+ cpuComponent));
+ alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+ gpsComponent));
+
+ // Step 2: generate another network alarm.
+ alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+ wifiComponent));
+
+ // Step 3: clear alarm store and save new alarms to store.
+ prepareAlarmStore(alarms, function(error, result) {
+ // Step 4: call getAlarms.
+ let options = null;
+ db.getAlarms("power", options, function(error, result) {
+ // Step 5: check if the function call succeed.
+ do_check_eq(error, null);
+
+ // Step 6: check results.
+ // The last element in alarms array is not our expected result,
+ // so pop that out first.
+ alarms.pop();
+ compareAlarms(alarms, result);
+ });
+ });
+});
+
+// Test removing designate network alarm from store.
+add_test(function test_removeNetworkAlarm() {
+ var manifestURL = "";
+ var serviceType = "";
+ var alarms = [];
+
+ // Step 1: generate one network alarm.
+ alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+ wifiComponent));
+
+ // Step 2: clear alarm store and save new alarms to store.
+ prepareAlarmStore(alarms, function(error, result) {
+ var alarmId = result;
+ // Step 3: remove the alarm.
+ db.removeAlarm("network", alarmId, function(error, result) {
+ // Step 4: check if the function call succeed.
+ do_check_eq(result, true);
+
+ // Step 5: dump store to check if the alarm is removed.
+ dumpStore("alarm_store", function(error, result) {
+ do_check_eq(error, null);
+ do_check_true(Array.isArray(result));
+ do_check_true(result.length === 0);
+
+ run_next_test(); // If success, run next test.
+ });
+ });
+ });
+});
+
+// Test removing designate power alarm from store.
+add_test(function test_removePowerAlarm() {
+ var manifestURL = "";
+ var serviceType = "";
+ var alarms = [];
+
+ // Step 1: generate one power alarm.
+ alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+ cpuComponent));
+
+ // Step 2: clear alarm store and save new alarms to store.
+ prepareAlarmStore(alarms, function(error, result) {
+ var alarmId = result;
+ // Step 3: remove the alarm.
+ db.removeAlarm("power", alarmId, function(error, result) {
+ // Step 4: check if the function call succeed.
+ do_check_eq(result, true);
+
+ // Step 5: dump store to check if the alarm is removed.
+ dumpStore("alarm_store", function(error, result) {
+ do_check_eq(error, null);
+ do_check_true(Array.isArray(result));
+ do_check_true(result.length === 0);
+
+ run_next_test(); // If success, run next test.
+ });
+ });
+ });
+});
+
+// Test removing designate network alarm from store.
+add_test(function test_removeAllNetworkAlarms() {
+ var manifestURL = "";
+ var serviceType = "";
+ var alarms = [];
+
+ // Step 1: generate two network alarms.
+ alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+ wifiComponent));
+ alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+ mobileComponent));
+
+ // Step 2: generate another power alarm.
+ alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+ cpuComponent));
+
+ // Step 3: clear alarm store and save new alarms to store.
+ prepareAlarmStore(alarms, function(error, result) {
+ // Step 4: remove all network alarms.
+ db.removeAllAlarms("network", function(error, result) {
+ // Step 5: check if the function call succeed.
+ do_check_eq(error, null);
+
+ // Step 6: dump store for comparison.
+ // Because the power alarm should not be removed, so it would be the
+ // only result returned by dumpStore.
+ var alarm = alarms.pop(); // The expected result.
+ dumpStore("alarm_store", function(error, result) {
+ do_check_eq(error, null);
+ do_check_true(Array.isArray(result));
+ do_check_true(result.length == 1);
+ do_check_eq(result[0].type, alarm.type);
+ do_check_eq(result[0].manifestURL, alarm.manifestURL);
+ do_check_eq(result[0].serviceType, alarm.serviceType);
+ do_check_eq(result[0].component, alarm.component);
+ do_check_eq(result[0].threshold, alarm.threshold);
+ do_check_eq(result[0].startTime, alarm.startTime);
+ do_check_eq(result[0].data, alarm.data);
+ do_check_eq(result[0].alarmId, alarm.alarmId);
+
+ run_next_test(); // If success, run next test.
+ });
+ });
+ });
+});
+
+// Test removing designate power alarm from store.
+add_test(function test_removeAllPowerAlarms() {
+ var manifestURL = "";
+ var serviceType = "";
+ var alarms = [];
+
+ // Step 1: generate two power alarms.
+ alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+ cpuComponent));
+ alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+ gpsComponent));
+
+ // Step 2: generate another network alarm.
+ alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+ wifiComponent));
+
+ // Step 3: clear alarm store and save new alarms to store.
+ prepareAlarmStore(alarms, function(error, result) {
+ // Step 4: remove all power alarms.
+ db.removeAllAlarms("power", function(error, result) {
+ // Step 5: check if the function call succeed.
+ do_check_eq(error, null);
+
+ // Step 6: dump store for comparison.
+ // Because the network alarm should not be removed, so it would be the
+ // only result returned by dumpStore.
+ var alarm = alarms.pop(); // The expected result.
+ dumpStore("alarm_store", function(error, result) {
+ do_check_eq(error, null);
+ do_check_true(Array.isArray(result));
+ do_check_true(result.length == 1);
+ do_check_eq(result[0].type, alarm.type);
+ do_check_eq(result[0].manifestURL, alarm.manifestURL);
+ do_check_eq(result[0].serviceType, alarm.serviceType);
+ do_check_eq(result[0].component, alarm.component);
+ do_check_eq(result[0].threshold, alarm.threshold);
+ do_check_eq(result[0].startTime, alarm.startTime);
+ do_check_eq(result[0].data, alarm.data);
+ do_check_eq(result[0].alarmId, alarm.alarmId);
+
+ run_next_test(); // If success, run next test.
+ });
+ });
+ });
+});
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
diff --git a/dom/resourcestats/tests/xpcshell/xpcshell.ini b/dom/resourcestats/tests/xpcshell/xpcshell.ini
new file mode 100644
index 000000000..c1848a540
--- /dev/null
+++ b/dom/resourcestats/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head =
+tail =
+
+[test_resourcestats_db.js]