From eb9c952e7b9ff2e780efcf37c1a54e36102fbbf6 Mon Sep 17 00:00:00 2001 From: Borting Chen Date: Mon, 10 Mar 2014 11:54:15 +0800 Subject: [PATCH] Bug 951976 - part 1, WebIDL implementation. r=gene --- b2g/app/b2g.js | 5 + b2g/chrome/content/shell.js | 1 + b2g/installer/package-manifest.in | 8 + dom/apps/src/PermissionsTable.jsm | 5 + dom/moz.build | 1 + dom/resourcestats/ResourceStatsDB.jsm | 537 ++++++++++++++++++ dom/resourcestats/ResourceStatsManager.js | 480 ++++++++++++++++ .../ResourceStatsManager.manifest | 14 + dom/resourcestats/ResourceStatsService.jsm | 334 +++++++++++ dom/resourcestats/moz.build | 18 + 10 files changed, 1403 insertions(+) create mode 100644 dom/resourcestats/ResourceStatsDB.jsm create mode 100644 dom/resourcestats/ResourceStatsManager.js create mode 100644 dom/resourcestats/ResourceStatsManager.manifest create mode 100644 dom/resourcestats/ResourceStatsService.jsm create mode 100644 dom/resourcestats/moz.build diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js index d25651c10a02..f8268412833a 100644 --- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -479,6 +479,11 @@ pref("dom.mozNetworkStats.enabled", true); pref("dom.webapps.firstRunWithSIM", true); #endif +// ResourceStats +#ifdef MOZ_WIDGET_GONK +pref("dom.resource_stats.enabled", true); +#endif + #ifdef MOZ_B2G_RIL // SingleVariant pref("dom.mozApps.single_variant_sourcedir", "/persist/svoperapps"); diff --git a/b2g/chrome/content/shell.js b/b2g/chrome/content/shell.js index fb89278fb5a5..58ece9d052a9 100644 --- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -19,6 +19,7 @@ Cu.import('resource://gre/modules/ErrorPage.jsm'); Cu.import('resource://gre/modules/AlertsHelper.jsm'); #ifdef MOZ_WIDGET_GONK Cu.import('resource://gre/modules/NetworkStatsService.jsm'); +Cu.import('resource://gre/modules/ResourceStatsService.jsm'); #endif // Identity diff --git a/b2g/installer/package-manifest.in b/b2g/installer/package-manifest.in index 4f1abba48dc2..7d7900a9c4e0 100644 --- a/b2g/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -420,6 +420,14 @@ @BINPATH@/components/WifiWorker.manifest #endif // MOZ_WIDGET_GONK +; ResourceStats +#ifdef MOZ_WIDGET_GONK +@BINPATH@/components/ResourceStats.js +@BINPATH@/components/ResourceStats.manifest +@BINPATH@/components/ResourceStatsManager.js +@BINPATH@/components/ResourceStatsManager.manifest +#endif // MOZ_WIDGET_GONK + ; RIL #if defined(MOZ_WIDGET_GONK) && defined(MOZ_B2G_RIL) @BINPATH@/components/MmsService.js diff --git a/dom/apps/src/PermissionsTable.jsm b/dom/apps/src/PermissionsTable.jsm index 3353f8a1b5db..7f99651d0f44 100644 --- a/dom/apps/src/PermissionsTable.jsm +++ b/dom/apps/src/PermissionsTable.jsm @@ -186,6 +186,11 @@ this.PermissionsTable = { geolocation: { privileged: DENY_ACTION, certified: ALLOW_ACTION }, + "resourcestats-manage": { + app: DENY_ACTION, + privileged: DENY_ACTION, + certified: ALLOW_ACTION + }, "wifi-manage": { app: DENY_ACTION, privileged: DENY_ACTION, diff --git a/dom/moz.build b/dom/moz.build index 87ce97ec30e4..5598f5388946 100644 --- a/dom/moz.build +++ b/dom/moz.build @@ -83,6 +83,7 @@ PARALLEL_DIRS += [ 'webidl', 'xbl', 'xslt', + 'resourcestats', ] if CONFIG['OS_ARCH'] == 'WINNT': diff --git a/dom/resourcestats/ResourceStatsDB.jsm b/dom/resourcestats/ResourceStatsDB.jsm new file mode 100644 index 000000000000..8b576ef9a9d2 --- /dev/null +++ b/dom/resourcestats/ResourceStatsDB.jsm @@ -0,0 +1,537 @@ +/* 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"); + +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 000000000000..649f1873a6bd --- /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 000000000000..985126ff2ed0 --- /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 000000000000..df4481eaba22 --- /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 000000000000..e46e830072de --- /dev/null +++ b/dom/resourcestats/moz.build @@ -0,0 +1,18 @@ +# -*- 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 = 'gklayout' +