michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ['NetworkStatsDB']; michael@0: michael@0: const DEBUG = false; michael@0: function debug(s) { dump("-*- NetworkStatsDB: " + s + "\n"); } michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); michael@0: Cu.importGlobalProperties(["indexedDB"]); michael@0: michael@0: const DB_NAME = "net_stats"; michael@0: const DB_VERSION = 8; michael@0: const DEPRECATED_STORE_NAME = "net_stats"; michael@0: const STATS_STORE_NAME = "net_stats_store"; michael@0: const ALARMS_STORE_NAME = "net_alarm"; michael@0: michael@0: // Constant defining the maximum values allowed per interface. If more, older michael@0: // will be erased. michael@0: const VALUES_MAX_LENGTH = 6 * 30; michael@0: michael@0: // Constant defining the rate of the samples. Daily. michael@0: const SAMPLE_RATE = 1000 * 60 * 60 * 24; michael@0: michael@0: this.NetworkStatsDB = function NetworkStatsDB() { michael@0: if (DEBUG) { michael@0: debug("Constructor"); michael@0: } michael@0: this.initDBHelper(DB_NAME, DB_VERSION, [STATS_STORE_NAME, ALARMS_STORE_NAME]); michael@0: } michael@0: michael@0: NetworkStatsDB.prototype = { michael@0: __proto__: IndexedDBHelper.prototype, michael@0: michael@0: dbNewTxn: function dbNewTxn(store_name, txn_type, callback, txnCb) { michael@0: function successCb(result) { michael@0: txnCb(null, result); michael@0: } michael@0: function errorCb(error) { michael@0: txnCb(error, null); michael@0: } michael@0: return this.newTxn(txn_type, store_name, callback, successCb, errorCb); michael@0: }, michael@0: michael@0: upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) { michael@0: if (DEBUG) { michael@0: debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!"); michael@0: } michael@0: let db = aDb; michael@0: let objectStore; michael@0: for (let currVersion = aOldVersion; currVersion < aNewVersion; currVersion++) { michael@0: if (currVersion == 0) { michael@0: /** michael@0: * Create the initial database schema. michael@0: */ michael@0: michael@0: objectStore = db.createObjectStore(DEPRECATED_STORE_NAME, { keyPath: ["connectionType", "timestamp"] }); michael@0: objectStore.createIndex("connectionType", "connectionType", { unique: false }); michael@0: objectStore.createIndex("timestamp", "timestamp", { unique: false }); michael@0: objectStore.createIndex("rxBytes", "rxBytes", { unique: false }); michael@0: objectStore.createIndex("txBytes", "txBytes", { unique: false }); michael@0: objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); michael@0: objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); michael@0: if (DEBUG) { michael@0: debug("Created object stores and indexes"); michael@0: } michael@0: } else if (currVersion == 2) { michael@0: // In order to support per-app traffic data storage, the original michael@0: // objectStore needs to be replaced by a new objectStore with new michael@0: // key path ("appId") and new index ("appId"). michael@0: // Also, since now networks are identified by their michael@0: // [networkId, networkType] not just by their connectionType, michael@0: // to modify the keyPath is mandatory to delete the object store michael@0: // and create it again. Old data is going to be deleted because the michael@0: // networkId for each sample can not be set. michael@0: michael@0: // In version 1.2 objectStore name was 'net_stats_v2', to avoid errors when michael@0: // upgrading from 1.2 to 1.3 objectStore name should be checked. michael@0: let stores = db.objectStoreNames; michael@0: if(stores.contains("net_stats_v2")) { michael@0: db.deleteObjectStore("net_stats_v2"); michael@0: } else { michael@0: db.deleteObjectStore(DEPRECATED_STORE_NAME); michael@0: } michael@0: michael@0: objectStore = db.createObjectStore(DEPRECATED_STORE_NAME, { keyPath: ["appId", "network", "timestamp"] }); michael@0: objectStore.createIndex("appId", "appId", { unique: false }); michael@0: objectStore.createIndex("network", "network", { unique: false }); michael@0: objectStore.createIndex("networkType", "networkType", { unique: false }); michael@0: objectStore.createIndex("timestamp", "timestamp", { unique: false }); michael@0: objectStore.createIndex("rxBytes", "rxBytes", { unique: false }); michael@0: objectStore.createIndex("txBytes", "txBytes", { unique: false }); michael@0: objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); michael@0: objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); michael@0: michael@0: if (DEBUG) { michael@0: debug("Created object stores and indexes for version 3"); michael@0: } michael@0: } else if (currVersion == 3) { michael@0: // Delete redundent indexes (leave "network" only). michael@0: objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME); michael@0: if (objectStore.indexNames.contains("appId")) { michael@0: objectStore.deleteIndex("appId"); michael@0: } michael@0: if (objectStore.indexNames.contains("networkType")) { michael@0: objectStore.deleteIndex("networkType"); michael@0: } michael@0: if (objectStore.indexNames.contains("timestamp")) { michael@0: objectStore.deleteIndex("timestamp"); michael@0: } michael@0: if (objectStore.indexNames.contains("rxBytes")) { michael@0: objectStore.deleteIndex("rxBytes"); michael@0: } michael@0: if (objectStore.indexNames.contains("txBytes")) { michael@0: objectStore.deleteIndex("txBytes"); michael@0: } michael@0: if (objectStore.indexNames.contains("rxTotalBytes")) { michael@0: objectStore.deleteIndex("rxTotalBytes"); michael@0: } michael@0: if (objectStore.indexNames.contains("txTotalBytes")) { michael@0: objectStore.deleteIndex("txTotalBytes"); michael@0: } michael@0: michael@0: if (DEBUG) { michael@0: debug("Deleted redundent indexes for version 4"); michael@0: } michael@0: } else if (currVersion == 4) { michael@0: // In order to manage alarms, it is necessary to use a global counter michael@0: // (totalBytes) that will increase regardless of the system reboot. michael@0: objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME); michael@0: michael@0: // Now, systemBytes will hold the old totalBytes and totalBytes will michael@0: // keep the increasing counter. |counters| will keep the track of michael@0: // accumulated values. michael@0: let counters = {}; michael@0: michael@0: objectStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor){ michael@0: return; michael@0: } michael@0: michael@0: cursor.value.rxSystemBytes = cursor.value.rxTotalBytes; michael@0: cursor.value.txSystemBytes = cursor.value.txTotalBytes; michael@0: michael@0: if (cursor.value.appId == 0) { michael@0: let netId = cursor.value.network[0] + '' + cursor.value.network[1]; michael@0: if (!counters[netId]) { michael@0: counters[netId] = { michael@0: rxCounter: 0, michael@0: txCounter: 0, michael@0: lastRx: 0, michael@0: lastTx: 0 michael@0: }; michael@0: } michael@0: michael@0: let rxDiff = cursor.value.rxSystemBytes - counters[netId].lastRx; michael@0: let txDiff = cursor.value.txSystemBytes - counters[netId].lastTx; michael@0: if (rxDiff < 0 || txDiff < 0) { michael@0: // System reboot between samples, so take the current one. michael@0: rxDiff = cursor.value.rxSystemBytes; michael@0: txDiff = cursor.value.txSystemBytes; michael@0: } michael@0: michael@0: counters[netId].rxCounter += rxDiff; michael@0: counters[netId].txCounter += txDiff; michael@0: cursor.value.rxTotalBytes = counters[netId].rxCounter; michael@0: cursor.value.txTotalBytes = counters[netId].txCounter; michael@0: michael@0: counters[netId].lastRx = cursor.value.rxSystemBytes; michael@0: counters[netId].lastTx = cursor.value.txSystemBytes; michael@0: } else { michael@0: cursor.value.rxTotalBytes = cursor.value.rxSystemBytes; michael@0: cursor.value.txTotalBytes = cursor.value.txSystemBytes; michael@0: } michael@0: michael@0: cursor.update(cursor.value); michael@0: cursor.continue(); michael@0: }; michael@0: michael@0: // Create object store for alarms. michael@0: objectStore = db.createObjectStore(ALARMS_STORE_NAME, { keyPath: "id", autoIncrement: true }); michael@0: objectStore.createIndex("alarm", ['networkId','threshold'], { unique: false }); michael@0: objectStore.createIndex("manifestURL", "manifestURL", { unique: false }); michael@0: michael@0: if (DEBUG) { michael@0: debug("Created alarms store for version 5"); michael@0: } michael@0: } else if (currVersion == 5) { michael@0: // In contrast to "per-app" traffic data, "system-only" traffic data michael@0: // refers to data which can not be identified by any applications. michael@0: // To further support "system-only" data storage, the data can be michael@0: // saved by service type (e.g., Tethering, OTA). Thus it's needed to michael@0: // have a new key ("serviceType") for the ojectStore. michael@0: let newObjectStore; michael@0: newObjectStore = db.createObjectStore(STATS_STORE_NAME, michael@0: { keyPath: ["appId", "serviceType", "network", "timestamp"] }); michael@0: newObjectStore.createIndex("network", "network", { unique: false }); michael@0: michael@0: // Copy the data from the original objectStore to the new objectStore. michael@0: objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME); michael@0: objectStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: db.deleteObjectStore(DEPRECATED_STORE_NAME); michael@0: return; michael@0: } michael@0: michael@0: let newStats = cursor.value; michael@0: newStats.serviceType = ""; michael@0: newObjectStore.put(newStats); michael@0: cursor.continue(); michael@0: }; michael@0: michael@0: if (DEBUG) { michael@0: debug("Added new key 'serviceType' for version 6"); michael@0: } michael@0: } else if (currVersion == 6) { michael@0: // Replace threshold attribute of alarm index by relativeThreshold in alarms DB. michael@0: // Now alarms are indexed by relativeThreshold, which is the threshold relative michael@0: // to current system stats. michael@0: let alarmsStore = aTransaction.objectStore(ALARMS_STORE_NAME); michael@0: michael@0: // Delete "alarm" index. michael@0: if (alarmsStore.indexNames.contains("alarm")) { michael@0: alarmsStore.deleteIndex("alarm"); michael@0: } michael@0: michael@0: // Create new "alarm" index. michael@0: alarmsStore.createIndex("alarm", ['networkId','relativeThreshold'], { unique: false }); michael@0: michael@0: // Populate new "alarm" index attributes. michael@0: alarmsStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: return; michael@0: } michael@0: michael@0: cursor.value.relativeThreshold = cursor.value.threshold; michael@0: cursor.value.absoluteThreshold = cursor.value.threshold; michael@0: delete cursor.value.threshold; michael@0: michael@0: cursor.update(cursor.value); michael@0: cursor.continue(); michael@0: } michael@0: michael@0: // Previous versions save accumulative totalBytes, increasing althought the system michael@0: // reboots or resets stats. But is necessary to reset the total counters when reset michael@0: // through 'clearInterfaceStats'. michael@0: let statsStore = aTransaction.objectStore(STATS_STORE_NAME); michael@0: let networks = []; michael@0: // Find networks stored in the database. michael@0: statsStore.index("network").openKeyCursor(null, "nextunique").onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: networks.push(cursor.key); michael@0: cursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: networks.forEach(function(network) { michael@0: let lowerFilter = [0, "", network, 0]; michael@0: let upperFilter = [0, "", network, ""]; michael@0: let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); michael@0: michael@0: // Find number of samples for a given network. michael@0: statsStore.count(range).onsuccess = function(event) { michael@0: // If there are more samples than the max allowed, there is no way to know michael@0: // when does reset take place. michael@0: if (event.target.result >= VALUES_MAX_LENGTH) { michael@0: return; michael@0: } michael@0: michael@0: let last = null; michael@0: // Reset detected if the first sample totalCounters are different than bytes michael@0: // counters. If so, the total counters should be recalculated. michael@0: statsStore.openCursor(range).onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: return; michael@0: } michael@0: if (!last) { michael@0: if (cursor.value.rxTotalBytes == cursor.value.rxBytes && michael@0: cursor.value.txTotalBytes == cursor.value.txBytes) { michael@0: return; michael@0: } michael@0: michael@0: cursor.value.rxTotalBytes = cursor.value.rxBytes; michael@0: cursor.value.txTotalBytes = cursor.value.txBytes; michael@0: cursor.update(cursor.value); michael@0: last = cursor.value; michael@0: cursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: // Recalculate the total counter for last / current sample michael@0: cursor.value.rxTotalBytes = last.rxTotalBytes + cursor.value.rxBytes; michael@0: cursor.value.txTotalBytes = last.txTotalBytes + cursor.value.txBytes; michael@0: cursor.update(cursor.value); michael@0: last = cursor.value; michael@0: cursor.continue(); michael@0: } michael@0: } michael@0: }, this); michael@0: }; michael@0: } else if (currVersion == 7) { michael@0: // Create index for 'ServiceType' in order to make it retrievable. michael@0: let statsStore = aTransaction.objectStore(STATS_STORE_NAME); michael@0: statsStore.createIndex("serviceType", "serviceType", { unique: false }); michael@0: michael@0: if (DEBUG) { michael@0: debug("Create index of 'serviceType' for version 8"); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: importData: function importData(aStats) { michael@0: let stats = { appId: aStats.appId, michael@0: serviceType: aStats.serviceType, michael@0: network: [aStats.networkId, aStats.networkType], michael@0: timestamp: aStats.timestamp, michael@0: rxBytes: aStats.rxBytes, michael@0: txBytes: aStats.txBytes, michael@0: rxSystemBytes: aStats.rxSystemBytes, michael@0: txSystemBytes: aStats.txSystemBytes, michael@0: rxTotalBytes: aStats.rxTotalBytes, michael@0: txTotalBytes: aStats.txTotalBytes }; michael@0: michael@0: return stats; michael@0: }, michael@0: michael@0: exportData: function exportData(aStats) { michael@0: let stats = { appId: aStats.appId, michael@0: serviceType: aStats.serviceType, michael@0: networkId: aStats.network[0], michael@0: networkType: aStats.network[1], michael@0: timestamp: aStats.timestamp, michael@0: rxBytes: aStats.rxBytes, michael@0: txBytes: aStats.txBytes, michael@0: rxTotalBytes: aStats.rxTotalBytes, michael@0: txTotalBytes: aStats.txTotalBytes }; michael@0: michael@0: return stats; michael@0: }, michael@0: michael@0: normalizeDate: function normalizeDate(aDate) { michael@0: // Convert to UTC according to timezone and michael@0: // filter timestamp to get SAMPLE_RATE precission michael@0: let timestamp = aDate.getTime() - aDate.getTimezoneOffset() * 60 * 1000; michael@0: timestamp = Math.floor(timestamp / SAMPLE_RATE) * SAMPLE_RATE; michael@0: return timestamp; michael@0: }, michael@0: michael@0: saveStats: function saveStats(aStats, aResultCb) { michael@0: let isAccumulative = aStats.isAccumulative; michael@0: let timestamp = this.normalizeDate(aStats.date); michael@0: michael@0: let stats = { appId: aStats.appId, michael@0: serviceType: aStats.serviceType, michael@0: networkId: aStats.networkId, michael@0: networkType: aStats.networkType, michael@0: timestamp: timestamp, michael@0: rxBytes: (isAccumulative) ? 0 : aStats.rxBytes, michael@0: txBytes: (isAccumulative) ? 0 : aStats.txBytes, michael@0: rxSystemBytes: (isAccumulative) ? aStats.rxBytes : 0, michael@0: txSystemBytes: (isAccumulative) ? aStats.txBytes : 0, michael@0: rxTotalBytes: (isAccumulative) ? aStats.rxBytes : 0, michael@0: txTotalBytes: (isAccumulative) ? aStats.txBytes : 0 }; michael@0: michael@0: stats = this.importData(stats); michael@0: michael@0: this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) { michael@0: if (DEBUG) { michael@0: debug("Filtered time: " + new Date(timestamp)); michael@0: debug("New stats: " + JSON.stringify(stats)); michael@0: } michael@0: michael@0: let request = aStore.index("network").openCursor(stats.network, "prev"); michael@0: request.onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: // Empty, so save first element. michael@0: michael@0: // There could be a time delay between the point when the network michael@0: // interface comes up and the point when the database is initialized. michael@0: // In this short interval some traffic data are generated but are not michael@0: // registered by the first sample. michael@0: if (isAccumulative) { michael@0: stats.rxBytes = stats.rxTotalBytes; michael@0: stats.txBytes = stats.txTotalBytes; michael@0: } michael@0: michael@0: this._saveStats(aTxn, aStore, stats); michael@0: return; michael@0: } michael@0: michael@0: let value = cursor.value; michael@0: if (stats.appId != value.appId || michael@0: (stats.appId == 0 && stats.serviceType != value.serviceType)) { michael@0: cursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: // There are old samples michael@0: if (DEBUG) { michael@0: debug("Last value " + JSON.stringify(value)); michael@0: } michael@0: michael@0: // Remove stats previous to now - VALUE_MAX_LENGTH michael@0: this._removeOldStats(aTxn, aStore, stats.appId, stats.serviceType, michael@0: stats.network, stats.timestamp); michael@0: michael@0: // Process stats before save michael@0: this._processSamplesDiff(aTxn, aStore, cursor, stats, isAccumulative); michael@0: }.bind(this); michael@0: }.bind(this), aResultCb); michael@0: }, michael@0: michael@0: /* michael@0: * This function check that stats are saved in the database following the sample rate. michael@0: * In this way is easier to find elements when stats are requested. michael@0: */ michael@0: _processSamplesDiff: function _processSamplesDiff(aTxn, michael@0: aStore, michael@0: aLastSampleCursor, michael@0: aNewSample, michael@0: aIsAccumulative) { michael@0: let lastSample = aLastSampleCursor.value; michael@0: michael@0: // Get difference between last and new sample. michael@0: let diff = (aNewSample.timestamp - lastSample.timestamp) / SAMPLE_RATE; michael@0: if (diff % 1) { michael@0: // diff is decimal, so some error happened because samples are stored as a multiple michael@0: // of SAMPLE_RATE michael@0: aTxn.abort(); michael@0: throw new Error("Error processing samples"); michael@0: } michael@0: michael@0: if (DEBUG) { michael@0: debug("New: " + aNewSample.timestamp + " - Last: " + michael@0: lastSample.timestamp + " - diff: " + diff); michael@0: } michael@0: michael@0: // If the incoming data has a accumulation feature, the new michael@0: // |txBytes|/|rxBytes| is assigend by differnces between the new michael@0: // |txTotalBytes|/|rxTotalBytes| and the last |txTotalBytes|/|rxTotalBytes|. michael@0: // Else, if incoming data is non-accumulative, the |txBytes|/|rxBytes| michael@0: // is the new |txBytes|/|rxBytes|. michael@0: let rxDiff = 0; michael@0: let txDiff = 0; michael@0: if (aIsAccumulative) { michael@0: rxDiff = aNewSample.rxSystemBytes - lastSample.rxSystemBytes; michael@0: txDiff = aNewSample.txSystemBytes - lastSample.txSystemBytes; michael@0: if (rxDiff < 0 || txDiff < 0) { michael@0: rxDiff = aNewSample.rxSystemBytes; michael@0: txDiff = aNewSample.txSystemBytes; michael@0: } michael@0: aNewSample.rxBytes = rxDiff; michael@0: aNewSample.txBytes = txDiff; michael@0: michael@0: aNewSample.rxTotalBytes = lastSample.rxTotalBytes + rxDiff; michael@0: aNewSample.txTotalBytes = lastSample.txTotalBytes + txDiff; michael@0: } else { michael@0: rxDiff = aNewSample.rxBytes; michael@0: txDiff = aNewSample.txBytes; michael@0: } michael@0: michael@0: if (diff == 1) { michael@0: // New element. michael@0: michael@0: // If the incoming data is non-accumulative, the new michael@0: // |rxTotalBytes|/|txTotalBytes| needs to be updated by adding new michael@0: // |rxBytes|/|txBytes| to the last |rxTotalBytes|/|txTotalBytes|. michael@0: if (!aIsAccumulative) { michael@0: aNewSample.rxTotalBytes = aNewSample.rxBytes + lastSample.rxTotalBytes; michael@0: aNewSample.txTotalBytes = aNewSample.txBytes + lastSample.txTotalBytes; michael@0: } michael@0: michael@0: this._saveStats(aTxn, aStore, aNewSample); michael@0: return; michael@0: } michael@0: if (diff > 1) { michael@0: // Some samples lost. Device off during one or more samplerate periods. michael@0: // Time or timezone changed michael@0: // Add lost samples with 0 bytes and the actual one. michael@0: if (diff > VALUES_MAX_LENGTH) { michael@0: diff = VALUES_MAX_LENGTH; michael@0: } michael@0: michael@0: let data = []; michael@0: for (let i = diff - 2; i >= 0; i--) { michael@0: let time = aNewSample.timestamp - SAMPLE_RATE * (i + 1); michael@0: let sample = { appId: aNewSample.appId, michael@0: serviceType: aNewSample.serviceType, michael@0: network: aNewSample.network, michael@0: timestamp: time, michael@0: rxBytes: 0, michael@0: txBytes: 0, michael@0: rxSystemBytes: lastSample.rxSystemBytes, michael@0: txSystemBytes: lastSample.txSystemBytes, michael@0: rxTotalBytes: lastSample.rxTotalBytes, michael@0: txTotalBytes: lastSample.txTotalBytes }; michael@0: michael@0: data.push(sample); michael@0: } michael@0: michael@0: data.push(aNewSample); michael@0: this._saveStats(aTxn, aStore, data); michael@0: return; michael@0: } michael@0: if (diff == 0 || diff < 0) { michael@0: // New element received before samplerate period. It means that device has michael@0: // been restarted (or clock / timezone change). michael@0: // Update element. If diff < 0, clock or timezone changed back. Place data michael@0: // in the last sample. michael@0: michael@0: // Old |rxTotalBytes|/|txTotalBytes| needs to get updated by adding the michael@0: // last |rxTotalBytes|/|txTotalBytes|. michael@0: lastSample.rxBytes += rxDiff; michael@0: lastSample.txBytes += txDiff; michael@0: lastSample.rxSystemBytes = aNewSample.rxSystemBytes; michael@0: lastSample.txSystemBytes = aNewSample.txSystemBytes; michael@0: lastSample.rxTotalBytes += rxDiff; michael@0: lastSample.txTotalBytes += txDiff; michael@0: michael@0: if (DEBUG) { michael@0: debug("Update: " + JSON.stringify(lastSample)); michael@0: } michael@0: let req = aLastSampleCursor.update(lastSample); michael@0: } michael@0: }, michael@0: michael@0: _saveStats: function _saveStats(aTxn, aStore, aNetworkStats) { michael@0: if (DEBUG) { michael@0: debug("_saveStats: " + JSON.stringify(aNetworkStats)); michael@0: } michael@0: michael@0: if (Array.isArray(aNetworkStats)) { michael@0: let len = aNetworkStats.length - 1; michael@0: for (let i = 0; i <= len; i++) { michael@0: aStore.put(aNetworkStats[i]); michael@0: } michael@0: } else { michael@0: aStore.put(aNetworkStats); michael@0: } michael@0: }, michael@0: michael@0: _removeOldStats: function _removeOldStats(aTxn, aStore, aAppId, aServiceType, michael@0: aNetwork, aDate) { michael@0: // Callback function to remove old items when new ones are added. michael@0: let filterDate = aDate - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1); michael@0: let lowerFilter = [aAppId, aServiceType, aNetwork, 0]; michael@0: let upperFilter = [aAppId, aServiceType, aNetwork, filterDate]; michael@0: let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); michael@0: let lastSample = null; michael@0: let self = this; michael@0: michael@0: aStore.openCursor(range).onsuccess = function(event) { michael@0: var cursor = event.target.result; michael@0: if (cursor) { michael@0: lastSample = cursor.value; michael@0: cursor.delete(); michael@0: cursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: // If all samples for a network are removed, an empty sample michael@0: // has to be saved to keep the totalBytes in order to compute michael@0: // future samples because system counters are not set to 0. michael@0: // Thus, if there are no samples left, the last sample removed michael@0: // will be saved again after setting its bytes to 0. michael@0: let request = aStore.index("network").openCursor(aNetwork); michael@0: request.onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor && lastSample != null) { michael@0: let timestamp = new Date(); michael@0: timestamp = self.normalizeDate(timestamp); michael@0: lastSample.timestamp = timestamp; michael@0: lastSample.rxBytes = 0; michael@0: lastSample.txBytes = 0; michael@0: self._saveStats(aTxn, aStore, lastSample); michael@0: } michael@0: }; michael@0: }; michael@0: }, michael@0: michael@0: clearInterfaceStats: function clearInterfaceStats(aNetwork, aResultCb) { michael@0: let network = [aNetwork.network.id, aNetwork.network.type]; michael@0: let self = this; michael@0: michael@0: // Clear and save an empty sample to keep sync with system counters michael@0: this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) { michael@0: let sample = null; michael@0: let request = aStore.index("network").openCursor(network, "prev"); michael@0: request.onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: if (!sample && cursor.value.appId == 0) { michael@0: sample = cursor.value; michael@0: } michael@0: michael@0: cursor.delete(); michael@0: cursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: if (sample) { michael@0: let timestamp = new Date(); michael@0: timestamp = self.normalizeDate(timestamp); michael@0: sample.timestamp = timestamp; michael@0: sample.appId = 0; michael@0: sample.serviceType = ""; michael@0: sample.rxBytes = 0; michael@0: sample.txBytes = 0; michael@0: sample.rxTotalBytes = 0; michael@0: sample.txTotalBytes = 0; michael@0: michael@0: self._saveStats(aTxn, aStore, sample); michael@0: } michael@0: }; michael@0: }, this._resetAlarms.bind(this, aNetwork.networkId, aResultCb)); michael@0: }, michael@0: michael@0: clearStats: function clearStats(aNetworks, aResultCb) { michael@0: let index = 0; michael@0: let stats = []; michael@0: let self = this; michael@0: michael@0: let callback = function(aError, aResult) { michael@0: index++; michael@0: michael@0: if (!aError && index < aNetworks.length) { michael@0: self.clearInterfaceStats(aNetworks[index], callback); michael@0: return; michael@0: } michael@0: michael@0: aResultCb(aError, aResult); michael@0: }; michael@0: michael@0: if (!aNetworks[index]) { michael@0: aResultCb(null, true); michael@0: return; michael@0: } michael@0: this.clearInterfaceStats(aNetworks[index], callback); michael@0: }, michael@0: michael@0: getCurrentStats: function getCurrentStats(aNetwork, aDate, aResultCb) { michael@0: if (DEBUG) { michael@0: debug("Get current stats for " + JSON.stringify(aNetwork) + " since " + aDate); michael@0: } michael@0: michael@0: let network = [aNetwork.id, aNetwork.type]; michael@0: if (aDate) { michael@0: this._getCurrentStatsFromDate(network, aDate, aResultCb); michael@0: return; michael@0: } michael@0: michael@0: this._getCurrentStats(network, aResultCb); michael@0: }, michael@0: michael@0: _getCurrentStats: function _getCurrentStats(aNetwork, aResultCb) { michael@0: this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) { michael@0: let request = null; michael@0: let upperFilter = [0, "", aNetwork, Date.now()]; michael@0: let range = IDBKeyRange.upperBound(upperFilter, false); michael@0: request = store.openCursor(range, "prev"); michael@0: michael@0: let result = { rxBytes: 0, txBytes: 0, michael@0: rxTotalBytes: 0, txTotalBytes: 0 }; michael@0: michael@0: request.onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes; michael@0: result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes; michael@0: } michael@0: michael@0: txn.result = result; michael@0: }; michael@0: }.bind(this), aResultCb); michael@0: }, michael@0: michael@0: _getCurrentStatsFromDate: function _getCurrentStatsFromDate(aNetwork, aDate, aResultCb) { michael@0: aDate = new Date(aDate); michael@0: this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) { michael@0: let request = null; michael@0: let start = this.normalizeDate(aDate); michael@0: let lowerFilter = [0, "", aNetwork, start]; michael@0: let upperFilter = [0, "", aNetwork, Date.now()]; michael@0: michael@0: let range = IDBKeyRange.upperBound(upperFilter, false); michael@0: michael@0: let result = { rxBytes: 0, txBytes: 0, michael@0: rxTotalBytes: 0, txTotalBytes: 0 }; michael@0: michael@0: request = store.openCursor(range, "prev"); michael@0: michael@0: request.onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes; michael@0: result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes; michael@0: } michael@0: michael@0: let timestamp = cursor.value.timestamp; michael@0: let range = IDBKeyRange.lowerBound(lowerFilter, false); michael@0: request = store.openCursor(range); michael@0: michael@0: request.onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: if (cursor.value.timestamp == timestamp) { michael@0: // There is one sample only. michael@0: result.rxBytes = cursor.value.rxBytes; michael@0: result.txBytes = cursor.value.txBytes; michael@0: } else { michael@0: result.rxBytes -= cursor.value.rxTotalBytes; michael@0: result.txBytes -= cursor.value.txTotalBytes; michael@0: } michael@0: } michael@0: michael@0: txn.result = result; michael@0: }; michael@0: }; michael@0: }.bind(this), aResultCb); michael@0: }, michael@0: michael@0: find: function find(aResultCb, aAppId, aServiceType, aNetwork, michael@0: aStart, aEnd, aAppManifestURL) { michael@0: let offset = (new Date()).getTimezoneOffset() * 60 * 1000; michael@0: let start = this.normalizeDate(aStart); michael@0: let end = this.normalizeDate(aEnd); michael@0: michael@0: if (DEBUG) { michael@0: debug("Find samples for appId: " + aAppId + " serviceType: " + michael@0: aServiceType + " network: " + JSON.stringify(aNetwork) + " from " + michael@0: start + " until " + end); michael@0: debug("Start time: " + new Date(start)); michael@0: debug("End time: " + new Date(end)); michael@0: } michael@0: michael@0: this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { michael@0: let network = [aNetwork.id, aNetwork.type]; michael@0: let lowerFilter = [aAppId, aServiceType, network, start]; michael@0: let upperFilter = [aAppId, aServiceType, network, end]; michael@0: let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); michael@0: michael@0: let data = []; michael@0: michael@0: if (!aTxn.result) { michael@0: aTxn.result = {}; michael@0: } michael@0: michael@0: let request = aStore.openCursor(range).onsuccess = function(event) { michael@0: var cursor = event.target.result; michael@0: if (cursor){ michael@0: data.push({ rxBytes: cursor.value.rxBytes, michael@0: txBytes: cursor.value.txBytes, michael@0: date: new Date(cursor.value.timestamp + offset) }); michael@0: cursor.continue(); michael@0: return; michael@0: } michael@0: michael@0: // When requested samples (start / end) are not in the range of now and michael@0: // now - VALUES_MAX_LENGTH, fill with empty samples. michael@0: this.fillResultSamples(start + offset, end + offset, data); michael@0: michael@0: aTxn.result.appManifestURL = aAppManifestURL; michael@0: aTxn.result.serviceType = aServiceType; michael@0: aTxn.result.network = aNetwork; michael@0: aTxn.result.start = aStart; michael@0: aTxn.result.end = aEnd; michael@0: aTxn.result.data = data; michael@0: }.bind(this); michael@0: }.bind(this), aResultCb); michael@0: }, michael@0: michael@0: /* michael@0: * Fill data array (samples from database) with empty samples to match michael@0: * requested start / end dates. michael@0: */ michael@0: fillResultSamples: function fillResultSamples(aStart, aEnd, aData) { michael@0: if (aData.length == 0) { michael@0: aData.push({ rxBytes: undefined, michael@0: txBytes: undefined, michael@0: date: new Date(aStart) }); michael@0: } michael@0: michael@0: while (aStart < aData[0].date.getTime()) { michael@0: aData.unshift({ rxBytes: undefined, michael@0: txBytes: undefined, michael@0: date: new Date(aData[0].date.getTime() - SAMPLE_RATE) }); michael@0: } michael@0: michael@0: while (aEnd > aData[aData.length - 1].date.getTime()) { michael@0: aData.push({ rxBytes: undefined, michael@0: txBytes: undefined, michael@0: date: new Date(aData[aData.length - 1].date.getTime() + SAMPLE_RATE) }); michael@0: } michael@0: }, michael@0: michael@0: getAvailableNetworks: function getAvailableNetworks(aResultCb) { michael@0: this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { michael@0: if (!aTxn.result) { michael@0: aTxn.result = []; michael@0: } michael@0: michael@0: let request = aStore.index("network").openKeyCursor(null, "nextunique"); michael@0: request.onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: aTxn.result.push({ id: cursor.key[0], michael@0: type: cursor.key[1] }); michael@0: cursor.continue(); michael@0: return; michael@0: } michael@0: }; michael@0: }, aResultCb); michael@0: }, michael@0: michael@0: isNetworkAvailable: function isNetworkAvailable(aNetwork, aResultCb) { michael@0: this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { michael@0: if (!aTxn.result) { michael@0: aTxn.result = false; michael@0: } michael@0: michael@0: let network = [aNetwork.id, aNetwork.type]; michael@0: let request = aStore.index("network").openKeyCursor(IDBKeyRange.only(network)); michael@0: request.onsuccess = function onsuccess(event) { michael@0: if (event.target.result) { michael@0: aTxn.result = true; michael@0: } michael@0: }; michael@0: }, aResultCb); michael@0: }, michael@0: michael@0: getAvailableServiceTypes: function getAvailableServiceTypes(aResultCb) { michael@0: this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { michael@0: if (!aTxn.result) { michael@0: aTxn.result = []; michael@0: } michael@0: michael@0: let request = aStore.index("serviceType").openKeyCursor(null, "nextunique"); michael@0: request.onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor && cursor.key != "") { michael@0: aTxn.result.push({ serviceType: cursor.key }); michael@0: cursor.continue(); michael@0: return; michael@0: } michael@0: }; michael@0: }, aResultCb); michael@0: }, michael@0: michael@0: get sampleRate () { michael@0: return SAMPLE_RATE; michael@0: }, michael@0: michael@0: get maxStorageSamples () { michael@0: return VALUES_MAX_LENGTH; michael@0: }, michael@0: michael@0: logAllRecords: function logAllRecords(aResultCb) { michael@0: this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { michael@0: aStore.mozGetAll().onsuccess = function onsuccess(event) { michael@0: aTxn.result = event.target.result; michael@0: }; michael@0: }, aResultCb); michael@0: }, michael@0: michael@0: alarmToRecord: function alarmToRecord(aAlarm) { michael@0: let record = { networkId: aAlarm.networkId, michael@0: absoluteThreshold: aAlarm.absoluteThreshold, michael@0: relativeThreshold: aAlarm.relativeThreshold, michael@0: startTime: aAlarm.startTime, michael@0: data: aAlarm.data, michael@0: manifestURL: aAlarm.manifestURL, michael@0: pageURL: aAlarm.pageURL }; michael@0: michael@0: if (aAlarm.id) { michael@0: record.id = aAlarm.id; michael@0: } michael@0: michael@0: return record; michael@0: }, michael@0: michael@0: recordToAlarm: function recordToalarm(aRecord) { michael@0: let alarm = { networkId: aRecord.networkId, michael@0: absoluteThreshold: aRecord.absoluteThreshold, michael@0: relativeThreshold: aRecord.relativeThreshold, michael@0: startTime: aRecord.startTime, michael@0: data: aRecord.data, michael@0: manifestURL: aRecord.manifestURL, michael@0: pageURL: aRecord.pageURL }; michael@0: michael@0: if (aRecord.id) { michael@0: alarm.id = aRecord.id; michael@0: } michael@0: michael@0: return alarm; michael@0: }, michael@0: michael@0: addAlarm: function addAlarm(aAlarm, aResultCb) { michael@0: this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { michael@0: if (DEBUG) { michael@0: debug("Going to add " + JSON.stringify(aAlarm)); michael@0: } michael@0: michael@0: let record = this.alarmToRecord(aAlarm); michael@0: store.put(record).onsuccess = function setResult(aEvent) { michael@0: txn.result = aEvent.target.result; michael@0: if (DEBUG) { michael@0: debug("Request successful. New record ID: " + txn.result); michael@0: } michael@0: }; michael@0: }.bind(this), aResultCb); michael@0: }, michael@0: michael@0: getFirstAlarm: function getFirstAlarm(aNetworkId, aResultCb) { michael@0: let self = this; michael@0: michael@0: this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) { michael@0: if (DEBUG) { michael@0: debug("Get first alarm for network " + aNetworkId); michael@0: } michael@0: michael@0: let lowerFilter = [aNetworkId, 0]; michael@0: let upperFilter = [aNetworkId, ""]; michael@0: let range = IDBKeyRange.bound(lowerFilter, upperFilter); michael@0: michael@0: store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: txn.result = null; michael@0: if (cursor) { michael@0: txn.result = self.recordToAlarm(cursor.value); michael@0: } michael@0: }; michael@0: }, aResultCb); michael@0: }, michael@0: michael@0: removeAlarm: function removeAlarm(aAlarmId, aManifestURL, aResultCb) { michael@0: this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { michael@0: if (DEBUG) { michael@0: debug("Remove alarm " + aAlarmId); michael@0: } michael@0: michael@0: store.get(aAlarmId).onsuccess = function onsuccess(event) { michael@0: let record = event.target.result; michael@0: txn.result = false; michael@0: if (!record || (aManifestURL && record.manifestURL != aManifestURL)) { michael@0: return; michael@0: } michael@0: michael@0: store.delete(aAlarmId); michael@0: txn.result = true; michael@0: } michael@0: }, aResultCb); michael@0: }, michael@0: michael@0: removeAlarms: function removeAlarms(aManifestURL, aResultCb) { michael@0: this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { michael@0: if (DEBUG) { michael@0: debug("Remove alarms of " + aManifestURL); michael@0: } michael@0: michael@0: store.index("manifestURL").openCursor(aManifestURL) michael@0: .onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: cursor.delete(); michael@0: cursor.continue(); michael@0: } michael@0: } michael@0: }, aResultCb); michael@0: }, michael@0: michael@0: updateAlarm: function updateAlarm(aAlarm, aResultCb) { michael@0: let self = this; michael@0: this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { michael@0: if (DEBUG) { michael@0: debug("Update alarm " + aAlarm.id); michael@0: } michael@0: michael@0: let record = self.alarmToRecord(aAlarm); michael@0: store.openCursor(record.id).onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: txn.result = false; michael@0: if (cursor) { michael@0: cursor.update(record); michael@0: txn.result = true; michael@0: } michael@0: } michael@0: }, aResultCb); michael@0: }, michael@0: michael@0: getAlarms: function getAlarms(aNetworkId, aManifestURL, aResultCb) { michael@0: let self = this; michael@0: this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) { michael@0: if (DEBUG) { michael@0: debug("Get alarms for " + aManifestURL); michael@0: } michael@0: michael@0: txn.result = []; michael@0: store.index("manifestURL").openCursor(aManifestURL) michael@0: .onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: if (!cursor) { michael@0: return; michael@0: } michael@0: michael@0: if (!aNetworkId || cursor.value.networkId == aNetworkId) { michael@0: txn.result.push(self.recordToAlarm(cursor.value)); michael@0: } michael@0: michael@0: cursor.continue(); michael@0: } michael@0: }, aResultCb); michael@0: }, michael@0: michael@0: _resetAlarms: function _resetAlarms(aNetworkId, aResultCb) { michael@0: this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { michael@0: if (DEBUG) { michael@0: debug("Reset alarms for network " + aNetworkId); michael@0: } michael@0: michael@0: let lowerFilter = [aNetworkId, 0]; michael@0: let upperFilter = [aNetworkId, ""]; michael@0: let range = IDBKeyRange.bound(lowerFilter, upperFilter); michael@0: michael@0: store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: if (cursor.value.startTime) { michael@0: cursor.value.relativeThreshold = cursor.value.threshold; michael@0: cursor.update(cursor.value); michael@0: } michael@0: cursor.continue(); michael@0: return; michael@0: } michael@0: }; michael@0: }, aResultCb); michael@0: } michael@0: };