1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/network/src/NetworkStatsDB.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1050 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ['NetworkStatsDB']; 1.11 + 1.12 +const DEBUG = false; 1.13 +function debug(s) { dump("-*- NetworkStatsDB: " + s + "\n"); } 1.14 + 1.15 +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; 1.16 + 1.17 +Cu.import("resource://gre/modules/Services.jsm"); 1.18 +Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); 1.19 +Cu.importGlobalProperties(["indexedDB"]); 1.20 + 1.21 +const DB_NAME = "net_stats"; 1.22 +const DB_VERSION = 8; 1.23 +const DEPRECATED_STORE_NAME = "net_stats"; 1.24 +const STATS_STORE_NAME = "net_stats_store"; 1.25 +const ALARMS_STORE_NAME = "net_alarm"; 1.26 + 1.27 +// Constant defining the maximum values allowed per interface. If more, older 1.28 +// will be erased. 1.29 +const VALUES_MAX_LENGTH = 6 * 30; 1.30 + 1.31 +// Constant defining the rate of the samples. Daily. 1.32 +const SAMPLE_RATE = 1000 * 60 * 60 * 24; 1.33 + 1.34 +this.NetworkStatsDB = function NetworkStatsDB() { 1.35 + if (DEBUG) { 1.36 + debug("Constructor"); 1.37 + } 1.38 + this.initDBHelper(DB_NAME, DB_VERSION, [STATS_STORE_NAME, ALARMS_STORE_NAME]); 1.39 +} 1.40 + 1.41 +NetworkStatsDB.prototype = { 1.42 + __proto__: IndexedDBHelper.prototype, 1.43 + 1.44 + dbNewTxn: function dbNewTxn(store_name, txn_type, callback, txnCb) { 1.45 + function successCb(result) { 1.46 + txnCb(null, result); 1.47 + } 1.48 + function errorCb(error) { 1.49 + txnCb(error, null); 1.50 + } 1.51 + return this.newTxn(txn_type, store_name, callback, successCb, errorCb); 1.52 + }, 1.53 + 1.54 + upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) { 1.55 + if (DEBUG) { 1.56 + debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!"); 1.57 + } 1.58 + let db = aDb; 1.59 + let objectStore; 1.60 + for (let currVersion = aOldVersion; currVersion < aNewVersion; currVersion++) { 1.61 + if (currVersion == 0) { 1.62 + /** 1.63 + * Create the initial database schema. 1.64 + */ 1.65 + 1.66 + objectStore = db.createObjectStore(DEPRECATED_STORE_NAME, { keyPath: ["connectionType", "timestamp"] }); 1.67 + objectStore.createIndex("connectionType", "connectionType", { unique: false }); 1.68 + objectStore.createIndex("timestamp", "timestamp", { unique: false }); 1.69 + objectStore.createIndex("rxBytes", "rxBytes", { unique: false }); 1.70 + objectStore.createIndex("txBytes", "txBytes", { unique: false }); 1.71 + objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); 1.72 + objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); 1.73 + if (DEBUG) { 1.74 + debug("Created object stores and indexes"); 1.75 + } 1.76 + } else if (currVersion == 2) { 1.77 + // In order to support per-app traffic data storage, the original 1.78 + // objectStore needs to be replaced by a new objectStore with new 1.79 + // key path ("appId") and new index ("appId"). 1.80 + // Also, since now networks are identified by their 1.81 + // [networkId, networkType] not just by their connectionType, 1.82 + // to modify the keyPath is mandatory to delete the object store 1.83 + // and create it again. Old data is going to be deleted because the 1.84 + // networkId for each sample can not be set. 1.85 + 1.86 + // In version 1.2 objectStore name was 'net_stats_v2', to avoid errors when 1.87 + // upgrading from 1.2 to 1.3 objectStore name should be checked. 1.88 + let stores = db.objectStoreNames; 1.89 + if(stores.contains("net_stats_v2")) { 1.90 + db.deleteObjectStore("net_stats_v2"); 1.91 + } else { 1.92 + db.deleteObjectStore(DEPRECATED_STORE_NAME); 1.93 + } 1.94 + 1.95 + objectStore = db.createObjectStore(DEPRECATED_STORE_NAME, { keyPath: ["appId", "network", "timestamp"] }); 1.96 + objectStore.createIndex("appId", "appId", { unique: false }); 1.97 + objectStore.createIndex("network", "network", { unique: false }); 1.98 + objectStore.createIndex("networkType", "networkType", { unique: false }); 1.99 + objectStore.createIndex("timestamp", "timestamp", { unique: false }); 1.100 + objectStore.createIndex("rxBytes", "rxBytes", { unique: false }); 1.101 + objectStore.createIndex("txBytes", "txBytes", { unique: false }); 1.102 + objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); 1.103 + objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); 1.104 + 1.105 + if (DEBUG) { 1.106 + debug("Created object stores and indexes for version 3"); 1.107 + } 1.108 + } else if (currVersion == 3) { 1.109 + // Delete redundent indexes (leave "network" only). 1.110 + objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME); 1.111 + if (objectStore.indexNames.contains("appId")) { 1.112 + objectStore.deleteIndex("appId"); 1.113 + } 1.114 + if (objectStore.indexNames.contains("networkType")) { 1.115 + objectStore.deleteIndex("networkType"); 1.116 + } 1.117 + if (objectStore.indexNames.contains("timestamp")) { 1.118 + objectStore.deleteIndex("timestamp"); 1.119 + } 1.120 + if (objectStore.indexNames.contains("rxBytes")) { 1.121 + objectStore.deleteIndex("rxBytes"); 1.122 + } 1.123 + if (objectStore.indexNames.contains("txBytes")) { 1.124 + objectStore.deleteIndex("txBytes"); 1.125 + } 1.126 + if (objectStore.indexNames.contains("rxTotalBytes")) { 1.127 + objectStore.deleteIndex("rxTotalBytes"); 1.128 + } 1.129 + if (objectStore.indexNames.contains("txTotalBytes")) { 1.130 + objectStore.deleteIndex("txTotalBytes"); 1.131 + } 1.132 + 1.133 + if (DEBUG) { 1.134 + debug("Deleted redundent indexes for version 4"); 1.135 + } 1.136 + } else if (currVersion == 4) { 1.137 + // In order to manage alarms, it is necessary to use a global counter 1.138 + // (totalBytes) that will increase regardless of the system reboot. 1.139 + objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME); 1.140 + 1.141 + // Now, systemBytes will hold the old totalBytes and totalBytes will 1.142 + // keep the increasing counter. |counters| will keep the track of 1.143 + // accumulated values. 1.144 + let counters = {}; 1.145 + 1.146 + objectStore.openCursor().onsuccess = function(event) { 1.147 + let cursor = event.target.result; 1.148 + if (!cursor){ 1.149 + return; 1.150 + } 1.151 + 1.152 + cursor.value.rxSystemBytes = cursor.value.rxTotalBytes; 1.153 + cursor.value.txSystemBytes = cursor.value.txTotalBytes; 1.154 + 1.155 + if (cursor.value.appId == 0) { 1.156 + let netId = cursor.value.network[0] + '' + cursor.value.network[1]; 1.157 + if (!counters[netId]) { 1.158 + counters[netId] = { 1.159 + rxCounter: 0, 1.160 + txCounter: 0, 1.161 + lastRx: 0, 1.162 + lastTx: 0 1.163 + }; 1.164 + } 1.165 + 1.166 + let rxDiff = cursor.value.rxSystemBytes - counters[netId].lastRx; 1.167 + let txDiff = cursor.value.txSystemBytes - counters[netId].lastTx; 1.168 + if (rxDiff < 0 || txDiff < 0) { 1.169 + // System reboot between samples, so take the current one. 1.170 + rxDiff = cursor.value.rxSystemBytes; 1.171 + txDiff = cursor.value.txSystemBytes; 1.172 + } 1.173 + 1.174 + counters[netId].rxCounter += rxDiff; 1.175 + counters[netId].txCounter += txDiff; 1.176 + cursor.value.rxTotalBytes = counters[netId].rxCounter; 1.177 + cursor.value.txTotalBytes = counters[netId].txCounter; 1.178 + 1.179 + counters[netId].lastRx = cursor.value.rxSystemBytes; 1.180 + counters[netId].lastTx = cursor.value.txSystemBytes; 1.181 + } else { 1.182 + cursor.value.rxTotalBytes = cursor.value.rxSystemBytes; 1.183 + cursor.value.txTotalBytes = cursor.value.txSystemBytes; 1.184 + } 1.185 + 1.186 + cursor.update(cursor.value); 1.187 + cursor.continue(); 1.188 + }; 1.189 + 1.190 + // Create object store for alarms. 1.191 + objectStore = db.createObjectStore(ALARMS_STORE_NAME, { keyPath: "id", autoIncrement: true }); 1.192 + objectStore.createIndex("alarm", ['networkId','threshold'], { unique: false }); 1.193 + objectStore.createIndex("manifestURL", "manifestURL", { unique: false }); 1.194 + 1.195 + if (DEBUG) { 1.196 + debug("Created alarms store for version 5"); 1.197 + } 1.198 + } else if (currVersion == 5) { 1.199 + // In contrast to "per-app" traffic data, "system-only" traffic data 1.200 + // refers to data which can not be identified by any applications. 1.201 + // To further support "system-only" data storage, the data can be 1.202 + // saved by service type (e.g., Tethering, OTA). Thus it's needed to 1.203 + // have a new key ("serviceType") for the ojectStore. 1.204 + let newObjectStore; 1.205 + newObjectStore = db.createObjectStore(STATS_STORE_NAME, 1.206 + { keyPath: ["appId", "serviceType", "network", "timestamp"] }); 1.207 + newObjectStore.createIndex("network", "network", { unique: false }); 1.208 + 1.209 + // Copy the data from the original objectStore to the new objectStore. 1.210 + objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME); 1.211 + objectStore.openCursor().onsuccess = function(event) { 1.212 + let cursor = event.target.result; 1.213 + if (!cursor) { 1.214 + db.deleteObjectStore(DEPRECATED_STORE_NAME); 1.215 + return; 1.216 + } 1.217 + 1.218 + let newStats = cursor.value; 1.219 + newStats.serviceType = ""; 1.220 + newObjectStore.put(newStats); 1.221 + cursor.continue(); 1.222 + }; 1.223 + 1.224 + if (DEBUG) { 1.225 + debug("Added new key 'serviceType' for version 6"); 1.226 + } 1.227 + } else if (currVersion == 6) { 1.228 + // Replace threshold attribute of alarm index by relativeThreshold in alarms DB. 1.229 + // Now alarms are indexed by relativeThreshold, which is the threshold relative 1.230 + // to current system stats. 1.231 + let alarmsStore = aTransaction.objectStore(ALARMS_STORE_NAME); 1.232 + 1.233 + // Delete "alarm" index. 1.234 + if (alarmsStore.indexNames.contains("alarm")) { 1.235 + alarmsStore.deleteIndex("alarm"); 1.236 + } 1.237 + 1.238 + // Create new "alarm" index. 1.239 + alarmsStore.createIndex("alarm", ['networkId','relativeThreshold'], { unique: false }); 1.240 + 1.241 + // Populate new "alarm" index attributes. 1.242 + alarmsStore.openCursor().onsuccess = function(event) { 1.243 + let cursor = event.target.result; 1.244 + if (!cursor) { 1.245 + return; 1.246 + } 1.247 + 1.248 + cursor.value.relativeThreshold = cursor.value.threshold; 1.249 + cursor.value.absoluteThreshold = cursor.value.threshold; 1.250 + delete cursor.value.threshold; 1.251 + 1.252 + cursor.update(cursor.value); 1.253 + cursor.continue(); 1.254 + } 1.255 + 1.256 + // Previous versions save accumulative totalBytes, increasing althought the system 1.257 + // reboots or resets stats. But is necessary to reset the total counters when reset 1.258 + // through 'clearInterfaceStats'. 1.259 + let statsStore = aTransaction.objectStore(STATS_STORE_NAME); 1.260 + let networks = []; 1.261 + // Find networks stored in the database. 1.262 + statsStore.index("network").openKeyCursor(null, "nextunique").onsuccess = function(event) { 1.263 + let cursor = event.target.result; 1.264 + if (cursor) { 1.265 + networks.push(cursor.key); 1.266 + cursor.continue(); 1.267 + return; 1.268 + } 1.269 + 1.270 + networks.forEach(function(network) { 1.271 + let lowerFilter = [0, "", network, 0]; 1.272 + let upperFilter = [0, "", network, ""]; 1.273 + let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); 1.274 + 1.275 + // Find number of samples for a given network. 1.276 + statsStore.count(range).onsuccess = function(event) { 1.277 + // If there are more samples than the max allowed, there is no way to know 1.278 + // when does reset take place. 1.279 + if (event.target.result >= VALUES_MAX_LENGTH) { 1.280 + return; 1.281 + } 1.282 + 1.283 + let last = null; 1.284 + // Reset detected if the first sample totalCounters are different than bytes 1.285 + // counters. If so, the total counters should be recalculated. 1.286 + statsStore.openCursor(range).onsuccess = function(event) { 1.287 + let cursor = event.target.result; 1.288 + if (!cursor) { 1.289 + return; 1.290 + } 1.291 + if (!last) { 1.292 + if (cursor.value.rxTotalBytes == cursor.value.rxBytes && 1.293 + cursor.value.txTotalBytes == cursor.value.txBytes) { 1.294 + return; 1.295 + } 1.296 + 1.297 + cursor.value.rxTotalBytes = cursor.value.rxBytes; 1.298 + cursor.value.txTotalBytes = cursor.value.txBytes; 1.299 + cursor.update(cursor.value); 1.300 + last = cursor.value; 1.301 + cursor.continue(); 1.302 + return; 1.303 + } 1.304 + 1.305 + // Recalculate the total counter for last / current sample 1.306 + cursor.value.rxTotalBytes = last.rxTotalBytes + cursor.value.rxBytes; 1.307 + cursor.value.txTotalBytes = last.txTotalBytes + cursor.value.txBytes; 1.308 + cursor.update(cursor.value); 1.309 + last = cursor.value; 1.310 + cursor.continue(); 1.311 + } 1.312 + } 1.313 + }, this); 1.314 + }; 1.315 + } else if (currVersion == 7) { 1.316 + // Create index for 'ServiceType' in order to make it retrievable. 1.317 + let statsStore = aTransaction.objectStore(STATS_STORE_NAME); 1.318 + statsStore.createIndex("serviceType", "serviceType", { unique: false }); 1.319 + 1.320 + if (DEBUG) { 1.321 + debug("Create index of 'serviceType' for version 8"); 1.322 + } 1.323 + } 1.324 + } 1.325 + }, 1.326 + 1.327 + importData: function importData(aStats) { 1.328 + let stats = { appId: aStats.appId, 1.329 + serviceType: aStats.serviceType, 1.330 + network: [aStats.networkId, aStats.networkType], 1.331 + timestamp: aStats.timestamp, 1.332 + rxBytes: aStats.rxBytes, 1.333 + txBytes: aStats.txBytes, 1.334 + rxSystemBytes: aStats.rxSystemBytes, 1.335 + txSystemBytes: aStats.txSystemBytes, 1.336 + rxTotalBytes: aStats.rxTotalBytes, 1.337 + txTotalBytes: aStats.txTotalBytes }; 1.338 + 1.339 + return stats; 1.340 + }, 1.341 + 1.342 + exportData: function exportData(aStats) { 1.343 + let stats = { appId: aStats.appId, 1.344 + serviceType: aStats.serviceType, 1.345 + networkId: aStats.network[0], 1.346 + networkType: aStats.network[1], 1.347 + timestamp: aStats.timestamp, 1.348 + rxBytes: aStats.rxBytes, 1.349 + txBytes: aStats.txBytes, 1.350 + rxTotalBytes: aStats.rxTotalBytes, 1.351 + txTotalBytes: aStats.txTotalBytes }; 1.352 + 1.353 + return stats; 1.354 + }, 1.355 + 1.356 + normalizeDate: function normalizeDate(aDate) { 1.357 + // Convert to UTC according to timezone and 1.358 + // filter timestamp to get SAMPLE_RATE precission 1.359 + let timestamp = aDate.getTime() - aDate.getTimezoneOffset() * 60 * 1000; 1.360 + timestamp = Math.floor(timestamp / SAMPLE_RATE) * SAMPLE_RATE; 1.361 + return timestamp; 1.362 + }, 1.363 + 1.364 + saveStats: function saveStats(aStats, aResultCb) { 1.365 + let isAccumulative = aStats.isAccumulative; 1.366 + let timestamp = this.normalizeDate(aStats.date); 1.367 + 1.368 + let stats = { appId: aStats.appId, 1.369 + serviceType: aStats.serviceType, 1.370 + networkId: aStats.networkId, 1.371 + networkType: aStats.networkType, 1.372 + timestamp: timestamp, 1.373 + rxBytes: (isAccumulative) ? 0 : aStats.rxBytes, 1.374 + txBytes: (isAccumulative) ? 0 : aStats.txBytes, 1.375 + rxSystemBytes: (isAccumulative) ? aStats.rxBytes : 0, 1.376 + txSystemBytes: (isAccumulative) ? aStats.txBytes : 0, 1.377 + rxTotalBytes: (isAccumulative) ? aStats.rxBytes : 0, 1.378 + txTotalBytes: (isAccumulative) ? aStats.txBytes : 0 }; 1.379 + 1.380 + stats = this.importData(stats); 1.381 + 1.382 + this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) { 1.383 + if (DEBUG) { 1.384 + debug("Filtered time: " + new Date(timestamp)); 1.385 + debug("New stats: " + JSON.stringify(stats)); 1.386 + } 1.387 + 1.388 + let request = aStore.index("network").openCursor(stats.network, "prev"); 1.389 + request.onsuccess = function onsuccess(event) { 1.390 + let cursor = event.target.result; 1.391 + if (!cursor) { 1.392 + // Empty, so save first element. 1.393 + 1.394 + // There could be a time delay between the point when the network 1.395 + // interface comes up and the point when the database is initialized. 1.396 + // In this short interval some traffic data are generated but are not 1.397 + // registered by the first sample. 1.398 + if (isAccumulative) { 1.399 + stats.rxBytes = stats.rxTotalBytes; 1.400 + stats.txBytes = stats.txTotalBytes; 1.401 + } 1.402 + 1.403 + this._saveStats(aTxn, aStore, stats); 1.404 + return; 1.405 + } 1.406 + 1.407 + let value = cursor.value; 1.408 + if (stats.appId != value.appId || 1.409 + (stats.appId == 0 && stats.serviceType != value.serviceType)) { 1.410 + cursor.continue(); 1.411 + return; 1.412 + } 1.413 + 1.414 + // There are old samples 1.415 + if (DEBUG) { 1.416 + debug("Last value " + JSON.stringify(value)); 1.417 + } 1.418 + 1.419 + // Remove stats previous to now - VALUE_MAX_LENGTH 1.420 + this._removeOldStats(aTxn, aStore, stats.appId, stats.serviceType, 1.421 + stats.network, stats.timestamp); 1.422 + 1.423 + // Process stats before save 1.424 + this._processSamplesDiff(aTxn, aStore, cursor, stats, isAccumulative); 1.425 + }.bind(this); 1.426 + }.bind(this), aResultCb); 1.427 + }, 1.428 + 1.429 + /* 1.430 + * This function check that stats are saved in the database following the sample rate. 1.431 + * In this way is easier to find elements when stats are requested. 1.432 + */ 1.433 + _processSamplesDiff: function _processSamplesDiff(aTxn, 1.434 + aStore, 1.435 + aLastSampleCursor, 1.436 + aNewSample, 1.437 + aIsAccumulative) { 1.438 + let lastSample = aLastSampleCursor.value; 1.439 + 1.440 + // Get difference between last and new sample. 1.441 + let diff = (aNewSample.timestamp - lastSample.timestamp) / SAMPLE_RATE; 1.442 + if (diff % 1) { 1.443 + // diff is decimal, so some error happened because samples are stored as a multiple 1.444 + // of SAMPLE_RATE 1.445 + aTxn.abort(); 1.446 + throw new Error("Error processing samples"); 1.447 + } 1.448 + 1.449 + if (DEBUG) { 1.450 + debug("New: " + aNewSample.timestamp + " - Last: " + 1.451 + lastSample.timestamp + " - diff: " + diff); 1.452 + } 1.453 + 1.454 + // If the incoming data has a accumulation feature, the new 1.455 + // |txBytes|/|rxBytes| is assigend by differnces between the new 1.456 + // |txTotalBytes|/|rxTotalBytes| and the last |txTotalBytes|/|rxTotalBytes|. 1.457 + // Else, if incoming data is non-accumulative, the |txBytes|/|rxBytes| 1.458 + // is the new |txBytes|/|rxBytes|. 1.459 + let rxDiff = 0; 1.460 + let txDiff = 0; 1.461 + if (aIsAccumulative) { 1.462 + rxDiff = aNewSample.rxSystemBytes - lastSample.rxSystemBytes; 1.463 + txDiff = aNewSample.txSystemBytes - lastSample.txSystemBytes; 1.464 + if (rxDiff < 0 || txDiff < 0) { 1.465 + rxDiff = aNewSample.rxSystemBytes; 1.466 + txDiff = aNewSample.txSystemBytes; 1.467 + } 1.468 + aNewSample.rxBytes = rxDiff; 1.469 + aNewSample.txBytes = txDiff; 1.470 + 1.471 + aNewSample.rxTotalBytes = lastSample.rxTotalBytes + rxDiff; 1.472 + aNewSample.txTotalBytes = lastSample.txTotalBytes + txDiff; 1.473 + } else { 1.474 + rxDiff = aNewSample.rxBytes; 1.475 + txDiff = aNewSample.txBytes; 1.476 + } 1.477 + 1.478 + if (diff == 1) { 1.479 + // New element. 1.480 + 1.481 + // If the incoming data is non-accumulative, the new 1.482 + // |rxTotalBytes|/|txTotalBytes| needs to be updated by adding new 1.483 + // |rxBytes|/|txBytes| to the last |rxTotalBytes|/|txTotalBytes|. 1.484 + if (!aIsAccumulative) { 1.485 + aNewSample.rxTotalBytes = aNewSample.rxBytes + lastSample.rxTotalBytes; 1.486 + aNewSample.txTotalBytes = aNewSample.txBytes + lastSample.txTotalBytes; 1.487 + } 1.488 + 1.489 + this._saveStats(aTxn, aStore, aNewSample); 1.490 + return; 1.491 + } 1.492 + if (diff > 1) { 1.493 + // Some samples lost. Device off during one or more samplerate periods. 1.494 + // Time or timezone changed 1.495 + // Add lost samples with 0 bytes and the actual one. 1.496 + if (diff > VALUES_MAX_LENGTH) { 1.497 + diff = VALUES_MAX_LENGTH; 1.498 + } 1.499 + 1.500 + let data = []; 1.501 + for (let i = diff - 2; i >= 0; i--) { 1.502 + let time = aNewSample.timestamp - SAMPLE_RATE * (i + 1); 1.503 + let sample = { appId: aNewSample.appId, 1.504 + serviceType: aNewSample.serviceType, 1.505 + network: aNewSample.network, 1.506 + timestamp: time, 1.507 + rxBytes: 0, 1.508 + txBytes: 0, 1.509 + rxSystemBytes: lastSample.rxSystemBytes, 1.510 + txSystemBytes: lastSample.txSystemBytes, 1.511 + rxTotalBytes: lastSample.rxTotalBytes, 1.512 + txTotalBytes: lastSample.txTotalBytes }; 1.513 + 1.514 + data.push(sample); 1.515 + } 1.516 + 1.517 + data.push(aNewSample); 1.518 + this._saveStats(aTxn, aStore, data); 1.519 + return; 1.520 + } 1.521 + if (diff == 0 || diff < 0) { 1.522 + // New element received before samplerate period. It means that device has 1.523 + // been restarted (or clock / timezone change). 1.524 + // Update element. If diff < 0, clock or timezone changed back. Place data 1.525 + // in the last sample. 1.526 + 1.527 + // Old |rxTotalBytes|/|txTotalBytes| needs to get updated by adding the 1.528 + // last |rxTotalBytes|/|txTotalBytes|. 1.529 + lastSample.rxBytes += rxDiff; 1.530 + lastSample.txBytes += txDiff; 1.531 + lastSample.rxSystemBytes = aNewSample.rxSystemBytes; 1.532 + lastSample.txSystemBytes = aNewSample.txSystemBytes; 1.533 + lastSample.rxTotalBytes += rxDiff; 1.534 + lastSample.txTotalBytes += txDiff; 1.535 + 1.536 + if (DEBUG) { 1.537 + debug("Update: " + JSON.stringify(lastSample)); 1.538 + } 1.539 + let req = aLastSampleCursor.update(lastSample); 1.540 + } 1.541 + }, 1.542 + 1.543 + _saveStats: function _saveStats(aTxn, aStore, aNetworkStats) { 1.544 + if (DEBUG) { 1.545 + debug("_saveStats: " + JSON.stringify(aNetworkStats)); 1.546 + } 1.547 + 1.548 + if (Array.isArray(aNetworkStats)) { 1.549 + let len = aNetworkStats.length - 1; 1.550 + for (let i = 0; i <= len; i++) { 1.551 + aStore.put(aNetworkStats[i]); 1.552 + } 1.553 + } else { 1.554 + aStore.put(aNetworkStats); 1.555 + } 1.556 + }, 1.557 + 1.558 + _removeOldStats: function _removeOldStats(aTxn, aStore, aAppId, aServiceType, 1.559 + aNetwork, aDate) { 1.560 + // Callback function to remove old items when new ones are added. 1.561 + let filterDate = aDate - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1); 1.562 + let lowerFilter = [aAppId, aServiceType, aNetwork, 0]; 1.563 + let upperFilter = [aAppId, aServiceType, aNetwork, filterDate]; 1.564 + let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); 1.565 + let lastSample = null; 1.566 + let self = this; 1.567 + 1.568 + aStore.openCursor(range).onsuccess = function(event) { 1.569 + var cursor = event.target.result; 1.570 + if (cursor) { 1.571 + lastSample = cursor.value; 1.572 + cursor.delete(); 1.573 + cursor.continue(); 1.574 + return; 1.575 + } 1.576 + 1.577 + // If all samples for a network are removed, an empty sample 1.578 + // has to be saved to keep the totalBytes in order to compute 1.579 + // future samples because system counters are not set to 0. 1.580 + // Thus, if there are no samples left, the last sample removed 1.581 + // will be saved again after setting its bytes to 0. 1.582 + let request = aStore.index("network").openCursor(aNetwork); 1.583 + request.onsuccess = function onsuccess(event) { 1.584 + let cursor = event.target.result; 1.585 + if (!cursor && lastSample != null) { 1.586 + let timestamp = new Date(); 1.587 + timestamp = self.normalizeDate(timestamp); 1.588 + lastSample.timestamp = timestamp; 1.589 + lastSample.rxBytes = 0; 1.590 + lastSample.txBytes = 0; 1.591 + self._saveStats(aTxn, aStore, lastSample); 1.592 + } 1.593 + }; 1.594 + }; 1.595 + }, 1.596 + 1.597 + clearInterfaceStats: function clearInterfaceStats(aNetwork, aResultCb) { 1.598 + let network = [aNetwork.network.id, aNetwork.network.type]; 1.599 + let self = this; 1.600 + 1.601 + // Clear and save an empty sample to keep sync with system counters 1.602 + this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) { 1.603 + let sample = null; 1.604 + let request = aStore.index("network").openCursor(network, "prev"); 1.605 + request.onsuccess = function onsuccess(event) { 1.606 + let cursor = event.target.result; 1.607 + if (cursor) { 1.608 + if (!sample && cursor.value.appId == 0) { 1.609 + sample = cursor.value; 1.610 + } 1.611 + 1.612 + cursor.delete(); 1.613 + cursor.continue(); 1.614 + return; 1.615 + } 1.616 + 1.617 + if (sample) { 1.618 + let timestamp = new Date(); 1.619 + timestamp = self.normalizeDate(timestamp); 1.620 + sample.timestamp = timestamp; 1.621 + sample.appId = 0; 1.622 + sample.serviceType = ""; 1.623 + sample.rxBytes = 0; 1.624 + sample.txBytes = 0; 1.625 + sample.rxTotalBytes = 0; 1.626 + sample.txTotalBytes = 0; 1.627 + 1.628 + self._saveStats(aTxn, aStore, sample); 1.629 + } 1.630 + }; 1.631 + }, this._resetAlarms.bind(this, aNetwork.networkId, aResultCb)); 1.632 + }, 1.633 + 1.634 + clearStats: function clearStats(aNetworks, aResultCb) { 1.635 + let index = 0; 1.636 + let stats = []; 1.637 + let self = this; 1.638 + 1.639 + let callback = function(aError, aResult) { 1.640 + index++; 1.641 + 1.642 + if (!aError && index < aNetworks.length) { 1.643 + self.clearInterfaceStats(aNetworks[index], callback); 1.644 + return; 1.645 + } 1.646 + 1.647 + aResultCb(aError, aResult); 1.648 + }; 1.649 + 1.650 + if (!aNetworks[index]) { 1.651 + aResultCb(null, true); 1.652 + return; 1.653 + } 1.654 + this.clearInterfaceStats(aNetworks[index], callback); 1.655 + }, 1.656 + 1.657 + getCurrentStats: function getCurrentStats(aNetwork, aDate, aResultCb) { 1.658 + if (DEBUG) { 1.659 + debug("Get current stats for " + JSON.stringify(aNetwork) + " since " + aDate); 1.660 + } 1.661 + 1.662 + let network = [aNetwork.id, aNetwork.type]; 1.663 + if (aDate) { 1.664 + this._getCurrentStatsFromDate(network, aDate, aResultCb); 1.665 + return; 1.666 + } 1.667 + 1.668 + this._getCurrentStats(network, aResultCb); 1.669 + }, 1.670 + 1.671 + _getCurrentStats: function _getCurrentStats(aNetwork, aResultCb) { 1.672 + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) { 1.673 + let request = null; 1.674 + let upperFilter = [0, "", aNetwork, Date.now()]; 1.675 + let range = IDBKeyRange.upperBound(upperFilter, false); 1.676 + request = store.openCursor(range, "prev"); 1.677 + 1.678 + let result = { rxBytes: 0, txBytes: 0, 1.679 + rxTotalBytes: 0, txTotalBytes: 0 }; 1.680 + 1.681 + request.onsuccess = function onsuccess(event) { 1.682 + let cursor = event.target.result; 1.683 + if (cursor) { 1.684 + result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes; 1.685 + result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes; 1.686 + } 1.687 + 1.688 + txn.result = result; 1.689 + }; 1.690 + }.bind(this), aResultCb); 1.691 + }, 1.692 + 1.693 + _getCurrentStatsFromDate: function _getCurrentStatsFromDate(aNetwork, aDate, aResultCb) { 1.694 + aDate = new Date(aDate); 1.695 + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) { 1.696 + let request = null; 1.697 + let start = this.normalizeDate(aDate); 1.698 + let lowerFilter = [0, "", aNetwork, start]; 1.699 + let upperFilter = [0, "", aNetwork, Date.now()]; 1.700 + 1.701 + let range = IDBKeyRange.upperBound(upperFilter, false); 1.702 + 1.703 + let result = { rxBytes: 0, txBytes: 0, 1.704 + rxTotalBytes: 0, txTotalBytes: 0 }; 1.705 + 1.706 + request = store.openCursor(range, "prev"); 1.707 + 1.708 + request.onsuccess = function onsuccess(event) { 1.709 + let cursor = event.target.result; 1.710 + if (cursor) { 1.711 + result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes; 1.712 + result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes; 1.713 + } 1.714 + 1.715 + let timestamp = cursor.value.timestamp; 1.716 + let range = IDBKeyRange.lowerBound(lowerFilter, false); 1.717 + request = store.openCursor(range); 1.718 + 1.719 + request.onsuccess = function onsuccess(event) { 1.720 + let cursor = event.target.result; 1.721 + if (cursor) { 1.722 + if (cursor.value.timestamp == timestamp) { 1.723 + // There is one sample only. 1.724 + result.rxBytes = cursor.value.rxBytes; 1.725 + result.txBytes = cursor.value.txBytes; 1.726 + } else { 1.727 + result.rxBytes -= cursor.value.rxTotalBytes; 1.728 + result.txBytes -= cursor.value.txTotalBytes; 1.729 + } 1.730 + } 1.731 + 1.732 + txn.result = result; 1.733 + }; 1.734 + }; 1.735 + }.bind(this), aResultCb); 1.736 + }, 1.737 + 1.738 + find: function find(aResultCb, aAppId, aServiceType, aNetwork, 1.739 + aStart, aEnd, aAppManifestURL) { 1.740 + let offset = (new Date()).getTimezoneOffset() * 60 * 1000; 1.741 + let start = this.normalizeDate(aStart); 1.742 + let end = this.normalizeDate(aEnd); 1.743 + 1.744 + if (DEBUG) { 1.745 + debug("Find samples for appId: " + aAppId + " serviceType: " + 1.746 + aServiceType + " network: " + JSON.stringify(aNetwork) + " from " + 1.747 + start + " until " + end); 1.748 + debug("Start time: " + new Date(start)); 1.749 + debug("End time: " + new Date(end)); 1.750 + } 1.751 + 1.752 + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { 1.753 + let network = [aNetwork.id, aNetwork.type]; 1.754 + let lowerFilter = [aAppId, aServiceType, network, start]; 1.755 + let upperFilter = [aAppId, aServiceType, network, end]; 1.756 + let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); 1.757 + 1.758 + let data = []; 1.759 + 1.760 + if (!aTxn.result) { 1.761 + aTxn.result = {}; 1.762 + } 1.763 + 1.764 + let request = aStore.openCursor(range).onsuccess = function(event) { 1.765 + var cursor = event.target.result; 1.766 + if (cursor){ 1.767 + data.push({ rxBytes: cursor.value.rxBytes, 1.768 + txBytes: cursor.value.txBytes, 1.769 + date: new Date(cursor.value.timestamp + offset) }); 1.770 + cursor.continue(); 1.771 + return; 1.772 + } 1.773 + 1.774 + // When requested samples (start / end) are not in the range of now and 1.775 + // now - VALUES_MAX_LENGTH, fill with empty samples. 1.776 + this.fillResultSamples(start + offset, end + offset, data); 1.777 + 1.778 + aTxn.result.appManifestURL = aAppManifestURL; 1.779 + aTxn.result.serviceType = aServiceType; 1.780 + aTxn.result.network = aNetwork; 1.781 + aTxn.result.start = aStart; 1.782 + aTxn.result.end = aEnd; 1.783 + aTxn.result.data = data; 1.784 + }.bind(this); 1.785 + }.bind(this), aResultCb); 1.786 + }, 1.787 + 1.788 + /* 1.789 + * Fill data array (samples from database) with empty samples to match 1.790 + * requested start / end dates. 1.791 + */ 1.792 + fillResultSamples: function fillResultSamples(aStart, aEnd, aData) { 1.793 + if (aData.length == 0) { 1.794 + aData.push({ rxBytes: undefined, 1.795 + txBytes: undefined, 1.796 + date: new Date(aStart) }); 1.797 + } 1.798 + 1.799 + while (aStart < aData[0].date.getTime()) { 1.800 + aData.unshift({ rxBytes: undefined, 1.801 + txBytes: undefined, 1.802 + date: new Date(aData[0].date.getTime() - SAMPLE_RATE) }); 1.803 + } 1.804 + 1.805 + while (aEnd > aData[aData.length - 1].date.getTime()) { 1.806 + aData.push({ rxBytes: undefined, 1.807 + txBytes: undefined, 1.808 + date: new Date(aData[aData.length - 1].date.getTime() + SAMPLE_RATE) }); 1.809 + } 1.810 + }, 1.811 + 1.812 + getAvailableNetworks: function getAvailableNetworks(aResultCb) { 1.813 + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { 1.814 + if (!aTxn.result) { 1.815 + aTxn.result = []; 1.816 + } 1.817 + 1.818 + let request = aStore.index("network").openKeyCursor(null, "nextunique"); 1.819 + request.onsuccess = function onsuccess(event) { 1.820 + let cursor = event.target.result; 1.821 + if (cursor) { 1.822 + aTxn.result.push({ id: cursor.key[0], 1.823 + type: cursor.key[1] }); 1.824 + cursor.continue(); 1.825 + return; 1.826 + } 1.827 + }; 1.828 + }, aResultCb); 1.829 + }, 1.830 + 1.831 + isNetworkAvailable: function isNetworkAvailable(aNetwork, aResultCb) { 1.832 + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { 1.833 + if (!aTxn.result) { 1.834 + aTxn.result = false; 1.835 + } 1.836 + 1.837 + let network = [aNetwork.id, aNetwork.type]; 1.838 + let request = aStore.index("network").openKeyCursor(IDBKeyRange.only(network)); 1.839 + request.onsuccess = function onsuccess(event) { 1.840 + if (event.target.result) { 1.841 + aTxn.result = true; 1.842 + } 1.843 + }; 1.844 + }, aResultCb); 1.845 + }, 1.846 + 1.847 + getAvailableServiceTypes: function getAvailableServiceTypes(aResultCb) { 1.848 + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { 1.849 + if (!aTxn.result) { 1.850 + aTxn.result = []; 1.851 + } 1.852 + 1.853 + let request = aStore.index("serviceType").openKeyCursor(null, "nextunique"); 1.854 + request.onsuccess = function onsuccess(event) { 1.855 + let cursor = event.target.result; 1.856 + if (cursor && cursor.key != "") { 1.857 + aTxn.result.push({ serviceType: cursor.key }); 1.858 + cursor.continue(); 1.859 + return; 1.860 + } 1.861 + }; 1.862 + }, aResultCb); 1.863 + }, 1.864 + 1.865 + get sampleRate () { 1.866 + return SAMPLE_RATE; 1.867 + }, 1.868 + 1.869 + get maxStorageSamples () { 1.870 + return VALUES_MAX_LENGTH; 1.871 + }, 1.872 + 1.873 + logAllRecords: function logAllRecords(aResultCb) { 1.874 + this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { 1.875 + aStore.mozGetAll().onsuccess = function onsuccess(event) { 1.876 + aTxn.result = event.target.result; 1.877 + }; 1.878 + }, aResultCb); 1.879 + }, 1.880 + 1.881 + alarmToRecord: function alarmToRecord(aAlarm) { 1.882 + let record = { networkId: aAlarm.networkId, 1.883 + absoluteThreshold: aAlarm.absoluteThreshold, 1.884 + relativeThreshold: aAlarm.relativeThreshold, 1.885 + startTime: aAlarm.startTime, 1.886 + data: aAlarm.data, 1.887 + manifestURL: aAlarm.manifestURL, 1.888 + pageURL: aAlarm.pageURL }; 1.889 + 1.890 + if (aAlarm.id) { 1.891 + record.id = aAlarm.id; 1.892 + } 1.893 + 1.894 + return record; 1.895 + }, 1.896 + 1.897 + recordToAlarm: function recordToalarm(aRecord) { 1.898 + let alarm = { networkId: aRecord.networkId, 1.899 + absoluteThreshold: aRecord.absoluteThreshold, 1.900 + relativeThreshold: aRecord.relativeThreshold, 1.901 + startTime: aRecord.startTime, 1.902 + data: aRecord.data, 1.903 + manifestURL: aRecord.manifestURL, 1.904 + pageURL: aRecord.pageURL }; 1.905 + 1.906 + if (aRecord.id) { 1.907 + alarm.id = aRecord.id; 1.908 + } 1.909 + 1.910 + return alarm; 1.911 + }, 1.912 + 1.913 + addAlarm: function addAlarm(aAlarm, aResultCb) { 1.914 + this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { 1.915 + if (DEBUG) { 1.916 + debug("Going to add " + JSON.stringify(aAlarm)); 1.917 + } 1.918 + 1.919 + let record = this.alarmToRecord(aAlarm); 1.920 + store.put(record).onsuccess = function setResult(aEvent) { 1.921 + txn.result = aEvent.target.result; 1.922 + if (DEBUG) { 1.923 + debug("Request successful. New record ID: " + txn.result); 1.924 + } 1.925 + }; 1.926 + }.bind(this), aResultCb); 1.927 + }, 1.928 + 1.929 + getFirstAlarm: function getFirstAlarm(aNetworkId, aResultCb) { 1.930 + let self = this; 1.931 + 1.932 + this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) { 1.933 + if (DEBUG) { 1.934 + debug("Get first alarm for network " + aNetworkId); 1.935 + } 1.936 + 1.937 + let lowerFilter = [aNetworkId, 0]; 1.938 + let upperFilter = [aNetworkId, ""]; 1.939 + let range = IDBKeyRange.bound(lowerFilter, upperFilter); 1.940 + 1.941 + store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) { 1.942 + let cursor = event.target.result; 1.943 + txn.result = null; 1.944 + if (cursor) { 1.945 + txn.result = self.recordToAlarm(cursor.value); 1.946 + } 1.947 + }; 1.948 + }, aResultCb); 1.949 + }, 1.950 + 1.951 + removeAlarm: function removeAlarm(aAlarmId, aManifestURL, aResultCb) { 1.952 + this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { 1.953 + if (DEBUG) { 1.954 + debug("Remove alarm " + aAlarmId); 1.955 + } 1.956 + 1.957 + store.get(aAlarmId).onsuccess = function onsuccess(event) { 1.958 + let record = event.target.result; 1.959 + txn.result = false; 1.960 + if (!record || (aManifestURL && record.manifestURL != aManifestURL)) { 1.961 + return; 1.962 + } 1.963 + 1.964 + store.delete(aAlarmId); 1.965 + txn.result = true; 1.966 + } 1.967 + }, aResultCb); 1.968 + }, 1.969 + 1.970 + removeAlarms: function removeAlarms(aManifestURL, aResultCb) { 1.971 + this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { 1.972 + if (DEBUG) { 1.973 + debug("Remove alarms of " + aManifestURL); 1.974 + } 1.975 + 1.976 + store.index("manifestURL").openCursor(aManifestURL) 1.977 + .onsuccess = function onsuccess(event) { 1.978 + let cursor = event.target.result; 1.979 + if (cursor) { 1.980 + cursor.delete(); 1.981 + cursor.continue(); 1.982 + } 1.983 + } 1.984 + }, aResultCb); 1.985 + }, 1.986 + 1.987 + updateAlarm: function updateAlarm(aAlarm, aResultCb) { 1.988 + let self = this; 1.989 + this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { 1.990 + if (DEBUG) { 1.991 + debug("Update alarm " + aAlarm.id); 1.992 + } 1.993 + 1.994 + let record = self.alarmToRecord(aAlarm); 1.995 + store.openCursor(record.id).onsuccess = function onsuccess(event) { 1.996 + let cursor = event.target.result; 1.997 + txn.result = false; 1.998 + if (cursor) { 1.999 + cursor.update(record); 1.1000 + txn.result = true; 1.1001 + } 1.1002 + } 1.1003 + }, aResultCb); 1.1004 + }, 1.1005 + 1.1006 + getAlarms: function getAlarms(aNetworkId, aManifestURL, aResultCb) { 1.1007 + let self = this; 1.1008 + this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) { 1.1009 + if (DEBUG) { 1.1010 + debug("Get alarms for " + aManifestURL); 1.1011 + } 1.1012 + 1.1013 + txn.result = []; 1.1014 + store.index("manifestURL").openCursor(aManifestURL) 1.1015 + .onsuccess = function onsuccess(event) { 1.1016 + let cursor = event.target.result; 1.1017 + if (!cursor) { 1.1018 + return; 1.1019 + } 1.1020 + 1.1021 + if (!aNetworkId || cursor.value.networkId == aNetworkId) { 1.1022 + txn.result.push(self.recordToAlarm(cursor.value)); 1.1023 + } 1.1024 + 1.1025 + cursor.continue(); 1.1026 + } 1.1027 + }, aResultCb); 1.1028 + }, 1.1029 + 1.1030 + _resetAlarms: function _resetAlarms(aNetworkId, aResultCb) { 1.1031 + this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { 1.1032 + if (DEBUG) { 1.1033 + debug("Reset alarms for network " + aNetworkId); 1.1034 + } 1.1035 + 1.1036 + let lowerFilter = [aNetworkId, 0]; 1.1037 + let upperFilter = [aNetworkId, ""]; 1.1038 + let range = IDBKeyRange.bound(lowerFilter, upperFilter); 1.1039 + 1.1040 + store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) { 1.1041 + let cursor = event.target.result; 1.1042 + if (cursor) { 1.1043 + if (cursor.value.startTime) { 1.1044 + cursor.value.relativeThreshold = cursor.value.threshold; 1.1045 + cursor.update(cursor.value); 1.1046 + } 1.1047 + cursor.continue(); 1.1048 + return; 1.1049 + } 1.1050 + }; 1.1051 + }, aResultCb); 1.1052 + } 1.1053 +};