/* 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); } };