diff options
Diffstat (limited to 'dom/network/NetworkStatsDB.jsm')
-rw-r--r-- | dom/network/NetworkStatsDB.jsm | 1285 |
1 files changed, 0 insertions, 1285 deletions
diff --git a/dom/network/NetworkStatsDB.jsm b/dom/network/NetworkStatsDB.jsm deleted file mode 100644 index aa74d40ad9..0000000000 --- a/dom/network/NetworkStatsDB.jsm +++ /dev/null @@ -1,1285 +0,0 @@ -/* 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 = ['NetworkStatsDB']; - -const DEBUG = false; -function debug(s) { dump("-*- NetworkStatsDB: " + 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/IndexedDBHelper.jsm"); -Cu.importGlobalProperties(["indexedDB"]); - -XPCOMUtils.defineLazyServiceGetter(this, "appsService", - "@mozilla.org/AppsService;1", - "nsIAppsService"); - -const DB_NAME = "net_stats"; -const DB_VERSION = 9; -const DEPRECATED_STATS_STORE_NAME = - [ - "net_stats_v2", // existed only in DB version 2 - "net_stats", // existed in DB version 1 and 3 to 5 - "net_stats_store", // existed in DB version 6 to 8 - ]; -const STATS_STORE_NAME = "net_stats_store_v3"; // since DB version 9 -const ALARMS_STORE_NAME = "net_alarm"; - -// Constant defining the maximum values allowed per interface. If more, older -// will be erased. -const VALUES_MAX_LENGTH = 6 * 30; - -// Constant defining the rate of the samples. Daily. -const SAMPLE_RATE = 1000 * 60 * 60 * 24; - -this.NetworkStatsDB = function NetworkStatsDB() { - if (DEBUG) { - debug("Constructor"); - } - this.initDBHelper(DB_NAME, DB_VERSION, [STATS_STORE_NAME, ALARMS_STORE_NAME]); -} - -NetworkStatsDB.prototype = { - __proto__: IndexedDBHelper.prototype, - - dbNewTxn: function dbNewTxn(store_name, txn_type, callback, txnCb) { - function successCb(result) { - txnCb(null, result); - } - function errorCb(error) { - txnCb(error, null); - } - return this.newTxn(txn_type, store_name, callback, successCb, errorCb); - }, - - /** - * The onupgradeneeded handler of the IDBOpenDBRequest. - * This function is called in IndexedDBHelper open() method. - * - * @param {IDBTransaction} aTransaction - * {IDBDatabase} aDb - * {64-bit integer} aOldVersion The version number on local storage. - * {64-bit integer} aNewVersion The version number to be upgraded to. - * - * @note Be careful with the database upgrade pattern. - * Because IndexedDB operations are performed asynchronously, we must - * apply a recursive approach instead of an iterative approach while - * upgrading versions. - */ - upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) { - if (DEBUG) { - debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!"); - } - let db = aDb; - let objectStore; - - // An array of upgrade functions for each version. - let upgradeSteps = [ - function upgrade0to1() { - if (DEBUG) debug("Upgrade 0 to 1: Create object stores and indexes."); - - // Create the initial database schema. - objectStore = db.createObjectStore(DEPRECATED_STATS_STORE_NAME[1], - { keyPath: ["connectionType", "timestamp"] }); - objectStore.createIndex("connectionType", "connectionType", { unique: false }); - objectStore.createIndex("timestamp", "timestamp", { unique: false }); - objectStore.createIndex("rxBytes", "rxBytes", { unique: false }); - objectStore.createIndex("txBytes", "txBytes", { unique: false }); - objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); - objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); - - upgradeNextVersion(); - }, - - function upgrade1to2() { - if (DEBUG) debug("Upgrade 1 to 2: Do nothing."); - upgradeNextVersion(); - }, - - function upgrade2to3() { - if (DEBUG) debug("Upgrade 2 to 3: Add keyPath appId to object store."); - - // In order to support per-app traffic data storage, the original - // objectStore needs to be replaced by a new objectStore with new - // key path ("appId") and new index ("appId"). - // Also, since now networks are identified by their - // [networkId, networkType] not just by their connectionType, - // to modify the keyPath is mandatory to delete the object store - // and create it again. Old data is going to be deleted because the - // networkId for each sample can not be set. - - // In version 1.2 objectStore name was 'net_stats_v2', to avoid errors when - // upgrading from 1.2 to 1.3 objectStore name should be checked. - let stores = db.objectStoreNames; - let deprecatedName = DEPRECATED_STATS_STORE_NAME[0]; - let storeName = DEPRECATED_STATS_STORE_NAME[1]; - if(stores.contains(deprecatedName)) { - // Delete the obsolete stats store. - db.deleteObjectStore(deprecatedName); - } else { - // Re-create stats object store without copying records. - db.deleteObjectStore(storeName); - } - - objectStore = db.createObjectStore(storeName, { keyPath: ["appId", "network", "timestamp"] }); - objectStore.createIndex("appId", "appId", { unique: false }); - objectStore.createIndex("network", "network", { unique: false }); - objectStore.createIndex("networkType", "networkType", { unique: false }); - objectStore.createIndex("timestamp", "timestamp", { unique: false }); - objectStore.createIndex("rxBytes", "rxBytes", { unique: false }); - objectStore.createIndex("txBytes", "txBytes", { unique: false }); - objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); - objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); - - upgradeNextVersion(); - }, - - function upgrade3to4() { - if (DEBUG) debug("Upgrade 3 to 4: Delete redundant indexes."); - - // Delete redundant indexes (leave "network" only). - objectStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[1]); - if (objectStore.indexNames.contains("appId")) { - objectStore.deleteIndex("appId"); - } - if (objectStore.indexNames.contains("networkType")) { - objectStore.deleteIndex("networkType"); - } - if (objectStore.indexNames.contains("timestamp")) { - objectStore.deleteIndex("timestamp"); - } - if (objectStore.indexNames.contains("rxBytes")) { - objectStore.deleteIndex("rxBytes"); - } - if (objectStore.indexNames.contains("txBytes")) { - objectStore.deleteIndex("txBytes"); - } - if (objectStore.indexNames.contains("rxTotalBytes")) { - objectStore.deleteIndex("rxTotalBytes"); - } - if (objectStore.indexNames.contains("txTotalBytes")) { - objectStore.deleteIndex("txTotalBytes"); - } - - upgradeNextVersion(); - }, - - function upgrade4to5() { - if (DEBUG) debug("Upgrade 4 to 5: Create object store for alarms."); - - // In order to manage alarms, it is necessary to use a global counter - // (totalBytes) that will increase regardless of the system reboot. - objectStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[1]); - - // Now, systemBytes will hold the old totalBytes and totalBytes will - // keep the increasing counter. |counters| will keep the track of - // accumulated values. - let counters = {}; - - objectStore.openCursor().onsuccess = function(event) { - let cursor = event.target.result; - if (!cursor){ - // upgrade4to5 completed now. - upgradeNextVersion(); - return; - } - - cursor.value.rxSystemBytes = cursor.value.rxTotalBytes; - cursor.value.txSystemBytes = cursor.value.txTotalBytes; - - if (cursor.value.appId == 0) { - let netId = cursor.value.network[0] + '' + cursor.value.network[1]; - if (!counters[netId]) { - counters[netId] = { - rxCounter: 0, - txCounter: 0, - lastRx: 0, - lastTx: 0 - }; - } - - let rxDiff = cursor.value.rxSystemBytes - counters[netId].lastRx; - let txDiff = cursor.value.txSystemBytes - counters[netId].lastTx; - if (rxDiff < 0 || txDiff < 0) { - // System reboot between samples, so take the current one. - rxDiff = cursor.value.rxSystemBytes; - txDiff = cursor.value.txSystemBytes; - } - - counters[netId].rxCounter += rxDiff; - counters[netId].txCounter += txDiff; - cursor.value.rxTotalBytes = counters[netId].rxCounter; - cursor.value.txTotalBytes = counters[netId].txCounter; - - counters[netId].lastRx = cursor.value.rxSystemBytes; - counters[netId].lastTx = cursor.value.txSystemBytes; - } else { - cursor.value.rxTotalBytes = cursor.value.rxSystemBytes; - cursor.value.txTotalBytes = cursor.value.txSystemBytes; - } - - cursor.update(cursor.value); - cursor.continue(); - }; - - // Create object store for alarms. - objectStore = db.createObjectStore(ALARMS_STORE_NAME, { keyPath: "id", autoIncrement: true }); - objectStore.createIndex("alarm", ['networkId','threshold'], { unique: false }); - objectStore.createIndex("manifestURL", "manifestURL", { unique: false }); - }, - - function upgrade5to6() { - if (DEBUG) debug("Upgrade 5 to 6: Add keyPath serviceType to object store."); - - // In contrast to "per-app" traffic data, "system-only" traffic data - // refers to data which can not be identified by any applications. - // To further support "system-only" data storage, the data can be - // saved by service type (e.g., Tethering, OTA). Thus it's needed to - // have a new key ("serviceType") for the ojectStore. - let newObjectStore; - let deprecatedName = DEPRECATED_STATS_STORE_NAME[1]; - newObjectStore = db.createObjectStore(DEPRECATED_STATS_STORE_NAME[2], - { keyPath: ["appId", "serviceType", "network", "timestamp"] }); - newObjectStore.createIndex("network", "network", { unique: false }); - - // Copy the data from the original objectStore to the new objectStore. - objectStore = aTransaction.objectStore(deprecatedName); - objectStore.openCursor().onsuccess = function(event) { - let cursor = event.target.result; - if (!cursor) { - db.deleteObjectStore(deprecatedName); - // upgrade5to6 completed now. - upgradeNextVersion(); - return; - } - - let newStats = cursor.value; - newStats.serviceType = ""; - newObjectStore.put(newStats); - cursor.continue(); - }; - }, - - function upgrade6to7() { - if (DEBUG) debug("Upgrade 6 to 7: Replace alarm threshold by relativeThreshold."); - - // Replace threshold attribute of alarm index by relativeThreshold in alarms DB. - // Now alarms are indexed by relativeThreshold, which is the threshold relative - // to current system stats. - let alarmsStore = aTransaction.objectStore(ALARMS_STORE_NAME); - - // Delete "alarm" index. - if (alarmsStore.indexNames.contains("alarm")) { - alarmsStore.deleteIndex("alarm"); - } - - // Create new "alarm" index. - alarmsStore.createIndex("alarm", ['networkId','relativeThreshold'], { unique: false }); - - // Populate new "alarm" index attributes. - alarmsStore.openCursor().onsuccess = function(event) { - let cursor = event.target.result; - if (!cursor) { - upgrade6to7_updateTotalBytes(); - return; - } - - cursor.value.relativeThreshold = cursor.value.threshold; - cursor.value.absoluteThreshold = cursor.value.threshold; - delete cursor.value.threshold; - - cursor.update(cursor.value); - cursor.continue(); - } - - function upgrade6to7_updateTotalBytes() { - if (DEBUG) debug("Upgrade 6 to 7: Update TotalBytes."); - // Previous versions save accumulative totalBytes, increasing although the system - // reboots or resets stats. But is necessary to reset the total counters when reset - // through 'clearInterfaceStats'. - let statsStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[2]); - let networks = []; - - // Find networks stored in the database. - statsStore.index("network").openKeyCursor(null, "nextunique").onsuccess = function(event) { - let cursor = event.target.result; - - // Store each network into an array. - if (cursor) { - networks.push(cursor.key); - cursor.continue(); - return; - } - - // Start to deal with each network. - let pending = networks.length; - - if (pending === 0) { - // Found no records of network. upgrade6to7 completed now. - upgradeNextVersion(); - return; - } - - networks.forEach(function(network) { - let lowerFilter = [0, "", network, 0]; - let upperFilter = [0, "", network, ""]; - let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); - - // Find number of samples for a given network. - statsStore.count(range).onsuccess = function(event) { - let recordCount = event.target.result; - - // If there are more samples than the max allowed, there is no way to know - // when does reset take place. - if (recordCount === 0 || recordCount >= VALUES_MAX_LENGTH) { - pending--; - if (pending === 0) { - upgradeNextVersion(); - } - return; - } - - let last = null; - // Reset detected if the first sample totalCounters are different than bytes - // counters. If so, the total counters should be recalculated. - statsStore.openCursor(range).onsuccess = function(event) { - let cursor = event.target.result; - if (!cursor) { - pending--; - if (pending === 0) { - upgradeNextVersion(); - } - return; - } - if (!last) { - if (cursor.value.rxTotalBytes == cursor.value.rxBytes && - cursor.value.txTotalBytes == cursor.value.txBytes) { - pending--; - if (pending === 0) { - upgradeNextVersion(); - } - return; - } - - cursor.value.rxTotalBytes = cursor.value.rxBytes; - cursor.value.txTotalBytes = cursor.value.txBytes; - cursor.update(cursor.value); - last = cursor.value; - cursor.continue(); - return; - } - - // Recalculate the total counter for last / current sample - cursor.value.rxTotalBytes = last.rxTotalBytes + cursor.value.rxBytes; - cursor.value.txTotalBytes = last.txTotalBytes + cursor.value.txBytes; - cursor.update(cursor.value); - last = cursor.value; - cursor.continue(); - } - } - }, this); // end of networks.forEach() - }; // end of statsStore.index("network").openKeyCursor().onsuccess callback - } // end of function upgrade6to7_updateTotalBytes - }, - - function upgrade7to8() { - if (DEBUG) debug("Upgrade 7 to 8: Create index serviceType."); - - // Create index for 'ServiceType' in order to make it retrievable. - let statsStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[2]); - statsStore.createIndex("serviceType", "serviceType", { unique: false }); - - upgradeNextVersion(); - }, - - function upgrade8to9() { - if (DEBUG) debug("Upgrade 8 to 9: Add keyPath isInBrowser to " + - "network stats object store"); - - // Since B2G v2.0, there is no stand-alone browser app anymore. - // The browser app is a mozbrowser iframe element owned by system app. - // In order to separate traffic generated from system and browser, we - // have to add a new attribute |isInBrowser| as keyPath. - // Refer to bug 1070944 for more detail. - let newObjectStore; - let deprecatedName = DEPRECATED_STATS_STORE_NAME[2]; - newObjectStore = db.createObjectStore(STATS_STORE_NAME, - { keyPath: ["appId", "isInBrowser", "serviceType", - "network", "timestamp"] }); - newObjectStore.createIndex("network", "network", { unique: false }); - newObjectStore.createIndex("serviceType", "serviceType", { unique: false }); - - // Copy records from the current object store to the new one. - objectStore = aTransaction.objectStore(deprecatedName); - objectStore.openCursor().onsuccess = function (event) { - let cursor = event.target.result; - if (!cursor) { - db.deleteObjectStore(deprecatedName); - // upgrade8to9 completed now. - return; - } - let newStats = cursor.value; - // Augment records by adding the new isInBrowser attribute. - // Notes: - // 1. Key value cannot be boolean type. Use 1/0 instead of true/false. - // 2. Most traffic of system app should come from its browser iframe, - // thus assign isInBrowser as 1 for system app. - let manifestURL = appsService.getManifestURLByLocalId(newStats.appId); - if (manifestURL && manifestURL.search(/app:\/\/system\./) === 0) { - newStats.isInBrowser = 1; - } else { - newStats.isInBrowser = 0; - } - newObjectStore.put(newStats); - cursor.continue(); - }; - } - ]; - - let index = aOldVersion; - let outer = this; - - function upgradeNextVersion() { - if (index == aNewVersion) { - debug("Upgrade finished."); - return; - } - - try { - var i = index++; - if (DEBUG) debug("Upgrade step: " + i + "\n"); - upgradeSteps[i].call(outer); - } catch (ex) { - dump("Caught exception " + ex); - throw ex; - return; - } - } - - if (aNewVersion > upgradeSteps.length) { - debug("No migration steps for the new version!"); - aTransaction.abort(); - return; - } - - upgradeNextVersion(); - }, - - importData: function importData(aStats) { - let stats = { appId: aStats.appId, - isInBrowser: aStats.isInBrowser ? 1 : 0, - serviceType: aStats.serviceType, - network: [aStats.networkId, aStats.networkType], - timestamp: aStats.timestamp, - rxBytes: aStats.rxBytes, - txBytes: aStats.txBytes, - rxSystemBytes: aStats.rxSystemBytes, - txSystemBytes: aStats.txSystemBytes, - rxTotalBytes: aStats.rxTotalBytes, - txTotalBytes: aStats.txTotalBytes }; - - return stats; - }, - - exportData: function exportData(aStats) { - let stats = { appId: aStats.appId, - isInBrowser: aStats.isInBrowser ? true : false, - serviceType: aStats.serviceType, - networkId: aStats.network[0], - networkType: aStats.network[1], - timestamp: aStats.timestamp, - rxBytes: aStats.rxBytes, - txBytes: aStats.txBytes, - rxTotalBytes: aStats.rxTotalBytes, - txTotalBytes: aStats.txTotalBytes }; - - return stats; - }, - - normalizeDate: function normalizeDate(aDate) { - // Convert to UTC according to timezone and - // filter timestamp to get SAMPLE_RATE precission - let timestamp = aDate.getTime() - aDate.getTimezoneOffset() * 60 * 1000; - timestamp = Math.floor(timestamp / SAMPLE_RATE) * SAMPLE_RATE; - return timestamp; - }, - - saveStats: function saveStats(aStats, aResultCb) { - let isAccumulative = aStats.isAccumulative; - let timestamp = this.normalizeDate(aStats.date); - - let stats = { appId: aStats.appId, - isInBrowser: aStats.isInBrowser, - serviceType: aStats.serviceType, - networkId: aStats.networkId, - networkType: aStats.networkType, - timestamp: timestamp, - rxBytes: isAccumulative ? 0 : aStats.rxBytes, - txBytes: isAccumulative ? 0 : aStats.txBytes, - rxSystemBytes: isAccumulative ? aStats.rxBytes : 0, - txSystemBytes: isAccumulative ? aStats.txBytes : 0, - rxTotalBytes: isAccumulative ? aStats.rxBytes : 0, - txTotalBytes: isAccumulative ? aStats.txBytes : 0 }; - - stats = this.importData(stats); - - this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) { - if (DEBUG) { - debug("Filtered time: " + new Date(timestamp)); - debug("New stats: " + JSON.stringify(stats)); - } - - let lowerFilter = [stats.appId, stats.isInBrowser, stats.serviceType, - stats.network, 0]; - let upperFilter = [stats.appId, stats.isInBrowser, stats.serviceType, - stats.network, ""]; - let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); - - let request = aStore.openCursor(range, 'prev'); - request.onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (!cursor) { - // Empty, so save first element. - - if (!isAccumulative) { - this._saveStats(aTxn, aStore, stats); - return; - } - - // There could be a time delay between the point when the network - // interface comes up and the point when the database is initialized. - // In this short interval some traffic data are generated but are not - // registered by the first sample. - stats.rxBytes = stats.rxTotalBytes; - stats.txBytes = stats.txTotalBytes; - - // However, if the interface is not switched on after the database is - // initialized (dual sim use case) stats should be set to 0. - let req = aStore.index("network").openKeyCursor(null, "nextunique"); - req.onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (cursor) { - if (cursor.key[1] == stats.network[1]) { - stats.rxBytes = 0; - stats.txBytes = 0; - this._saveStats(aTxn, aStore, stats); - return; - } - - cursor.continue(); - return; - } - - this._saveStats(aTxn, aStore, stats); - }.bind(this); - - return; - } - - // There are old samples - if (DEBUG) { - debug("Last value " + JSON.stringify(cursor.value)); - } - - // Remove stats previous to now - VALUE_MAX_LENGTH - this._removeOldStats(aTxn, aStore, stats.appId, stats.isInBrowser, - stats.serviceType, stats.network, stats.timestamp); - - // Process stats before save - this._processSamplesDiff(aTxn, aStore, cursor, stats, isAccumulative); - }.bind(this); - }.bind(this), aResultCb); - }, - - /* - * This function check that stats are saved in the database following the sample rate. - * In this way is easier to find elements when stats are requested. - */ - _processSamplesDiff: function _processSamplesDiff(aTxn, - aStore, - aLastSampleCursor, - aNewSample, - aIsAccumulative) { - let lastSample = aLastSampleCursor.value; - - // Get difference between last and new sample. - let diff = (aNewSample.timestamp - lastSample.timestamp) / SAMPLE_RATE; - if (diff % 1) { - // diff is decimal, so some error happened because samples are stored as a multiple - // of SAMPLE_RATE - aTxn.abort(); - throw new Error("Error processing samples"); - } - - if (DEBUG) { - debug("New: " + aNewSample.timestamp + " - Last: " + - lastSample.timestamp + " - diff: " + diff); - } - - // If the incoming data has a accumulation feature, the new - // |txBytes|/|rxBytes| is assigend by differnces between the new - // |txTotalBytes|/|rxTotalBytes| and the last |txTotalBytes|/|rxTotalBytes|. - // Else, if incoming data is non-accumulative, the |txBytes|/|rxBytes| - // is the new |txBytes|/|rxBytes|. - let rxDiff = 0; - let txDiff = 0; - if (aIsAccumulative) { - rxDiff = aNewSample.rxSystemBytes - lastSample.rxSystemBytes; - txDiff = aNewSample.txSystemBytes - lastSample.txSystemBytes; - if (rxDiff < 0 || txDiff < 0) { - rxDiff = aNewSample.rxSystemBytes; - txDiff = aNewSample.txSystemBytes; - } - aNewSample.rxBytes = rxDiff; - aNewSample.txBytes = txDiff; - - aNewSample.rxTotalBytes = lastSample.rxTotalBytes + rxDiff; - aNewSample.txTotalBytes = lastSample.txTotalBytes + txDiff; - } else { - rxDiff = aNewSample.rxBytes; - txDiff = aNewSample.txBytes; - } - - if (diff == 1) { - // New element. - - // If the incoming data is non-accumulative, the new - // |rxTotalBytes|/|txTotalBytes| needs to be updated by adding new - // |rxBytes|/|txBytes| to the last |rxTotalBytes|/|txTotalBytes|. - if (!aIsAccumulative) { - aNewSample.rxTotalBytes = aNewSample.rxBytes + lastSample.rxTotalBytes; - aNewSample.txTotalBytes = aNewSample.txBytes + lastSample.txTotalBytes; - } - - this._saveStats(aTxn, aStore, aNewSample); - return; - } - if (diff > 1) { - // Some samples lost. Device off during one or more samplerate periods. - // Time or timezone changed - // Add lost samples with 0 bytes and the actual one. - if (diff > VALUES_MAX_LENGTH) { - diff = VALUES_MAX_LENGTH; - } - - let data = []; - for (let i = diff - 2; i >= 0; i--) { - let time = aNewSample.timestamp - SAMPLE_RATE * (i + 1); - let sample = { appId: aNewSample.appId, - isInBrowser: aNewSample.isInBrowser, - serviceType: aNewSample.serviceType, - network: aNewSample.network, - timestamp: time, - rxBytes: 0, - txBytes: 0, - rxSystemBytes: lastSample.rxSystemBytes, - txSystemBytes: lastSample.txSystemBytes, - rxTotalBytes: lastSample.rxTotalBytes, - txTotalBytes: lastSample.txTotalBytes }; - - data.push(sample); - } - - data.push(aNewSample); - this._saveStats(aTxn, aStore, data); - return; - } - if (diff == 0 || diff < 0) { - // New element received before samplerate period. It means that device has - // been restarted (or clock / timezone change). - // Update element. If diff < 0, clock or timezone changed back. Place data - // in the last sample. - - // Old |rxTotalBytes|/|txTotalBytes| needs to get updated by adding the - // last |rxTotalBytes|/|txTotalBytes|. - lastSample.rxBytes += rxDiff; - lastSample.txBytes += txDiff; - lastSample.rxSystemBytes = aNewSample.rxSystemBytes; - lastSample.txSystemBytes = aNewSample.txSystemBytes; - lastSample.rxTotalBytes += rxDiff; - lastSample.txTotalBytes += txDiff; - - if (DEBUG) { - debug("Update: " + JSON.stringify(lastSample)); - } - let req = aLastSampleCursor.update(lastSample); - } - }, - - _saveStats: function _saveStats(aTxn, aStore, aNetworkStats) { - if (DEBUG) { - debug("_saveStats: " + JSON.stringify(aNetworkStats)); - } - - if (Array.isArray(aNetworkStats)) { - let len = aNetworkStats.length - 1; - for (let i = 0; i <= len; i++) { - aStore.put(aNetworkStats[i]); - } - } else { - aStore.put(aNetworkStats); - } - }, - - _removeOldStats: function _removeOldStats(aTxn, aStore, aAppId, aIsInBrowser, - aServiceType, aNetwork, aDate) { - // Callback function to remove old items when new ones are added. - let filterDate = aDate - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1); - let lowerFilter = [aAppId, aIsInBrowser, aServiceType, aNetwork, 0]; - let upperFilter = [aAppId, aIsInBrowser, aServiceType, aNetwork, filterDate]; - let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); - let lastSample = null; - let self = this; - - aStore.openCursor(range).onsuccess = function(event) { - var cursor = event.target.result; - if (cursor) { - lastSample = cursor.value; - cursor.delete(); - cursor.continue(); - return; - } - - // If all samples for a network are removed, an empty sample - // has to be saved to keep the totalBytes in order to compute - // future samples because system counters are not set to 0. - // Thus, if there are no samples left, the last sample removed - // will be saved again after setting its bytes to 0. - let request = aStore.index("network").openCursor(aNetwork); - request.onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (!cursor && lastSample != null) { - let timestamp = new Date(); - timestamp = self.normalizeDate(timestamp); - lastSample.timestamp = timestamp; - lastSample.rxBytes = 0; - lastSample.txBytes = 0; - self._saveStats(aTxn, aStore, lastSample); - } - }; - }; - }, - - clearInterfaceStats: function clearInterfaceStats(aNetwork, aResultCb) { - let network = [aNetwork.network.id, aNetwork.network.type]; - let self = this; - - // Clear and save an empty sample to keep sync with system counters - this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) { - let sample = null; - let request = aStore.index("network").openCursor(network, "prev"); - request.onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (cursor) { - if (!sample && cursor.value.appId == 0) { - sample = cursor.value; - } - - cursor.delete(); - cursor.continue(); - return; - } - - if (sample) { - let timestamp = new Date(); - timestamp = self.normalizeDate(timestamp); - sample.timestamp = timestamp; - sample.appId = 0; - sample.isInBrowser = 0; - sample.serviceType = ""; - sample.rxBytes = 0; - sample.txBytes = 0; - sample.rxTotalBytes = 0; - sample.txTotalBytes = 0; - - self._saveStats(aTxn, aStore, sample); - } - }; - }, this._resetAlarms.bind(this, aNetwork.networkId, aResultCb)); - }, - - clearStats: function clearStats(aNetworks, aResultCb) { - let index = 0; - let stats = []; - let self = this; - - let callback = function(aError, aResult) { - index++; - - if (!aError && index < aNetworks.length) { - self.clearInterfaceStats(aNetworks[index], callback); - return; - } - - aResultCb(aError, aResult); - }; - - if (!aNetworks[index]) { - aResultCb(null, true); - return; - } - this.clearInterfaceStats(aNetworks[index], callback); - }, - - getCurrentStats: function getCurrentStats(aNetwork, aDate, aResultCb) { - if (DEBUG) { - debug("Get current stats for " + JSON.stringify(aNetwork) + " since " + aDate); - } - - let network = [aNetwork.id, aNetwork.type]; - if (aDate) { - this._getCurrentStatsFromDate(network, aDate, aResultCb); - return; - } - - this._getCurrentStats(network, aResultCb); - }, - - _getCurrentStats: function _getCurrentStats(aNetwork, aResultCb) { - this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) { - let request = null; - let upperFilter = [0, 1, "", aNetwork, Date.now()]; - let range = IDBKeyRange.upperBound(upperFilter, false); - let result = { rxBytes: 0, txBytes: 0, - rxTotalBytes: 0, txTotalBytes: 0 }; - - request = store.openCursor(range, "prev"); - - request.onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (cursor) { - result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes; - result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes; - } - - txn.result = result; - }; - }.bind(this), aResultCb); - }, - - _getCurrentStatsFromDate: function _getCurrentStatsFromDate(aNetwork, aDate, aResultCb) { - aDate = new Date(aDate); - this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) { - let request = null; - let start = this.normalizeDate(aDate); - let upperFilter = [0, 1, "", aNetwork, Date.now()]; - let range = IDBKeyRange.upperBound(upperFilter, false); - let result = { rxBytes: 0, txBytes: 0, - rxTotalBytes: 0, txTotalBytes: 0 }; - - request = store.openCursor(range, "prev"); - - request.onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (cursor) { - result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes; - result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes; - } - - let timestamp = cursor.value.timestamp; - let range = IDBKeyRange.lowerBound(lowerFilter, false); - request = store.openCursor(range); - - request.onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (cursor) { - if (cursor.value.timestamp == timestamp) { - // There is one sample only. - result.rxBytes = cursor.value.rxBytes; - result.txBytes = cursor.value.txBytes; - } else { - result.rxBytes -= cursor.value.rxTotalBytes; - result.txBytes -= cursor.value.txTotalBytes; - } - } - - txn.result = result; - }; - }; - }.bind(this), aResultCb); - }, - - find: function find(aResultCb, aAppId, aBrowsingTrafficOnly, aServiceType, - aNetwork, aStart, aEnd, aAppManifestURL) { - let offset = (new Date()).getTimezoneOffset() * 60 * 1000; - let start = this.normalizeDate(aStart); - let end = this.normalizeDate(aEnd); - - if (DEBUG) { - debug("Find samples for appId: " + aAppId + - " browsingTrafficOnly: " + aBrowsingTrafficOnly + - " serviceType: " + aServiceType + - " network: " + JSON.stringify(aNetwork) + " from " + start + - " until " + end); - debug("Start time: " + new Date(start)); - debug("End time: " + new Date(end)); - } - - // Find samples of browsing traffic (isInBrowser = 1) first since they are - // needed no matter browsingTrafficOnly is true or false. - // We have to make two queries to database because we cannot filter correct - // records by a single query that sets ranges for two keys (isInBrowser and - // timestamp). We think it is because the keyPath contains an array - // (network) so such query does not work. - this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { - let network = [aNetwork.id, aNetwork.type]; - let lowerFilter = [aAppId, 1, aServiceType, network, start]; - let upperFilter = [aAppId, 1, aServiceType, network, end]; - let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); - - let data = []; - - if (!aTxn.result) { - aTxn.result = {}; - } - aTxn.result.appManifestURL = aAppManifestURL; - aTxn.result.browsingTrafficOnly = aBrowsingTrafficOnly; - aTxn.result.serviceType = aServiceType; - aTxn.result.network = aNetwork; - aTxn.result.start = aStart; - aTxn.result.end = aEnd; - - let request = aStore.openCursor(range).onsuccess = function(event) { - var cursor = event.target.result; - if (cursor){ - // We use rxTotalBytes/txTotalBytes instead of rxBytes/txBytes for - // the first (oldest) sample. The rx/txTotalBytes fields record - // accumulative usage amount, which means even if old samples were - // expired and removed from the Database, we can still obtain the - // correct network usage. - if (data.length == 0) { - data.push({ rxBytes: cursor.value.rxTotalBytes, - txBytes: cursor.value.txTotalBytes, - date: new Date(cursor.value.timestamp + offset) }); - } else { - data.push({ rxBytes: cursor.value.rxBytes, - txBytes: cursor.value.txBytes, - date: new Date(cursor.value.timestamp + offset) }); - } - cursor.continue(); - return; - } - - if (aBrowsingTrafficOnly) { - this.fillResultSamples(start + offset, end + offset, data); - aTxn.result.data = data; - return; - } - - // Find samples of app traffic (isInBrowser = 0) as well if - // browsingTrafficOnly is false. - lowerFilter = [aAppId, 0, aServiceType, network, start]; - upperFilter = [aAppId, 0, aServiceType, network, end]; - range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); - request = aStore.openCursor(range).onsuccess = function(event) { - cursor = event.target.result; - if (cursor) { - var date = new Date(cursor.value.timestamp + offset); - var foundData = data.find(function (element, index, array) { - if (element.date.getTime() !== date.getTime()) { - return false; - } - return element; - }, date); - - if (foundData) { - foundData.rxBytes += cursor.value.rxBytes; - foundData.txBytes += cursor.value.txBytes; - } else { - // We use rxTotalBytes/txTotalBytes instead of rxBytes/txBytes - // for the first (oldest) sample. The rx/txTotalBytes fields - // record accumulative usage amount, which means even if old - // samples were expired and removed from the Database, we can - // still obtain the correct network usage. - if (data.length == 0) { - data.push({ rxBytes: cursor.value.rxTotalBytes, - txBytes: cursor.value.txTotalBytes, - date: new Date(cursor.value.timestamp + offset) }); - } else { - data.push({ rxBytes: cursor.value.rxBytes, - txBytes: cursor.value.txBytes, - date: new Date(cursor.value.timestamp + offset) }); - } - } - cursor.continue(); - return; - } - this.fillResultSamples(start + offset, end + offset, data); - aTxn.result.data = data; - }.bind(this); // openCursor(range).onsuccess() callback - }.bind(this); // openCursor(range).onsuccess() callback - }.bind(this), aResultCb); - }, - - /* - * Fill data array (samples from database) with empty samples to match - * requested start / end dates. - */ - fillResultSamples: function fillResultSamples(aStart, aEnd, aData) { - if (aData.length == 0) { - aData.push({ rxBytes: undefined, - txBytes: undefined, - date: new Date(aStart) }); - } - - while (aStart < aData[0].date.getTime()) { - aData.unshift({ rxBytes: undefined, - txBytes: undefined, - date: new Date(aData[0].date.getTime() - SAMPLE_RATE) }); - } - - while (aEnd > aData[aData.length - 1].date.getTime()) { - aData.push({ rxBytes: undefined, - txBytes: undefined, - date: new Date(aData[aData.length - 1].date.getTime() + SAMPLE_RATE) }); - } - }, - - getAvailableNetworks: function getAvailableNetworks(aResultCb) { - this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { - if (!aTxn.result) { - aTxn.result = []; - } - - let request = aStore.index("network").openKeyCursor(null, "nextunique"); - request.onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (cursor) { - aTxn.result.push({ id: cursor.key[0], - type: cursor.key[1] }); - cursor.continue(); - return; - } - }; - }, aResultCb); - }, - - isNetworkAvailable: function isNetworkAvailable(aNetwork, aResultCb) { - this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { - if (!aTxn.result) { - aTxn.result = false; - } - - let network = [aNetwork.id, aNetwork.type]; - let request = aStore.index("network").openKeyCursor(IDBKeyRange.only(network)); - request.onsuccess = function onsuccess(event) { - if (event.target.result) { - aTxn.result = true; - } - }; - }, aResultCb); - }, - - getAvailableServiceTypes: function getAvailableServiceTypes(aResultCb) { - this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { - if (!aTxn.result) { - aTxn.result = []; - } - - let request = aStore.index("serviceType").openKeyCursor(null, "nextunique"); - request.onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (cursor && cursor.key != "") { - aTxn.result.push({ serviceType: cursor.key }); - cursor.continue(); - return; - } - }; - }, aResultCb); - }, - - get sampleRate () { - return SAMPLE_RATE; - }, - - get maxStorageSamples () { - return VALUES_MAX_LENGTH; - }, - - logAllRecords: function logAllRecords(aResultCb) { - this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { - aStore.mozGetAll().onsuccess = function onsuccess(event) { - aTxn.result = event.target.result; - }; - }, aResultCb); - }, - - alarmToRecord: function alarmToRecord(aAlarm) { - let record = { networkId: aAlarm.networkId, - absoluteThreshold: aAlarm.absoluteThreshold, - relativeThreshold: aAlarm.relativeThreshold, - startTime: aAlarm.startTime, - data: aAlarm.data, - manifestURL: aAlarm.manifestURL, - pageURL: aAlarm.pageURL }; - - if (aAlarm.id) { - record.id = aAlarm.id; - } - - return record; - }, - - recordToAlarm: function recordToalarm(aRecord) { - let alarm = { networkId: aRecord.networkId, - absoluteThreshold: aRecord.absoluteThreshold, - relativeThreshold: aRecord.relativeThreshold, - startTime: aRecord.startTime, - data: aRecord.data, - manifestURL: aRecord.manifestURL, - pageURL: aRecord.pageURL }; - - if (aRecord.id) { - alarm.id = aRecord.id; - } - - return alarm; - }, - - addAlarm: function addAlarm(aAlarm, aResultCb) { - this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { - if (DEBUG) { - debug("Going to add " + JSON.stringify(aAlarm)); - } - - let record = this.alarmToRecord(aAlarm); - store.put(record).onsuccess = function setResult(aEvent) { - txn.result = aEvent.target.result; - if (DEBUG) { - debug("Request successful. New record ID: " + txn.result); - } - }; - }.bind(this), aResultCb); - }, - - getFirstAlarm: function getFirstAlarm(aNetworkId, aResultCb) { - let self = this; - - this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) { - if (DEBUG) { - debug("Get first alarm for network " + aNetworkId); - } - - let lowerFilter = [aNetworkId, 0]; - let upperFilter = [aNetworkId, ""]; - let range = IDBKeyRange.bound(lowerFilter, upperFilter); - - store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) { - let cursor = event.target.result; - txn.result = null; - if (cursor) { - txn.result = self.recordToAlarm(cursor.value); - } - }; - }, aResultCb); - }, - - removeAlarm: function removeAlarm(aAlarmId, aManifestURL, aResultCb) { - this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { - if (DEBUG) { - debug("Remove alarm " + aAlarmId); - } - - store.get(aAlarmId).onsuccess = function onsuccess(event) { - let record = event.target.result; - txn.result = false; - if (!record || (aManifestURL && record.manifestURL != aManifestURL)) { - return; - } - - store.delete(aAlarmId); - txn.result = true; - } - }, aResultCb); - }, - - removeAlarms: function removeAlarms(aManifestURL, aResultCb) { - this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { - if (DEBUG) { - debug("Remove alarms of " + aManifestURL); - } - - store.index("manifestURL").openCursor(aManifestURL) - .onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (cursor) { - cursor.delete(); - cursor.continue(); - } - } - }, aResultCb); - }, - - updateAlarm: function updateAlarm(aAlarm, aResultCb) { - let self = this; - this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { - if (DEBUG) { - debug("Update alarm " + aAlarm.id); - } - - let record = self.alarmToRecord(aAlarm); - store.openCursor(record.id).onsuccess = function onsuccess(event) { - let cursor = event.target.result; - txn.result = false; - if (cursor) { - cursor.update(record); - txn.result = true; - } - } - }, aResultCb); - }, - - getAlarms: function getAlarms(aNetworkId, aManifestURL, aResultCb) { - let self = this; - this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) { - if (DEBUG) { - debug("Get alarms for " + aManifestURL); - } - - txn.result = []; - store.index("manifestURL").openCursor(aManifestURL) - .onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (!cursor) { - return; - } - - if (!aNetworkId || cursor.value.networkId == aNetworkId) { - txn.result.push(self.recordToAlarm(cursor.value)); - } - - cursor.continue(); - } - }, aResultCb); - }, - - _resetAlarms: function _resetAlarms(aNetworkId, aResultCb) { - this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { - if (DEBUG) { - debug("Reset alarms for network " + aNetworkId); - } - - let lowerFilter = [aNetworkId, 0]; - let upperFilter = [aNetworkId, ""]; - let range = IDBKeyRange.bound(lowerFilter, upperFilter); - - store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (cursor) { - if (cursor.value.startTime) { - cursor.value.relativeThreshold = cursor.value.threshold; - cursor.update(cursor.value); - } - cursor.continue(); - return; - } - }; - }, aResultCb); - } -}; |